Java基础之《Java核心卷1》第8章 泛型程序设计

8. 泛型程序设计

8.1泛型基础

以ArrayList为例,最早它的内部是一个Object类型,这导致2个问题:(1)每次使用get(i)都要强制类型转换;(2)可以往ArrayList里面添加任何类型

解决方法:泛型提供了类型参数,此后变为:ArrayList< String> list = new ArrayList< String>();,提供可读性和安全性

新的问题:当Son是Father的子类时,希望可以实现:addAll能将 ArrayList< Son> 全部添加到 ArrayList< Father>里面去,反之不行

//即实现以下规则(实际上也是现在Java具备的功能)	
fathers.addAll(sons);//成功
sons.addAll(fathers);//报错

解决方法:为了实现这一目标,提出了通配符类型

//泛型类
public class Pair<T, U> {
    
     . . . }
//泛型方法
public static <T> T getMiddle(T... a)//注意:<T>放在修饰符后面,返回类型前面

在调用泛型方法时,多数情况不用指明类似的类型,因为会进行自动推导

  • 示例:Pair

    class Pair<T>{
          
          
        private T first;
        private T second;
        public Pair(){
          
          first=null; second=null;}
        public Pair(T first, T second){
          
          this.first=first; this.second=second;};
        public T getFirst(){
          
          return first;};
        public void setFirst(T first){
          
          this.first=first;};
    }
    

8.4 类型变量的限定

场景:定义 T smallest,之后需要用到 smallest.compareTo 函数,如何确定它所属的类有这个函数呢?

限定类型变量

public static <T extends Comparable> T min(T[] a);

(1)尽管Comparable是接口,但用的仍然是extends,而非implements
(2)不能对模板参数的类型加以限制
(3)一个类型变量或通配符可以有多个限定,如

T extends Comparable & Serializable

类型擦除(表达式)

虚拟机没有泛型类型对象——所有对象都属于普通类

泛型类型都有相应的原始类型,对于有限定的类型变量,它的原始类型是第一个限定类型;无限定则是Object类型,例如

public class Interval <T extends Comparable & Serializableimplements Serializable{
    
    
    private T lower;
}

它的原始类型是:

public class Interval implements Serializable{
    
    
    private Comparable lower;
}
//对于被忽略掉的限定类型,编译器会在必要时插入强制类型转换

原始类型和强制类型转换:

Pair<Employee> buddies = ...
Employee buddy = buddies.getFirst();
/**
* 编译针对泛型类型,将进行以下两步
* 1. 对原始方法Pair.getFirst的调用
* 2. 将返回的Object类型强制转换为Employee类型
*/

类型擦除(方法)

public static <T extends ComparableT min(T[] a)
//擦除后变成:
public static Comparable min(Comparable[] a)

存在的问题:多态和类型擦除的冲突

class DateInterval extends Pair<LocalDate>{
    
    
    public void setSecond(LocalDate second){
    
    
        ...
    }
}
//擦除后变成
class DateInterval extends Pair{
    
    
    public void setSecond(LocalDate second){
    
    
        ...
    }
}

此时,DateInterval中还有从Pair继承而来的另一个setSecond方法,即

public void setSecond(Object second);//继承而来
public void setSecond(LocalDate second);//自身的
/**
* 本意是在子类中覆盖掉超类的setSecond方法,但因为类型擦除的存在,没有覆盖住
*/

当执行以下语句时

DateInterval interval = new DateInterval(. . .);
Pair<LocalDate> pair = interval; // OK assignment to superclass
pair.setSecond(aDate);

