Kotlin第十三讲---初识泛型

内容简介

泛型,在 Java 中个人认为是一个比较难的东西。它理解起来很简单,但是要想把它用好真的很难。

每当我看到别人用泛型来完成巧夺天工的设计,我都很是羡慕。

Java 泛型

讲解 Kotlin 泛型之前,先要将 Java 的泛型理解清楚,因为 Kotlin 的本质还是 Java ( Java 是 Kotlin 的爸爸)。

Java 的泛型有什么用呢?我个人认为:主要是减少代码上类型的转换,以及类型约束,在编译前期将错误暴露出来。

例如:我们常用的集合,试想想如果没有泛型。我们的 List 集合会怎么定义呢?可能类似就要这样定义了 StringList 丶 IntList  或者定义一个 ObjectList (万物之祖宗 OBJ),试想下若这样的设计给我们开发带来了多少的类型转换与不便呢?

有了泛型,我们在定义类或方法时只需声明泛型,这样编译器就知道类型,让编译器做类型检测和类型转换。

Java 泛型擦除

前面说了 Java 泛型,我们了解到泛型是给编译器看的,让编译器在编译的时候帮你做强制转换。

其实在最后定义的泛型是会被清理掉的,这个过程俗称 泛型擦除

为何要擦除掉呢?其实在 Java 初期认为 C 中的泛型没啥用,就没有设计。但后续发现没泛型有点麻烦,后续版本就设计了泛型。但是为了兼容以前的版本,用了这种折中的方案 (个人猜想)。

public class TestJava {
    public strictfp static void main(String[] args) {
        /**
         * 告诉编译器 strs 存的是一个 String 类型
         *
         * 编译器只会做2件事:
         *
         * 1. 只有检测到 add 不是一个 String 类型,就编译不通过
         *
         * 2. 只要有 get 这个参数操作,帮我强制转换成 String
         */
        List<String> strs = new ArrayList();
        strs.add("我是 Kotlin");
        strs.add("我是 java");
        for (int i = 0; i < strs.size(); i++) {
            /**
             * 注意这里哦
             */
            String s = strs.get(i);
            System.out.println(s);
        }
    }
}

接下来看下,反编译后的代码。

public class TestJava {
    public TestJava() {
    }
    public static strictfp void main(String[] args) {
        List<String> strs = new ArrayList();
        strs.add("我是 Kotlin");
        strs.add("我是 java");
        for(int i = 0; i < strs.size(); ++i) {
            // 看到没,这里自动帮我们强制转换了
            String s = (String)strs.get(i);
            System.out.println(s);
        }
    }
}

看到了吧?在取值的时候,是做了强制类型转换的。此时有人说,你不是说泛型会擦除吗?怎么反编译的 Java 还存在 List<String> 呢?

其实反编译的 Java 还保留了泛型,是因为 JDK1.5 版本之后 class 文件的属性表中添加了 Signature 和 LocalVariableTypeTable 等属性来支持泛型识别的问题,这些信息可以在配置混淆的时候可以删除掉。

行,那我们看下 ByteCode ,让那些杠精死心。

通配符

Java 通配符其实是个 约定,这个约定是给编译器看的。 Java 是强类型语言,变量类型的泛型参数也是区分类型的(注意我现在说的是通配符和泛型是2个东西哦)。

请问下方代码报错吗?(友情提示: Integer 是 Number 的子类)

public class TestJava {
    public strictfp static void main(String[] args) {
        ArrayList<Integer> integers = null;
        ArrayList<Number> numbers = null;
        // 请问这句代码报错吗?
        numbers = integers;
    }
}

没错编译器不通过,因为编译器认为这是 2 种类型,直接报错。

上界通配符

上方的代码编译不通过,那有没有办法让 Java 的泛型类型也存在多态的关系呢?

是可以的哦,通配符 ? 的出现解决了这个问题。我们改下代码。

public class TestJava {
    public strictfp static void main(String[] args) {
        ArrayList<Integer> integers = null;
        ArrayList<? extends Number> numbers = null;
        // 请问这句代码报错吗?
        numbers = integers;
    }
}

定义改成 ArrayList<?extendsNumber>,意思我能接收一个属于 Number 子类的的泛型。

通过这样定义会让编译器不报错,但这样就会多一个约束。

若通过上界通配符修饰泛型,只能调用返回泛型类型的方法(编译器约束:只能用不能改),啥意思呢?

