Java & Kotlin 泛型知识点汇总

115 阅读6分钟

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;

参考

  1. 获取泛型类型
  2. 深入理解Kotlin中的泛型(协变、逆变)
  3. Java泛型的定义和使用详解
  4. Java泛型的协变、逆变和不变
  5. PECS(Producer Extends Consumer Super)原则
  6. kotlin官方