Kotlin 的泛型,reified泛型实化

3,675 阅读10分钟

Kotlin 的泛型,reified泛型实化

Java泛型

那么先得从最简单的讲起 JAVA泛型使用开始讲起

在JAVA1.5后加入泛型 在1.5前是没有泛型的 我们泛型用的比较多的就是集合了

public static void main(String[] args) {
        ArrayList arrayList = new ArrayList();
        arrayList.add("1");
        arrayList.add("2");
        arrayList.add("3");
        arrayList.add(4);
        for (Object s : arrayList) {
            System.out.println((String) s);
    }
}
//输出
1
2
3
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
 at TestDemo.main(TestDemo.java:16)

首先先创建一个集合 这个集合是Object类型 可以插入任何类型

上面这段代码本来是没有问题的 但是又在最后集合中插入一个int类型 那么在强转的时候 就会发送类型转换异常了 使用没有泛型的集合中很容易在一个集合中 插入2个以上的类型 在类型转换的时候 如果类型不一致 就会异常了。这样是集合是类型不安全

public static void main(String[] args) {
        ArrayList<String> arrayList = new ArrayList();
        arrayList.add("1");
        arrayList.add("2");
        arrayList.add("3");
        arrayList.add(4);//这里会发送错误  因为没有 .add(int o);的方法
        for (String s : arrayList) {
            System.out.println(s);   
     }
}

可以看到 加入泛型后 我们在编译期 就可以发现错误 及时更改 防止在运行期异常奔溃。

泛型为我们解决了什么?
  1. 多种类型的相同执行代码得到了更好的复用
  2. 类型安全 使用泛型提高了我们代码的稳定性 不用担心强制转换类型发现异常奔溃
泛型的使用
泛型类
class Box<T> {
        private T t;
        
        public T getBox() {
            return t;
        }

        public void setT(T t) {
            this.t = t;
        }
    }
泛型接口
//定义泛型接口
interface Base<T>{
        get();
}
//实现泛型接口1
class View<Timplements TestDemo.Base<T> {

    @Override
    public T get() {
        return null;
    }
}
//实现泛型接口2
class View implements TestDemo.Base<String> {

    @Override
    public String get() {
        return "";
    }
}
泛型方法
public <T> T getEntity(T t) {
        return t;
}

以上都是都是泛型的基本操作和使用 泛型协变逆变就留给后面的Kotlin 还有就是用Kotlin讲更容易理解。

泛型类型擦除

关于Java的泛型我们最后还要讲一点 泛型类型擦除

public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        List<Integer> integers = new ArrayList<>();
        System.out.println(strings.getClass().equals(integers.getClass()));
}

在不了解泛型类型擦除之前 你应该觉得输出是 false 吧 然后结果为 ture

泛型在编译期进行擦除 都会变成 Object 所以为什么结果会为 ture 这下你明白了吧。相当等于

List<Object> objects = new ArrayList<>();
List<Object> objects2 = new ArrayList<>();
objects.equals(objects2)

我们可以用数组验证下

public static void main(String[] args) {
  String[] strings1 = new String[0];
        int[] ints = new int[0];
        System.out.println(ints.getClass().equals(strings1.getClass()));
}

结果为 false 数组是没有类型擦除的

那为什么 java 要进行类型擦除呢? 前面我们点到了 在java1.5之前是没有泛型的 1.5之后引入泛型 那为什么兼容早期1.5的版本 所以使用了类型擦除 。然而类型擦除也带来了一堆毛病。所有基于JVM的语言 泛型都是通过类型擦除实现的。而Kotlin 通过一些小技巧 可以解决泛型擦除的问题。

Kotlin 中的泛型

java的泛型就到这里了 接下来 看看Kotlin

kotlin号称100%兼容java 那么Java的性质Kotlin肯定是都有的 由于也是基于JVM语言 类型擦除 Kotlin也不例外。

fun <T> printNum(t: T) {
    println(t)
}

fun main() {
    printNum("11")
    printNum(1)
    printNum(1.234)ko
}

可以发现泛型和使用Java 差不多 接下来我们要开始进阶了

泛型实体化

inline

泛型实体化 听起来很神奇啊 JVM不是会在编译器进行类型擦除吗? 接下来我们借助Kotlin中的 inline 实现泛型实化 由于本章主要讲的是泛型 对 inline 不进行过多的讲解。