举个例子:

public class TestJava {
    public strictfp static void main(String[] args) {
        ArrayList<Integer> integer = new ArrayList();
        integer.add(1);
        integer.add(2);
        /**
         * 赋值不会报错
         */
        ArrayList<? extends Number> numbers = integer;
        /**
         * 调用 get 方法 不报错
         */
        Number number = numbers.get(1);
        /**
         * 这里 编译不通过
         */
        numbers.add(new Integer(3))
    }
}

上面的代码,只能调用 ArrayList 的 get 方法,不能调用 add 方法。

这下明白了吧,在细细品味味我上面的那句话 只能调用返回泛型类型的方法。所以说类似 publicE remove(intindex) 也能调用(只能用不能改)。

思考问题,为何只能用不能改呢?其实这是 Java 处于安全考虑。

例如上面的例子,因为我通过上界通配符打开了一定的范围,代码角度考虑能保证返回的类型一定是 Number 的子类,根据多态我一定可以用 Number 类型的变量去接收。但是存储我就不能确定存的是什么类型了。

例如:

返回 List 集合中的最大值。想想我不能为所有的 Int 丶 Double 丶 Long 都写重载一个方法吧?

应用上界通配符,我们只需要定义一个方法。

public class TestJava {
    public strictfp static void main(String[] args) {
        ArrayList<Integer> integer = new ArrayList();
        integer.add(1);
        integer.add(2);
        /**
         * 可以 Integer 的
         */
        System.out.println(max(integer));
        ArrayList<Long> longs = new ArrayList();
        longs.add(3L);
        longs.add(4L);
        /**
         * 可以计算过 long 的
         */
        System.out.println(max(longs));
    }
    /**
     * 用 double 是考虑所有类型
     */
    public static double max(List<? extends Number> datas) {
        double max = datas.get(0).doubleValue();
        for (int i = 1; i < datas.size(); i++) {
            if (max < datas.get(i).doubleValue()) {
                max = datas.get(i).doubleValue();
            }
        }
        return max;
    }
}

上面的代码,返回值是 double类型,是为了考虑精度不要损失的问题。

其实需求应该是我传入的是 Long 的就返回 Long 类型的吗,若传入的是 Integer 的就返回 Integer 类型。后续讲解到方法泛型能解决这个问题,我们后续在来完善这个代码。

下界通配符

有天堂就有地狱,有上界就用下界。下界通配符和上界通配符相反。
上界:子类泛型参数变量,能赋值给通过上界修饰符修饰父类泛型的泛型变量(只能用,不能改)。
下界:父类泛型参数变量,能赋值给通过下界修饰符修饰子类泛型的泛型变量(不能用,能改)。

上面 2 句话是不是懵逼了?的确当时我也懵逼!

不明白?那我们就看个图。

回想下上界通配符的时候,只能调用 get 方法,不能用 add 方法(只能用,不能改)。

奇怪?下界通配符怎么 get 与 add 方法都可以调用呢?

其实这里大家注意:下界通配符调用的 get 方法返回的是 Object 类型,并没有返回真正的类型。因为鬼知道接收的是一个什么样的父类类型,但是编译器知道它祖宗一定是 Object

其实功能就是根据特性来定制的,下界通配符就是 不能用,能改 。说实话感觉 下界通配符 的一直没想到一个特别好的例子,感觉 下界通配符 用的好少。

泛型声明

记住上面说的 ? 代表的是通配符,它的功能是约束只能使用或者只能修改。而泛型声明和通配符,根本就是 2 个东西,他们没有任何关系。泛型声明 T 一般是说在声明类的时候或者方法的时候告诉编译器,你要强制转换的类型。

  /**
    * 定义人的接口
    */
   public interface People {
       void play();
       void eat();
       void sleep();
   }
   /**
    * 男人
    */
   public static class Man implements People {
       @Override
       public void play() {
           System.out.println("玩LOL");
       }
       @Override
       public void eat() {
           System.out.println("吃辣条");
       }
       @Override
       public void sleep() {
           System.out.println("打呼噜");
       }
   }
   /**
    * 女人
    */
   public static class Woman implements People {
       @Override
       public void play() {
           System.out.println("玩QQ炫舞");
       }
       @Override
       public void eat() {
           System.out.println("啥都吃");
       }
       @Override
       public void sleep() {
           System.out.println("抱娃娃");
       }
   }
   /**
    * 创建代理人的 代理类
    */
   public static class PeopleProxy<T extends People> implements People {
       private T people;
       public PeopleProxy(T people) {
           this.people = people;
       }
       /**
        * 加入修改的方法
        *
        * @param people
        */
       public void modifyPeople(T people) {
           this.people = people;
       }
       public T getPeople() {
           return people;
       }
       @Override
       public void play() {
           people.play();
       }
       @Override
       public void eat() {
           people.eat();
       }
       @Override
       public void sleep() {
           people.sleep();
       }
   }

