05.泛型
05.泛型
5.1 泛型基础
- 使用泛型设计的原因:泛型设计意味着可以对不同类型的对象重用,不用重复编写收集不同对象完成相同操作的类
- 类型参数:在java增加泛型之前,泛型的设计是由继承完成的,在这之前就有ArrayList类,它的数据存储由Object数组完成,但这样有一个问题,当获取一个非Object值,必须使用强制转换,而且没有任何检查,数组中可以存入任何值,而现在java加入了类型参数<>,在使用时不用进行强制转换
- 编译器可以利用这个类型参数,在使用get这样的函数时,编译器知道返回值是哪个类型,不用进行强制转换,而且可以检查,防止插入错误的类型对象
- 泛型程序设计:虽然像ArrayList这样的类型会被普遍采用,但是泛型的设计没有那么容易,泛型的设计要考虑泛型所有可能的用法,这个任务会有多难呢,比如:ArrayList中有一个addAll方法,添加另一个泛型的元素,如何设计这个方法使从ArrayList<Manger>转到ArrayList<Employee>中,但使反过来不行,这需要使用通配符类型(wildcard type),利用它可以完成编写,大部分只需知道如何使用就行,JDK的开发人员已经编写了大量的泛型类,所有的集合类都已经完成了泛型设计
5.2 定义简单泛型
- 泛型类就是有一个或多个类型变量的类这样的类引入了类型变量,用<>括了起来,放在类名后面,例如:定义泛型Pair类
public class Pair<T,U>{ ... },在类中可以指定这些类型的变量
常见的做法是类型变量使用大写字母,在java库中,使用E代表集合的元素类型,K和V分别代表键和值,T(必要时使用U和S)表示任意类型
- 在使用时可以用对应的类型变量来实例化泛型,例如:
Pair<String>
从表面来看,泛型似乎和C++的模板类类似,唯一不同的是java没有template关键字,但是他们有明显的不同,在C++中会对所有类型实例化产生不同的类型,在这被称为代码膨胀,java中不会这样,而且C++不能对类型变量做出限制,如果使用时出现问题会得到含糊不清的错误消息
- 定义泛型方法:可以在返回值前使用<>包括泛型类型,例如:
public static <T> T getMiddle(T... a){ ... }这个声明应该在修饰符后面,返回值前面,泛型方法可以由任何的类定义,当调用时,应该使用类名.<String>getMiddle("John","Q","Public");- 大多数情况下,调用时<String>可以省略,因为编译器能有足够的信息判断出这个类型,在括号中使用的就是字符串可变参数列表,它能够识别出这个类型
- 几乎所有情况,这个识别都没问题,只有极少数情况,识别到多个或0个匹配的识别,例如:
getMiddle(3.14,1729,0),这个有两种合法的解释,3.14是Double对象,1729和0都是Integer对象,编译器会寻找他们共同的超类型(包括超类和接口),他们有共同超类Number和共同接口Comparable,在这种情况下,编译器会给出晦涩的错误消息(不同编译器可能不同),这种情况应该将所有的参数写为double类型
5.3 类型变量的限定
- 有的时候,类和方法需要对某些类型变量加以约束,限定这个变量必需继承了某个类或扩展了某个接口,不然使用这个类或方法会产生一个编译错误,例如:
<T extends Employee & Comparable>多个类或接口之间使用&分隔
5.4 泛型代码与虚拟机
- 类型擦除:无论何时定义一个泛型类型,都会提供一个原始类型,这个原始类型就是去掉类型参数的泛型类型名,即将类型变量替换为第一个限定类型(没有则替换为Object),例如:
<T extends Employee & Comparable>会替换为Employee,在使用后方其他接口时,自动插入强制类型转换,建议在书写限定类型时,将标签接口放在后面,减少强制转换次数,提高效率 - 转换泛型表达式:在对或使用一个泛型对象赋值时,如果发生了类型擦除,编译器会插入强制类型转换,这样的话,在使用带泛型的表达式时,编译器会执行表达式右侧,得到返回值,然后将返回值进行强制类型转换,如果不赋值(修改其他变量的值),强制类型转换即便写了也不会发生,但返回时,编译器却认为写了的强制转换肯定发生了,如下代码能通过运行