pair是超类变量,但引用了子类对象,执行setSecond时,它将调用子类对象,即DateInterval.setSecond()
问题在于:DataInterval中有2个setSecond,怎样才能调用到合适的哪个呢?

  • 桥方法:桥方法被合成来保持多态

    当面临多态和类型擦除带来的矛盾时:编译器会在子类中生成一个桥方法,如下:

    public void setSecond(Object second) {
          
           setSecond((Date) second); 
    

    当执行pair.setSecond(aDate)时,编译器工作步骤:
    (1)调用这个桥方法
    (2)桥方法再调用子类的方法

  • 进一步:子类覆盖了超类的方法

    class DateInterval extends Pair<LocalDate>{
          
          
    	public LocalDate getSecond() {
          
           return (Date) super.getSecond().clone(); }
    }
    

    此时子类中有2个方法:

    Object getSecond();//继承而来
    LocalDate getSecond();//自身所有
    

    程序员不允许编写仅返回类型不同的函数,但JVM会用参数类型和返回类型来确定一个方法

    @SuppressWarnings(“unchecked”):这个注解会关闭对方法中所有代码的检査

8.6 约束和局限性

(1)不能用基本类型实例化类型参数

原因是类型擦除后Object不能存储基本类型的值

(2)运行时类型查询只适用于原始类型

Pair<String> stringPair = ...
Pair<Employee> employeePair = ...
if (stringPair.getClass() == employeePair.getClass()) //getClass会获取到原始类型,将得到相等的结果
Pair<String> pairString = new Pair<>();
if(pairString instanceof Pair<String>)//报错
if(pairString instanceof Pair)//成功

(3)不能创建参数化类型的数组

Pair<String>[] table = new Pair<String>[10];//Error,不能这样使用

原因:参考博客:https://blog.csdn.net/qq_41286138/article/details/105250938
主要是为了避免数组里出现类型不一致的元素

Pair[] table = new Pair[10];
table[0] = new Object(); //编译错误

编译器会记住创建时的类型,如果赋值不一致类型则报错。那么将数组向上转换一下,再存储呢?

Pair[] table = new Pair[10];
Object[] o = table; //自动转换
o[0] = new Object();

此时编译器是不报错,但是运行时会抛出ArrayStoreException异常
但是类型擦除会破坏这种机制

Pair<String>[] table = new Pair<String>[10] //假设可以
Object[] o = table; //泛型擦除变为Pair[],向上自动转换为Object[]
o[0] = new Pair<Double>();//成功
o[1] = new Object(); // 编译不报错,运行抛出异常

为了安全性,java禁止了创建参数化类型的数组

可以使用通配符完成这种数组创建,但仍然是不安全的做法

(4)Varargs 警告

public static <T> void addAll(Collections<T> coll, T...ts){
    
    
	for (t : ts) coll.add(t)}

调用:

Collection<Pair<String>> table = . . .;
Pair<String> pairl = . . .;
Pair<String> pair2 = . .
addAll (table, pairl, pair2);

此时addAll中的ts实际上是一个数组,违反了不能创建了泛型数组的规定。这里的规则放松一点,只会警告,不会报错
可以使用@SuppressWarnings(“unchecked”)或者@SafeVarargs标注这个方法
@SafeVarargs也可以用来解决泛型数组的问题,它能够顺利编译且运行,但在使用时会在别处得到一个异常

(5)不能实例化类型变置

不能使用new T(…),newT[…] 或T.class 这样的表达式,例如

//这段代码将报错
class Pair<T>{
    
    
    private T first;
    private T second;
    public Pair() {
    
     first = new T(); second = new T(); }//报错
}
//类型擦除会使得new T(),变成new Object()

最好的解决方法是:

  • Supplier函数式接口,表示一个无参数而且返回类型为 T 的函数
class Pair<T>{
    
    
    private T first;
    private T second;
    public Pair(T t1, T t2) {
    
    this.first=t1; this.second=t2;}
    //public Pair() { first = new T(); second = new T(); }//报错
    public static<T> Pair<T> makePair(Supplier<T> constr){
    
    
        return new Pair<>(constr.get(), constr.get());
    }
}
public class Test{
    
    
    public static void main(String[] args){
    
    
        //1. 推荐方法:构造器方式
        Pair<String> p = Pair.makePair(String::new);//推荐方式
        //2. 相同效果:lambda方式
        Pair<String> p = Pair.makePair(()->new String());
        //3. 更好理解的:匿名内部类方式
        Pair<String> p = Pair.makePair(new Supplier<String>() {
    
    
            @Override
            public String get() {
    
    
                return new String();
            }
        });
    }
}

传统方法是使用newInstance

//makePair
public static <T> Pair<T> makePair(Class<T> cl){
    
    
    try{
    
    
        return new Pair<>(cl.newInstance(), cl.newInstance());      
    }catch(Exception ex) {
    
     return null; }
}
//调用
Pair<String> p = Pair.makePair(String.class);//能进行自动类型推导

Java中反射的效率是比较低的,newInstance是通过反射的方式;Supplier方式是直接new,效率更高

(6)不能构造泛型数组

可以采用如下形式

//方法
public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T...a){
    
    
    Arrays.sort(a);
    T[] mm = constr.apply(2);
    mm[0] = a[0];
    mm[1] = a[a.length-1];
    return mm;
}
//调用和测试
String[] ss = Pair.minmax(String[]::new, "Tom", "Dick", "Harry");
Integer[] ii = Pair.minmax(Integer[]::new, 3, 1, 4);
System.out.println(Arrays.toString(ss));
System.out.println(Arrays.toString(ii));

或者老式方法(通过反射):

