联系我们
简单又实用的WordPress网站制作教学
当前位置:网站首页 > 程序开发学习 > 正文

java/kotlin 泛型知识点汇总

作者:访客发布时间:2024-01-01分类:程序开发学习浏览:97


导读:泛型擦除Classc1=newArrayList<Integer>().getClass();Classc2=newArrayList<S...

泛型擦除

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
public void print(List<Integer> list){ }  
public void print(List<String> list){ }  
  • 静态方法无法引用类泛型参数(可以给静态方法单独加上泛型参数),因为只有类实例化的时候才会知道泛型参数,而静态方法不需要持有类的实例

泛型信息

泛型信息获取

附加的签名信息特定场景下反射可以获取。获取到泛型类型的条件

  1. 必须具有真实类型的存在
  2. 泛型的类型是明确的

第一个很好理解,如果连要获取的类都不存在,即未定义,那自然是获取不到的;
第二个条件,举个例子,假设存在User,那么List就是明确的,List是不明确的。
满足上面两点,就可以获取泛型的类型了。

  • 因为类型擦除,创建子类才可以获取到父类的泛型信息。如果是继承基类而来的泛型,就用 getGenericSuperclass() , 转型为 ParameterizedType 来获得实际类型。如果是实现接口而来的泛型,就用 getGenericInterfaces() , 针对其中的元素转型为 ParameterizedType 来获得实际类型。可以看出都跟继承类/实现接口有关,因为在类或接口定义的类型参数(泛型)其实是不确定的,只有子类继承或接口实现才能确定类型参数的具体类型。比如:接口Service,类型参数T是不确定的,说白了其实就是个占位符,而实现类UserServiceImpl才能确定类型参数T是User。一般情况下为了获取泛型类型,会创建一个匿名对象来获取,参见下面 gson 的获取
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  
  • 获取方法中的泛型信息
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中泛型签名的应用
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的匿名内部类的实例,在它的内部通过以下代码就能获取父类的泛型参数。

Type superclass = getClass().getGenericSuperclass();  
ParameterizedType parameterized = (ParameterizedType)superclass;  
//parameterized.getActualTypeArguments()[0] 获取到的泛型参数  
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 的原因

泛型信息的保存

泛型擦除后是如何保存泛型信息的?

public class Sign {  
public static void test(List<String> list) {//加了泛型信息  
System.out.println("test");  
}  
}  

通过javac Sign.java后再通过javap -v Sign.class看下反编译后的class字节码。

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[] 的子类。
但数组的协变会导致潜在的问题,例如如下程序:

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>的子类。

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>,这里生产者指的是能用来读取的对象,消费者指的是用来写入的对象。例如如下程序:

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()); // 编译错误  

对于更通用的泛型来说,对于支持协变的泛型,程序只能调用以泛型为返回值类型的方法;不能调用形参为泛型的方法。

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)

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处理)。

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及其父类型。

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 两个型变关键字还可以用于泛型声明的时候。

public interface Collection<out E> : Iterable<E> {  
...  
}  
  
// 错误,这里只能用val,不能用var  
class Source<out T>(var t: T) {  
...  
}  

reified

Java 在用泛型时不能够直接地使用类型,通常解决方法是以函数参数形式传递类,这使得代码更复杂。 kotlin 如果不使用 reified 时,也跟 java 实现一样:

// 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 的关键。

inline fun <reified T : Activity> Activity.startActivity(context: Context) {  
startActivity(Intent(context, T::class.java))  
}  
  
startActivity<NewActivity>(context)  

github.com/ccnio/Wareh…


标签:知识点javakotlin


程序开发学习排行
最近发表
网站分类
标签列表