泛型的前世(java)与今生(kotlin)

737 阅读9分钟

泛型算是一个比较典型的易学难精通的东东. 比如说写一个栈, 大家都知道要用class Stack<T> {..} 这样的写法, 但是一到更复杂的场景, 我们就经常被IDE报出来的莫名其妙的泛型错误给搞晕. 不知道为什么出错, 也不知道如何修复. 所以这篇文章不会像网上很多其它介绍泛型的文章一样一个个知识点地介绍. 而是侧重于实例, 来讲解工作中的泛型难题.

I. 泛型为什么产生

1. 老版本中, 集合中的get()可能出错

在java1.0到java1.4的时候里, 是没有泛型的. 所以我们的List等集合可以放任意数据. 但list.get(0)取出来时, 就要强转 String value = (String) list.get(0)

这样的问题就是当我们不小心地在前面操作过list.add(20)时, 这时的强转就会出错.

泛型就是想让我们在存,取时都是限定了某一类型. 这样get()时就不用强制转换, 就少了ClassCastException这样的crash了

2. java全版本中, 数组其实是"类型不安全"的

什么叫"类型不安全"? 举例来说, 当我们有一个Animal类, 其有Cat, Dog两个子类.

    Animal
    /    \
  Cat    Dog

那下面的代码肯定在编译时不会报错 (但智能的IDE, 如Intellij IDEA会报警告),

image.png

可是, 上面的代码一运行就会crash (是的, 发生了Array Store的异常):

我们先来看为什么编译时不报错. : 因为animal[]本身就是Animal[], 所以放入dog没问题啊

那为什么运行时又有错呢? : 这时因为animals = cats, 这就让animals数组本质上是Cat[]数组. 这时再加入Dog对象, 就会报ArraySotreException. 毕竟Java对数组存储的类型是有强限制的, 一个Cat[]数组里不能存Dog对象.

3. 泛型就是想解决上面两种问题

1). List变成List<T>, 一开始就限制好读写时的类型

2). 数组这样的类型不安全, 要是也用于集合类型, 那就容易在编码时不报错, 一上线就crash, 是个隐患. List就是想以后List<Animal>里不能被cats给赋值, 即不能List<Animal> animals = cats

这一点很重要, 也极其容易混淆. 你要是还看不明白这句话, 不要紧, 后面我们会更详细讲. 读到到这里, 只要理会了数组的类型是不安全的, 就是可以了的 - 泛型就是想改进它.

II. 泛型的实例

1. 实例一

我有一个类, 其中有一个List<Animal>成员. 在构造函数里, 我可能要去存值, 或取值.

List<Animal> animals = new ArrayList<>();
animals.add(new Cat());
Animal cat = animals.get(0);

上面的代码都没错, 大家也司空见惯了, 似乎没什么特别的. 是的. 但我们在另外的场景里, 类似的代码就有大大的问题了.

2. 实例二: RecyclerView.Adapter

我现在有一个展示宠物的App. 其中一个RecyclerView.Adapter可以被展示猫咪的CatList页, 也可以被展示狗狗的DogList页给展示. 两个页面的UI一样, 都是一个RecyclerView, 只是数据不一样; 因此我们的Adapter有一个setData()方法.

因为我们的数据在CatList页面里可以放入List<Cat>, 也可以在DogList页面里放入List<Dog>, 即可以放入一个Animal的List. 所以我就写成了这样:public void setData(List<Animal> animals) {...}

但是我在CatList页面调用setData()时却出错了

意识到上面的图里, java编译器给我们大声抱怨了"这有一个问题. 我期待一个List<Animal>, 但却得到了一个List<Cat>! ". 这里就要转回到上面的数组了.

在数组里, 我们用下面代码是没问题的:

Cat[] cats = new Cat[1];
Animal[] animals = cats;

但在集合中, 或者说有泛型了之后的集合中, 不能这样用了:

// 这段代码是错的
List<Cat> catList = new ArrayList<>();
List<Animal> animalList = new ArrayList<>();
animalList = catList; //ERROR!!!

原因上面讲过了, 就是因为数组这样的"类型不安全", 可能把问题要到runtime时才暴露, 所以java泛型不打算这样. java泛型要求更严格的类型检查. 对java来说, List<Cat>List<Animal>根本不是父子关系 (虽然Cat是Animal的子类), 所以不能使用父类型 obj = 子类型object这样的赋值. (为了简便, 后文都将这样的赋值称为"父子赋值")

p.s. 要是上面理解没问题, 那这一段就算是额外说明. 要是上面还晕着, 这一段就不看也没事:

这其实就是java泛型的类型擦除.
即不管你是List<A>还是List<B>, 这些泛型集合的操作都只在编译时检查. 一旦到了运行时, 其实都只是被java当成了List, 即没了A, B的类型了. 
所以叫类型擦除, 被抹除了, 再也没有泛型的类型消息了. 

类型擦除其实是java1.5引入泛型后, 为了让.class文件仍与旧版本java兼容, 所采用的没办法的办法. 
不然以前版本的java, List就是一个List, 现在java1.5却是一个List<A>, 那原来的程序可能crash了. 

3. 解决上面Adapter的问题

上面一小节主要是讲为什么java向我们抱怨代码有问题. 可是回到现实生活中, 要想把cats赋值给animalList, 这个问题到底要如何解决呢?

