美文网首页
浅谈java的泛型

浅谈java的泛型

作者: 傲娇的狗蛋 | 来源:发表于2019-09-26 09:42 被阅读0次

    什么是泛型

    这个概念很抽象,举个例子List<View> list = new ArrayList<>(); View就是List的泛型,表示这个List只能存放View类型或者View子类的对象。这么说也有些笼统,我直接把百科的介绍拿过来吧,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数,可以用在类,接口,方法中。看完本篇文章你就会明白泛型到底是是什么。

    泛型的好处

    举例

            List arrayList = new ArrayList();
            arrayList.add("aaaa");
            arrayList.add(100);
    
            for(int i = 0; i< arrayList.size();i++){
                String item = (String)arrayList.get(i);
            }
    

    用了无数遍的例子,这里的List没有指定泛型,那默认的泛型就是Object 所以这段代码在编译时完全没问题。运行程序肯定会崩溃。这里存放的是Object类型,使用的时候以String类型使用。但是实际上添加了一个Integer类型的100,所以取出时会有类型强转错误。

    修改一下代码

            List<String> arrayList = new ArrayList();
            arrayList.add("aaaa");
            arrayList.add(100);
    
            for(int i = 0; i< arrayList.size();i++){
                String item = (String)arrayList.get(i);
            }
    

    好嘛,修改完编辑器直接报错。大家应该都知道为什么报错。声明了String类型的List却存放了一个100

    image

    这样把本来运行时候发生的错误直接提前到编译期,减少了代码出错的风险,这就泛型的好处之一

    泛型擦除

    真泛型

    泛型存在于编译器和运行期

    伪泛型

    泛型仅仅存在于编译器

    真泛型的代表有c#,关于真泛型不在本章类容里。学过java的人都知道java是伪泛型,如何验证呢?这也很简单

    java可以方法重载,重载的原则是方法名一样,参数不同。

        public void fun(List<Integer> integers) {        
        }
    
        public void fun(List<String> strings) {
        }
    

    根据重载的原则,List<Integer>!=List<String>重载应该是成立的。然鹅...

    image

    这里报错了,报错信息存在里两个相同的方法。以上的代码写到c#中就不会报错。

    再看一个例子

            List<Integer> integers = new ArrayList<>();
            List<String> strings = new ArrayList<>();
            if (integers.getClass().equals(strings.getClass())) {
                System.out.println("类型相同");
            }
    

    以上代码运行会输出日志,也可以证明java是伪泛型,泛型在运行时会擦除。以上的 List<Integer> List<String>最后都会变成List

    泛型的使用

    泛型方法

    现在有这样的需求,传入两个对象,返回较大的对象。伪代码如下

        public Object getMax(Object a, Object b) {
            //……比较大小
            return a;
        }
    
        public void fun() {       
            int a = 1;
            int b = 2;
            Object max = getMax(a, b);
        }
    

    传入两个Object对象,比较大小后返回较大的。直接写的话返回值肯定也是Object。其实这样很不好,在fun方法中调用时候传入了1和2,返回的却是个Object。很明显这里返回int类型会更好。修改一下代码

        public <T> T getMax(T a, T b) {
            //……比较大小
            return a;
        }
    
        public void fun() {
    
            int a = 1;
            int b = 2;
            int max = getMax(a, b);
        }
    

    在类上也要声明T public class JavaTest<T>

    这里使用T代表泛型,getMax方法声明参数类型<T > ,这样调用的时候传入的参数是什么类型就会返回什么类型

    泛型类

    public class JavaTest<T> {
        
        private T t;
    
        public JavaTest(T t) {
            this.t = t;
        }
    
        public T getT() {
            return t;
        }
    }
    
        //声明JavaTest对象的时候指定泛型类型,传入的t参数的类型必须要一致
        JavaTest<String> javaTest1 = new JavaTest<>("String");
        JavaTest<Integer> javaTest2 = new JavaTest<>(1);
        JavaTest<Boolean> javaTest3 = new JavaTest<>(true);
    

    协变,逆变和不变

    协变

    先看一段代码

    public class Father {
        
    }
    
    public class Son extends Father{
    
    }
    
            Father father = new Father();
            Son son = new Son();
            father = son;
    
            List<Father> fathers = new ArrayList<Son>();
    

    Son是继承于Father的,根据多态father = son;这是完全没问题的,那List<Father> fathers = new ArrayList<Son>();应该也没问题吧。然鹅...

    image

    这里报错了,因为Java的泛型是不变性质的,也就是在List<Father>List<Son>的类型不一致。Java中子类的泛型类型不属于父类泛型类型的子类。在这个例子里就是 List<Son>并不是List<Father>的子类。

    这种把子类的泛型对象赋值给父类的泛型的引用叫协变,因为java的泛型擦除,所以不支持协变。但是实际使用中又会遇到这样的需求。

    这里就要使用 通配符 ? extends

            List<? extends Father> fathers = new ArrayList<Son>();
    

    上界通配符,表示这个list是个未知的类型。extends Father 限制了未知类型的上界限,虽然是未知类型,但是必须是Father的子类。

    所以以下的情况都是可以用的:

            List<?extends Father> list1 = new ArrayList<Father>();  //本身
            List<?extends Father> list2 = new ArrayList<Son>();     //直接子类
            List<?extends Father> list3 = new ArrayList<Son的子类>();//间接子类
    

    你以为这样就没问题了吗

    image

    调用add方法报错了,使用? extends Father虽然解除了协变的限制,却又带来了新的限制

    List<? extends Father>的泛型是个未知泛型,只是限制了必须是Father的子类。所以fathers.get();得到的肯定是fathers的。当然也可以强转成fathers的子类

    使用add方法,既然List<? extends Father> 是Father的子类的未知类型,那它可能是List<Father>也可能是List<Son>。如果是List<Son>的话就不能添加Father了。编辑器根本不知道List的实际类型也就无法确定add(father)是否正确。所以干脆不让用add方法。

    那这样的协变又有什么用呢???

        public void fun1(List<? extends Father> list) {
    
            for (int i = 0; i < list.size(); i++) {
                list.get(i).toString();
            }
        }
    

    以上的场景中fun1方法接受一个Father子类的list,然后遍历调用toString方法。这时你有个List<Son>依然也是可以调用这个方法的。如果不使用? extends就无法调用。在遇到只需要读取数据不修改数据数据的时候就可以使用? extends让java支持协变

    由于这种限制,使用协变的泛型只能提供数据而不能修改数据。所以Java的协变是向外提供数据的一方,被称为生产者 Producer

    逆变

    ? extends对应有? super 下界通配符,与上界通配符对应,这里 super 限制了? 的子类。

     List<? super Son> list  = new ArrayList<Father>();
    

    super限制了泛型的下界,必须要满足 引用 super 对象 这个条件( son super Father )即 后边的泛型类型必须是前面的泛型类型的父类,正好与协变返过来。

    以下这些写法都是可以的

            List<? super Son> list1 = new ArrayList<Son>();     //本身
            List<? super Son> list2 = new ArrayList<Father>();  //直接父类
            List<? super Son> list3 = new ArrayList<Object>();  //间接父类
    

    同样使用? super实现了逆变,也带来了别的限制。限制也正好与? extends相反

            List<? super Son> sons = new ArrayList<Father>();
            sons.add(new Son());
            Object object = sons.get(0);
    

    同理,?表示未知类型。这里的泛型只要是 Son 的父类就可以,所以add一个Son是可以的。

    调用get方法,泛型无法确定具体的类型,只能向上取值,取到最大的值就是Object,如果你足够自信的活当然可以强转成Son,实际使用上肯定存在强转风险。

    那..逆变又有什么用?

        public void fun2(List<? super Father> list) {
            Son son = new Son();
            list.add(son);
        }
    

    fun2接受一个泛型? super Father的list的,将内部创建的Son对象添加到list中。

    这时你有一个Father类型的List的,只是想在Father类型中添加一个Son的数据,根据多态的特性是完全合理的,语法上就可以使用? super来解决这个问题

    Java逆变的特性确定它只能修数据不能获取获取,通常只拿来添加数据,往List中添加数据,这种泛型类型也叫消费者 Consumer

    不变

    不变是最好理解的,Java默认的泛型就是不变类型。即引用和对象并不存在什么继承关系

    小结

    关于Java的协变和逆变也被总结成PECS 法则:Producer-extends, Consumer-super

    说直白点就是,从数据流来看,extends是限制数据来源的(生产者),而super是限制数据流入的(消费者)。例如上面例子中,使用<? extends Father> 限制存放的是Father类型的及其子类,所以调用get方法一定能得到Father,但因为具体类型不明确,无法调用add方法。使用<? super Father>限制了泛型是Fathe及其父类,也就限制了add方法必须添加Father以及其父类,也因为具体类型不明确,调用get方法时候会向上取最大兼容的类型,也就是Object。

    kotlin中的泛型

    kotlin完全兼容java,所以泛型的特点也都继承自java。kotlin也是伪泛型,泛型的写法也都类似,同样也有协变和逆变的问题。

    in out

    Kotlin使用关键字 out 来支持协变,等同于 Java 中的上界通配符 ? extends ( <? extends Father> = <out Father> )

    Kotlin使用关键字 in 来支持逆变,等同于 Java 中的下界通配符

    ? super( <? super Father> = <in Father> )

    kotlin中泛型的使用:

    class Producer<T> {
        fun produce(): T {
            ...
        }
    }
    
    val producer: Producer<out TextView> = Producer<Button>()
    val textView: TextView = producer.produce() //  相当于 'List' 的 `get`
    
    
    class Consumer<T> {
        fun consume(t: T) {
            ...
        }
    }
    
    val consumer: Consumer<in Button> = Consumer<TextView>()
    consumer.consume(Button(context)) //  相当于 'List' 的 'add'
    

    kotlin 泛型与java不同的地方

    通配符* :

    泛型中使用* 和 Java中使用通配符?一样

    java中单独使用?相当于 ?extends Object

    kotlin使用*相当于out Any

    reified 关键字:

    在Java和Kotlin中都不能检查一个对象是否是T类型

    fathers instanceof T //java 会报错的
    100 is T //kotlin 也会报错的
    

    这个问题在Java中通过添加一个Class<T> 类型的参数 来解决

    public<T> void check(Object item, Class<T> type) {
        if (type.isInstance(item)) {
            System.out.println(item);
        }
    }
    

    Kotlin中也可以这么做,但是还有另外一个方法。

    在inline函数中配合使用reified关键字

        inline fun <reified T> printIfTypeMatch(item: Any) {
            if (item is T) { 
                println(item)
            }
        }
    

    类声明处使用out和in

    在类的声明时候使用out和in,也就定位了这个类是用来输入还是输出。

    class Producer<out T> {
        fun produce(): T {
            ...
        }
    }
    
    val producer: Producer<TextView> = Producer<Button>() //  这里不写 out 也不会报错
    
    
    
    class Consumer<in T> {
        fun consume(t: T) {
            ...
        }
    }
    val consumer: Consumer<Button> = Consumer<TextView>() //  这里不写 in 也不会报错
    

    型变点:

    在类中使用out或者in,型变点就是这个泛型的类型,也就是T

    协变时,型变点只能作为返回值使用

    逆变时,型变点只能作为参数使用

    如果遇到协变时型变点要作为参数使用,或者逆变时型变点要作为返回值使用。可以使用@UnsafeVariance解除限制(仅仅是解除编辑器报错,并不会影响协变逆变对数据读取修改的特性)

    interface KotlinGenericity<out T> {
    
        fun get():T
    
        fun add(t:@UnsafeVariance T)
    
    }
    

    上例中add方法中如果不使用@UnsafeVariance是会报错的。

    image

    总结

    不变类型的泛型的直接使用上应该是没什么难的地方,主要是协变和逆变的地方。说实话我对这玩意还是有些晕,而且越想越晕。这里总结的一下关于型变的特点,大家在用的时候记住这个特点就好了。

    协变:正向的继承关系,只能读取数据不能修改数据,java中使用? extends,kotlin中使用out,协变的型变点只能作为返回值使用

    逆变:与协变相反,逆向的继承关系,只能修改数据,不能读取数据,java中使用? super,kotlin中使用in,逆变的型变点只能作为参数使用

    不变:不存在继承关系,既能修改数据也能读取数据,型变点既可以当参数也可以当返回值

    相关资料:

    Kotlin 的泛型
    java 泛型详解-绝对是对泛型方法讲解最详细的
    协变与逆变

    相关文章

      网友评论

          本文标题:浅谈java的泛型

          本文链接:https://www.haomeiwen.com/subject/cepgyctx.html