kotlin-泛型篇之泛型的理解

1,586 阅读9分钟

第十三讲 Kotlin泛型篇

前介

泛型,在 Java 中就是一个比较难的东西。我个人认为,它理解起来很简单,但是要想把它用好真的很难。看到别人用泛型巧夺天工的设计,我都湿了。

Java 泛型

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

Java 的泛型有什么用呢?我个人认为:主要是减少代码上类型的转换,以及类型约束。

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

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

Java 泛型擦除

前面说了 Java 泛型,本质是给编译器看的,让编译器帮你做强制转换,并将这个泛型擦除掉。这个过程俗称 泛型擦除

public class TestJava {
    public strictfp static void main(String[] args) {
        /**
         * 告诉编译器 strs 存的是一个 String 类型
         *
         * 编译器只会做2件事:
         *
         * 1. 只有检测到 add 不是一个 String 类型,就编译不通过
         *
         * 2. 只要有 取这个参数操作,帮我强制转换成 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 还保留了泛型,是因为 JDK 1.5 版本之后 class 文件的属性表中添加了 SignatureLocalVariableTypeTable 等属性来支持泛型识别的问题,这些信息可以在配置混淆的时候可以删除掉。

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

上界通配符

说泛型之前,先理解下通配符。

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

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

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<? extends Number>,意思我能接收一个属于 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))
    }
}

上面的代码,只能调用 ArrayListget 方法,不能调用 add 方法。这下明白了吧,这次在回味我上面的那句话,只能调用对应泛型类的,返回泛型的方法,所以说类似 public E remove(int index) 也能调用(只能用不能改)。

思考问题,为何只能用不能改呢?其实这只是 Java 处于安全考虑。
例如上面的例子,因为我通过上界通配符打开了一定的范围,代码角度考虑能保证返回的类型一定是 Number 的子类,根据多态我一定可以用 Number 类型的变量去接收。但是存储我就不能确定存的是什么类型了。

例如:

我要做一个寻找 List 集合,所有数字的最大值并返回。想想我不能为所有的 IntDoubleLong 都写重载一个方法吧?

通过分析,我们只用不改集合,因此我们就可以定义这样的一个方法。

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());
    }
}

上面代码明白了吧?一定要把 ? 通配符T 区分开,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 中对应的是 *,由于用处太少了,大家自行查阅资料吧。其实理解了通配符约束和泛型,在理解就简单多了。但是想要把泛型用在项目上还需要很多设计上的积累,理解容易得心应手真的很难。

总感觉举的例子不好,大家有更好的例子吗?