泛型算是一个比较典型的易学难精通的东东. 好比说写一个栈, 你们都知道要用class Stack<T> {..}
这样的写法, 可是一到更复杂的场景, 咱们就常常被IDE报出来的莫名其妙的泛型错误给搞晕. 不知道为何出错, 也不知道如何修复. 因此这篇文章不会像网上不少其它介绍泛型的文章同样一个个知识点地介绍. 而是侧重于实例, 来说解工做中的泛型难题.java
在java1.0到java1.4的时候里, 是没有泛型的. 因此咱们的List等集合能够听任意数据. 但list.get(0)取出来时, 就要强转 String value = (String) list.get(0)
数组
这样的问题就是当咱们不当心地在前面操做过list.add(20)
时, 这时的强转就会出错.安全
泛型就是想让咱们在存,取时都是限定了某一类型. 这样get()时就不用强制转换, 就少了ClassCastException这样的crash了bash
什么叫"类型不安全"? 举例来讲, 当咱们有一个Animal类, 其有Cat, Dog两个子类.函数
Animal
/ \
Cat Dog
复制代码
那下面的代码确定在编译时不会报错 (但智能的IDE, 如Intellij IDEA会报警告),编码
Dog dog = new Dog();
Cat[] cats = new Cat[1];
Animal[] animals = cats;
animals[0] = dog;
复制代码
但是, 上面的代码一运行就会crash: spa
咱们先来看为何编译时不报错. : 由于animal[]自己就是Animal[], 因此放入dog没问题啊设计
那为何运行时又有错呢? : 这时由于animals = cats, 这就让animals数组本质上是Cat[]数组. 这时再加入Dog对象, 就会报ArraySotreException. 毕竟Java对数组存储的类型是有强限制的, 一个Cat[]数组里不能存Dog对象.3d
1). List
变成List<T>
, 一开始就限制好读写时的类型code
2). 数组这样的类型不安全, 要是也用于集合类型, 那就容易在编码时不报错, 一上线就crash, 是个隐患. List就是想之后List<Animal>
里不能被cats给赋值, 即不能List<Animal> animals = cats
这一点很重要, 也极其容易混淆. 你要是还看不明白这句话, 没关系, 后面咱们会更详细讲. 读到到这里, 只要理会了数组的类型是不安全的, 就是能够了的 - 泛型就是想改进它.
我有一个类, 其中有一个List<Animal>
成员. 在构造函数里, 我可能要去存值, 或取值.
List<Animal> animals = new ArrayList<>();
animals.add(new Cat());
Animal cat = animals.get(0);
复制代码
上面的代码都没错, 你们也司空见惯了, 彷佛没什么特别的. 是的. 但咱们在另外的场景里, 相似的代码就有大大的问题了.
我如今有一个展现宠物的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了.
复制代码
上面一小节主要是讲为何java向咱们抱怨代码有问题. 但是回到现实生活中, 咱们但是有deadline的, 这个问题到底要如何解决呢?
个人需求就是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){ }
复制代码
这时咱们再赋值就不会出错了:
如今产品和咱们说, 为了营利, 咱们页面接入了广告商. 如今不论是CatList页, 仍是DogList页, 咱们的展现的数据第一个都是广告商提供的宠物. 因此咱们要小小修改一下代码
class AdPet extends Animal{... }
复制代码
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, 这个仍是能保证不出错的). 再写个小实例, 来加深咱们的印象.
父子赋值
. 如: List<? extends Animal> animalList = new ArrayList<Cat>();
泛型的通配符除了extends, 还有一个suepr:
它正好与extends相反, 它能够add(Animal子类), 但它不能get(). -- 更精确地说是:
这里要说明的是: 当咱们有一个Animal的父类, 叫Being时:
Being
|
Animal
/ \
Cat Dog
复制代码
List<? super Animal>
也支持父子赋值
, 但这时赋值的右值得是一个List<Animal或其父类>
这就理解了为什么Animal a = animals.get(0)
会报错, 由于它返回的多是一个Being对象, 而不是Animal对象!
父子赋值
. 如: List<? super Animal> animalList = new ArrayList<Being>();
其实还有一种通配符, 就是<?>
通配符. 不过这个出现得很少, 并且让人更晕, 因此这篇文章中就不介绍它了.
List<Animal>
呢?这个问题好, 说明你在思考了: "List<Animal>
能够写, 能够读, 能够支持父子赋值吗?"
实例说明一切,咱们来看一下例子:
即List<Animal>
能写能读, 但就是不支持父子赋值.
因此当你在使用, 或设计带泛型的类时, 就能够这样选用: (PC-assign就是我简称的"parent-child assign", 是我为了简便称呼自用的术语.)
上面讲了总结出来的理论. 咱们如今再用一个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, 以及为什么要用通配符. 那恭喜你, 这篇文章你过关了.
泛型其实不少细节点, 像什么泛型方法, 这些我都没在介绍. 由于我想在这篇文章里集中介绍一些很重要但又很让人头晕的知识点.
对应上面的东东, kotlin其实就是in, out两个来代替了extends与super. 其实还是看Collections.copy()的kotlin版本, 咱们就理解了in, out了:
static fun <T> copy(dest: MutableList<in T>, src: List<out T>) {
...
}
复制代码
其实这篇文章主要是讲泛型里的通配符的知识. 泛型还有一些其它部分, 如泛型方法这些我都没有讲解, 由于我感受通配符最容易让人晕, 因此我优先讲这一块.
也欢迎你们评论中说说本身碰到的泛型难题. 有表明性的我就解决后再整理下, 再写一篇泛型难题的文章