一文弄懂Kotlin与Java的泛型

457 阅读6分钟

kotlin中的 out和in 这2个泛型关键字 就和 java中的 ?extends 和? super 是一一对等的关系

声明一个父类的list ,如果赋值一个子类的list对象, 在java中是不被允许的,看一段代码来理解这句话(这一点在java和kotlin中都一样):

package com.wuyue;

import java.util.ArrayList;
import java.util.List;

public class TestJavaG {
    public static void main(String[] args) {
        //这样是可以的 这样的代码 相信大家经常写
        AClass aClass = new BClass();
        //但是如果你是用父类来声明一个list,然后赋值的时候 用子类的list来赋值,那么在java中这是不允许的编译就报错
        List<AClass> alist = new ArrayList<BClass>();

        //list不行 但是数组却是可以的,因为数组没有在编译时擦除类型 类型擦除会导致类型不安全的问题
        AClass[] aArray = new BClass[3];

    }
}

//父类
class AClass {

}

//是A的子类
class BClass extends AClass {

}


当然如果你非要这样做,也不是不可以

//这样就不会报错了 子类的泛型对象 可以赋值给父类的泛型类型声明
List<? extends AClass> a_list = new ArrayList<BClass>();
//下面这2个语句在调用的时候 都会报错,因为我们虽然用 ? extends解除了上述的限制
//但带来的负作用就是我们只能使用a_list 而不能修改他 
a_list.add(new AClass());
a_list.add(new BClass())



很多人在这里 不理解,这个东西有什么用? 其实还是有用的。如果你某个函数 只想使用参数的值,而不想修改他,那么就用这个?extends 就可以大大扩大这个函数的使用范围。避免很多麻烦

package com.wuyue;

import java.util.ArrayList;
import java.util.List;

public class TestJavaG {

    //没有使用?extends 通配符 所以他的参数 必须是List<AClass>  声明的
    static void printlnAll(List<AClass> aClassList) {
        for (AClass aClass : aClassList) {
            aClass.printlnInfo();
        }
    }

    //使用了 ?extends 通配符 所以他的参数可以是任何Aclsss的子类声明的
    static void printlnAll2(List<? extends AClass> aClassList) {
        for (AClass aClass : aClassList) {
            aClass.printlnInfo();
        }
    }

    public static void main(String[] args) {

        List<AClass> aList = new ArrayList<>();
        List<BClass> bList = new ArrayList<>();
        //不报错 大家都知道
        printlnAll(aList);
        //这边就会编译报错了
        printlnAll(bList);
        //这边我们换了个方法 就不会编译报错了,原因就是方法签名中使用了?extends通配符
        //还是那句话 如果你的泛型  只想使用 而不想修改,那么就用?extends
        printlnAll2(bList);

    }
}


//父类
class AClass {
    public void printlnInfo() {
        System.out.println("a class");
    }
}

//是A的子类
class BClass extends AClass {
    public void printlnInfo() {
        System.out.println("B class");
    }
}



? super和?extends 恰恰就是相反的。我们来看几个例子

package com.wuyue;

import java.util.ArrayList;
import java.util.List;

public class TestJavaG {

   

    public static void main(String[] args) {

        //这样的代码肯定编译报错,java初学者都知道
        BClass bClass = new AClass();
        //所以下面的代码就更不行了 一样编译报错
        List<BClass> blist = new ArrayList<AClass>();

        //我们一旦使用? super 这个通配符,那么就不会报错了
        //看起来好像挺疯狂的样子,子类声明,然后父类赋值 这样都行? 但这样确实是可以的。。。
        List<? super BClass> blist2 = new ArrayList<AClass>();

    }
}


//父类
class AClass {
    public void printlnInfo() {
        System.out.println("a class");
    }
}

//是A的子类
class BClass extends AClass {
    public void printlnInfo() {
        System.out.println("B class");
    }
}



再看解除以后的作用

package com.wuyue;

import java.util.ArrayList;
import java.util.List;

public class TestJavaG {

    static void addOne(List<BClass> bClassList) {
        bClassList.add(new BClass());

    }

    //加了通配符以后 我们就可以解除限制
    static void addOne2(List<? super BClass> bClassList) {
        bClassList.add(new BClass());
    }


    public static void main(String[] args) {

        List<AClass> aClasses = new ArrayList<>();
        //报错了,
        addOne(aClasses);
        //不报错
        addOne2(aClasses);


    }
}


//父类
class AClass {
    public void printlnInfo() {
        System.out.println("a class");
    }
}

