泛型
泛型是什么?
泛型,即“参数化类型”。类型像参数一样,具有多种类型,在使用时才确定。
比如我们需要一个装 int 类型的容器,和一个装 String 类型的容器,要分别制造几个容器吗?比如 IntArrayList 和 StringArrayList ,这样就需要无数个容器了,这种场景就需要泛型。
List<Integer> intList = new ArrayList();
List<String> strList = new ArrayList();
参数化类型意味着可以通过执行泛型类型调用时分配一个类型,用分配的具体类型替换泛型类型。下面 ArrayList 中<E>
就是泛型类型,在使用时才分配具体类型:
public class ArrayList<E> extends AbstractList<E>
通俗的说,就是我要一个篮子,可能用这个篮子装水果,那它就是一个水果篮,也可能水果吃完后用来装垃圾,那它就是垃圾篮,没必要写死这个篮子只能装水果或者垃圾,但是装水果的时候不希望篮子里有垃圾,装垃圾的时候不希望有水果。这时候就要泛型了。
泛型的好处
- 提高安全性: 将运行期的错误转换到编译期. 如果我们在对一个对象所赋的值不符合其泛型的规定, 就会编译报错.
- 避免强转: 比如我们在使用List时, 如果我们不使用泛型, 当从List中取出元素时, 其类型会是默认的Object, 我们必须将其向下转型为String才能使用。比如:
List l = new ArrayList();
l.add("abc");
String s = (String) l.get(0);
而使用泛型,就可以保证存入和取出的都是String类型, 不必在进行cast了,也可以直接调用类型独有的方法,比如:
List<String> l = new ArrayList();
l.add("abc");
l(0).split("b");
如何使用泛型
类型参数用作占位符,在运行时为类分配类型。根据需要,可能有一个或多个类型参数,根据惯例,类型参数是单个大写字母,该字母用于指示所定义的参数类型。下面列出每个用例的标准类型参数:
- E:元素
- K:键
- N:数字
- T:类型
- V:值
- S、U、V 等:多参数情况中的第 2、3、4 个类型
public class Test<T> {
private T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}
使用:
Test<String> t = new Test<>();
t.setObj("abc");
在JDK1.7时就推出了一个新特性叫菱形泛型(The Diamond), 就是说后面的泛型可以省略直接写成<>, 反正前后一致。
泛型中的通配符
?和关键字extends或者super在一起其实就是泛型的高级应用:通配符。
固定上边界通配符 <? extends E>
interface Fruit {
double getWeight();
}
class Apple implements Fruit{
@Override
public double getWeight() {
return 5;
}
}
class Orange implements Fruit{
@Override
public double getWeight() {
return 4;
}
}
加入小明买了一个果篮,可以装苹果,也可以装橘子。
ArrayList<Fruit> fruits = new ArrayList<>();
Apple apple = new Apple();
fruits.add(apple);
Orange orange = new Orange();
fruits.add(orange);
但是小明只想让这个篮子装橘子:
ArrayList<Fruit> fruits = new ArrayList<Orange>(); // 编译报错
这样编辑器就报错了,因为泛型不支持向上转型。那怎么实现小明的需求呢:
ArrayList<? extends Fruit> fruits = new ArrayList<Orange>();
这样编辑器就不报错了,但是问题又来了,这个篮子不能装东西,会报错:
ArrayList<? extends Fruit> fruits = new ArrayList<Orange>();
fruits.add(orange); // 编译报错
这是因为,如果可以装水果,那并不知道你装的是苹果还是橘子,如果你装了苹果,小明女朋友想吃橘子,取出的是苹果,小明女朋友就要分手了。
编辑器也为了我们想取苹果的时候取出橘子,导致类型错误,所以不允许调用 add
,set
,等参数是泛型的方法。
那这个篮子什么用呢?其实,还真有用,比如小明要给水果称重,你不知道是苹果篮还是橘子篮,所以这样写:
static double getWeight(List<? extends Fruit> list) {
double weight = 0;
for (int i = 0; i < list.size(); i++) {
weight += list.get(i).getWeight();
}
return weight;
}
ArrayList<Orange> oranges = new ArrayList<>();
oranges.add(orange);
ArrayList<Apple> apples = new ArrayList<>();
apples.add(apple);
getWeight(apples);
getWeight(oranges);
这样就可以给水果称重了。
<? extends E>
就是固定上界通配符
重点说明:我们不能对List<? extends E>使用add方法。原因是,我们不确定该List的类型, 也就不知道add方法的参数类型。
但是也有特例,可以添加null
:
ArrayList<? extends Fruit> fruits = new ArrayList<Orange>();
fruits.add(null);
固定下边界通配符 <? super E>
小明女朋友喜欢橘子,也喜欢喝橙汁,小明送了2个橘子,一个放果篮里准备生吃,一个放厨房篮子做果汁:
interface Fruit {
double getWeight();
}
interface Juice {
}
class Orange implements Fruit,Juice {
@Override
public double getWeight() {
return 4;
}
public void addList(List<? super Orange> list) {
list.add(this);
}
}
List<Juice> juices = new ArrayList<>();
List<Fruit> fruits = new ArrayList<>();
Orange orange1 = new Orange();
Orange orange2 = new Orange();
orange1.addList(juices);
orange2.addList(fruits);
Fruit object = fruits.get(0);
小明女朋友想要装篮子的时候,取出第一个:
public void addList(List<? super Orange> list) {
Juice object = list.get(0); // 编译报错
list.add(this);
}
竟然报错了,并不知道是水果篮子还是橙汁篮子。万一小明女朋友想从水果篮子拿出一个,结果拿的是橙子篮子的,那么小明又要被分手了。
重点说明:我们不能对List<? super E>使用 get 方法。
原因是,我们不确定该List的类型, 也就不知道 get 方法的参数类型。
但是也有特例, Object 类型就可以:
public void addList(List<? super Orange> list) {
Object object = list.get(0);
list.add(this);
}
无边界通配符 <?>
无边界的通配符的主要作用就是让泛型能够接受未知类型的数据。
小明女朋友有个篮子,没有告诉小明是装什么的,于是小明用来装了橙子:
List<?> fruits = new ArrayList<Orange>();
fruits.add(orange); // 编译报错
fruits.get(0); // 编译报错
小明又被分手了,why?
小明事后想到:这个篮子可能是小明女朋友装脏袜子的
<?>
就是无边界通配符,它具有上边界和下边界的限制,不能 add 也不能 get 。因为不能确定类型。
但是也有特例,就是可以 get 到 Object,也可以存入 null。
总结
泛型限定符有一描述:上界不存下界不取。
上界不存的原因:例如 List,编译器只知道容器内是 Fruit 及其子类,具体是什么类型并不知道,编译器在看到 extends 后面的 Fruit 类,只是标上一个 CAP#1 作为占位符,无论往里面插什么,编译器都不知道能不能和 CAP#1 匹配,所以就不允许插入。
下界不取的原因:下界限定了元素的最小粒度,实际上是放松了容器元素的类型控制。例如 List, 元素是 Orange,可以存入 Orange 及其超类。但编译器并不知道哪个是 Orange 的超类,如 Juice。读取的时候,自然不知道是什么类型,只能返回 Object,这样元素信息就全部丢失了。
kotlin 中的泛型
和 Java 泛型一样,Kolin 中的泛型也有通配符:
- 使用关键字 out 来支持协变,等同于 Java 中的上界通配符 ? extends。
- 使用关键字 in 来支持逆变,等同于 Java 中的下界通配符 ? super。
泛型方法和类型推断
小明的前女友柳岩只吃橘子这一种水果,会收各种礼物,因为礼物类型不确定,所以收礼物需要泛型方法,为什么不用Obje呢?因为泛型方法具有类型推断,不用强转,避免类型转换异常。
interface GirlFriend<T> {
T eatFruit(T i);
<E> E getGift(E e);
}
class LiuYan<T> implements GirlFriend<T> {
@Override
public T eatFruit(T t) {
return t;
}
@Override
public <E> E getGift(E e) {
return null;
}
}
GirlFriend<Orange> liuyan = new LiuYan<>();
liuyan.eatFruit(new Orange());
Apple gift = liuyan.getGift(new Apple());
送柳岩一个苹果,因为有类型推断,所以 Apple 不要强转。
理解嵌套
小明把前女友分类,爱吃水果的分一类:
interface GirlFriend<T extends Fruit> {
T eatFruit(T i);
<E> E getGift(E e);
}
class LiuYan<T extends Fruit> implements GirlFriend<T> {
@Override
public T eatFruit(T t) {
return t;
}
@Override
public <E> E getGift(E e) {
return null;
}
}
List<? extends GirlFriend<? extends Fruit>> list = new ArrayList<? extends GirlFriend<? extends Fruit>>(); // 编译报错
列表右边和左边一样,报错了,右边去掉 ?:
List<? extends GirlFriend<? extends Fruit>> list = new ArrayList< GirlFriend<? extends Fruit>>();
不报错了,为什么呢?
泛型实例化的时候要确定类型,所以 List 实例化的时候要确定具体类型, GirlFriend 代表这是个女朋友列表。那为什么 GirlFriend 后面的泛型却可以不确定呢?因为这个列表是女朋友列表,但是那种类型的女朋友列表不管。在装进去的时候才确定。
类型擦除
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass()==list2.getClass());
上面输出是 true,因为虚拟机只会看到 List,泛型被擦除了。
public class ObjectContainer<T> {
private T contained;
public ObjectContainer(T contained) {
this.contained = contained;
}
public T getContained() {
return contained;
}
}
这段代码,编译器会生成以下代码:
public class ObjectContainer {
private Object contained;
public ObjectContainer(Object contained) {
this.contained = contained;
}
public Object getContained() {
return contained;
}
}
为什么会有泛型擦除呢?是因为泛型的支持是在JDK1.5之后,那么以前的版本运行时JVM是不能识别泛型的,所以有了一个擦除机制,擦除之后,类型信息转为它的边界类型。
擦除会带来2个问题,那就是继承中方法的重载问题,还有就是类型信息擦除之后如何获取信息的问题。
void putList(List<Integer> list){
}
void putList(List<String> list){
}
因为泛型擦除,所以上面两个方法签名一致,重载失败,编译器报错。
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
用一个子类继承:
class DateInter extends Pair<Date> {
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
}
那么问题来了,不是泛型擦除了吗?
编译后的 Pair,
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
子类重写的方法:
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
方法重写的话,要求参数返回值一致,为什么子类会重写成功呢?
class DateInter extends Pair {
// 我们重写的方法
public void setValue(Date value) {
super.setValue(value);
}
// 我们重写的方法
public Date getValue() {
return (Date) super.getValue();
}
// 虚拟机生成的桥接方法
@Override
public Object getValue() {
return getValue();
}
// 虚拟机生成的桥接方法
@Override
public void setValue(Object value) {
setValue( (Date)value);
}
}
从编译的结果来看,我们本意重写setValue和getValue方法的子类,竟然有4个方法,其实不用惊奇,最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的 setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。
并且,还有一点也许会有疑问,子类中的桥方法 Object getValue()和Date getValue()是同 时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起 来“不合法”的事情,然后交给虚拟器去区别。
这样就巧妙的解决了重载的问题。
泛型类型获取
List<Fruit> ps = gson.fromJson(str, new TypeToken<List<Fruit>>(){}.getType());
既然类型擦除了,为什么 Gson 在转 json 的时候还能获取到?
List<Fruit> list = new ArrayList<Fruit>();
我们获取到的 class 类型是 List,因为 ArrayList<E> 这个类并没有类型。但是我们写个子类继承 ArrayList<E>,就能获取子类的类型:
class FruitArrayList extends ArrayList<Fruit>{}
List<Fruit> list = new ArrayList<Fruit>();
List<Fruit> list2 = new FruitArrayList();
list2 的类型是 FruitArrayList,在看下面的 list3 :
List<Fruit> list = new ArrayList<Fruit>();
List<Fruit> list2 = new FruitArrayList();
List<Fruit> list3 = new ArrayList<Fruit>(){};
list3 和 list1 的区别是后面有个中括号,代表这个就是一个 ArrayList<Fruit> 的类,就能获取这个类型。所以 Gson 在转化的时候,是一样的方法:
new TypeToken<List<Fruit>>(){}.getType()
如果改为
new TypeToken<List<Fruit>>().getType()
就获取不到类型,当然编辑器就报错了。