官方描述

使用高阶函数会带来一些运行时的效率损失:每一个函数都是一个对象,并且会捕获一个闭

包。 即那些在函数体内会访问到的变量。 内存分配(对于函数对象和类)和虚拟调用会引入

运行时间开销。

Kotlin的函数 通过一个 inline 的修饰符 就可以申明为内联函数。这个函数会在编译器直接内联到源码中

fun main() {
    hello()
    println("world")
}

private inline fun hello() {
    println("Hello")
}

上面是个简单的内联函数 我们通过字节码反编译下生成的java代码

这是内联函数生成的代码

public static void main() {
      String var1 = "Hello";
      System.out.println(var1);
      String var3 = "world";
      System.out.println(var3);
}

这是普通函数生成的代码

public static void main() {
      hello();
      String var0 = "world";
      System.out.println(var0);
}

private static final void hello() {
      String var0 = "Hello";
      System.out.println(var0);
}
reified

在使用内联函数时 在声明的泛型前面加上 reified 修饰符 可以发现 泛型实化

fun main() {
    isString(1)
    isString("")
}

private inline fun <reified T> isString(t: T) {
    println("" is T)
}

false
true

可以使泛型实化 并且可用 isT:class:java 这样的语法了 这几乎是java不可能做到的 那么这个泛型实化有什么好处吗?

  • 我们可以得到泛型的类型了
  • 语法更加简洁了

比如在Android开发过程中 我们启动另一个activity时 正常语法如下

val intent = Intent(this, MainActivity::class.java)
startActivity(intent)

现在改造下

startActivity<MainActivity>(this)

inline fun <reified T> startActivity(context: Context) {
        val intent = Intent(context, T::class.java)
        context.startActivity(intent)
}

是不是让代码更优雅了? 没错 现在Gson Retrofit你都可以这么加以改造下 让代码更加优雅。

泛型 协变

泛型中的协变 和 逆变 虽然不常用 但是在Kotlin中用到了很多协变和逆变的特性 我们常用的List就是一个天生支持协变的,接下来了解下协变。

image-20200802141342399
image-20200802141342399

接受参数的地方为 in 位置 返回值的地方为 out 位置 这两个得记住 协变和逆变的理解就靠这张图了。

open class A
class B : A()

fun main() {
 val b = B()
    get(b)
}

fun get(a: A) {
//    ...
}

这样的代码是没问题的对吧 我们加入泛型试试

open class A
class B : A()

fun main() {
    val b = B()
 val mutableListOf = mutableListOf(b)
    get(mutableListOf)//这里错误了
}

fun get(a: MutableList<A>) {
//    ...
}

发现在调用get的时候发生错误了。为什么错了呢 在第一段代码get函数中传入B的实例 这是可以的 因为B是A的子类 第二代代码为什么错了呢 因为 MutableList 不是 MutableList 的子类 所以不能这么用。但是现在我们改一下 代码

open class A
class B : A()

fun main() {
    val b = B()
    val list = listOf(b)
    get(list)
}

fun get(a: List<A>) {
//    ...
}

可以发现代码没问题了,是不是很神奇 为什么呢 我们前面讲到了 List天生支持协变 我们看看List的源码

public interface List<out E> : Collection<E> {
    ...
  override fun iterator(): Iterator<E>
 public fun listIterator(): ListIterator<E>
  public fun listIterator(index: Int): ListIterator<E>
 public fun subList(fromIndex: Int, toIndex: Int): List<E>
}

List在泛型前面 加了一个 outout 关键字是进行协变声明,那么现在的List所有关于泛型只能作为输出。这就是为什么List声明的集合只读而不可写。

我们回过来看看上面的图片 是不是能加强理解了 。

这里再总结下

泛型 逆变

协变截然相反 逆变是只能写 而不能读 在声明泛型前面 加入 in 关键字

open class A
class B : A()

fun main() {
    val a = A()
    val mutableListOf = mutableListOf(a)
    get(mutableListOf)
}

fun get(b: MutableList<in B>) {
//    ...
}

泛型的协变与逆变都讲完了 虽然开发过程中用的比较少 但是在学习Kotlin和进阶的路上我们还是会用的越来越多的。

文章代码居多 理论较少 多去实验下就会明白泛型的使用。