泛型擦除
java
Class c1 = new ArrayList<Integer>().getClass();
Class c2 = new ArrayList<String>().getClass();
System.out.println(c1 == c2); //true
ArrayList 和 ArrayList 在编译的时候是完全不同的类型,你无法在写代码时,把一个 String 类型的实例加到 ArrayList 中。但是在程序运行时,的确会输出true。这就是 Java 泛型的类型擦除造成的,因为不管是ArrayList 还是 ArrayList ,在编译时都会被编译器擦除成了 ArrayList。Java中的泛型基本上都是在编译器这个层次来实现的,编译器在编译的时候去掉。我们所说的 Java 泛型在字节码中会被擦除,并不总是擦除为 Object 类型,而是擦除到上限类型。
泛型缺陷
- 基本类型无法作为泛型实参(因为编译时被擦除为Object类型),只能用包装类型,装箱开箱有开销
- 泛型类型无法当做真实的类型使用,因为编译后的泛型为Object类型
- 泛型类型无法用方法重载,因为编译后都是List list
java
public void print(List<Integer> list){ }
public void print(List<String> list){ }
- 静态方法无法引用类泛型参数(可以给静态方法单独加上泛型参数),因为只有类实例化的时候才会知道泛型参数,而静态方法不需要持有类的实例
泛型信息
泛型信息获取
附加的签名信息特定场景下反射可以获取。获取到泛型类型的条件
- 必须具有真实类型的存在
- 泛型的类型是明确的
第一个很好理解,如果连要获取的类都不存在,即未定义,那自然是获取不到的;
第二个条件,举个例子,假设存在User,那么List就是明确的,List是不明确的。
满足上面两点,就可以获取泛型的类型了。
- 因为类型擦除,创建子类才可以获取到父类的泛型信息。如果是继承基类而来的泛型,就用 getGenericSuperclass() , 转型为 ParameterizedType 来获得实际类型。如果是实现接口而来的泛型,就用 getGenericInterfaces() , 针对其中的元素转型为 ParameterizedType 来获得实际类型。可以看出都跟继承类/实现接口有关,因为在类或接口定义的类型参数(泛型)其实是不确定的,只有子类继承或接口实现才能确定类型参数的具体类型。比如:接口Service,类型参数T是不确定的,说白了其实就是个占位符,而实现类UserServiceImpl才能确定类型参数T是User。一般情况下为了获取泛型类型,会创建一个匿名对象来获取,参见下面 gson 的获取
kotlin
val map: Map<String, Int> = object : HashMap<String, Int>() {}
val type = map.javaClass.genericSuperclass as ParameterizedType
val typeArguments: Array<Type> = type.actualTypeArguments
for (typeArgument in typeArguments) {
Log.d(TAG, "genericInfo: $typeArgument")
}
// class java.lang.String
// class java.lang.Integer
- 获取方法中的泛型信息
java
class SuperClass<T> {
}
class SubClass extends SuperClass<String> {
public List<Map<String, String>> getValue() {
return null;
}
}
public void test4() throws NoSuchMethodException {
Class<SubClass> aClass3 = SubClass.class;
ParameterizedType genericSuperclass = (ParameterizedType)aClass3
.getMethod("getValue")
.getGenericReturnType();
System.out.println(genericSuperclass.getActualTypeArguments()[0]);
}
//java.util.Map<java.lang.String, java.lang.String>
- Gson中泛型签名的应用
java
Gson gson = new Gson();
//反序列化(把字节序列恢复为Java对象的过程)
List<User> userList = gson.fromJson(json,
new TypeToken<List<User>>() {}.getType());
System.out.println(userList);
gson 就是利用"创建子类才可以获取到父类的泛型信息"来获取泛型信息的。
gson用于反序列化时可能需要提供泛型参数,因为JAVA的这种泛型机制,运行时是获取不到真实的泛型参数的。这里 new TypeToken<Collection>(){}创建了一个继承自TypeToken的匿名内部类的实例,在它的内部通过以下代码就能获取父类的泛型参数。
ini
Type superclass = getClass().getGenericSuperclass();
ParameterizedType parameterized = (ParameterizedType)superclass;
//parameterized.getActualTypeArguments()[0] 获取到的泛型参数
java
public class TypeToken<T> {
private final Class<? super T> rawType;
private final Type type;
private final int hashCode;
protected TypeToken() { // protected
this.type = getTypeTokenTypeArgument();
this.rawType = (Class<? super T>) $Gson$Types.getRawType(type);
this.hashCode = type.hashCode();
}
}
简而言之,TypeToken类的作用就是希望你直接使用 new TypeToken<Collection>(){}(而不是new TypeToken())来规避JAVA的类型擦除,这就是为什么 TypeToken 的构造函数为什么是 protected 的原因
泛型信息的保存
泛型擦除后是如何保存泛型信息的?
java
public class Sign {
public static void test(List<String> list) {//加了泛型信息
System.out.println("test");
}
}
通过javac Sign.java后再通过javap -v Sign.class看下反编译后的class字节码。
java
public static void test(java.util.List<java.lang.String>);
descriptor: (Ljava/util/List;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String test
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 11: 0
line 12: 8
Signature: #14 // (Ljava/util/List<Ljava/lang/String;>;)V
1-12行字节码两者完全一样,但加了泛型后的字节码文件多了一个最后一行的签名Signature,保存到就是泛型的信息。泛型类型只会在类,字段,以及方法形参保存泛型信息,正是因为有了Signature对泛型信息的保存,我们才能获取他们。
泛型擦除后,类、字段和方法的形参泛型信息是会保存到Signature中的,另外可以通过匿名内部类的方式获取泛型的实参类型,比如Gson中的使用。
混淆事项
泛型混淆,签名问题,混淆后签名找不到了,导致反射后拿不到
- 保留签名信息:-keepattributes Signature
- Kotlin中: -keep class kotlin.Metadata {*;}
协变、逆变
定义
如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类)
- 当A≤B时有f(B)≤f(A)成立,f(⋅)是逆变(contravariant)的
- 当A≤B时有f(A)≤f(B)成立,f(⋅)是协变(covariant)的
- 当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关,f(⋅)是不变(invariant)的
泛型是Java最基础的语法之一,众所周知:出于安全原因,泛型默认不能支持型变(否则会引入危险),因此Java提供了通配符上限和通配符下限来支持型变,其中通配符上限 extends 就泛型协变,通配符下限 super 就是泛型逆变。
数组是协变的
由于来自早期的设计,所以Java的数组默认就支持协变:只要A是B的子类,那么A[]就相当于B[]的子类,比如 Integer 是 Number 的子类,因此 Integer[] 就相当于 Number[] 的子类。
但数组的协变会导致潜在的问题,例如如下程序:
java
Integer[] intArr = new Integer[5];
// 数组默认就支持型变,因此下面代码是正确的
Number[] numArr = intArr;
// numArr只要求集合元素是Number,因此下面代码也可通过编译
numArr[0] = 3.4; // ① 但运行时会出现异常
在向数组中放置异构类型时抛出异常 ArrayStoreException。
泛型是不变的
泛型默认不支持型变。为了避免重蹈Java数组的覆辙,Java泛型显然不能再继续支持默认的型变。这意味着:即使A是B的子类,那么 List<A> 也不是List<B>的子类,比如Integer是Number的子类,而List<Integer>却并不是List<Number>的子类。
java
List<Fruit> flist = new ArrayList<Apple>(); // 编译错误
List<Number> flist = new ArrayList<Integer>(); // 编译错误
Java泛型是不变的,可有时需要实现协变,在两个类型之间建立某种类型的向上转型关系,怎么办呢?这时,通配符派上了用场。
协变
协变:通配符上限 extends
为了让泛型支持协变,Java引入了通配符上限语法:如果A是B的子类,那么List<A>相当于是List<? extends B>的子类,比如Integer是Number的子类,List就相当于List<? extends Number>的子类。
对于支持协变的泛型集合,例如List<? extends Number>,Java编译器只知道该List集合的元素是Number的子类------但具体是哪个子类则无法确定。
因此对于协变的泛型集合,程序只能从集合中取出元素------取出的元素的类型肯定能保证是上限;但程序不能向集合添加元素------因此程序无法确定程序要求的集合元素具体是上限的哪个子类。还有一种说法就是:生产者适合用 <? extends T>,而消费者适合用 <? super T>,这里生产者指的是能用来读取的对象,消费者指的是用来写入的对象。例如如下程序:
java
List<Integer> intList = new ArrayList<>();
// List<? extends Number>支持协变,
// 因此只要元素是Number子类的List集合,就可以赋值给numList集合
List<? extends Number> numList = intList;
// 取出的元素被当成Number处理
Number n1 = numList.get(0);
// 即使是Number类型也无法加入,因为虽然声明为Number但指向的可能是Number的子类。
Number num = 2;
numList.add(num); // 编译错误
List<? extends Fruit> flist = new ArrayList<Apple>();
flist.add(new Apple()); // 编译错误
flist.add(new Fruit()); // 编译错误
flist.add(new Object()); // 编译错误
对于更通用的泛型来说,对于支持协变的泛型,程序只能调用以泛型为返回值类型的方法;不能调用形参为泛型的方法。
javascript
class Apple<T> {
private T info;
public Apple(T info) {
this.info = info;
}
public void setInfo(T info) {
this.info = info;
}
public T getInfo() {
return this.info;
}
}
public class GenericCovariance {
public static void main(String[] args) {
// 指定泛型T为Integer类型
Apple<Integer> intApp = new Apple<>(2);
// 协变
Apple<? extends Number> numApp = intApp;
// 协变的泛型,调用以泛型为返回值的方法,正确。
// 该方法的返回值是T,该T总是Number类或其子类
Number n = numApp.getInfo();
// 协变的泛型,不能调用以泛型为参数的方法,编译报错
// 因此编译器只能确定T必须是Number的子类,但具体是哪个子类则无法确定,因此编译出错
numApp.setInfo(3); // 编译报错
}
}
逆变
逆变:通配符下限
如果A是B的父类,那么List<A>反而相当于是List<? super B>的子类,比如Number是Integer的父类,List反而相当于List<? super Integer>的子类------这种型变方式被称为逆变。
对于支持逆变的泛型集合,例如List<? super Integer>,Java编译器只知道该List集合的元素是Integer的父类------但具体是哪个父类则无法确定。 因此对于逆变的泛型集合,程序只能向集合中添加元素;但程序不能从集合中取出元素------因为编译器无法确定集合元素具体是下限的哪个父类------除非你把取出的集合元素总是当成Object处理(众生皆Object)
javascript
List<Number> numList = new ArrayList<>();
numList.add(2);
numList.add(4.3);
List<Object> objList = new ArrayList<>();
objList.add("Java");
objList.add(3.5f);
// List<? super Integer>支持逆变,
// 因此只要元素是Integer父类的List集合,就可以赋值给intList1集合
List<? super Integer> intList1 = numList; // ①
// 逆变的集合添加元素完全没问题------集合元素肯定是Integer的父类(此处为Number)
intList1.add(20); // ②
System.out.println(intList1);
// List<? super Integer>支持逆变,
// 因此只要元素是Integer父类的List集合,就可以赋值给intList2集合
List<? super Integer> intList2 = objList; // ①
// 逆变的集合添加元素完全没问题------集合元素肯定是Integer的父类(此处为Object)
intList2.add(30); // ②
System.out.println(intList2);
// 取出集合元素时,集合元素只能被当成Object处理
Object ob1 = intList1.get(0); // 只能用 object 类型接收
Object ob2 = intList2.get(0); // 只能用 object 类型接收
总结来说,支持逆变的集合只能添加元素,不能取出元素(除非取出元素都当成Object)。
对于更通用的泛型来说,对于支持逆变的泛型,程序只能调用以泛型为形参的方法;不能调用形参为返回值类型的方法(除非将返回值当成Object处理)。
javascript
class Apple<T> {
private T info;
public Apple(T info) {
this.info = info;
}
public void setInfo(T info) {
this.info = info;
}
public T getInfo() {
return this.info;
}
}
public class GenericContravariance2 {
public static void main(String[] args) {
// 指定泛型T为Object类型
Apple<Object> objApp = new Apple<>("疯狂Java");
// 逆变
Apple<? super Integer> intApp = objApp;
// 逆变的泛型,调用以泛型为形参的方法,正确。
// 该方法的Integer参数总符合下限,下限一定派生自父类
intApp.setInfo(3);
// 逆变的泛型,调用以泛型为返回值的方法,该返回值只能被当成Object处理
Object o = intApp.getInfo(); // 只能 Object 类型接收
}
}
生产者、消费者
什么使用extends,什么时候使用super。《Effective Java》给出精炼的描述:producer-extends, consumer-super(PECS)。
说直白点就是,从数据流来看,extends是限制数据来源的(生产者),而super是限制数据流入的(消费者)。<? extends Apple>限制了get方法返回的类型必须是Apple及其父类型。
csharp
static void readFrom(List<? extends Apple> apples) { // 限制读
Apple apple = apples.get(0);
Jonathan jonathan = apples.get(0); // 编译错误
Fruit fruit = apples.get(0);
}
static void writeTo(List<? super Apple> apples) { // 限制写入
apples.add(new Apple());
apples.add(new Jonathan());
apples.add(new Fruit()); // 编译错误
}
Kotlin 的泛型
与 java 的不同
在 Kotlin 中 out 相当于 <? extends T>,in 相当于 <? super T>,那么会不会哪里有所不同呢?
使用处型变
在 Java 中,通配符 只能用在_参数、属性、变量或者返回值_中,不能在泛型声明处使用,所以才叫做使用处型变 。
所以 Java 和 Kotlin 都提供使用处型变 。
声明处型变
但不同的是,Kotlin 还提供 Java 所不具备的声明处型变。 out 和 in 两个型变关键字还可以用于泛型声明的时候。
kotlin
public interface Collection<out E> : Iterable<E> {
...
}
// 错误,这里只能用val,不能用var
class Source<out T>(var t: T) {
...
}
reified
Java 在用泛型时不能够直接地使用类型,通常解决方法是以函数参数形式传递类,这使得代码更复杂。 kotlin 如果不使用 reified 时,也跟 java 实现一样:
kotlin
// Function
fun <T : Activity> Activity.startActivity(context: Context, clazz: Class<T>) {
startActivity(Intent(context, clazz))
}
// Caller
startActivity(context, NewActivity::class.java)
reified 能在用泛型时直接地使用类型 T,以 reified 修饰类型后就能够在函数内部使用相关类型了。reified 必须用在 inline 泛型方法上,这是为什么能够使用 T::class 的关键。
kotlin
inline fun <reified T : Activity> Activity.startActivity(context: Context) {
startActivity(Intent(context, T::class.java))
}
startActivity<NewActivity>(context)