什么是泛型?
泛型程序设计(generic programming)是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。各种程序设计语言和其编译器、运行环境对泛型的支持均不一样。Ada、Delphi、Eiffel、Java、C#、F#、Swift) 和 Visual Basic .NET 称之为泛型(generics);ML、Scala 和 Haskell 称之为参数多态(parametric polymorphism);C++ 和 D称之为模板)。具有广泛影响的1994年版的《Design Patterns》一书称之为参数化类型(parameterized type)。
为什么使用泛型?
泛型的程序设计意味着编写的代码可以被很多不同类型的对象所重用。
在 Java 中增加泛型类之前,泛型程序设计是用继承实现的。那时的ArrayList
类只维护一个Object
引用的数组:1
2
3
4
5
6// 未使用泛型时
public class ArrayList{
private Object[] elementData;
public Object get(int i) { . . , }
public void add(Object o) { . . . }
}
为什么后来使用泛型来代替该方法呢?这是因为其存在下面两个问题:
① 当获取一个值时必须进行强制类型转换:
1
2ArrayList files = new ArrayList();
String filename = (String) files.get(0);② 因为没有错误检査,所以可以向数组列表中添加任何类的对象:
1
files.add(new File("...");
对于这个调用,编译和运行都不会出错。 然而在其他地方,比如将 ② 处代码get
的结果强制类型转换为String
类型, 就会产生错误。
而泛型提供了一个更好的解决方案:类型参数。
现在ArrayList
类有一个类型参数用来指示元素的类型:
1 | ArrayList<String> files = new ArrayList<>(); |
可以看到:使用类型参数后带来如下好处:
- 代码具有更好的可读性:人们一看就知道这个数组列表中包含的是
String
对象 - 编译器错误检查:
add
方法加入错误的对象时是无法通过编译的,这就避免插人错误类型的对象 - 无需强制类型转换:当调用
get
方法时, 编译器知道返回值类型为String
, 而不是Object
简而言之:类型参数(泛型) 使得程序具有更好的可读性和安全性。
泛型的定义
定义简单泛型类
一个泛型类(generic class)就是具有一个或多个类型变量的类。
泛型类:在类的后面加上尖括号<>
且在其中引入一个类型变量(一般用大写字母表示),示例如下:
1 | public class Thing<T> { |
上面我们定义一个具有一个类型变量的泛型类,不仅如此,泛型类还可以有多个类型变量。 其中第一个变量和第二个变量使用不同的类型:1
public class Thing<E,V> {}
常见多个类型的泛型类有映射类Map<K,V>
及它许多的许多子类。
类定义中的类型变量会指定方法的返回类型以及变量和局部变量的类型。 例如:1
private T someThing;
当使用该类时,可以将T
替换为具体的类型,比如将T
替换为String
,如Thing<String>
。
你可以将替换后的结果想象为带有构造器和方法的普通类:
1 | public Thing(String someThing, String anotherThing) { |
换句话说,泛型类可看作普通类的工厂。
泛型接口
泛型接口的定义和泛型类差不多,略。
泛型方法
既然类可以定义为泛型类,那么类中的方法也可以定义为泛型方法。而且泛型方法不仅可以定义在泛型类中,还可以定义普通类在中。
如何定义泛型方法?
定义泛型方法的格式如下:
- 修饰符 <泛型> 返回值类型 方法名(参数列表(使用泛型)){方法体}
下面为一个示例:1
2
3
4
5public claa Test{
public static <T> T getNumber(T t){
return t.length();
}
}
注意哦
: 类型变量需要放在修饰符(此处为public static
) 的后面,返回类型的前面。
如何调用泛型方法?
调用一个泛型方法时,曾经需要在方法名前的尖括号中放入具体的类型:1
Inter a = Test.<String>getNumber("asdbc");
当然,如今的优化使得类型变量String
可以省略,编译器能自动识别,因此可简写为:1
Inter a = Test.getNumber("asdbc");
类型变量的限定
有时候,类或方法需要对类型变量加以约束。
例子
假设现在要计算数组中的最小元素:1
2
3
4
5
6
7
8
9class ArrayMin {
public static <T> T getMin(T[] a) {
if (a null || a.length = 0)return null;
T smallest = a[0];
for (int i = 1; i < a.length; i++)
if (smallest.compareTo(a[i]) > 0) smallest = a[i];
return smallest;
}
}
变量smallest
类型为 T , 这意味着它可以是任何一个类的对象,可是并不是所有类型都是能互相比较大小的(比如我们自定义的一个Person
类)。
问题
那么,我们如何比较一个对象的大小?
在 Java 中,存在一个Comparable
接口,重写它的compareTo
方法就可以比较对象大小。
那么,怎么才能确信 T 所属的类有这个compareTo
方法呢?
解答
我们可以这么做:1
public static <T extends Comparable<E>> T getMin(T[] a) {}
我们将 T 限制为实现了Comparable
接口,也就是说,现在,泛型的min
方法只能被实现了Comparable
接口的类(如String
、LocalDate
等)的数组调用,其他未实现该接口的类调用该方法将会报错。
读者或许会感到奇怪,在此为什么使用关键字extends
而不是implements
?毕竟,Comparable
是一个接口而不是一个类。
选择关键字extends
的原因在于其更接近子类的概念,且 Java 的设计者也不打算在语言中再添加一个新的关键字(如 sub)。
下面的记法:
1 | <T extends BoundingType> |
表示 T 应该是绑定类型的子类型。T 和绑定类型可以是类,也可以是接口。
并且,一个类型变量或通配符可以有多个限定, 例如:
1 | T extends Comparable & Serializable |
限定类型用符号&
分隔, 而逗号用来分隔类型变量。
在 Java 的继承中,可以根据需要拥有多个接口超类型, 但限定中至多有一个类。 而且如果用一个类作为限定,它必须是限定列表中的第一个。
泛型通配符
当使用泛型类或接口时,传递的数据中,若泛型类型不确定,可通过通配符<?>
来表示。
注意哦
:一旦使用泛型的通配符后,只能使用 Object 类中的方法,集合中元素的自身方法将无法使用。
通配符基本使用
泛型的通配符:不知道使用什么类型来接收的时可以使用?
来表示位置通配符。
此时只能接收数据,而不能往该集合中存储数据。举个栗子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34/**
* 泛型通配符:?代表任意的数据类型
* 使用方式:只能作为方法的参数使用,不能作为对象使用
*
*/
public void test() {
ArrayList<String> list1 = new ArrayList<>();
list1.add("a");
list1.add("b");
list1.add("c");
ArrayList<Integer> list2 = new ArrayList<>();
list2.add(1);
list2.add(2);
list2.add(3);
print(list1);
print(list2);
}
/**
* 遍历集合中的数据
* 这时候我们不知道 ArrayList 使用什么类型数据,可以使用泛型的通配符?来接受数据类型
*
* @param list
*/
public void print(ArrayList<?> list) {
Iterator<?> iterator = list.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
}
注意:泛型不存在继承关系,所以若ArrayList<?>
这么写是错误的:ArrayList<Object>
;
通配符高级使用–受限泛型
Java 的泛型可以指定一个泛型的上限和下限:
① 泛型的上限:
- 格式:类型名称
<? extends 类 >
对象名称 - 限制:只能接受该类型及其子类
② 泛型的下限:
- 格式:类型名称
<? super 类 >
对象名称 - 限制:只能接受该类型及其父类
举个栗子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30/**
* 类之间的继承关系:
* Integer extends Number extends Object
* String extends Object
*/
public void constraint() {
ArrayList<String> strings = new ArrayList<>();
ArrayList<Integer> integers = new ArrayList<>();
ArrayList<Number> numbers = new ArrayList<>();
ArrayList<Object> objects = new ArrayList<>();
getElement1(strings); // 报错,不是 Number 或其子类
getElement1(integers);
getElement1(numbers);
getElement1(objects); // 报错,不是 Number 或其子类
getElement2(strings); // 报错,不是 Number 或其父类
getElement2(integers); // 报错,不是 Number 或其父类
getElement2(numbers);
getElement2(objects);
}
// 泛型的上限:此时的 ? 必须是 Number 或 Number 的子类
public void getElement1(Collection<? extends Number> collection) {
}
// 泛型的下限:此时的 ? 必须是 Number 或 Number 的父类
public void getElement2(Collection<? super Number> collection) {
}
<T> VS <?>
不同点:
<T>
用于 泛型的定义,例如class MyGeneric<T> {...}
<?>
用于 泛型的声明,即泛型的使用,例如MyGeneric<?> g = new MyGeneric<>();
相同点:都可以指定上界和下界
泛型擦除
Java 的泛型是在编译器层次实现的。
在编译生成的字节码中不包含泛型中的类型参数,类型参数会在编译时去掉。
例如:List<String>
和 List<Integer>
在编译后都变成 List
。
类型擦除的基本过程:将代码中的类型参数替换为具体的类,同时去掉 <>
的内容。
参考
- Cay S. Horstmann. Java 核心技术卷一 [M]. 机械工业出版社,2016