//方法
public static <T extends Comparable> T[] minmax(T... a){
    
    
	T[] mm = (T[]) Array.newlnstance(a.getClass().getComponentType() , 2);
}
//调用
String[] ss = Pair.minmax( "Tom", "Dick", "Harry");

ArrayList的toString所有所不同,有机会看看源码学习

(7)不能在静态域或方法中引用类型变量

public class Singleton<T>{
    
    
    private static T singleInstance; // Error
    public static T getSingleInstance(){
    
    }; //Error
}

原理:以静态域singlelnstance为例,当同时声明一个Singleton< Random> 共享随机数生成器和一个Singleton< JFileCh00Ser> 共享文件选择器对话框时,经过类型擦除,就只剩下一个singlelnstance域,存在问题

(8)不能抛出或捕获泛型类的实例

不能抛出或捕获泛型类,泛型类不能扩展Throwable !

public class Problem<T> extends Exception{
    
    }//报错

catch语句中,不能有泛型。但在异常规范中使用类型变量是允许的。如

//报错!!
public static <T extends Throwable> void doWork(Class<T> t){
    
    
    try{
    
    }
    catch (T e){
    
    }
}
//通过!!
public static <T extends Throwable> void doWork(T t) throws T{
    
    
    try{
    
    }
    catch (Throwable e){
    
    
        t.initCause(e);
        throw t;
    }
}

(9)可以消除对受查异常的检查

场景:假设存在一个方法body(),要求其必须抛出异常(即受查),如下

abstract class Block{
    
    
    public abstract void body() throws Exception;//声明一个方法,并规定该方法必须抛出Exception
    public void test() throws Exception {
    
    
        //这里如果是不 throws 或者 throws RuntimeException 都将报错!!
        body();
    }
}

用于消除受查异常检测的重要代码

@SuppressWarnings("unchecked")
public static <T extends Throwable> void throwAs(Throwable e) throws T{
    
    
    throw (T)e;
}
//假设这个方法包含在类Block中,如果调用下属语句,就相当于告诉编译器,这是一个非受查异常
//RuntimeExceptions是非受查异常
Block.< RuntimeException>throwAs(t);

修改之后

abstract class Block{
    
    
    public abstract void body() throws Exception;//声明一个方法,并规定该方法必须抛出Exception
    public void test() {
    
    
        try {
    
    
            body();
        }catch (Throwable e){
    
    
            //throw new RuntimeException();//这条语句也能执行
            Block.< RuntimeException>throwAs (e);
        }
    }
    @SuppressWarnings("unchecked")
    public static <T extends Throwable> void throwAs(Throwable e) throws T{
    
    
        throw (T)e;
    }
}

用法是这样用,但意义暂且模糊:把一个受查异常包装为非受查异常,但要保留其实质是受查异常的
throw new RuntimeException()的实质也是非受查的

(10)注意擦除后的冲突

下列代码将报错

class Pair<T>{
    
    
    public boolean equals(T value){
    
    return true;}//报错,clashes with 'equals(Object)'
    public boolean equals(String value){
    
    return true;}//这样就可以,因为T会被擦除为Object,而String不会
}

这是因为Pair默认继承Object类,这样它里面就会产生两个equals,例如考虑Pair< String>

Pair<String>
//产生的两个equals分别是
public boolean equals(Object value);//自己定义的equals经过类型擦除之后的结果
public boolean equals(Object value);//来自Object
//注意,自己定义的方法编译后不是equals(String value),而是泛型T在类型擦除的影响下也变成equals(Object value)
//所以前面的代码会报错:clashes with 'equals(Object)'

可以通过重命名的方式解决这一问题
还有一个重要原则:一个类不能同时 implements 同一接口的不同参数化

class Employee implements Comparable<Employee>{
    
     ... }
class Manager extends Employee implements Comparable<Manager>{
    
     ... }//报错
//这里将报错:'java.lang.Comparable' cannot be inherited with different type arguments: 'pack.Employee' and 'pack.Manager'

这里 Manager 是 Employee 的子类,所以它implements Comparable< Employee>
Manager 本身又 implements Comparable< Manager>
这种情况是不被允许的,不被允许的原因可能是和桥方法冲突有关

(11)泛型类型将破坏原有的继承关系

Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair<Employee> employeeBuddies = managerBuddies;//尽管Employee是Manager的超类,但泛型后继承关系被破坏了,这里将报错

这样规定的原因:

//假设上面不报错,执行下面语句
employeeBuddies.setFirst(lowlyEmployee);
//这将普通员工lowlyEmployee赋值给了manager,实际中不应当这样做

泛型和数组的一个重要区别:数组中保留了继承关系,如下

