泛型
本文为书籍《Java编程的逻辑》1和《剑指Java:核心原理与应用实践》2阅读笔记
Java
语言是一门强类型的编程语言,即在编写时就必须明确变量的类型,否则编译不通过。Java
语言的多态特性让我们可以把某些只能在运行时确定的类型在编译时使用父类或父接口表示,这确实解决了很多问题。但是有时程序员在声明某些变量时不知道它的具体父类或父接口,只能无奈地选择公共父类Object
类型,这很不方便,为了解决这个问题,JDK 1.5
引入了泛型的概念,让我们在程序中可以用某种方式表示完全未知的类型,使得程序顺利编写并通过编译,等到使用时再确定它的具体类型。
1.1 泛型类使用案例
package com.ieening.learnGenricConatiner;
public class Pair<T> {
private T first;
private T second;
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public T getSecond() {
return second;
}
}
Pair
就是一个泛型类,与普通类的区别体现在:
- 类名后面多了一个
<T>
; first
和second
的类型都是T
。
T
是什么呢?T
表示类型参数,泛型就是类型参数化,处理的数据类型不是固定的,而是可以作为参数传入。怎么用这个泛型类,并传递类型参数呢?看代码:
@Test
public void testPairInteger() {
int first = 1;
int second = 2;
Pair<Integer> minmax = new Pair<Integer>(first, second);
assertTrue(first == minmax.getFirst());
assertTrue(second == minmax.getSecond());
}
Pair<Integer>
中的Integer
就是传递的实际类型参数。Pair
类的代码和它处理的数据类型不是绑定的,具体类型可以变化。上面是Integer
,也可以是String
,比如:
@Test
public void testPairString() {
String first = "name";
String second = "ieening";
Pair<String> minmax = new Pair<String>(first, second);
assertTrue(first == minmax.getFirst());
assertTrue(second == minmax.getSecond());
}
类型参数可以有多个,Pair
类中的first
和second
可以是不同的类型,多个类型之间以逗号分隔,来看改进后的Pair
类定义:
public class PairTwoGenric<U, V> {
private U first;
private V second;
public PairTwoGenric(U first, V second) {
this.first = first;
this.second = second;
}
public U getFirst() {
return first;
}
public V getSecond() {
return second;
}
}
可以这样使用:
@Test
public void testPairTwoGenric() {
String first = "age";
int second = 28;
PairTwoGenric<String, Integer> pairTwoGenric = new PairTwoGenric<String, Integer>(first, second);
assertTrue(first == pairTwoGenric.getFirst());
assertTrue(second == pairTwoGenric.getSecond());
}
<String,Integer>
既出现在了声明变量时,也出现在了new
后面,比较烦琐,从Java 7
开始,支持省略后面的类型参数,可以如下使用:
PairTwoGenric<String, Integer> pairTwoGenric = new PairTwoGenric<>(first, second);
1.2 基本原理
泛型类型参数到底是什么呢?为什么一定要定义类型参数呢?定义普通类,直接使用Object
不就行了吗?比如,Pair
类可以写为:
package com.ieening.learnGenricConatiner;
public class PairObject {
private Object first;
private Object second;
public PairObject(Object first, Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public Object getSecond() {
return second;
}
}
使用Pair
的代码可以为:
@Test
public void testPairObject() {
String first = "age";
int second = 28;
PairObject pairObject = new PairObject(first, second);
assertTrue(first == (String) pairObject.getFirst());
assertTrue(second == (Integer) pairObject.getSecond());
}
这样是可行的。实际上,Java
泛型的内部原理就是这样的。我们知道,Java
有Java
编译器和Java
虚拟机,编译器将Java
源代码转换为.class
文件,虚拟机加载并运行.class
文件。对于泛型类,Java
编译器会将泛型代码转换为普通的非泛型代码,就像上面的普通Pair
类代码及其使用代码一样,将类型参数T
擦除,替换为Object
,插入必要的强制类型转换。Java
虚拟机实际执行的时候,它是不知道泛型这回事的,只知道普通的类及代码。
再强调一下,Java
泛型是通过擦除实现的,类定义中的类型参数如T
会被替换为Object
,在程序运行过程中,不知道泛型的实际类型参数,比如Pair<Integer>
,运行中只知道Pair
,而不知道Integer
。认识到这一点是非常重要的,它有助于我们理解Java
泛型的很多限制。Java
为什么要这么设计呢?泛型是Java 5
以后才支持的,这么设计是为了兼容性而不得已的一个选择。
1.3 泛型接口使用案例
接口也可以是泛型的,比如,Comparable
接口都是泛型的,它们的代码如下:
public interface Comparable<T> {
public int compareTo(T o);
}
与前面一样,T
是类型参数。实现接口时,应该指定具体的类型,比如,对Integer
类,实现代码是:
public final class Integer extends Number implements Comparable<Integer>, Constable, ConstantDesc {
......
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
......
}
通过implements Comparable<Integer>
,Integer
实现了Comparable
接口,指定了实际类型参数为Integer
,表示Integer
只能与Integer
对象进行比较。
1.4 泛型方法
除了类和接口,方法也可以使用泛型,
public static <T> int indexOf(T[] arr, T elm){
for(int i=0; i<arr.length; i++){
if(arr[i].equals(elm)){
return i;
}
}
return -1;
}
使用如下所示:
@Test
public void testIndexOf() {
assertTrue(-1 == indexOf(new Integer[] { 1, 3, 5 }, 10));
assertTrue(0 == indexOf(new String[] { "hello", "你好", "嘛" }, "hello"));
}
1.5 类型参数的限定
无论是泛型类、泛型方法还是泛型接口,关于类型参数,我们都知之甚少,只能把它当作Object
,但Java
支持限定这个参数的一个上界,也就是说,参数必须为给定的上界类型或其子类型,这个限定是通过extends
关键字来表示的。这个上界可以是某个具体的类或者某个具体的接口,也可以是其他的类型参数。
1.5.1 上界为某个具体类
如下面NumberPair
,规定两个类型参数必须为Number
,代码如下:
package com.ieening.learnGenricConatiner;
public class NumberPair<U extends Number, V extends Number> extends PairTwoGenric<U, V> {
public NumberPair(U first, V second) {
super(first, second);
}
public double sum() {
return getFirst().doubleValue() + getSecond().doubleValue();
}
}
限定类型后,就可以使用该类型的方法了。比如,对于NumberPair
类,first
和second
变量就可以当作Number
进行处理了。比如定义的求和方法sum
,使用方法如下:
@Test
public void testNumberPairSum() {
int first = 12;
double second = 14.0;
NumberPair<Integer, Double> numberPair = new NumberPair<>(first, second);
assertTrue((first + second) == numberPair.sum());
}
限定类型后,如果类型使用错误,编译器会提示。指定边界后,类型擦除时就不会转换为Object
了,而是会转换为它的边界类型。
1.5.2 上界为某个接口
在泛型方法中,一种常见的场景是限定类型必须实现Comparable
接口,我们来看代码:
public static <T extends Comparable<T>> T max(T[] arr) {
if (Objects.isNull(arr) || arr.length == 0) {
return null;
}
T max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i].compareTo(max) > 0) {
max = arr[i];
}
}
return max;
}
@Test
public void testComparableMax() {
assertTrue(7.1 == max(new Double[] { 1.2, 4.3, 0.9, 7.1 }));
}
max
方法计算一个泛型数组中的最大值。计算最大值需要进行元素之间的比较,要求元素实现Comparable
接口,所以给类型参数设置了一个上边界Comparable
,T
必须实现Comparable
接口。<T extends Comparable<T>>
是一种令人费解的语法形式,这种形式称为递归类型限制,可以这么解读:T
表示一种数据类型,必须实现Comparable
接口,且必须可以与相同类型的元素进行比较。
1.5.3 上界为其他类型参数
上面的限定都是指定了一个明确的类或接口,Java
支持一个类型参数以另一个类型参数作为上界。为什么需要这个呢?我们看个例子,给定一个DynamicArray
类,代码如下所示:
package com.ieening.learnGenricConatiner;
import java.util.Arrays;
public class DynamicArray<E> {
private static final int DEFAULT_CAPACITY = 10;
private int size;
private Object[] elementData;
public DynamicArray() {
this.elementData = new Object[DEFAULT_CAPACITY];
}
private void ensureCapacity(int minCapacity) {
int oldCapacity = elementData.length;
if (oldCapacity >= minCapacity) {
return;
}
int newCapacity = oldCapacity * 2;
if (newCapacity < minCapacity)
newCapacity = minCapacity;
elementData = Arrays.copyOf(elementData, newCapacity);
}
public void add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
}
@SuppressWarnings("unchecked")
public E get(int index) {
return (E) elementData[index];
}
public int size() {
return size;
}
public E set(int index, E element) {
E oldValue = get(index);
elementData[index] = element;
return oldValue;
}
}
增加一个实例方法addAll
,这个方法将参数容器中的所有元素都添加到当前容器里来,直觉上,代码可以如下书写:
public void addAll(DynamicArray<E> addedDynamicArray) {
for (int i = 0; i < addedDynamicArray.size(); i++) {
add(addedDynamicArray.get(i));
}
}
但这么写有一些局限性,我们看使用它的代码:
DynamicArray<Number> numbers = new DynamicArray<>();
DynamicArray<Integer> ints = new DynamicArray<>();
ints.add(100);
ints.add(34);
numbers.addAll(ints);
numbers
是一个Number
类型的容器,ints
是一个Integer
类型的容器,我们希望将ints
添加到numbers
中,因为Integer
是Number
的子类,应该说,这是一个合理的需求和操作。但Java
会在numbers.addAll(ints)
这行代码上提示编译错误:The method addAll(DynamicArray<Number>) in the type DynamicArray<Number> is not applicable for the arguments (DynamicArray<Integer>)
。addAll
需要的参数类型为DynamicArray<Number>
,而传递过来的参数类型为DynamicArray<Integer>
,不适用。Integer
是Number
的子类,怎么会不适用呢?
事实就是这样,确实不适用,而且是很有道理的,假设适用,我们看下会发生什么。
DynamicArray<Integer> ints = new DynamicArray<>();
DynamicArray<Number> numbers = ints; //假设这行是合法的
numbers.add(new Double(12.34));
那最后一行就是合法的,这时,DynamicArray<Integer>
中就会出现Double
类型的值,而这显然破坏了Java
泛型关于类型安全的保证。
虽然Integer
是Number
的子类,但DynamicArray<Integer>
并不是DynamicArray<Number>
的子类,DynamicArray<Integer>
的对象也不能赋值给DynamicArray<Number>
的变量,这一点初看上去是违反直觉的,但这是事实,必须要理解这一点。不过,我们的需求是合理的,将Integer
添加到Number
容器中并没有问题。这个问题可以通过类型限定来解决:
public <T extends E> void addAll(DynamicArray<T> addedDynamicArray) {
for (int i = 0; i < addedDynamicArray.size(); i++) {
add(addedDynamicArray.get(i));
}
}
E
是DynamicArray
的类型参数,T
是addAll
的类型参数,T
的上界限定为E
,这样,下面的代码就没有问题了:
@Test
public void testDynamicArrayAddAll(){
DynamicArray<Number> numbers = new DynamicArray<>();
DynamicArray<Integer> ints = new DynamicArray<>();
ints.add(100);
ints.add(34);
numbers.addAll(ints);
assertArrayEquals(numbers.getElementData(), ints.getElementData());
}
1.6 通配符
1.6.1 无限定通配符 <?>
public static int indexOf(DynamicArray<?> arr, Object elm) {
for (int i = 0; i < arr.size(); i++) {
if (arr.get(i).equals(elm)) {
return i;
}
}
return -1;
}
形如DynamicArray<?>
称为无限定通配符,其实 public static int indexOf(DynamicArray<? > arr, Object elm)
和public static <T> int indexOf(DynamicArray<T> arr, Object elm)
是等价的。
1.6.2 有限定通配符 <? extends E>
我们改写DynamicArray
中addAll
方法,代码如下:
public void addAll(DynamicArray<? extends E> c) {
for(int i=0; i<c.size; i++){
add(c.get(i));
}
}
这个方法没有定义类型参数,c
的类型是DynamicArray<? extends E>
,?
表示通配符,<? extends E>
表示有限定通配符,匹配E
或E
的某个子类型,具体什么子类型是未知的。使用这个方法的代码不需要做任何改动,还可以是:
@Test
public void testDynamicArrayAddAll(){
DynamicArray<Number> numbers = new DynamicArray<>();
DynamicArray<Integer> ints = new DynamicArray<>();
ints.add(100);
ints.add(34);
numbers.addAll(ints);
assertArrayEquals(numbers.getElementData(), ints.getElementData());
}
这里,E
是Number类型,DynamicArray<? extends E>
可以匹配DynamicArray<Integer>
。那么问题来了,同样是extends
关键字,同样应用于泛型,<T extends E>
和<?extends E>
到底有什么关系?它们用的地方不一样,我们解释一下:
<T extends E>
用于定义类型参数,它声明了一个类型参数T
,可放在泛型类定义中类名后面、泛型方法返回值前面。<? extends E>
用于实例化类型参数,它用于实例化泛型变量中的类型参数,只是这个具体类型是未知的,只知道它是E
或E
的某个子类型。
虽然它们不一样,但两种写法经常可以达成相同目标,比如,前面例子中,下面两种写法都可以:
public void addAll(DynamicArray<? extends E> c)
public <T extends E> void addAll(DynamicArray<T> c)
1.6.3 无限定通配符和有限定通配符限制
无限定通配符和有限定通配符限制有一个重要的限制:只能读,不能写。比如下面的代码:
DynamicArray<Integer> ints = new DynamicArray<>();
DynamicArray<? extends Number> numbers = ints;
Integer a = 200;
numbers.add(a); //错误!
numbers.add((Number)a); //错误!
numbers.add((Object)a); //错误!
三种add
方法都是非法的,无论是Integer
,还是Number
或Object
,编译器都会报错。为什么呢?答案就是表示类型安全无知,? extends Number
表示是Number
的某个子类型,但不知道具体子类型,如果允许写入,Java就无法确保类型安全性,所以干脆禁止。我们来看个例子,看看如果允许写入会发生什么:
DynamicArray<Integer> ints = new DynamicArray<>();
DynamicArray<? extends Number> numbers = ints;
Number n = new Double(23.0);
Object o = new String("hello world");
numbers.add(n);
numbers.add(o);
如果允许写入Object
或Number
类型,则最后两行编译就是正确的,也就是说,Java
将允许把Double
或String
对象放入Integer
容器,这显然违背了Java
关于类型安全的承诺。
大部分情况下,这种限制是好的,但这使得一些理应正确的基本操作无法完成,比如交换两个元素的位置,看如下代码:
public static void swap(DynamicArray<?> arr, int i, int j) {
Object tmp = arr.get(i);
arr.set(i, arr.get(j));
arr.set(j, tmp);
}
这个代码看上去应该是正确的,但Java
会提示编译错误,两行set
语句都是非法的。不过,借助带类型参数的泛型方法,这个问题可以如下解决:
private static <T> void swapInternal(DynamicArray<T> arr, int i, int j) {
T tmp = arr.get(i);
arr.set(i, arr.get(j));
arr.set(j, tmp);
}
public static void swap(DynamicArray<?> arr, int i, int j) {
swapInternal(arr, i, j);
}
swap
可以调用swapInternal
,而带类型参数的swapInternal
可以写入。Java
容器类中就有类似这样的用法,公共的API
是通配符形式,形式更简单,但内部调用带类型参数的方法。
除了这种需要写的场合,如果参数类型之间有依赖关系,也只能用类型参数,比如,将src
容器中的内容复制到dest
中:
public static <D, S extends D> void copy(DynamicArray<D> dest, DynamicArray<S> src) {
for (int i = 0; i < src.size(); i++) {
dest.add(src.get(i));
}
}
S
和D
有依赖关系,要么相同,要么S
是D
的子类,否则类型不兼容,有编译错误。不过,上面的声明可以使用通配符简化,两个参数可以简化为一个,如下所示:
public static <D> void copy(DynamicArray<D> dest, DynamicArray<? extends D> src) {
for (int i = 0; i < src.size(); i++) {
dest.add(src.get(i));
}
}
如果返回值依赖于类型参数,也不能用通配符,比如,计算动态数组中的最大值,如下所示:
public static <T extends Comparable<T>> T max(DynamicArray<T> arr) {
T max = arr.get(0);
for (int i = 1; i < arr.size(); i++) {
if (arr.get(i).compareTo(max) > 0) {
max = arr.get(i);
}
}
return max;
}
上面的代码就难以用通配符代替。
现在我们再来看泛型方法到底应该用通配符的形式还是加类型参数。两者到底有什么关系?我们总结如下。
- 通配符形式都可以用类型参数的形式来替代,通配符能做的,用类型参数都能做。
- 通配符形式可以减少类型参数,形式上往往更为简单,可读性也更好,所以,能用通配符的就用通配符。
- 如果类型参数之间有依赖关系,或者返回值依赖类型参数,或者需要写操作,则只能用类型参数。
- 通配符形式和类型参数往往配合使用,比如,上面的
copy
方法,定义必要的类型参数,使用通配符表达依赖,并接受更广泛的数据类型。
1.6.4 超类型通配符<? super E>
<?>
和<? extends T>
对写入都有很大的限制,但是当我们需要写入时,该怎么办呢?答案就是,使用超类型通配符(<? super E>
),表示E
的某个父类型。
我们先来看一段代码:
public void copyTo(DynamicArray<E> dest) {
for (int i = 0; i < size; i++) {
dest.add(get(i));
}
}
public static void main(String[] args) {
DynamicArray<Integer> ints = new DynamicArray<Integer>();
ints.add(100);
ints.add(34);
DynamicArray<Number> numbers = new DynamicArray<Number>();
ints.copyTo(numbers);
}
copyTo
代码很简单,将当前容器中的元素添加到传入的目标容器中。main
函数中,Integer
是Number
的子类,将Integer
对象拷贝入Number
容器,这种用法应该是合情合理的,但Java
会提示编译错误,为什么呢?原因是,期望的参数类型是DynamicArray<Integer>
,DynamicArray<Number>
并不适用。如之前所说,一般而言,不能将DynamicArray<Integer>
看作DynamicArray<Number>
,但我们这里的用法是没有问题的,Java
解决这个问题的方法就是超类型通配符,可以将copyTo
代码改为:
public void copyTo(DynamicArray<? super E> dest) {
for (int i = 0; i < size; i++) {
dest.add(get(i));
}
}
这样,就没有问题了。
超类型通配符另一个常用的场合是Comparable/Comparator
接口。同样,我们先来看下如果不使用会有什么限制。以前面计算最大值的方法为例,它的方法声明是:
public static <T extends Comparable<T>> T max(DynamicArray<T> arr)
这个声明有什么限制呢?举个简单的例子,有两个类Base
和Child
,Base
的代码是:
package com.ieening.learnGenricConatiner;
public class Base implements Comparable<Base> {
private int sortOrder;
public Base(int sortOrder) {
this.sortOrder = sortOrder;
}
@Override
public int compareTo(Base o) {
if (sortOrder < o.sortOrder) {
return -1;
} else if (sortOrder > o.sortOrder) {
return 1;
} else {
return 0;
}
}
}
Base
代码很简单,实现了Comparable
接口,根据实例变量sortOrder
进行比较。Child
代码是:
package com.ieening.learnGenricConatiner;
public class Child extends Base{
public Child(int sortOrder) {
super(sortOrder);
}
}
这里,Child
同样简单,只是继承了Base
。注意:Child
没有重新实现Comparable
接口,因为Child
的比较规则和Base
是一样的。我们可能希望使用前面的max
方法操作Child
容器,如下所示:
DynamicArray<Child> childs = new DynamicArray<Child>();
childs.add(new Child(20));
childs.add(new Child(80));
Child maxChild = max(childs);
遗憾的是,Java
会提示编译错误,类型不匹配。为什么不匹配呢?我们可能会认为,Java
会将max
方法的类型参数T
推断为Child
类型,但类型T
的要求是extends Comparable<T>
,而Child
并没有实现Comparable<Child>
,它实现的是Comparable<Base>
。
但我们的需求明显是合理的,Base
类的代码已经有了关于比较所需要的全部数据,它应该可以用于比较Child
对象。解决这个问题的方法,就是修改max
的方法声明,使用超类型通配符,如下所示:
public static <T extends Comparable<? super T>> T max(DynamicArray<T> arr)
这么修改一下就可以了,这种写法比较抽象,将T
替换为Child
,就是:Child extends Comparable<? super Child>
,<? super Child>
可以匹配Base
,所以整体就是匹配的。
我们比较一下类型参数限定与超类型通配符,类型参数限定只有extends
形式,没有super
形式,比如,前面的copyTo
方法的通配符形式的声明为:
public void copyTo(DynamicArray<? super E> dest)
如果类型参数限定支持super
形式,则应该是:
public <T super E> void copyTo(DynamicArray<T> dest)
事实是,Java
并不支持这种语法。前面我们说过,对于有限定的通配符形式<? extends E>
,可以用类型参数限定替代,但是对于类似上面的超类型通配符,则无法用类型参数替代。
1.6.5 通配符比较
本节介绍了泛型中的三种通配符形式<? >
、<? super E>
和<? extends E>
,并分析了与类型参数形式的区别和联系,它们比较容易混淆,我们总结比较如下:
- 它们的目的都是为了使方法接口更为灵活,可以接受更为广泛的类型。
<? super E>
用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象,它不能被类型参数形式替代。<? >
和<? extends E>
用于灵活读取,使得方法可以读取E
或E
的任意子类型的容器对象,它们可以用类型参数的形式替代,但通配符形式更为简洁。
Java
容器类的实现中,有很多使用通配符的例子,比如,类Collections
中就有如下方法:
public static <T extends Comparable<? super T>> void sort(List<T> list)
public static <T> void sort(List<T> list, Comparator<? super T> c)
public static <T> void copy(List<? super T> dest, List<? extends T> src)
public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp)
1.7 泛型细节和局限性
Java
中,泛型是通过类型擦除来实现的,类型参数在编译时会被替换为Object
,运行时Java
虚拟机不知道泛型这回事,这带来了很多局限性。
1.7.1 基本类型不能用于实例化类型参数
Java
中,因为类型参数会被替换为Object
,所以Java
泛型中不能使用基本数据类型,也就是说,类似下面的写法是不合法的:
Pair<int> minmax = new Pair<int>(1,100);
解决方法是使用基本类型对应的包装类。
1.7.2 运行时类型信息不适用于泛型
在内存中每个类都有一份类型信息,而每个对象也都保存着其对应类型信息的引用。在Java
中,这个类型信息也是一个对象,它的类型为Class
,Class
本身也是一个泛型类,每个类的类型对象可以通过<类名>.class
的方式引用,比如String.class
、Integer.class
。这个类型对象也可以通过对象的getClass()
方法获得,比如:
Class<?> cls = "hello".getClass();
这个类型对象只有一份,与泛型无关,所以Java
不支持类似如下写法:
Pair<Integer>.class
一个泛型对象的getClass
方法的返回值与原始类型对象也是相同的,比如,下面代码的输出都是true
:
@Test
public void testGenricClass() {
Pair<Integer> p1 = new Pair<Integer>(1, 100);
Pair<String> p2 = new Pair<String>("hello", "world");
assertTrue(Pair.class == p1.getClass());
assertTrue(Pair.class == p2.getClass());
}
instanceof
关键字后面是接口或类名,instanceof
是运行时判断,也与泛型无关,所以,Java
也不支持类似如下写法:
if(p1 instanceof Pair<Integer>)
不过,Java
支持如下写法:
if(p1 instanceof Pair<? >)
1.7.3 类型擦除可能会引发一些冲突
比如上面Child
代码,如果我们想自定义Comaprable
接口,这时,根据直觉,写出下面的代码:
public class Child extends Base implements Comparable<Child>
这时,编译器,却提示:public class Child extends Base implements Comparable<Child>
,Comparable
接口不能被实现两次,且两次实现的类型参数还不同,一次是Comparable<Base>
,一次是Comparable<Child>
。为什么不允许呢?因为类型擦除后,实际上只能有一个。那Child
有什么办法修改比较方法呢?只能是重写Base
类的实现。
另外,你可能认为可以如下定义重载方法:
public static void test(DynamicArray<Integer> intArr)
public static void test(DynamicArray<String> strArr)
虽然参数都是DynamicArray
,但实例化类型不同,一个是DynamicArray<Integer>
,另一个是DynamicArray<String>
,同样,遗憾的是,Java
不允许这种写法,理由同样是类型擦除后它们的声明是一样的。
1.7.4 不能通过类型参数创建对象
比如,T
是类型参数,下面的写法都是非法的:
T elm = new T();
T[] arr = new T[10];
为什么非法呢?因为如果允许,那么用户会以为创建的就是对应类型的对象,但由于类型擦除,Java
只能创建Object
类型的对象,而无法创建T
类型的对象,容易引起误解,所以Java
干脆禁止这么做。那如果确实希望根据类型创建对象呢?需要设计API
接受类型对象,即Class
对象,并使用Java
中的反射机制。如果类型有默认构造方法,可以调用Class
的newInstance
方法构建对象,类似这样:
public static <T> T create(Class<T> type){
try {
return type.newInstance();
} catch (Exception e) {
return null;
}
}
使用代码如下:
Date date = create(Date.class);
StringBuilder sb = create(StringBuilder.class);
1.7.5 泛型类类型参数不能用于静态变量和静态方法
对于泛型类声明的类型参数,可以在实例变量和方法中使用,但在静态变量和静态方法中是不能使用的。类似下面这种写法是非法的:
public class Singleton<T> {
private static T instance;
public synchronized static T getInstance(){
if(instance==null){
//创建实例
}
return instance;
}
}
如果合法,那么对于每种实例化类型,都需要有一个对应的静态变量和方法。但由于类型擦除,Singleton
类型只有一份,静态变量和方法都是类型的属性,且与类型参数无关,所以不能使用泛型类类型参数。不过,对于静态方法,它可以是泛型方法,可以声明自己的类型参数,这个参数与泛型类的类型参数是没有关系的(一个是使用别人的泛型类型参数,一个是定义自己的再使用)。
1.7.6 多个类型限定的语法
之前介绍类型参数限定的时候,我们提到上界可以为某个类、某个接口或者其他类型参数,但上界都是只有一个,Java
中还支持多个上界,多个上界之间以&
分隔,类似这样:
T extends Base & Comparable & Serializable
Base
为上界类,Comparable
和Serializable
为上界接口。如果有上界类,类应该放在第一个,类型擦除时,会用第一个上界替换。
1.7.7 不能创建泛型数组
引入泛型后,我们会发现,不能创建泛型数组。比如,我们可能想这样创建一个Pair
的泛型数组:
PairTwoGenric<Object, Integer>[] options = new PairTwoGenric<Object, Integer>[] {
new Pair("1元", 7), new Pair("2元", 2), new Pair("10元", 1)
};
Java
会提示编译错误,Cannot create a generic array of PairTwoGenric<Object,Integer>
,不能创建泛型数组。这是为什么呢?我们先来进一步理解一下数组。前面我们解释过,类型参数之间有继承关系的容器之间是没有关系的,比如,一个DynamicArray<Integer>
对象不能赋值给一个DynamicArray<Number>
变量。不过,数组是可以的,看代码:
Integer[] ints = new Integer[10];
Number[] numbers = ints;
Object[] objs = ints;
后面两种赋值都是允许的。数组为什么可以呢?数组是Java
直接支持的概念,它知道数组元素的实际类型,知道Object
和Number
都是Integer
的父类型,所以这个操作是允许的。虽然Java
允许这种转换,但如果使用不当,可能会引起运行时异常,比如:
@Rule
public final ExpectedException exception = ExpectedException.none();
@Test
public void testArrayStoreException() {
Integer[] ints = new Integer[10];
Object[] objs = ints;
exception.expect(ArrayStoreException.class);
objs[0] = "hello";
}
}
编译是没有问题的,运行时会抛出ArrayStoreException
,因为Java
知道实际的类型是Integer
,所以写入String
会抛出异常。理解了数组的这个行为,我们再来看泛型数组。如果Java
允许创建泛型数组,则会发生非常严重的问题,我们看看具体会发生什么:
Pair<Object, Integer>[] options = new Pair<Object, Integer>[3];
Object[] objs = options;
objs[0] = new Pair<Double, String>(12.34, "hello");
如果可以创建泛型数组options
,那它就可以赋值给其他类型的数组objs
,而最后一行明显错误的赋值操作,则既不会引起编译错误,也不会触发运行时异常,因为Pair<Double, String>
的运行时类型是Pair
,和objs
的运行时类型Pair[]
是匹配的。但我们知道,它的实际类型是不匹配的,在程序的其他地方,当把objs[0]
作为Pair<Object, Integer>
进行处理的时候,一定会触发异常。也就是说,如果允许创建泛型数组,那就可能会有上面这种错误操作,它既不会引起编译错误,也不会立即触发运行时异常,却相当于埋下了一颗炸弹,不定什么时候爆发,为避免这种情况,Java
干脆就禁止创建泛型数组。
但现实需要能够存放泛型对象的容器,怎么办呢?可以使用原始类型的数组,比如:
Pair[] options = new Pair[]{
new Pair<String, Integer>("1元",7),
new Pair<String, Integer>("2元", 2),
new Pair<String, Integer>("10元", 1)};
前面我们自己实现的DynamicArray
,可以存放泛型对象,比如:
DynamicArray<Pair<String, Integer>> options = new DynamicArray<>();
options.add(new Pair<String, Integer>("1元",7));
options.add(new Pair<String, Integer>("2元",2));
options.add(new Pair<String, Integer>("10元",1));
DynamicArray
内部的数组为Object
类型,一些操作插入了强制类型转换,外部接口是类型安全的,对数组的访问都是内部代码,可以避免误用和类型异常。
1.7.8 泛型容器转换为数组
有时,我们希望转换泛型容器为一个数组,比如,对于DynamicArray
,我们可能希望它有这么一个方法:
public E[] toArray()
并且希望可以这么用:
DynamicArray<Integer> ints = new DynamicArray<Integer>();
ints.add(100);
ints.add(34);
Integer[] arr = ints.toArray();
先使用动态容器收集一些数据,然后转换为一个固定数组,这也是一个常见的合理需求,怎么来实现这个toArray
方法呢?可能想先这样:
E[] arr = new E[size];
显然,如之前所述,这是不合法的。Java
运行时根本不知道E
是什么,也就无法做到创建E
类型的数组。另一种想法是这样:
public E[] toArray(){
Object[] copy = new Object[size];
System.arraycopy(elementData, 0, copy, 0, size);
return (E[])copy;
}
或者使用Arrays
中copyOf
方法:
public E[] toArray(){
return (E[])Arrays.copyOf(elementData, size);
}
结果都是一样的,没有编译错误了,但运行时会抛出ClassCastException
异常,原因是Object
类型的数组不能转换为Integer
类型的数组。那怎么办呢?可以利用Java
中的运行时类型信息和反射机制。Java
必须在运行时知道要转换成的数组类型,类型可以作为参数传递给toArray
方法,比如:
public E[] toArray(Class<E> type){
Object copy = Array.newInstance(type, size);
System.arraycopy(elementData, 0, copy, 0, size);
return (E[])copy;
}
Class<E>
表示要转换成的数组类型信息,有了这个类型信息,Array
类的newInstance
方法就可以创建出真正类型的数组对象。调用toArray
方法时,需要传递需要的类型,比如,可以这样:
@Test
public void testToArray() {
Integer[] ints = new Integer[] { 1, 2, 3, 4, 5 };
DynamicArray<Integer> intDynamicArray = new DynamicArray<>();
for (int i : ints) {
intDynamicArray.add(i);
}
assertArrayEquals(ints, intDynamicArray.toArray(Integer.class));
}
我们来稍微总结下泛型与数组的关系:
Java
不支持创建泛型数组。- 如果要存放泛型对象,可以使用原始类型的数组,或者使用泛型容器。
- 泛型容器内部使用
Object
数组,如果要转换泛型容器为对应类型的数组,需要使用反射。