1. Java 泛型设计目的
Java泛型在代码架构设计中应用的非常普遍。在设计通用类、通用方法的时候,可以使用泛型来扩展类型,同时在编译时做类型检查。泛型相比较Object作为方法参数,可以增强类型检查能力,减少类型转换带来的风险。
泛型举例:
public class A<T>{//可以修饰类
void fun1(T t){
}
<B> void fun2(B b){//可以修饰方法
}
}
2. Java 泛型擦除
泛型擦除指的是泛型信息在编译成的字节码中会擦除掉。可以在编译生成的class文件中查看到使用泛型的类型删除掉了泛型定义,如下例子:
ArrayList<String> list = new ArrayList<String>();
// 字节码: LOCALVARIABLE list Ljava/util/ArrayList;
3. Java 泛型信息存储在class文件的位置
Java泛型实际是存储在class的常量区,在对象的签名信息中会指向常量区的泛型信息位置。
源码:
public class Genericity {
static ArrayList<String> list;
public static void main(String[] args) {
list = new ArrayList<>();
}
}
通过javap -verbose Genericity.class获取到class文件内容。可以看到在常量区第10个位置定义了“ #10 = Utf8 Ljava/util/ArrayList<Ljava/lang/String;>;”,在list对象的签名信息处使用了#10的常量。
public class sample.Genericity
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #5 // sample/Genericity
super_class: #6 // java/lang/Object
interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #6.#25 // java/lang/Object."<init>":()V
#2 = Class #26 // java/util/ArrayList
#3 = Methodref #2.#25 // java/util/ArrayList."<init>":()V
#4 = Fieldref #5.#27 // sample/Genericity.list:Ljava/util/ArrayList;
#5 = Class #28 // sample/Genericity
#6 = Class #29 // java/lang/Object
#7 = Utf8 list
#8 = Utf8 Ljava/util/ArrayList;
#9 = Utf8 Signature
#10 = Utf8 Ljava/util/ArrayList<Ljava/lang/String;>;
#11 = Utf8 <init>
// ...
#29 = Utf8 java/lang/Object
{
static java.util.ArrayList<java.lang.String> list;
descriptor: Ljava/util/ArrayList;
flags: (0x0008) ACC_STATIC
Signature: #10 // Ljava/util/ArrayList<Ljava/lang/String;>;
public sample.Genericity();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lsample/Genericity;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: putstatic #4 // Field list:Ljava/util/ArrayList;
10: return
LineNumberTable:
line 8: 0
line 9: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 args [Ljava/lang/String;
MethodParameters:
Name Flags
args
}
SourceFile: "Genericity.java"
4. Java 泛型type获取
在日常开发中最常见的获取泛型信息的是Json数据反序列化为JavaBean,例如:
List<String> list = new ArrayList<>();
list.add("test");
TypeToken<List<String>> typeToken = new TypeToken(){};
Gson gson = new Gson();
List<String> results = gson.fromJson(list.toString(),typeToken.getType());
这里是通过匿名内部类继承父类,再在匿名内部类中调用getGenericSuperclass获取父类信息,进而调用getActualTypeArguments获取泛型信息。也可以通过getGenericInterfaces获取接口信息,再获取接口中的泛型信息,实现了多个接口getActualTypeArguments方法返回的数组中就会有多个对象。这里的ParameterizedType是一个接口,表示参数化类型,也就是带有泛型参数的类,实现类是ParameterizedTypeImpl。 可以如下实现:
public static void main(String[] args) {
TypeToken tt = new TypeToken<ArrayList<String>>(){};
System.out.println(tt.type);
}
static class TypeToken<T>{
public Type type;
TypeToken(){
ParameterizedType parameterizedType = (ParameterizedType)this.getClass().getGenericSuperclass();
type = parameterizedType.getActualTypeArguments()[0];
}
}
5. Java 泛型通配符上界&下界
Java泛型不支持型变,使用通配符来做限制。
<? extends T>表示类型转换的上界,即协变;
<? super T>表示类型转换的下界,即逆变。
规则1:泛型通配符修饰变量时,赋值的变量类型中的泛型必须是按照上下界来限定,上界声明类型只能赋值成上界以下的泛型类型,下界亦然。
规则2:泛型通配符修饰变量时,执行变量对象方法时,方法参数使用泛型变量修饰的(如执行list的add方法),上界通配符修饰的变量无法调用该方法(适合生产者),下界通配符修饰的变量调用该方法只可以传泛型类型的子类变量(适合消费者)。
static class A{}
static class B extends A{}
static class C extends B{}
static class Test<T>{
T t;
T get(){
return t;
}
void set(T t){
this.t = t;
}
}
public static void main(String[] args) {
Test<? extends B> t1 = new Test<C>();//ok
Test<? extends B> t2 = new Test<A>();//Error,A超过B的上界
Test<? super B> t3 = new Test<A>();//ok
Test<? super B> t4 = new Test<C>();//Error,C地于B的下界
// 作为参数,作为返回值的限制
t1.set(new A());// Error
t1.set(new C());// Error
t3.set(new A());// Error
t3.set(new C());// Ok
B b1 = t1.get();// OK
B b2 = t3.get();// Error,返回类型只能是一个Objec
}
通过字节码可以看到通配符也是会被擦除的,但是在函数参数签名中能看到具体的泛型类型。extends上界通配符使用的是+号,super下界通配符使用的是-号
#29 = Utf8 (Lsample/Genericity$Test<+Lsample/Genericity$B;>;)V
#30 = Utf8 set2
#31 = Utf8 Lsample/Genericity$Test<-Lsample/Genericity$B;>;
6. Java 泛型通配符在源码的实践
案例,在ArrayList中forEach方法,addAll方法,Collecttions中的copy方法
// action作为消费者,会执行action的带参数的方法
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
action.accept(elementData[i]);
}
}
// c作为生产者,只会执行c的无参方法,读取c中的数据
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
//......
}
// 目的列表使用的是逆变,源列表使用的是协变
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
//...
for (int i=0; i<srcSize; i++) {
dest.set(i, src.get(i));
}
//...
}
7. Kolitn 泛型
kotlin泛型和Java泛型类似,都是伪泛型,都会运行时擦除泛型信息。
8. kotlin 泛型type获取
我们知道Kotlin的内联函数是在编译的时候,编译器把内联函数的字节码直接插入到调用的地方,所以参数类型也会被插入到字节码中。而在内联函数中加上reified关键字就可以获取泛型类型。但是这里获取到的是最外层的泛型类型,比如Map<String,String>返回的是Map。
inline fun <reified T> getType(): Class<T> {
return T::class.java
}
// 也可以类似java的子类获取父类信息方法获取
open class GenericsToken<T> {
var type: Type = Any::class.java
init {
val superClass = this.javaClass.genericSuperclass
type = (superClass as ParameterizedType).getActualTypeArguments()[0]
}
}
9. kotlin 泛型协变out和逆变in
kotlin协变,也就是如果类型A是类型B的子类型,那么Generic<A>也是Generic<B>的子类。类比Java中的<? extends T>。kotlin逆变,就相反,Generic<B>也是Generic<A>的子类,类似Java中的<? super T>。
同样适用上面Java通配符的两个规则,一个是赋值的上下界,一个是作为生产者消费者的使用场景约束。例子:
open class A{}
open class B:A(){}
open class C:B(){}
class Test<T>{
var t:T = TODO()
fun set(t:T){
}
fun get():T{
return t
}
}
fun test(t1: Test<out B>, t2: Test<in B>):B{
t1.set(B()) // set任何值都会编译报错
t2.set(A()) // 编译报错
t2.set(C()) // OK
// return t2.get() // 报错 in是逆变,只有下界,上线是any,any无法转换成B
return t1.get() // Ok out是协变,只有上界,类型都是B的子类,可以当做B返回
}
查看kotlin的字节码,和Java的类似,泛型的信息也是保存在常量表中,且使用函数签名信息指向具体的泛型定义。
Constant pool:
// ...
#7 = Utf8 (Lcom/example/mvvmdemo/kotlin/Test<+Lcom/example/mvvmdemo/kotlin/B;>;Lcom/example/mvvmdemo/kotlin/Test<-Lcom/example/mvvmdemo/kotlin/B;>;)Lcom/example/mvvmdemo/kotlin/B;
// 方法定义
// ...
Signature: #7 // (Lcom/example/mvvmdemo/kotlin/Test<+Lcom/example/mvvmdemo/kotlin/B;>;Lcom/example/mvvmdemo/kotlin/Test<-Lcom/example/mvvmdemo/kotlin/B;>;)Lcom/example/mvvmdemo/kotlin/B;