其实例子就是静态代理设计模式,代理类上声明泛型 T 加了约束,传入的泛型必须是 People 的子类(也就是说能代理所有 People 的子类)。
接下来我们就可以根据业务需求,可以加一定 上界 或者 下界 约束条件。

public class TestJava {
    public  static void main(String[] args) {
        /**
         * 这时候业务逻辑来了,我根据一些信息判断出了他的性别,我们可以确定后续我们只是使用不修改
         *
         * 所以通过 上界进行约束
         *
         */
        PeopleProxy<? extends People> proxy = null;
        if ("穿的是超短裙吗?") {
            proxy = new PeopleProxy(new Woman());
        } else {
            proxy = new PeopleProxy(new Man());
        }
        /**
         * 尝试调用 getPeople 方法
         *
         * 编译器通过
         */
        People people = proxy.getPeople();
        /**
         * 尝试调用 modifyPeople 修改的方法
         *
         * 编译器报错
         */
        proxy.modifyPeople(new Man());
    }
}

上面代码明白了吧?我们一定要把 通配符 和 泛型声明 区分开,它们是 2 个不同的东西。 T 泛型声明可以告诉编译器类型检测和帮我强制转换。通配符能限定条件,只能用还是只能改。

类上可以声明泛型,方法上也是可以的哦。还记得讲解 上界通配符 时候,获取一个数字集合中的最大值吗?我当时返回的都是 Double 类型,当时只是想说明只能用不了改的 上界通配符,实际开发我们并不这样写。

public class TestJava {
    public static <T extends Number> T max(List<T> datas) {
        T max = datas.get(0);
        for (int i = 0; i < datas.size(); i++) {
            if (max.doubleValue() < datas.get(i).doubleValue()) {
                max = datas.get(i);
            }
        }
        return max;
    }
    public strictfp static void main(String[] args) {
        ArrayList<Integer> integer = new ArrayList();
        integer.add(1);
        integer.add(2);
        /**
         * 可以 Integer 的,并且自动转化成 Integer 了
         */
        Integer max = max(integer);
        System.out.println(max);
        ArrayList<Long> longs = new ArrayList();
        longs.add(3L);
        longs.add(4L);
        /**
         * 可以计算 Long 的,并且类型也正确
         */
        Long max2 = max(longs);
        System.out.println(max2);
    }
}

Kotlin 泛型

理解了 Java 泛型, Kotlin 就更加的轻车熟路了。 Kotlin 产物也是 class 文件,所以泛型也是会被擦除的。

Kotlin 的通配符

Kotlin 定义通配符和 Java 定义方式不一样,功能都一样,请看下方代码。

fun main() {
    /**
     * 使用 out 定义上界通配符
     * <out Number> 等价于 <? extends Number>
     */
    val ints: MutableList<out Number> = mutableListOf()
    /**
     * 可以返回
     */
    val number = ints.get(0)
    /**
     * 修改报错
     */
    ints.add(0)
    /**
     * 使用 out 定义下界通配符
     * <in Long> 等价于 <? super Long>
     */
    val longs: MutableList<in Long> = mutableListOf()
    /**
     * 使用 返回的是 Any 类型,也就是 Object
     */
    val l = longs.get(0)
    /**
     * 修改不报错,成功
     */
    longs.add(0)
}

Kotlin 定义泛型

其实和 Java 一样的,只不过定义的关键词不一样了。

/**
 * Java 是 <T extends Number>
 * Kotlin 是 <T : Number>
 */
class Test<T : Number>{
}

总结

其实 Java 通配符中还存在只写  情况,在 Kotlin 中对应的是 *,由于用处太少了,大家自行查阅资料吧。其实理解了通配符约束和泛型,在使用起来就简单多了。但是想要把泛型用在项目上还需要很多设计上的积累,理解容易得心应手真的很难。

推荐阅读

--END--

识别二维码,关注我们

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值