Skip to content

Java 与 Kotlin 中的泛型


泛型的意义

泛型的提出是为了编写重用性更好的代码

  • JDK 1.5 之前 想提高代码重用性,可以使用 Object 作为属性或参数。但由于 Object 是所有类型的父类,导致获取的时候需要强转为指定的类型,这就导致了以下两个问题:
    1. 每次获取时都需要手动强转,降低了代码可读性。
    2. 强转操作无法在编译器确认是否异常,只能运行时确定,降低了代码稳定性。
  • JDK 1.5 之后 针对上面问题,JDK1.5开始支持泛型。带来了以下好处:
    1. 编译器可确定类型转化是否有异常,有异常会报 ClassCastException,做到了早发现早治疗。
    2. 在获取时已确定数据类型,不需要做强转操作,保证了代码可读性。

泛型通配符

泛型通配符有三种:

  1. <?> 无限制通配符
  2. <? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
  3. <? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类。

<?> 无限制通配符

使用场景: 在不关心实际操作的时候使用。但如果有数据操作就会报错,例如 get/set
举例:

java
public void test1(List<?> list){
    String str = list.get(0);  //编译期会提示,这里是 ? 无法转成指定 String 类
    //但用 Object 接收就可以,不过也就无法保证代码可读性了
    Object str = list.get(0); 
}
java
public void test1(List<?> list){
    Object str = list.get(0);
    list.add(str); //编译器会提示,这里接收的是 ? 无法接收指定类型 Object
}

<? extends E> 上界通配符

使用场景:指定未知类型的属性或参数类型为 E 或 E的子类。只能用在 get 操作。

java
public void test(List<? extends Integer> list){
    Integer str = list.get(0);  
    list.add(0);  
}

<? super E> 下界通配符

使用场景:指定未知类型的属性或参数类型为 E 或 E的父类。只能用在 set 操作。

java
public void test(List<? super Integer> list){
    Integer str = list.get(0);   
    list.add(0); 
}

泛型类型参数

类型参数 与 通配符 区分:

  • 类型参数 <T> / <T extends Number> / <T super Integer>
    类型参数为指定确认类型,可放在泛型类定义中类名后面、泛型方法返回值前面、泛型方法参数
java
public class Test<T>{
    public <T> T test(E t){}
}
  • 通配符 <?> / <? extends Number> / <? super Integer>
    通配符为未知的类型
java
public void addAll(List<? extends E> list){}
  • 实际上通配符能做的事,类型参数也能实现

  • 通配符形式可以减少类型参数,形式上往往更为简单,可读性也更好

java
//类型参数实现方式
public <T> void test(List<T> list){}
//通配符实现方式
public void test(List<?> list)
  • 类型参数之间有依赖关系,或者返回值依赖类型参数,或者需要写操作,则只能用类型参数。
java
public <T> T back(T e){}

不变型

java 和 kotlin 都是不变型的。例如 List<String> 并不是 List<Object> 的子类。

java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!!此处的编译器错误让我们避免了之后的运行时异常
objs.add(1); // 这里我们把一个整数放入一个字符串列表
String s = strs.get(0); // !!! ClassCastException:无法将整数转换为字符串

协变/上界

java 中是 <? extends Number>,kotlin 中是 <out Number>。只能读不能写。因为读的时候可以用上界接收,但写的话无法保证数据类型。

kotlin
val list: ArrayList<out Number> = ArrayList<>()
list.add(0) //Error
list.add(1f) //Error
val number: Number = list.get(0) //Success

逆变/下界

java 中是 <? super Integer>, kotlin 中是 <in Integer>。只能写不能读。因为只能保证写入的数据类,无法保证读取类型是什么。

kotlin
val list: ArrayList<in Integer> = ArrayList<>()
list.add(0) //Success
list.add(1f) //Success
val number: Integer  = list.get(1) //Error

星投影/无限制通配符

java 中是 <?> ,kotlin 中是 <*> 。不能写,读只能读到顶层 Object/Any。

<?>/<*> 与 <Object>/<Any?>的区别:

  • 前者是未知类型,约等于<? extends Object>/<out Any?>;

  • 后者是确定的泛型类型。


类型擦除

原理

Java 编辑器会将泛型代码中的类型完全擦除,使其变成原始类型。
当然,这时的代码类型和我们想要的还有距离,接着 Java 编译器会在这些代码中加入类型转换,将原始类型转换成想要的类型。这些操作都是编译器后台进行,可以保证类型安全。
总之泛型就是一个语法糖,它运行时没有存储任何类型信息。

java
List<String> strings = ArrayList<>()
List<Number> numbers = ArrayList<>()
strings.getClass() == numbers.getClass()  // true

针对擦除问题的解决方法

  • 类型擦除时会寻找第一个上界,如果未指定上界则为 Object,所以可以通过 <? extends E> 来指定上界。
java
public class Test <T extends Number & Custom>{
    T number;
}
//类型擦除后,number 指定为上界 Number
public class Test{
    Number number; 
}

//未指定上界时
public class Test <T>{
    T number;
}
//类型擦除后,number 未找到上界,则为 Object
public class Test{
    Object number;
}
  • 通过 getGenericReturnType() 获取泛型返回类型
kotlin
//例如,泛型返回参数
fun getDeviceMode(): Call<BaseResp>

//通过 getGenericReturnType() 可以获取泛型返回类型
@Test
fun test(){
    println("${TestCls::class.java.getMethod("testReturn").genericReturnType}") //java.util.List<java.lang.String>
    println("${TestCls::class.java.getMethod("testReturn").genericReturnType is ParameterizedType}") //true
    println("${(TestCls::class.java.getMethod("testReturn").genericReturnType as ParameterizedType).rawType}") //interface java.util.List
    println("${(TestCls::class.java.getMethod("testReturn").genericReturnType as ParameterizedType).actualTypeArguments[0]}") //class java.lang.String
}

class TestCls{
    fun testReturn(): List<String>{
        return listOf()
    }
}
  • 添加包装类,通过 getGenericSuperclass() 获取泛型参数类型
kotlin
@Test
fun test(){
    println("${A::class.java.genericSuperclass}") //TestCls<java.lang.String>
    println("${A::class.java.genericSuperclass is ParameterizedType}") //true
    println("${(A::class.java.genericSuperclass as ParameterizedType).actualTypeArguments[0]}") // class java.lang.String
}

class A : TestCls<String>()

open class TestCls<T>



参考资料

Released under the MIT License.