写在最前:本笔记全程参考《Java核心技术卷I》,添加了一些个人的思考和整理
泛型概述
1. 为什么要使用泛型
泛型程序设计(generic programming)意味着编写的代码可以对多种不同类型的对象重用。
在还没有泛型之前,泛型程序设计是通过继承实现的,例如ArrayList
只维护一个Object[]
引用的数组。这样存在两个问题:获取值时需要强转,且可以存入任意值
泛型提供了类型参数(type parameter)用于指定类型,解决了上述的问题。它会让你的程序更加易读、安全
泛型的作用是:用户想要使用哪种类型,就在创建的时候指定类型。使用的时候,该泛型就会自动转换成用户想要使用的类型。
泛型类和泛型方法
2. 定义简单的泛型类
泛型类就是有一个或多个变量的类。
public class Pair<T> {} // 一个泛型变量
public class Pair<T, U> {} // 多个泛型变量
类型变量在整个类定义中用于指定方法的返回值类型以及字段和局部变量的类型:
private T getFirst(); // 泛型类中的泛型方法
3. 泛型类的实际应用
如果有一个结点类:
public class Node {
private int data;
private Node next;
public void setData(int data) {
this.data = data;
}
public int getData() {
return data;
}
// 其他的setter getter,以及链表的功能,比如get remove等等
}
这个节点类只能够存储int
类型的数据
但是如果我想要一个能储存double
类型的结点,我就需要重写一遍这个代码,同时把所有的代码都复制一次(比如get
/remove
等等),这么麻烦就只为了把data
还有get/set
方法改成double
类型。现在使用泛型就可以很好地解决这个问题:
public class Node<T> { // 声明为泛型类
// 使用泛型替代原有的数据类型
T data;
Node next;
// 使用泛型替代原有的数据类型
public void setData(T data) {
this.data = data;
}
public T getData() {
return data;
}
// 其他的setter getter,以及链表的功能,比如get remove等等
}
此时的泛型结点,不仅支持基本数据类型(的包装类型),还支持自定义的类对象:
严格来说,泛型不支持基本数据类型,如
int/float/double
等等,不过可以转换成包装类型Integer/Float/Double
。
具体原因请看:《Java核心技术卷I》泛型篇笔记(三) 泛型的特性和局限
例如对于员工Employee
类,可以使用Node<Employee> node = new Node<>();
来创建员工类的结点,进而拓展到员工链表。这样子,每次拥有一个新的类,要一个类型的链表时,不必完全复制一次原代码,就可以轻松完成需求。
4. 泛型方法
声明泛型方法
如果泛型方法不在泛型类中声明,则还需要在方法处声明泛型:
public static <T> T getMiddle(T... a) {}
注意,在非泛型类中,使用泛型T
作为返回值之前,需要先用<T>
声明泛型方法。
实际上,只要不是在泛型类中,每个用到泛型的方法的地方都要先使用<T>
声明泛型:
public class Test {
// 不是泛型类
// 返回值为void,但是参数用到了泛型,所以方法声明中需要先写上<T>
public <T> void set(T t) {...}
}
在C++中,需要将类型参数放在方法名之后,这样可能会导致解析的二义性,例如:
getMiddle(fun<a,b>(c))
可以理解为“用fun<a, b>(c)
的结果调用getMiddle
”,或者理解为“用两个布尔值fun<a
和b>(c)
调用getMiddle
”
调用泛型方法
在调用一个泛型方法之前,可以把具体的类型包围在建括号中,放在方法前,也可以省略:
String middle = ArrayAlg.<String>getMiddle("John", "Q.", "Public");
String middle = ArrayAlg.getMiddle("John", "Q.", "Public");
有些时候,如果参数或返回值会引起歧义,编译器可能会报错:
double middle = ArrayAlg.getMiddle(3.14, 123, 0); // 出错
上面这句代码,编译器将把参数自动装箱为一个Double
和2个Integer
对象,然后寻找这些类共同的父类:Number
和Comparable
接口。此时的补救措施是将所以参数都定义为double
型
泛型方法的使用:
public class GenericMethodTest {
// 泛型方法,E表示Element,其实你写成T也是可以的,T表示Type
public static <E> void printArray(E[] inputArray) {
// 输出数组元素
for (E element : inputArray){
System.out.printf("%s ", element);
}
System.out.println();
}
public static void main( String args[]) {
// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = {1, 2, 3, 4, 5};
Double[] doubleArray = {1.1, 2.2, 3.3, 4.4};
Character[] charArray = {'H', 'E', 'L', 'L', 'O'};
System.out.println("Array integerArray contains:");
printArray(intArray); // 传递一个整型数组
System.out.println("\nArray doubleArray contains:");
printArray(doubleArray); // 传递一个双精度型数组
System.out.println("\nArray characterArray contains:");
printArray(charArray); // 传递一个字符型型数组
}
}
类型变量的限定
限定类型变量继承于某个类或接口(规定上界):
public static <T extends Comparable> T min(T[] a)
一个类型变量或通配符可以有多个限定,但最多只能有一个类限定,且它必须是限定列表的第一个限定类型,限定类型使用&
分隔,而泛型使用,
分隔:
T extends Comparable & Serializable