- 转换泛型方法:如果使用泛型方法,在类型擦除后,也会成为原始类型的函数,其中的泛型会被替换,但是会带来问题:
- 一个从该方法定义类继承的使用Object擦除的方法也会出现,这样就会出现两个不同的方法,但是他们不应该是不同的函数,这是因为希望方法调用具有多态性,会调用最合适的方法,由于不一定调用这个方法时,需要处理的类型是擦除后的类型,编译器会生成一个桥方法,例:
public void setSecond(Object second){ setSecond((LocalDate) second);}我们不能这样编写同名同参的方法,桥方法在编译器中是由参数类型和返回值共同指定的(没有数据交换似乎不用写成泛型方法),JVM能够正确识别这种情况
- 一个从该方法定义类继承的使用Object擦除的方法也会出现,这样就会出现两个不同的方法,但是他们不应该是不同的函数,这是因为希望方法调用具有多态性,会调用最合适的方法,由于不一定调用这个方法时,需要处理的类型是擦除后的类型,编译器会生成一个桥方法,例:
在之前使用方法覆盖超类方法时,桥方法其实出现过,子类方法可以使用比父类更严格的返回值类型,这就是因为有桥方法的存在,桥方法会调用那个重写的方法
- 虚拟机泛型事实:
- 虚拟机中没有泛型,只有普通的类和方法
- 所有类型参数都会替换为它的限定类型
- java会合成桥方法来保持多态
- 为保证类型安全性,必要时会插入强制类型转换
5.5 遗留代码的调用
- 泛型的设计目标之一就是允许和遗留代码互操作,在刚出泛型时,一些类和方法进行了泛型更新,但是之后却从未更新,在那个java5的年代,没有使用泛型类型,所有的泛型都是来自原始类型,可能存在兼容性问题
- 对原始类型使用确定泛型类型参数赋值,由于编译器无法确定这个泛型是否是执行正确的操作的泛型对象,编译器会发出一个警告,编译失败,如果实在需要使用遗留代码,可以使用
@SuppressWarnings("unchecked")注解忽略下方定义的函数或操作警告 - 对现有确定泛型使用原始类型泛型赋值,同样也会出现警告,在确认操作后也可以使用上方注解使之消失
- 对原始类型使用确定泛型类型参数赋值,由于编译器无法确定这个泛型是否是执行正确的操作的泛型对象,编译器会发出一个警告,编译失败,如果实在需要使用遗留代码,可以使用
5.6 泛型的限制和局限性
- 泛型不是万能的,由于类型擦除,在使用时会带来大量问题和限制
不能使用基本类型实例化类型参数:由于泛型的类型参数会擦除为Object类,所有的类型参数必须为继承于Object类
运行时类型查询只适用于原始类型:在进行类型检查(instanceof)时,只能判断它是否属于原始类型例如:
instanceof Pair<T>而不是instanceof Pair<String>,否则会得到一个警告,这也使T不同的泛型对象类型查询的结果是一致的,而且也不存在确定T的泛型类的强制转换不能创建参数化类型数组:由于数组会记住它的元素类型,在JVM中,泛型数组会被类型擦除成Object数组,但数组会记住元素类型,虽然赋值时擦除会导致这种检查无效,但是仍会导致一个类型错误(实例化的时候),java数组必须知道它持有的所有对象的具体类型,而泛型的这种运行时擦除机制违反了数组安全检查的原则
不允许是因为使用像普通数组一样使用
new 类型名[数组长度]的方法会导致一个类型错误,声明泛型类型数组是合法的,只不过需要使用(Pair<String>[]) new Pair<?>[10];通配符类型,这个结果将是不安全的,可能存入非该类型的泛型变量,又或者可以通过创建一个例如Pair[]这样的非参数化类型的数组,然后将他强制类型转换为一个参数化类型数组,如果实在有需要,可以使用ArrayList<Pair<String>>更安全有效Varargs警告:如果你希望使用泛型可变参数列表的话,JVM就不得不创建泛型数组了,在这种情况下,使用可变参数列表会得到一个警告,如果你确定要这么做,可以使用
@SuppressWarnings("unchecked")或SafeVarargs注解该方法,对于任何只需读取数组元素的方法都可以使用@SafeVarargs注解(只能用于static、final或(java 9)private构造器和方法),其它方法可能被覆盖使这种注解没有意义,使用这种注解在对数组元素进行修改时,可能会出现危险,会在别处得到异常不能实例化类型变量:不能在类似
new T()的表达式中使用类型变量,因为类型擦除后会变成Object这样的限定类型,这肯定不是调用者期望的- 在Java8之前,传统的方法是使用反射,传入需要使用的类型的class值,使用它调用反射中的Constructor.newInstance()方法,例如创建
Pair<String>变量的具体实现为:传入String.class(在定义处使用Class<T> cl泛型定义),然后使用cl.getConstructor()得到Constructor变量,调用newInstance()方法,即:cl.getConstructor().newInstance(); - 在Java8之后,引入了lambda表达式,解决方法就变成了传入
String::new这样的构造器表达式,然后使用函数接口(例如在这里可以使用Supplier<T>接口),完成对应的创建
- 在Java8之前,传统的方法是使用反射,传入需要使用的类型的class值,使用它调用反射中的Constructor.newInstance()方法,例如创建
不能构造泛型数组:这与第3点不同,泛型数组是
T[]这样的数组,即对象数组,因为类型擦除,这会导致构造的数组变成擦除后的类型数组,然后会使用强制类型转换,但这转换这是一种假象,它依旧是擦除后的类型数组,但编译器无法察觉,如果仅仅作为私有字段,那么能成功,但如果是在方法中,则不行,在这个数组定义之后,需要使用其他对象引用或返回给其它对象时,会出现ClassCastException异常,但和实例化变量一样能够解决- 传统的办法,是利用反射,调用Array.newInstance()方法,它有两个参数,一个是该类的一个数组的类信息,另一个是新数组的长度,通过获取指定类来创建,就不会被擦除,例:
var result = (T[]) Array.newInstance(a.getClass().getComponentType(),2); - 目前最好的方法是提供数组构造器表达式,然后使用
IntFuntion<T[]>接口接收使用之后返回,例如:提供的是String[]::new,可以用于生成字符串数组 - ArrayList类的toArray方法却不是这样实现的,他要生成
T[]数组,但没有提供类型信息,因此不带参数的toArray方法返回的是Object[],因此无参toArray生成的就是Object[],如果需要得到对应类型的数组,应该使用带对应类型参数的数组,如果数组够大,它会使用它,不然会定义足够长度的数组
- 传统的办法,是利用反射,调用Array.newInstance()方法,它有两个参数,一个是该类的一个数组的类信息,另一个是新数组的长度,通过获取指定类来创建,就不会被擦除,例:
不能抛出捕获泛型化实例:不能用泛型类扩展异常类,异常处理中的catch的不能是类型变量,但泛型变量可以是异常类,即:
<T extends Throwable>,在使用时,catch(Trowable)类异常即可取消检查型异常:在java异常处理中,必须为检查型异常提供处理器,使用一个泛型方法,抛出Trowable异常,在捕获时,调用这个方法,就可以抛出检查型异常,应用场景就是对于从一些未声明抛出一些检查型异常的函数函数中抛出一些检查型异常,在调用的地方进行处理
- 参考于博客
- 老方法是抛出异常的详细信息,使用非静态
initCause()方法,将一个未使用的RuntimeException异常的信息变成这个检查型异常的信息,抛出这个异常
RuntimeException t = ...; try{ do work }catch (Throwable realCause) { t.initCause(realCause); throw t; }- 使用泛型的方法会更加简单,但要注意因为使用了RuntimeException,会有几个误区:
- 其实最后抛出的是原类型异常,强制转换并未发生(泛型的强制转换添加在赋值部分,如果值未被取出,就不会强制转换),编译器认为catch块中已经处理了该异常
- 该静态方法会抛出非检查型异常,编译器不会报错,让它抛出,抛出的受查异常并没有用非受查异常的变量去接收,去取值,再次catch会找到它实际的类型,即非检查类型
- 这并没有违反不能抛出泛型异常,因为强制转换并没有发生,其实抛出的是确定异常
//消除产生的警告信息 @SuppressWamings("unchecked") public static <T extends Throwable> void throwAs(Throwable e)throws T{ throw (T) e; } //编译器就会认为 t 是一个非受查异常。 Block.<RuntimeException>throwAs(t);泛型类的静态上下文中,类型变量无效:在泛型类中定义静态泛型变量或者静态普通函数处理泛型看起来不错,但是行不通,在idea中会报无法获取
this指针的错误,类型擦除后,无论声明什么类型,他们都共用一个static变量和方法,因此被禁止使用public class Singleton<T> { private static T singletonInstance;//Error public static T getSingletonInstance(){ //Error if(singletonInstance == null){ //construct new instance of T } return singletonInstance; } }类型擦除后的冲突:在泛型类中定义与父类或接口继承来的方法名称相同,会产生冲突,比如:
equals(Object)方法是从Object类中继承的,如果在泛型类<T>定义了public boolean equals(T value)方法,会带来冲突,因为类型擦除后,T变成了Object,这两个方法发生了命名冲突,需要将该方法重命名
这也就带来了另一个原则:一个类不能作为一个泛型类或泛型接口的不同参数化的子类,例如:
Employee implements Comparable<Employee>,Manager extends Employee implements Comparable<Manager>是错误的,这样Manager同时implements了Comparable的不同参数化接口
5.7 泛型的继承规则
- 在使用泛型时,或许希望像java数组一样,利用多态性将子类存储在父类数组中,但这在泛型中不成立,因为无论类型变量T和S究竟有何关系,
Pair<T>和Pair<S>都没有关系,他们也不建议存储在原始类型Pair中(没有类型检查,会有警告),这保证了泛型的安全,因为泛型没有像数组一样的特别保护(如果使用父类数组引用生成好的子类数组,依然不能往其中放父类的值) - 泛型类可以扩展其他泛型类,在这一点上和普通类没有区别,例如:
ArrayList<Manager>可以转换为List<Manager>,泛型的继承只与本身类有关和类型参数无关
5.8 通配符类型
- 由于严格的泛型类型使用起来并不愉快,设计者发明了巧妙的解决方案:通配符类型
- 在通配符类型中,允许类型参数发生变化,例如:
pair<? extends Employee>表示了任何pair类型,只要它的类型参数是Employee的子类,只要能满足这个条件的,都能被它存储,如果你希望一个函数的参数是这样的,就可以使用通配符类型 - 使用通配符也可以引用满足这样条件的泛型,因为通配符类型,在使用通配符类型为参数的函数时,需要使用通配符类型赋值,通配符不是固定类型,在返回引用时可以使用满足通配符的类型接收
- 现在在通配符引用后能够了解哪些方法是安全的访问器方法或者不安全的通配符更改器方法,当通配符引用使用访问器方法时,没有问题,但是,使用更改器方法时,需要向其中传入一个通配符类型,就会产生不匹配,因为系统认为通配符和特定类型不是同一类型,你甚至不能用extends的类作为参数,因为通配符类型把函数改变了,例如:
T getA()和void setA(? extends ... aA)变成了? extends ... getA()和void setA(? extends ...),对于get方法,没问题,因为编译器知道可以把返回对象转换为一个正确的类型,但是对于set方法,编译器无法知道具体的类型(不知道类型是否正确),所以会拒绝调用 - 通配符类型的超类限定:<? super Manager>这个通配符限定了必须是Manager的超类类型
- 用法:1️⃣超类型限定通常用于作为函数的参数类型,有的时候需要传入某类型的超类,例如:比较的接口传入一个可比的参数,
int CompareTo(? super T)是目前compareTo方法的设计;2️⃣作为更细致的描述和限定,在定义寻找最小值的方法<T extends Compareable<T>> T min(T[] t)中,如果你希望使用的比较存在于较低继承的层面,可以使用Comparable<? super T>代替,获得更详细的描述和限定
- 用法:1️⃣超类型限定通常用于作为函数的参数类型,有的时候需要传入某类型的超类,例如:比较的接口传入一个可比的参数,
- 无限定通配符:Pair<?>这种没有限定的通配符,只能使用其中的访问器,返回值只能赋给Object对象,修改器无法使用(除非你愿意设置null),必须传入通配符变量,但是通配符变量不为任何确定变量,在不需要关注其中数据和其类型的情况下就可以使用它,可读性更好,不需要声明为泛型函数
- 通配符的捕获:?不是一种类型,在需要使用通配符变量类型定义新变量的时候,可以定义一个泛型函数
<T> T setHelper(Pair<T>)定义捕获通配符?并进行处理,虽然有的时候可以不使用泛型方法实现,但通配符捕获机制不可避免- 捕获的限制是必须为非常限定,你不能捕获无限定通配符,它可能有不同类型
反射和泛型
- 反射能在运行时获取对象信息,但泛型对象发生了类型擦除,很多信息无法获取
- 下面的是能够获得的泛型类的信息
- 泛型Class类:现在的Class类已经被实现为了泛型类,例如:String.class是
Class<String>的对象(事实上是唯一的)Class<T>类有一些重要的方法如下:
//返回一个无参构造器构造的T的实例
T newInstance()
//如果obj可以转换成T则返回obj,否则抛出BadCastException异常
T cast(Object obj)
//如果T属于枚举类型,返回所有值组成的数组,否则返回null
T[] getEnumConstants()
//返回类的超类,如果这个对象不是类对象或为Object对象,返回null
Class<? super T> getSuperclass()
//获得公共构造器或有给定参数的构造器,parameterTypes填入满足希望赋值的变量形式的变量
//区别是getConstructors只返回公共构造函数
//而getDeclaredConstructors返回所有的构造函数,
//可通过setAccessible(True)设置是否返回非公共的构造函数
Constructor<T> getConstructor(Class... parameterTypes)
Constructor<T> getDeclaredConstructor(Class... parameterTypes)
//构造器Constructor拥有newInstance(Object...)方法,可以使用获得的构造器构造实例- 使用Class<T>参数进行匹配:在使用时传入类型变量
class<T>,然后可以使用这个类型变量返回生成的对应变量
//示例
public static <T> Pair<T> makePair(Class <T> c)throws
InstantiationException,IllegalAccessException{
//如果使用makePair(Emmployee.class),类型参数T和Employee匹配
//编译器可以推断出方法会返回Pair<Employe>对象
return new Pair<>(c.newInstance(),c.newInstance());
}- 虚拟机中的泛型信息:java泛型的特性是类型擦除,但擦除后的类仍然保留了一些记忆,例如:原始类Pair知道它来自于
Pair<T>,只不过不知道T是什么类型,类似的,对于限定的类型public static <T extends Comparable<? super T>> T min(T[] a),反射能够确定:- 这个泛型方法有类型参数T -> 通过class类
- 这个类型有子限定,其本身也是个泛型 -> 通过TypeVariable接口,描述类型变量,如:
T extends Comparable<? super T> - 这个限定类型有一个通配符 -> WildcardType接口,描述通配符,如:
? super T - 通配符的参数为超类型限定 -> ParameterizedType接口,描述泛型类或接口类型,如:
Comparable<? super T> - 这个泛型方法有泛型数组参数 -> GenericArrayType接口,描述泛型数组,如:
T[]
- 以上的类和接口都拓展了Type类型,它们有很多类似的方法从Type类继承而来,例如都有getName()方法
- 具体使用见此文
- 类型字面量:你可能会希望由值的类型决定程序行为,例如:希望用户指定一种方法来存储某个特定类的对象,但对泛型类,擦除会带来问题,泛型的不同类型声明擦除后都为同一个原始类型,如果对泛型类,需要捕获对应泛型类型的Type实例,然后构造一个匿名子类,捕获泛型的超类型(CDI和Guice等注入框架,就使用类型字面量来控制泛型类型的注入)
//例子:
ArrayList<Integer> arrayList = new ArrayList<Integer>() {};
Type type = arrayList.getClass().getGenericSuperclass();
ParameterizedType parameterizedType = (ParameterizedType) type;
for (Type t : parameterizedType.getActualTypeArguments()) {
System.out.println(t.getTypeName()); // java.lang.Integer
}使用匿名内部类


未使用匿名内部类

详细补充内容可以看Java泛型
附反射和泛型交互的部分主要方法:



