一、什么是泛型,用来解决什么问题
泛型适用于 参数类型不确定 的情况,例如一个容器,不确定其中存放的元素是Integer
还是String
类型,那么就可以将该元素的类型定义成为泛型。
也就是说,泛型是 用于将具体类型参数化。
二、泛型的分类
根据泛型的应用场景,可以分为三类:
- 泛型类
- 泛型接口
- 泛型方法
2.1 泛型类
一个标准的泛型类如下:
/**
* 标准的泛型类。
*
* @author lizejun
**/
public class Generic<T> {
private T value;
public T getValue() {
return value;
}
}
在实例化泛型类的时候,我们可以一开始就指定 泛型实参,也可以不指定,如下所示:
public static void main(String[] args) {
//1.实例化的时候指定泛型实参。
Holder<String> holder = new Holder<>();
holder.setValue("value");
//2.实例化的时候不指定泛型实参。
Holder holder2 = new Holder<>();
holder2.setValue("value");
}
这两者的区别在于,如果我们在实例化泛型类的时候指定了泛型实参,那么在编译的时候,会在对象进入和离开方法的边界处添加类型检查,即下面这样预先指定了类型实参为String
,但是又尝试传递一个整数类型的代码,是不能编译通过的。
public static void main(String[] args) {
//实例化的时候指定泛型实参。
Holder<String> holder = new Holder<>();
holder.setValue("value");
holder.setValue(1);
}
2.2 泛型接口
泛型接口的定义与泛型类基本类似:
/**
* 标准的泛型接口。
*
* @author lizejun
**/
public interface HolderInterface<T> {
T getValue();
}
需要注意的点在于,实现泛型接口的时候,有两种选择。
传入类型实参
这种情况下,所有使用泛型的地方都要替换成传入的实参类型。
/**
* @author lizejun
**/
public class HolderImpl implements HolderInterface<String> {
@Override
public String getValue() {
return null;
}
}
不传入类型实参
这种情况下,实现类需要声明为泛型类。
/**
* @author lizejun
**/
public class HolderImpl<T> implements HolderInterface<T> {
@Override
public T getValue() {
return null;
}
}
2.3 泛型方法
2.3.1 基本使用
当方法上的类型参数不确定时,有两种解决方法:
- 使用泛型类中定义的泛型形参
- 定义泛型方法
泛型方法和泛型类的区别在于 指定类型实参的时机,前者是在方法调用时,后者是在实例化泛型类的时候。当两者名字相同时(例如都为 T
),泛型方法会 覆盖 泛型类中的泛型形参。
/**
*
* @author lizejun
**/
public class Holder<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
public <T> void setMethodValue1(T t) {
System.out.println("t=" + t);
}
public <E> void setMethodValue2(E e) {
System.out.println("e=" + e);
}
}
2.3.2 静态方法与泛型
静态方法无法访问类定义上的泛型,因此,如果 静态方法的参数类型是可变的,那么要将 静态方法定义成泛型方法。
三、泛型通配符
3.1 无边界通配符
泛型操作符用于解决当泛型实参的类型不确定的情况,用?
来替代类型实参,可以将其看成所有类型的父类。
public static void showValue2(Holder<?> g) {}
3.2 上下边界通配符
类型通配符最主要的用处是结合 泛型上下边界 来使用。
- 对象类型必须是指定类型或其子类型,
Holder<? extends 上边界>
- 对象的类型必须是指定类型或其父类型,
Holder<? super 下边界>
关于上下界,有一个著名的原则PECS(Producer Extends, Consumer Super)
:
- 如果希望使用
Object
类方法,就使用?
通配符。 - 使用
extends
关键字,固定上边界,那么就将该对象当做一个只读对象。 - 使用
super
关键字,固定下边界,那么就将该对象当做一个只写对象。 - 如果需要一个既能读,又能写,就不要使用通配符。
为什么会有这个准则呢,关键在于 在参数类型可变时,找到固定不变的部分,并遵循已知的原则, 以一个接收List
参数的函数为例,已知的原则是:
- 原则一:
get
的元素,可以调用 声明的类型,声明类型的父类型 的方法。 - 原则二:
add
的元素,只能是 声明的类型、声明类型的子类型或者null
。
? - 无边界通配符
这时候不变的部分就是 List 中的元素必定是 Object 或它的子类,因此获取出来的元素必定可以调用Object
的方法,但是 不确定究竟是Object
还是它的子类,所以只能添加null
。
private static void printList(List<?> list) {
//1. add - 只允许添加 null。
list.add(1); //不通过。
list.add(null); //通过。
//2. get - 获取的元素可以调用 Object 的方法。
list.get(0).hashCode();
}
? extends - 固定上边界
这时候固定不变的部分就是 List 中的元素必定是 Integer 或它的子类,因此取出来的元素必定可以调用Integer
的方法,但是究竟是Integer
还是它的子类呢,不能确定,所以也就只能添加null
。
private static void printList(List<? extends Integer> list) {
//1. add - 只允许添加 null。
list.add(new Integer(1)); //不通过。
list.add(null); //通过。
//2. get - 获取的元素可以调用 Integer 的方法。
list.get(0).intValue();
}
? super - 固定下边界
这时候确定的部分是 List 中的元素必定是 Integer 或它的父类,假如是父类,那么Integer
的方法就不可以调用了,因此获取出来的元素只能调用Object
的方法,而添加的元素,由于确定了声明的是Integer
或其父类,那么自然就可以添加Integer
元素了。
private static void printList(List<? super Integer> list) {
//1. add - 只允许添加 null 或者 Integer。
list.add(new Integer(1)); //通过。
list.add(null); //通过。
//2. get - 获取的元素只可以调用 Object 的方法。
list.get(0).toString(); //通过。
list.get(0).intValue(); //不通过。
}
3.3 参考文章
四、类型擦除
4.1 什么是类型擦除
泛型信息只存在于代码编译阶段,编译后生成的二进制代码是不包含泛型信息的,这个过程即 类型擦除。
例如下面的代码返回true
,其类型为java.util.ArrayList
。
/**
* @author lizejun
*/
public class MainApp {
public static void main(String[] args) {
List<String> s = new ArrayList<>();
List<String> i = new ArrayList<>();
//s=i:true,name=java.util.ArrayList
System.out.println("s=i:" + (s.getClass() == i.getClass()) + ",name=" + i.getClass().getName());
}
}
还有其它的场景:
-
List<String>
、List<T>
擦除后的类型为List
。 -
List<String>[]
、List<T>[]
擦除后的类型为List[]
。 -
List<? extends E>
、List<? super E>
擦除后的类型为List<E>
-
List<T extends Serialzable & Cloneable>
擦除后的类型为List<Serialzable >
。
4.2 类型擦除的目的
- 避免对
JVM
的改动,如果JVM
将泛型类型延续到运行期,那么到运行期时JVM
就要进行大量的重构工作。 - 版本兼容。在编译期擦除可以更好地支持原生类型。
4.3 类型擦除后,类型参数如何处理
在泛型类的类型被擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,<T>
就会被转译成普通的Object
类型;如果指定了上限<T extends String>
,类型参数就会被替换成类型上限。
分别定义两个泛型类,其中一个指定了上限。
/**
*
* @author lizejun
**/
public class Holder<T> {
private T value;
public T getValue() {
return value;
}
}
/**
* @author lizejun
**/
public class HolderEx<T extends String> {
private T value;
public T getValue() {
return value;
}
}
/**
* @author lizejun
*/
public class MainApp {
public static void main(String[] args) {
printClass(Holder.class);
printClass(HolderEx.class);
}
private static void printClass(Class clz) {
for (Field field : clz.getDeclaredFields()) {
System.out.println("field=" + field.getType().getName());
}
}
}
输出结果为:
//没有指定上界。
field=java.lang.Object
//指定了上界为 String。
field=java.lang.String
4.4 类型擦除的后果
4.4.1 为什么不能创建泛型数组
在Java
中,Object[]
数组可以是任何数组的父类,或者说,任何一个数组都可以向上转型成它在定义时指定元素类型的父类的数组,这个时候,如果我们往里面放入不同于原始数据类型,但是满足向上转型后的父类类型,编译不会有问题,在运行时才会检查加入数组的对象的类型,抛出ArrayStoreException
异常。
/**
* @author lizejun
*/
public class MainApp {
public static void main(String[] args) {
String[] strArray = new String[20];
Object[] objArray = strArray;
//Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer。
objArray[0] = 1;
}
}
假如我们允许创建泛型数组,例如下面这样:
ArrayList<String>[] lists = new ArrayList<String>[10];
前面我们已经介绍过,在编译后由于类型擦除,本来的数组就会变成:
ArrayList[] lists = new ArrayList[10];
在随后的代码中,有可能将其转型为Object[]
,再往里面放入ArrayList<Integer>
(由于类型擦除,在运行期,会变成ArrayList
,数组的元素类型也是ArrayList
),这样 运行期也无法进行检查出错误,我们就有可能将ArrayList<Integer>
类型的数据存入到ArrayList<String>
的数组中,造成逻辑错误。
4.4.2 instanceof 不能存在泛型参数
下面的代码编译无法通过,illegal generic type of instanceof
。
/**
* @author lizejun
*/
public class MainApp {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
//编译期异常:illegal generic type of instanceof
System.out.println("instanceof=" + (list instanceof List<String>));
}
}