Java与kotlin 泛型那些事

1. Java中泛型擦除

Java的泛型并不是真正的范型,而是编译期泛型,在编译完成之后泛型就不存在了,签名中的范型 会变成 Object,
如:

public  T first(List list){
        return list.get(0);
}

会变成

public Object first(List list){
        return list.get(0);
}

范型擦除会导致的问题

1.泛型擦除导致的转型问题

由于在运行时,类型信息已经被擦除了,因此无法获得运行时的类型,导致某些框架如Gson,传入Class才可以正确的转型。
编译之后的Gson.fromJson的方法体中,范型 会被转换成了Object , 运行时无法用T object只能用 Object object , 要获得T的信息 ,只能通过Class来获得 ,想要转型也通过class.cast(obj);

public  T fromJson(String json, Class classOfT) throws JsonSyntaxException {
    Object object = fromJson(json, (Type) classOfT);
    return Primitives.wrap(classOfT).cast(object);
  }

而在其他语言中,如C#可以直接在带有泛型的方法体中直接转型,因为他们的泛型是真正的泛型,在编译之后,会将泛型方法,编译成对应的具体类型方法,好处就是真泛型,操作方便,坏处就是使得编译后的代码量增多(因为传入多少种真实的类型,就会编译出多少个方法)

同为JVM平台下的kotlin,kotlin可以通过內联机制,绕过因泛型擦除而导致的无法在运行时直接获取类型的问题,因为具体类型已经被编译內联到调用处的代码中了。

如:Java

public  boolean isTypeOf(Object o) {
    // 返回任意对象是否是T类型
    // 无法编译
    return o instanceof T;
}

在kotlin中:

inline fun  isTypeOf(any: Any): Boolean {
    // 返回任意对象是否是T类型
    // 能够正常编译
    return any is T
}
    
    
    //测试
    println(isTypeOf(1))
    println(isTypeOf(""))
    
    //打印结果
    false
    true

2.泛型擦除导致的方法重载问题

由于编译之后,泛型被擦除成Object,实际上对于以下两个方法,他们的方法签名是相同的,因此无法重载。

public void printList(List list) {
    System.out.println(list);
    
}

public void printList(List list) {
    System.out.println(list);
}

3.泛型擦除导致无法使用基本数据类型

如:
List , List,在Java中是无法实现的,必须使用引用类型才可以实现,因为所有的泛型在擦除之后变成Object,而int,double等基本类型没有父类型。

2. Java中的泛型,不可以形变

1. 协变

协变指的是Foo<父> = Foo<子> 的这种操作

作为生产者的时候,以下在我们看来是十分安全的操作,在Java中是无法编译的。

class Father{}
class Son extends Father{}

List sons = new ArrayList();
//下面编译报错
List fathers = sons;
Father father = fathers.get(0);

为了解决上面的问题,Java引入了通配符,通配符的加入使得这种操作变为可能,虽然看上去不太优雅。

List sons = new ArrayList();
List fathers = sons;
Father father = fathers.get(0);

这种表达方式看起非常的奇怪。
其实在其他语言中,如C#与kotlin上述行为叫做协变
在kotlin中,这样是可以通过编译的。

class Son: Father()
open class Father

val sons = listOf()
//可以通过编译
val fathers: List = sons

Kotlin是如何实现以上协变的?看一下对List的定义

public interface List : Collection

在定义List的泛型参数前面加了一个 out 代表了 List类在声明处发生了协变。

但是声明处协变是有限制的,限制就是该泛型只能用于生产者,也就是说泛型只能出现在,返回值,而不能出现在参数里。
如果泛型要强行出现在参数里需要增加@UnsafeVariance的注解

public interface List : Collection{
 ...
 public fun indexOf(element: @UnsafeVariance E): Int   
 ...
}
    

好处就是在声明的时候就已经确定该类是可以进行协变的了,因而可以直接进行协变的转换。

2. 逆变

逆变指的是Foo<子> = Foo<父> 的这种操作

作为消费者的时候,以下在我们看来是十分安全的操作,在Java中是无法编译的。

 // Double 是 Number的子类
public void demo(Comparable x){
    //这一步编译就报错
    Comparable y = x;
    y.compareTo(1.0)
}

同样的java可以通过通配符可以进行逆变

public void demo(Comparable x){
    //编译通过
    Comparable y = x;
    y.compareTo(1.0);
}

而在kotlin中完全不需要这么麻烦

interface Comparable {
    operator fun compareTo(other: T): Int
}

fun demo(x: Comparable) {
    // 因此,我们可以将 x 赋给类型为 Comparable  的变量
    val y: Comparable = x // OK!
    y.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型
}

可以看出kotlin在接口的声明处使用 in 关键字进行了逆变。
同样在声明处逆变是有限制的,限制就是该泛型只能用于消费者,也就是说泛型只能出现在,参数中,而不能出现在返回值里。
如果泛型要强行出现在返回值上需要增加@UnsafeVariance的注解

你可能感兴趣的:(Java与kotlin 泛型那些事)