我的需求就是setData()可以传入List<Cat>, 也可以传入List<Dog>. 这时通配符就来帮我们忙了.

我们可以在setData()中, 把参数由List<Animal>改为List<? extends Animals>. 这样接受的参数就可以是List<Animal>, 或是List<Animal子类> ! 即:

// 原来的出错代码:
public void setData(List<Animal> animals){ }

// 现在改为:
public void setData(List<? extends Animal> animals){ }

这时我们再赋值就不会出错了:

III. List<? extends Animal>

现在产品和我们说, 为了营利, 我们页面接入了广告商. 现在不管是CatList页, 还是DogList页, 我们的展示的数据第一个都是广告商提供的宠物. 所以我们要小小修改一下代码

  1. Animal类新有一个子类:
class AdPet extends Animal{... }
  1. Adapter.setData(list)也要确保第一个就是广告商的宠物.
   public void setData(List<? extends Animal> animals){
        animals.add(0, new AdPet()); //ERROR!
        // ....
    }

这里代码却报错了:

这里其实就是泛型让人很晕的地方了. 这明明是一个List<? extends Animal>, 怎么add一个Animal的子类,却不行呢? : 仍是要回到上面那个很重要的数组是类型不安全的例子上来. 数组在Animal[] animals = cats之后, 仍能animals.add(dog); -- 但它不应该还能add(dog), 这时应该只能add(cat)了.

泛型就是这样的 当一个List类型是List<? extends Animal>时, 它可以被父子赋值catList, 或是dogList, setData()里面根本不知道. 既然我不知道, 那你再add任何Animal子类可能都会出现类似上面ArrayStoreException的错误, 所以我干脆就不让你add()了!!!

是的, List<? extends Animal>的对象, 不能add, 只能get(). (它get()返回的自然就是Animal, 这个还是能保证不出错的). 再写个小实例, 来加深我们的印象.

2. List<? extends Animal>的特点:

  • 它可以被父子赋值. 如: List<? extends Animal> animalList = new ArrayList<Cat>();
  • 它不能add(), 即不能写入
  • 它能get(), 即能读取它

IV. List<? super Animal>

泛型的通配符除了extends, 还有一个suepr:

它正好与extends相反, 它可以add(Animal子类), 但它不能get(). -- 更精确地说是:

  • 它可以add(animal子类)
  • 它的get(i)只能返回一个Object类型

这里要说明的是: 当我们有一个Animal的父类, 叫Being时:

        Being
          |
        Animal
      /       \
    Cat       Dog

List<? super Animal>也支持父子赋值, 但这时赋值的右值得是一个List<Animal或其父类>

这就理解了为何Animal a = animals.get(0)会报错, 因为它返回的可能是一个Being对象, 而不是Animal对象!

2. List<? super Animal>的特点:

  • 它可以被父子赋值. 如: List<? super Animal> animalList = new ArrayList<Being>();
  • 它能add(), 可写入Animal与Animal的子类
  • 它不能get()并返回一个Animal对象. -- 它返回的只能是一个Object对象

V. 总结

其实还有一种通配符, 就是<?>通配符. 不过这个出现得不多, 而且让人更晕, 所以这篇文章中就不介绍它了.

1. 那List<Animal>呢?

这个问题好, 说明你在思考了: "List<Animal>可以写, 可以读, 可以支持父子赋值吗?"

实例说明一切,我们来看一下例子:

List<Animal>能写能读, 但就是不支持父子赋值.

2. 大总结

所以当你在使用, 或设计带泛型的类时, 就可以这样选用: (PC-assign就是我简称的"parent-child assign", 是我为了简便称呼自用的术语.)

VI. 彩蛋

上面讲了总结出来的理论. 我们现在再用一个JDK里的源码来验证我们的理论. 这个源码出自Collections类的copy()方法:

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    int srcSize = src.size();
    for(int i = 0; i < srcSize; ++i) {
        dest.set(i, src.get(i));
    }       
}

我想经过上面的解释, 我们现在就知道为何两个参数, 一个是super, 另一个是extends了. : 我要支持父子赋值, 所以得用通配符. 即copy<Animal>(catList, catList2)得行, 所以我要用通配符.

同时又因为dist只要add(), 而src只要get()就行了, 所以我们就相应地加上了extends与super.

可以说, 只要你理解了这个copy()的两个参数为何一个是super, 为何另一个是extends, 以及为何要用通配符. 那恭喜你, 这篇文章你过关了.

VII. Kotlin中的泛型

泛型其实很多细节点, 像什么泛型方法, 这些我都没在介绍. 因为我想在这篇文章里集中介绍一些很重要但又很让人头晕的知识点.

对应上面的东东, kotlin其实就是in, out两个来代替了extends与super. 其实仍是看Collections.copy()的kotlin版本, 我们就理解了in, out了:

fun <T> copy(dest: MutableList<in T>, src: List<out T>) {
      ...
}

VIII. 结尾

其实这篇文章主要是讲泛型里的通配符的知识. 泛型还有一些其它部分, 如泛型方法这些我都没有讲解, 因为我感觉通配符最容易让人晕, 所以我优先讲这一块.

也欢迎大家评论中说说自己碰到的泛型难题. 有代表性的我就解决后再整理下, 再写一篇泛型难题的文章