Java从入门到放弃 - 泛型
引入泛型的背景
在Java中当我们使用容器存储元素的时候(建议先了解一部分Java容器知识),实际上使用的是Object存储的元素,比如下面ArrayList部分源码
java
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
private static final long serialVersionUID = 8683452581122892189L;
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = new Object[0];
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
transient Object[] elementData;
private int size;
}
从这部分源码我们可以看出ArrayList使用的是Object[] 数组存储的元素,但是这里就引出了一个问题。我们使用Object存储,是不是就把原始类型就给丢弃了。导致我们在从容器中拿出元素之后,想要进一步使用元素还需要向下转型,这让代码变得麻烦、臃肿、不安全,并且容易发生类型转换异常。
怎么解决这个问题?
因为存储的时候是通用的容器,用Object没有问题,但是使用的时候我们还是需要知道原始类型。
解决方式一
使用特定容器不用通用容器不就好了, 比如存储String我们就写一个存储String数组的ArrayList,存储Integer 就写一个存储Integer数组的ArrayList
java
public class StringArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
private static final long serialVersionUID = 8683452581122892189L;
private static final int DEFAULT_CAPACITY = 10;
private static final String[] EMPTY_ELEMENTDATA = new Object[0];
private static final String[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
transient String[] elementData;
private int size;
}
java
public class IntegerArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
private static final long serialVersionUID = 8683452581122892189L;
private static final int DEFAULT_CAPACITY = 10;
private static final Integer[] EMPTY_ELEMENTDATA = new Object[0];
private static final Integer[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
transient Integer[] elementData;
private int size;
}
这样可不可以呢? 当然是可以的,但是有没有问题? 聪明的你,看上面的代码一下子就看出来了,这样写,会产生一大堆重复代码,而且还要写一大堆类。 这肯定不是一种很好的实现方式。
所以有没有更好的实现方式呢?
解决方式二
Java是强类型编程语言,强类型的意思就是在编写代码的时候必须要明确变量的类型,不然的话编译不通过。但是有时候我们不知道某些变量的具体类型是什么,只能用公共父类Object类型,这就是容器或者上面代码ArrayList里面为啥使用Object[]数组存储元素,因为我们不知道具体要存储的数据类型。其实观察上面两段代码,我们其实发现大部分都是相同的,只有标记用什么类型存储数组的地方是不一样的。解决这个问题的方式就是我们在程序的某个地方标识完全未知的类型,使程序顺利通过编译,等到使用的时候确定具体类型。 泛型(Generics)就是泛化的类型,即使用 表示一个暂时没有确定的类型。
使用泛型
接下我们就通过实际代码了解如何使用泛型
代码案例一
java
public class Pair<T> {
T first;
T second;
public Pair(T first, T second){
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public T getSecond() {
return second;
}
}
java
Pair<Integer> minmax = new Pair<Integer>(1,1);
Integer min = minmax.getFirst();
Integer max = minmax.getSecond();
通过使用泛型,我们达到了使得Pair类的代码和它处理的数据类型不是绑定的,具体类型可以变化。也就是说Pair类即可以处理Integer也可以处理String等等
代码案例二
类型参数还可以是多个
java
public class Pair<K, V> {
K first;
V second;
public Pair(K first, V second){
this.first = first;
this.second = second;
}
public K getFirst() {
return first;
}
public V getSecond() {
return second;
}
}
java
Pair<Integer, String> test = new Pair<Integer, String>(1,"test");
Integer num = test.getFirst();
String test = mintest.getSecond();
Pair<Integer, String> test = new Pair<>(1,"test") 这样写,因为编译器可以自动推断泛型类型
通配符的使用
ArrayList 不是 ArrayList的父类, 你可以这样理解。 动物类是人类的父类, 装动物类的汽车类不是装人类的汽车类的父类。
extends 通配符
java
public void addAll(ArrayList<Number> array) {
}
对于这个把所有Number的list放到一个其他容器的方法,Integer是number的子类,Number能放的 Integer也可以放进去。但是上面我们说的继承关系可以知道。ArrayList 跟ArrayList 没有父子关系,直接把ArrayList 传入这个方法,会报错的。
怎么办? 这时候可以使用extends关键字
java
public void addAll(ArrayList< ? extends Number> array) {
}
这样使得方法接收所有泛型类型为Number或Number子类的ArrayList类型了,这样就解决了我们的问题。
super 通配符
ArrayList<? super Integer>表示,方法参数接受所有泛型类型为Integer或Integer父类的ArrayList类型。
对比extends和super通配符
- <? extends T>允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外);
- <? super T>允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)。
一个是允许读不允许写,另一个是允许写不允许读。
泛型的原理
这个是我们写的泛型类,编译器看到的就是我们写的源代码,就是下面这个案例写的这样,
java
public class Pair<T> {
T first;
T second;
public Pair(T first, T second){
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public T getSecond() {
return second;
}
}
实际上JVM执行的时候看到的代码是,
java
public class Pair<T> {
Object first;
Object second;
public Pair(Object first, Object second){
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public Object getSecond() {
return second;
}
}
使用泛型的地方,编译器看到的源码,
java
Pair<Integer> minmax = new Pair<Integer>(1,1);
Integer min = minmax.getFirst();
Integer max = minmax.getSecond();
这段代码实际上JVM看到的是
java
Pair minmax = new Pair(1,1);
Integer min = (Integer)minmax.getFirst();
Integer max = (Integer)minmax.getSecond();
所以Java是通过擦除法实现泛型
- 编译器会把 类似泛型地方的写法都会写成Object
- 编译器根据 实现安全的类型转换
所以实际上Java的泛型是由编译器在编译时实行的,编译器泛型视为Object处理,在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型。