Manager[] managers = new Manager[5];
Employee[] employees = managers;
employees[0] = new Employee();//这句能通过编译,但运行时报错ArrayStoreException
//注意这里的employees只能存储manager对象,否则报错ArrayStoreException
//这是因为数组的类型会被记住,修改数组变量类型除了逃避编译器外反而没有太大作用

泛型:可以将参数化类型转换给原始类型,从而与遗留代码兼容(但不太安全,有可能出现类型错误)

Manager ceo = new Manager();
Manager cfo = new Manager();
Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair rawBuddies = managerBuddies;
rawBuddies.setFirst(new File("F://test.txt")); //only a compile-time warning
//这里不会报错

8.8 通配符类型

(子类型限定)通配符

无论类S和类T有着怎样的关系,Pair< S>和Pair< T>都是无关的,不能赋值Pair< S> … = Pair< T>…
假设S和T有继承关系,且希望 泛型能够具有同样的继承关系,那么可以使用 通配符 来解决这个问题

Pair<? extends Employee> //表示任何泛型Pair类型,它的类型参数是Employee的子类

这里可以做个比较:
Pair< ? extends Employee> 是为了保留继承关系,它可以接收 Pair< Employee的子类>
Pair< T extends Comparable> 是为了保证T实现了某个接口,它可以接收 Pair<Comparable的实现类>

  • 失败场景:使用普通泛型

    public static void printBuddies(Pair<Employee> p){
          
          
        System.out.println(p.getFirst().getName());
    }
    public static void main(String[] args){
          
          
        Employee employee = new Employee("Tony");
        Pair<Employee> employeePair = new Pair<>(employee, new Employee());
        printBuddies(employeePair);//成功,输出 Tony
    
        Manager manager = new Manager("Admin");
        Pair<Manager> managerPair = new Pair<>(manager, new Manager());
        printBuddies(managerPair);//报错!!
    }
    

    原因:Pair< Employee> 不能接收 Pair< Manager>

  • 成功场景:使用通配符

    //将方法改为
    public static void printBuddies(Pair<? extends Employee> p){
          
          
        System.out.println(p.getFirst().getName());
    }
    //此时成功输出 Tony和Admin
    

    类型Pair 是Pair<? extends Employee> 的子类型

  • ?也不能使父类对象被子类引用

    Pair<? extends Employee> wildBuddies = managerPair;//ok,managerPair是Pair<Manager>类型
    wildBuddies.setFirst(new Employee("Buddy"));//error,这里将直接报错,因为这里将一个父类Employee赋给子类空间managerPair
    

    使用getFirst不存在这个问题,因为任意子类都可以赋值给超类Employee

超类型限定通配符

? super Manager

带有 ? super 的通配符和上一节相反,可以使用setFirst(),但不能使用getFirst

//Pair<? super Manager>
? super Manager getFirst();//error; 这里返回的是Manager的任意一个超类,不能将超类返回给子类,所以
void setFirst(? super Manager);//ok; 相反,? super Manager表示任意的超类,可以用超类接子类

? super超类型限定 set 写入,? extends子类型限定 get 读出

无限定通配符

Pair<?>

它的get和set方法变为

? getFirst();//它的返回值只能赋值给Object对象变量,类似于<? extends Object>时的情况
void setFirst(?);//set方法直接不能被调用,哪怕是用Object去调用

Pair<?>和Pair本质的不同在于:可以用任意Object对象调用原始Pair类的setObject方法
无限定通配符的作用:对于许多简单操作有用,如

public static boolean hasNulls(Pair<?> p){
    
    
    return p.getFirst()==null || p.getSecond()==null;
}
//它的等价泛型方法是
public static <T> boolean hasNUlls(Pair<T> p);

通配符捕获

场景:要写一个方法,交换 Pair 的 first 和 second

  • 失败写法

    public static void swap(Pair<?> p);//失败
    //因为 ? 不能作为一种类型,但在交换的时候必须临时保存第一个元素
    ? t = p.getFirst();
    
  • 成功写法

    public static <T> void swapHelper(Pair<T> p);
    //T可以作为类型,执行临时保存
    T t = p.getFirst();
    
  • 当用 ? 调用 T 方法,即 swap 调用 swapHelper

    public static void swap(Pair<?> p) {
          
           swapHelper(p); }
    

