自JDK1.5后,Java引入了泛型。泛型,即“参数化类型”。将类型由原来具体的类型参数化,类似于方法中的变量参数,此时参数也定义成参数形式(可以称之为类型形参),然后在使用时传入具体的类型(类型实参)。
为什么需要泛型
在泛型出现之前,集合中可以存放任意非基本类型,取出来的时候要强转,因此使用的时候可能会出现ClassCastException错误。例如,在下面的代码中,List中存放了Integer类型、String类型以及Boolean类型,在使用的时候强转必然会抛异常。
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List list = new ArrayList();
list.add(1);
list.add("11");
list.add(true);
for(int i=0;i<list.size();i++){
System.out.println((String) list.get(i));
}
}
}
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at Main.main(Main.java:13)
泛型出现之后,集合中只能存在指定类型的数据,以上提到的问题在编译期编译器就会报错而不会等到运行时才出错。
可以看到List在添加Integer类型和Bollean类型的时候编译器已经报错了,并且在for循环中String强转也变成灰色,即不需要强转了。另外,使用泛型也利于代码的复用。
泛型的使用
泛型可以使用在类、接口以及方法的定义中,使用了泛型的类、接口和方法,分别称作泛型类、泛型接口和泛型方法。
- 泛型类:在类名后加上泛型参数即可,也可以添加多个参数,例如<T,K>,<T,K,V>。这里的T,K,V都是一种表现形式而已,并没有什么特殊的意义,你愿意的话也可以定义为<Q>、<W>、<E>、<R>。
class GenericClass<T>{
T item;
public void setItem(T t) {
this.item=t;
}
public T getItem() {
return this.item;
}
}
2. 泛型接口:在接口名后加上泛型参数,与泛型类类似。
public interface GenericInterface<T> {
T sing();
}
3. 泛型方法:在方法返回值前加上泛型参数列表。下面代码中,只是用了泛型参数的方法(例如getItem()和setItem())不是泛型方法,必须要是定义了泛型参数列表的才是泛型方法。即使泛型方法中的泛型与泛型类的泛型声明都为T,该方法也是泛型方法,且这两个T之间没有任何关系。
class GenericClass<T>{
T item;
public void setItem(T t) {
this.item=t;
}
public T getItem() {
return this.item;
}
//泛型方法
public <E> void sing(E e) {
System.out.println(e);
}
}
对于泛型类和泛型接口,在创建对象时就需要指定参数类型;而对于泛型方法,在使用时再指定参数类型即可。
不可协变
在介绍泛型的不可协变之前,我们先来了解一下数组的协变性。数组的协变性指的是如果类a是类b的子类,那么a[]也是b[]的子类。类似的,不可协变是说如果类a是类b的子类,但使用a和b的泛型类之间没有任何关系,例如ArrayList<a>与ArrayList<b>没有任何关系。
举个例子,现在有水果类及其子类如下:
public class Fruit{
protected String name;
protected String color;
public String getName(){
return this.name;
}
public void setName(String name){
this.name = name;
}
public String getColor(){
return color;
}
public void setColor(){
this.color = color;
}
}
public class Apple extends Fruit{
public Apple(){
name = "apple";
color = "green";
}
}
public class Banana extends Fruit{
public Banana(){
name = "Banana";
color = "yellow";
}
}
然而在像下面这样使用时会有编译错误,印象中的向上转型在这好像不起作用了?
事实上,这是由泛型的不可协变导致的。泛型的不可协变是指Apple是Fruit的子类,但List<Apple>和List<Fruit>没有父子关系。与之相反的,数组具有协变性,指Apple是Fruit的子类,那么Apple[]也是Fruit的子类。
如上,编译可以通过。但数组的协变性也会导致Exception in thread "main" java.lang.ArrayStoreException: Banana
at Main.main(Main.java:8),试图将错误类型的对象存储到一个对象数组时抛出的异常。
通配符
为了让泛型参数可以传入一定范围的类型,泛型中引入了通配符?来表示一种未知类型,并使用以下通配符:
- 上界通配符<? extents T>:只能保存T及它的子类。无法写数据,因为只允许保存一种T,不是每种T都可以保存,但可以读取数据,因为其上界为T。
- 下界通配符<? super T>:只能存放T及其T的基类类型的数据。可以向里边写T及其派生类的数据,但读出来的数据类型只能是Object的。
- 无限定通配符<?>:只读,不能增加修改。
PECS原则:上界通配符不能往里存,只能往外取,适合频繁往外面读取内容的场景;下界通配符不影响往里存,但取出来只能放在Object对象里,适合经常往里插入数据的场景。
类型擦除
泛型只在编译期起作用,在运行时泛型为了兼容JDK1.5之前的旧代码使用了“类型擦除”机制,编译器在编译过程中会移除参数的类型信息,泛型参数会被擦除到它的第一个边界,如果没有指明边界,将会被擦除到Object。
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> integerList = new ArrayList<>();
List<String> stringList = new ArrayList<>();
System.out.println(integerList.getClass().equals(stringList.getClass()));
}
}
由于类型擦除机制,以上代码的运行结果为true,因为运行时传入的Integer和String被丢掉了,原始类型都变为第一个边界Object。对于List<? extends Comparable>来说,类型擦除后的原始类型变为Comparable。