泛型
约 3307 字大约 11 分钟
2023-03-26
背景
更好的可读性和安全性
Java 集合(Collection)中元素的类型是多种多样的,Java 允许程序员构建一个元素类型为 Object 的集合,其中的元素可以是任何类型。
在没有泛型的情况下,通过对类型 Object 的引用来实现参数的“任意化”,这样做的缺点是要作显式地强制类型转换,而这种转换是要求开发者对实际参数类型可以在预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。
例子:
public class Main {
public static void main(String[] args) {
List list = new ArrayList();
// 插入元素时 list 不会记住元素的类型
list.add("apple");
list.add(123);
for(int i = 0; i < list.size(); i++) {
// 取出元素时需要显示地强制类型转换
System.out.println((String) list.get(i));
}
}
}
程序执行结果:
apple
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
那么如何能够使集合能够记住集合内元素的类型,且能够达到只要编译时不出现问题,运行时就不会出现类型转换异常呢,答案就是使用泛型,指定集合内的元素类型,程序的可读性也大大提高。
例子:
public class Main {
public static void main(String[] args) {
// 指定 list 只存放 String 类型的元素
List<String> list = new ArrayList();
list.add("apple");
// 添加其他类型的元素在编译阶段就会报错
list.add(123);
for(int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
}
程序执行结果:
Error:(12, 13) java: 对于add(int), 找不到合适的方法
方法 java.util.Collection.add(java.lang.String)不适用
(参数不匹配; int无法转换为java.lang.String)
方法 java.util.List.add(java.lang.String)不适用
(参数不匹配; int无法转换为java.lang.String)
更广泛的表达能力
如果现在需要实现一个方法,对传入的任意类型的数组进行排序,可以怎么做?
当然可以给每一种类型都写一个方法,当需要对某种类型的数组进行排序时,调用相应的方法就好了。但这样有个很明显的弊端,那就是类型有很多种,还不包括用户自定义的类,不可能穷尽,而且排序的逻辑基本相同,没有必要重复地写这些相同的代码。
那如何优雅地解决这个问题,答案是可以使用泛型,可以写一个泛型方法来对一个对象数组排序,然后调用该泛型方法来对任意类型的数组等进行排序,这个泛型方法就拥有了更广泛的表达能力。
例子:
public class Main {
public static <E extends Comparable<E>> void sort(E[] es) {
int n = es.length;
// 冒泡排序
for(int i = 0; i < n - 1; i++) {
for(int j = 0; j < n - 1 - i; j++) {
if (es[j + 1].compareTo(es[j]) < 0) {
E temp = es[j];
es[j] = es[j + 1];
es[j + 1] = temp;
}
}
}
}
public static void main(String[] args) {
Integer[] ints = {3, 5, 1, 0, 9, 7};
Character[] chars = {'m', 'a', 't', 'l', 'a', 'b'};
String[] strings = {"apple", "pear", "banana", "beach", "orange"};
sort(ints);
sort(chars);
sort(strings);
for(Integer integer : ints) {
System.out.print(integer + " ");
}
System.out.println();
for(Character character : chars) {
System.out.print(character + " ");
}
System.out.println();
for(String string : strings) {
System.out.print(string + " ");
}
}
}
程序执行结果:
0 1 3 5 7 9
a a b l m t
apple banana beach orange pear
引入
Java 泛型(Generics)是 JDK 5 中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
类似于方法声明时的形参,调用该方法时方法里的形参由实参代替。这里的参数化就是将具体的类型变成一个参数,也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
泛型的使用
在 Java 中,经常用 T(type)、E(element)、K(key)、V(value) 等形式的参数来表示泛型参数。
泛型类
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分(用<>尖括号括起来)。
泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
下面是 JDK 8 中 ArrayList 类和 HashMap 类的源码声明:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {...}
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {...}
例子:
public class Main {
public static void main(String[] args) {
// 创建两个 DataHolder 对象
DataHolder<Integer> dataHolder1 = new DataHolder<>();
DataHolder<String> dataHolder2 = new DataHolder<>();
// 向其中加入元素
dataHolder1.add(123);
dataHolder2.add("Hello world!");
// 打印结果
System.out.println(dataHolder1.get());
System.out.println(dataHolder2.get());
}
}
// 自定义泛型类 DataHolder
class DataHolder<E> {
private E e;
public E get() {
return e;
}
public void add(E e) {
this.e = e;
}
}
程序输出结果:
123
Hello world!
泛型接口
泛型接口与泛型类的定义及使用基本相同,在接口名后添加类型参数声明部分(用<>尖括号括起来)。
泛型类实现泛型接口时需要在类首添加泛型的声明,否则编译会报错。
下面是 JDK 8 中 List 接口和 Map 接口的源码声明:
public interface List<E> extends Collection<E> {...}
public interface Map<K,V> {...}
例子:
public class Main {
public static void main(String[] args) {
DataHolder<String> dataHolder = new DataHolder<>();
System.out.println(dataHolder.isEmpty());
dataHolder.add("Hello world!");
System.out.println(dataHolder.isEmpty());
}
}
interface Holder<E> {
public boolean isEmpty();
}
class DataHolder<E> implements Holder<E> {
private E e;
public E get() {
return e;
}
public void add(E e) {
this.e = e;
}
@Override
public boolean isEmpty() {
return e != null;
}
}
程序输出结果:
false
true
泛型方法
泛型方法体的声明和普通方法类似,所有泛型方法声明都有一个类型参数声明部分(用<>尖括号括起来),该类型参数声明部分在方法返回类型之前。
类型参数能被用来声明返回值类型,并且可以作为泛型方法的形参类型。
如上面介绍的例子中 sort 排序方法,参数是一个类型为 E 的数组:
public static <E extends Comparable<E>> void sort(E[] es) {...}
需要注意的是,类型参数只能代表引用型类型,不能是原始类型(如 int,double,char 等)。
int[] nums = {3, 5, 1, 0, 9, 7};
// 这样编译会报错
sort(nums);
通配符
类型通配符
类型通配符,也即非限定符,无界通配符。一般是使用 ? 代替任意具体的类型参数。例如 List<?> 在逻辑上是 List ,List 等所有 List<具体类型实参> 的父类。
例子:
public class Main {
public static void main(String[] args) {
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
list1.add("Hello World!");
list2.add(123);
getData(list1);
getData(list2);
}
public static void getData(List<?> list){
System.out.println(list.get(0));
}
}
程序输出结果:
Hello World!
123
通配符上下界
在使用泛型的时候,有时候需要为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。
上界
<? extends ClassType>
该通配符为 ClassType 的所有子类类型,表示任何类型都是 ClassType 类型的子类。
正常使用
例子:
public class Main {
public static void main(String[] args) {
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
List<Double> list3 = new ArrayList<>();
list1.add("Hello World!");
list2.add(123);
list3.add(3.14);
// 因为 String 不是 Number 的子类,所以会报错
// getData(list1);
getData(list2);
getData(list3);
}
public static void getData(List<? extends Number> list){
System.out.println(list.get(0));
}
}
程序输出结果:
123
3.14
非法使用
public class Main {
public static void main(String[] args) {
List<? extends Number> list1 = new ArrayList<Integer>();
List<? extends Number> list2 = new ArrayList<Double>();
// 下面两句都会报错
list1.add(123);
list2.add(3.14);
// 下面四句正常
list1.add(null);
list2.add(null);
list1.get(0);
list2.get(0);
}
}
正常使用时,List<? extends Number> 可以代表 List 或 List ,那为什么在上面这个例子中无法向 list 中添加 Integer 或者 Double 类型的变量呢?
其实也很好理解,假如可以向 list 中添加任意类型的元素,那么在取出 list 中的元素时,取出的元素应该是什么类型呢,必要时还要强制转换吗?编译器只知道 list 是 Number 的某个子类的 list ,但是不知道具体是哪一个子类,所以编译器就阻止向 list 中加入任何子类。其实这就和泛型产生的原因一样了,为了更好的安全性,编译器不允许这样做。
因此不能向 List<? extends ClassType> 中添加任意 ClassType 子类的类型对象,除了null;使用通配符上界会使存入数据的操作(set)失效。
但是编译器始终知道 list 是 Number 的某个子类的 list,数据总能够被读取成 Number 的对象。
下界
<? super ClassType>
该通配符为 ClassType 的所有父类类型,表示任何类型都是 ClassType 类型的父类。
正常使用
例子:
public class Main {
public static void main(String[] args) {
List<? super Number> list = new ArrayList<>();
list.add(123);
list.add(3.14);
System.out.println(list.get(0));
System.out.println(list.get(1));
}
}
程序输出结果:
123
3.14
非法使用
public class Main {
public static void main(String[] args) {
List<? super Number> list = new ArrayList<>();
list.add(123);
list.add(3.14);
// 下面两句都会报错
Integer num1 = list.get(0);
Double num2 = list.get(1);
// // 下面两句正常
Object o1 = list.get(0);
Object o2 = list.get(1);
}
}
可以看到,能向 list 中添加任何 Number 的子类,这是因为 Number 及其子类都可被看做是 Number ,编译器会自动向上转型,所以可以添加成功,但是由于编译器并不知道 list 的内容究竟是 Number 的哪个父类(实际中 Number 没有父类,这里为了说明问题,假设其有父类),因此不允许加入任何特定父类的元素。
因此不能向 List<? super ClassType> 中添加任意 ClassType 父类的类型对象;使用通配符下界会使读取数据的操作(get)失效。
因为 Object 是任何 Java 类的父类,所以读取数据只能返回 Object 对象。
PECS原则
其实上面的正常使用和非法使用的情况就是遵循了 PECS 原则。
PECS(Producer Extends Consumer Super),生产者(Producer)使用extends,消费者(Consumer)使用super。
- 当需要频繁读取内容,而不需要存入内容时使用 <? extends ClassType> 通配符;
- 当需要频繁存入内容,而不需要读取内容时使用 <? super ClassType> 通配符;
- 当既需要存入内容,又需要读取内容时,不使用任何通配符。
泛型的实现原理
泛型擦除
Java 在编译阶段擦除了所有的泛型信息,在 Java 生成的字节码文件中不包含任何的泛型的类型信息,在 Java 运行时也无法获取到泛型信息,这也是Java的泛型被称为伪泛型的原因。
例子:
public class Main {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
Class stringListClass = stringList.getClass();
Class integerListClass = integerList.getClass();
System.out.println(stringListClass + "\n" + integerListClass);
System.out.println(stringListClass == integerListClass);
}
}
程序输出结果:
class java.util.ArrayList
class java.util.ArrayList
true
编译时泛型
所有的泛型类型最终都是一种原始类型,也就是说,泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。类型擦除后会替换成 Object(如果通过 extends 设置了上界,则替换成上界类型)。
泛型擦除会导致任何在运行时进行类型查询的操作无法编译通过:
public class Main {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
// 编译不通过
if(stringList instanceof ArrayList<String>){
System.out.println("test1");
}
// 编译通过
if(integerList instanceof ArrayList<?>) {
System.out.println("test2");
}
}
}
Java 编译器编译泛型的步骤:
- 检查泛型类型,获得目标类型并检查是否合法,如果不合法编译无法通过;
- 擦除类型变量,替换为限定类型;
- 调用相关函数,在类型参数出现的地方插入强制转换的相关指令。
一些问题
List<?> 是一个未知类型的 List,而 List
原始类型和带参数类型之间的主要区别在于,在编译时编译器不会对原始类型进行类型安全检查,而会对带参数的类型进行检查。
使用泛型时,泛型类型必须为引用数据类型,不能为基本数据类型。
在 static 方法中不可以使用泛型,泛型变量也不可以用 static 关键字来修饰。
不可以定义泛型数组。