8.9 反射和泛型

  • 泛型Class类

    String.class实际上是一个Class类的对象
    Class 中的方法使用的类型参数

    T newInstance()	//返回一个实例,这个实例所属的类由默认的构造器获得,它的返回类型与Class<T>描述的类相同,避免类型转换
    T cast(Object obj)	//强制转换类型
    T[] getEnumConstants()	//如果这个类不是enum类或类型T的枚举值的数组,getEnumConstants方法将返回null
    Class<? super T> getSuperclass()	//返回超类
    Constructor<T> getConstructor(C1ass... parameterTypes)
    Constructor<T> getDeclaredConstructor(Class... parameterTypes)
    
  • 使用Class 参数进行类型匹配

    //标准示例
    public static <T> Pair<T> makePair(Class<T> c) throws InstantiationException, IllegalAccessException{
          
          
    	return new Pair<>(c.newInstance(), c.newInstance());
    }
    //调用
    makePair(Employee.class)
    /**
    * Employee.class是类型Class<Employee>的一个对象,T同Employee匹配
    * 编译器可以推断出这个方法将返回一个Pair<Employee>
    */
    
  • 虚拟机中的泛型类型信息

    擦除的类仍然保留一些泛型祖先的微弱记忆

    //泛型方法
    public static <T extends Comparable<? super T>> T min(T[] a)
    //擦除后
    public static Comparable min(Comparable[] a)
    

个人理解:
Comparable<? super T>,表示Compareble< S>,S是T的父类
T extends Comparable<? super T>,T继承自Compareble< S>

java.lang.reflect 包中的接口Type包含以下子类型:

- Class 类,描述具体类型
- TypeVariable 接口,描述类型变量(如T extends Comparable<? super T>)
- WildcardType 接口, 描述通配符(如?super T)
- ParameterizedType 接口, 描述泛型类或接口类型(如Comparable<? super T>)
- GenericArrayType 接口, 描述泛型数组(如T[])

在这里插入图片描述

  • API

    • Class

      //java.lang.Class<T> 1.0
      T newInstance();//返回无参数构造器构造的一个新实例
      T cast(Object obj);//如果obj为null或有可能转换成类型T,则返回obj;否则拋出BadCastException异常
      T[] getEnumConstants();// 5.0,如果T是枚举类型,则返回所有值组成的数组,否则返回null
      Class<? super T> getSuperclass();//返回这个类的超类。如果T不是一个类或Object类,则返回null。
      Constructor<T> getConstructor(Class.. .parameterTypes);// 1.1
      Constructor<T> getDeclaredConstructor(Class... parameterTypes);
      //1.1 获得公有的构造器,或带有给定参数类型的构造器
      
      //5.0 如果被声明为泛型类型,则获得泛型类型变量,否则获得一个长度为0的数组
      TypeVariable[] getTypeParameters();
      //5.0 获得被声明为这一类型的超类的泛型类型;如果这个类型是Object或不是一个类类型,则返回null
      Type getGenericSuperclass();
      //5.0 获得被声明为这个类型的接口的泛型类型(以声明的次序),否则,如果这个类型没有实现接口,返回长度为0的数组。
      Type[] getGenericInterfaces();
      
    • Constructor

      //java.lang.reflect.Constructor<T> 1.1
      T newInstance(0bject... parameters );//返回用指定参数构造的新实例
      
    • Method

      //java.lang.reflect.Method 1.1
      TypeVariable[] getTypeParameters();//5.0,如果这个方法被声明为泛型方法,则获得泛型类型变量,否则返回长度为0的数组
      Type getGenericReturnType();//5.0,获得这个方法被声明的泛型返回类型
      Type[] getGenericParameterTypes();//5.0,获得这个方法被声明的泛型参数类型。如果这个方法没有参数,返回长度为0的数组
      
    • TypeVariable

      //java.lang.reflect.TypeVariable 5.0
      String getName();//获得类型变量的名字
      Type[] getBounds();//获得类型变量的子类限定,否则,如果该变量无限定,则返回长度为0 的数组
      
    • WildcardType

      //java.lang.reflect.WildcardType 5.0
      Type[] getUpperBounds();//获得这个类型变量的子类(extends)限定,否则,如果没有子类限定,则返回长度为0的数组
      Type[] getLowerBounds();//获得这个类型变量的超类(super)限定,否则 如果没有超类限定,则返回长度为0的数组
      
    • ParameterizedType

      //java.lang.reflect.ParameterizedType 5.0
      Type getRawType();//获得这个参数化类型的原始类型
      Type[] getActualTypeArguments();//获得这个参数化类型声明时所使用的类型参数
      Type getOwnerType();//如果是内部类型,则返回其外部类型,如果是一个顶级类型,则返回null
      
    • GenericArrayType

      //java.lang.reflect.GenericArrayType 5.0
      Type getGenericComponentType();//获得声明该数组类型的泛型组件类型
      

猜你喜欢

转载自blog.csdn.net/widsoor/article/details/128167096