//是A的子类
class BClass extends AClass {
    public void printlnInfo() {
        System.out.println("B class");
    }
}

kotlin 中 out和in 就分别对应着 ?extends和?super ,个人认为 kotlin的关键字 比java中的关键字 更加言简意赅。

out 也就是?extends 就意味着 你只能读我,不能写我。 因为out是对外输出的意思

in 也就是?super, 就意味着 你只能写我,不能读我,因为in是对内写入的意思

package com.wuyue

//这样就可以避免在每个使用的地方 都加上 out 和 in 这种关键字。
//这是kotlin 比java 方便的一个地方 :直接在类型创建的时候 写上out或者是in 
interface Producer<out T> {
    fun produce(): T
}

interface Consumer<in T> {
    fun consume(): T
}

//javaz中 这2种写法是等效的 
List<?> alist = new ArrayList<BClass>();
List<? extends Object> alist2 = new ArrayList<BClass>();//kotlin中 这2种写法是等价的
var tv:List<*>
var tv2:List<out Any>

最后 java中 使用泛型的时候 是没办法 做类型判断的 比如下面的代码你就肯定会编译报错(假设T是一个泛型):

objectA instanceof T



但是在kotlin中的inline函数+reified 关键字,可以完成类似的功能:

inline fun <reified T> printTypeMath(item: Any) {
    if (item is T) {
        println("match")
    } else {
        println("miss")
    }
}


到这里 可能还会有一些人 有点迷,java中的泛型 到底是用来干嘛的,为什么需要这个东西,类型擦除和类型安全 又是什么,在这里 简单扩展一下,适当的了解深入一些 ,有助于我们在今后的工作中用好泛型

简单来说,java的泛型就是java的编译器帮忙实现的。最终生成的字节码中 是不包含泛型整个东西的。比如大家经常写的List<String> List<Integer> 在编译以后 都是List.

所谓类型擦除,就是将代码中的类型参数 在编译期 直接替换成具体的类。仅此而已。

带来的优势就是:很多错误 在编译期就可以发现了,这是多么方便的事!

举个例子:

public class JavaCode {
    public static Object setAndReturn(Object obj) {
        return obj;
    }

    public static void main(String[] args) {
        Integer i = (Integer) setAndReturn(new String("abc"));
    }
}

那 这里显然 是会在执行期 crush了, 因为一个string 无法转成integer


相信大家在实际开发中,这种错误肯定不会少见。 但是如果用泛型来写,那么就压根不会发生这样的错误,因为在编译的时候就会报错了,编译报错。

public class JavaCode {
    public static <T> T setAndReturn(T t) {
        return t;
    }

    public static void main(String[] args) {
        Integer i = (Integer) setAndReturn(new String("abc"));
    }
}

此外,泛型还可以帮我们省略大量代码(强制类型转换)!懒人专用。 看下面的代码:

class Fruit{}
class Apple extends Fruit{}
class Banana extends Fruit{}
class Bucket{
    public Apple getApple() {
        return apple;
    }

    public void setApple(Apple apple) {
        this.apple = apple;
    }

    Apple apple;
}

水果我们只定义了两种,然后定义了一个篮子,但是这个篮子 只支持苹果类型,这个时候 如果想让篮子 支持其他水果类型怎么做?

class Bucket{
    public Object getObject() {
        return object;
    }

    public void setObject(Object object) {
        this.object = object;
    }

    Object object;
}


这个时候 看起来虽然可以了,但是我们在使用这个Bucket的时候 最终肯定要加上强制类型转换的

Bucket bucket=new Bucket();
bucket.setObject(new Apple());
Apple apple=(Apple)bucket.getObject();

很麻烦,而且稍微不注意 就要强制类型转换报错了。用泛型 就可以简单很多:

class Bucket<T> {
    public T getObject() {
        return t;
    }

    public void setObject(T t) {
        this.t = t;
    }

    T t;
}


这样 使用起来 就会比较方便了.

Bucket<Apple> bucket = new Bucket();
bucket.setObject(new Apple());
Apple apple = bucket.getObject();

这里要注意的是,虽然写法上 泛型会比较方便。但是经过编译器的泛型擦除以后,在字节码上的表示其实是一样的。

不管你是Bucket<Apple>还是Bucket<Banana> 在编译以后 和Bucket<Object> 的写法上是等效的。没有任何区别

参考资料:www.bilibili.com/video/av663…

              kaixue.io/kotlin-gene…

             zhuanlan.zhihu.com/p/26965437