Android技术知识Android开发

Java | 关于泛型能问的都在这里了(含Kotlin)

2020-08-20  本文已影响0人  彭旭锐

前言

1、下列代码中,编译出错的是:
public class MyClass<T> {
    private T t0; // 0
    private static T t1; // 1 
    private T func0(T t) { return t; } // 2
    private static T func1(T t) { return t; } // 3
    private static <T> T func2(T t) { return t; } // 4
}
2、泛型的存在是用来解决什么问题?
3、请说明泛型的原理,什么是泛型擦除机制,具体是怎样实现的?

延伸文章


目录

1. 泛型基础

源码:
public class Parent<T> {
    public void func(T t){
    }
}

public class Child<T extends Number> extends Parent<T> {
    public T get() {
        return null;
    }
    public void func(T t){
    }
}

void test(){
    Child<Integer> child = new Child<>();
    Integer i = child.get();
}
---------------------------------------------------------
字节码:
public class Parent {
    public void func(Object t){
    }
}

public class Child extends Parent {
    public Number get() {
        return null;
    }
    public void func(Number t) {
    }
    // 桥方法 - 暂略
}

void test() {
    Child<Integer> child = new Child();
    // 插入强制类型转换
    Integer i = (Integer) child.get();
}

步骤1:Parent 中的类型参数 T 被擦除为 Object,而 Child 中的类型参数 T 被擦除为 Number;
步骤2:child.get(); 插入了强制类型转换
步骤3:为什么子类中需要增加桥方法呢?可以这么理解:假如没有桥方法,下列代码调用的是子类还是父类方法:

Parent<Integer> child = new Child<>();
Parent<Integer> parent = new Parent<>();
        
child.func(1); // Parent#func(); 若不理解,可以阅读延伸文章《Java | 深入理解方法调用的本质(含重载与重写区别)
parent.func(1); // Parent#func(); 

很明显,这里调用的都是父类的方法,这样就失去了多态性。因此,才需要在泛型子类中添加桥方法:

public class Child extends Parent {
    public Number get() {
        return null;
    }
    // 桥方法 - synthetic
    public void func(Object t){
        func((String)t);
    }
    public void func(Number t) {
    }
}
反编译Parent.class,可以看到 T ,不是已经擦除了吗?

public class Parent<T> {
    public Parent() {
    }

    public void func(T t) {
    }
}

答:泛型中所谓的类型擦除,其实只是擦除Code 属性中的泛型信息,在类常量池属性中其实还保留着泛型信息,这也是在运行时可以反射获取泛型信息的根本依据。具体来说:
Signature属性、LocalVariableTypeTable属性

Editting...

泛型的限制

2. Kotlin的实化类型参数

前面我们提到,由于类型擦除的影响,在运行期是不清楚类型实参的实际类型的。例如下面的代码是不合法的,因为 T 并不是一个真正的类型,而仅仅是一个符号:

在这个函数里,我们传入一个List,企图从中过滤出 T 类型的元素:

Java:
<T> List<T> filter(List list) {
    List<T> result = new ArrayList<>();
    for (Object e : list) {
        if (e instanceof T) { // compiler error
            result.add(e);
        }
    }
    return result;
}
---------------------------------------------------
Kotlin:
fun <T> filter(list: List<*>): List<T> {
    val result = ArrayList<T>()
    for (e in list) {
        if (e is T) { // cannot check for instance of erased type: T
            result.add(e)
        }
    }
    return result
}

Kotlin中,有一种方法可以突破这种限制,即:带实化类型参数的内联函数

Kotlin:
inline fun <reified T> filter(list: List<*>): List<T> {
    val result = ArrayList<T>()
    for (e in list) {
        if (e is T) {
            result.add(e)
        }
    }
    return result
}

关键在于inlinereified,这两者的语义是:

规则很好理解,对吧。很明显,当发生方法内联时,方法体字节码就变成了:

调用:
val list = listOf("", 1, false)
val strList = filter<String>(list)
---------------------------------------------------
内联后:
val result = ArrayList<String>()
for (e in list) {
    if (e is String) {
        result.add(e)
    }
}

需要注意的是,内联函数整个方法体字节码会被插入到调用位置,因此控制内联函数体的大小。如果函数体过大,应该将不依赖于T的代码抽取到单独的非内联函数中。

注意,无法从 Java 代码里调用带实化类型参数的内联函数

实化类型参数的另一个妙用是代替Class对象引用,例如:

fun Context.startActivity(clazz: Class<*>) {
    Intent(this, clazz).apply {
        startActivity(this)
    }
}

inline fun <reified T> Context.startActivity() {
    Intent(this, T::class.java).apply {
        startActivity(this)
    }
}

调用方:
context.startActivity(MainActivity::class.java)
context.startActivity<MainActivity>() // 第二种方式会简化一些

3. 变型:协变 & 逆变 & 不变

变型(Variant)描述的是相同原始类型的不同参数化类型之间的关系。说起来有点绕,其实就是说:IntegerNumber的子类型,问你List<Integer>是不是List<Number>的子类型?

变型的种类具体分为三种:协变型 & 逆变型 & 不变型

在 Java 中,类型参数默认是不变型的,例如:

List<Number> l1;
List<Integer> l2 = new ArrayList<>();
l1 = l2; // compiler error

相比之下,数组是支持协变型的:

Number[] nums;
Integer[] ints = new Integer[10]; 
nums = ints; // OK 协变,子类型关系被保留

那么,当我们需要将List<Integer>类型的对象,赋值给List<Number>类型的引用时,应该怎么做呢?这个时候我们需要限定通配符

泛型代码的设计,应遵循PECS原则(Producer extends Consumer super):

  • 如果只需要获取元素,使用 <? extends T>
  • 如果只需要存储,使用<? super T>

举例:

// Collections.java
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
}

在 Kotlin 中,变型写法会有些不同,但是语义是完全一样的:

协变:
val l0: MutableList<*> 相当于MutableList<out Any?>
val l1: MutableList<out Number>
val l2 = ArrayList<Int>()
l0 = l2 // OK
l1 = l2 // OK
---------------------------------------------------
逆变:
val l1: MutableList<in Int>
val l2 = ArrayList<Number>()
l1 = l2 // OK

另外,Kotlin 的in & out不仅仅可以用在类型实参上,还可以用在泛型类型声明的类型参数上。其实这是一种简便写法,表示类设计者知道类型参数在整个类上只能协变或逆变,避免在每个使用的地方增加,例如 Kotlin 的List被设计为不可修改的协变型:

public interface List<out E> : Collection<E> {
    ...
}

注意:在 Java 中,只支持使用点变型,不支持 Kotlin 类似的声明点变型

小结一下:


参考资料

推荐阅读

感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的简书!

上一篇 下一篇

猜你喜欢

热点阅读