参数类型的好处
在 Java 引入泛型之前,泛型程序设计是用继承实现的。ArrayList 类只维护一个 Object 引用的数组:
public class ArrayList { // before generic classes
private Object[] elementData;
...
public Object get(int i) { ... }
public void add(Object o) { ... }
}
这种方法有两个问题:
- 当获取一个值时必须要进行强制类型转换
- 添加值时没有错误检查,可以向 ArrayList 中添加任何类的对象
ArrayList al = new ArrayList();
// 无法进行错误检查,File 对象可以添加进去,编译器和运行期都可以通过
al.add(new File());
String first = (String) al.get(0); // 类型转换失败导致运行失败
没有泛型的程序导致的后果:
- 程序的可读性有所降低,因为可以不受限制往集合中添加任意对象
- 程序的安全性遭到质疑,类型转换失败将导致程序运行失败
泛型提供了一个更好的解决方案:类型参数。ArrayList 类有一个类型参数用来指示元素的类型:
ArrayList<String> al = new ArrayList<String>();
在 Java 7 以及以后的版本中,构造函数中可以省略泛型类型:
ArrayList<String> al = new ArrayList<>();
省略的类型可以从变量的类型推断得出
编译器也可以很好地利用这个信息。当调用 get 的时候,不需要进行强制类型转换,编译器就知道返回值类型为 String,而不是 Object:
String s = al.get(0);
编译器还知道 ArrayList<String> 中 add 方法有一个类型为 String 的参数。这将比使用 Object 类型的参数更安全一些。现在,编译器可以进行检查,避免插入错误类型的对象。例如:
al.add(new File("..")); // can only add String objects to an ArrayList<String>
是无法通过编译的。出现编译错误比类在运行时出现类的强制类型转换异常要好很多
边界符
现在我们要实现这样一个功能,查找一个泛型数组中大于某个特定元素的个数,我们可以这样实现:
public static <T> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e > elem) // compiler error
++count;
return count;
}
但是这样很明显是错误的,因为除了 short, int, double, long, float, byte, char 等原始类型,其他的类并不一定能使用操作符 >,所以编译器报错,那怎么解决这个问题呢?答案是使用边界符
public interface Comparable<T> {
public int compareTo(T o);
}
做一个类似于下面这样的声明,这样就等于告诉编译器类型参数 T 代表的都是实现了 Comparable 接口的类,这样等于告诉编译器它们都至少实现了 compareTo 方法
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e.compareTo(elem) > 0)
++count;
return count;
}
通配符的子类型限定
在了解通配符之前,我们首先必须要澄清一个概念,假设我们添加一个这样的方法:
public void boxTest(Box<Number> n) { /* ... */ }
那么现在 Box<Number> n 允许接受什么类型的参数?我们是否能够传入 Box<Integer> 或者 Box<Double> 呢?答案是否定的,虽然 Integer 和 Double 是 Number 的子类,但是在泛型中 Box<Integer> 或者 Box<Double> 与 Box<Number> 之间并没有任何的关系。这一点非常重要,接下来我们通过一个完整的例子来加深一下理解
首先我们先定义几个简单的类,下面我们将用到它:
class Fruit {}
class Apple extends Fruit {}
class Orange extends Fruit {}
下面这个例子中,我们创建了一个泛型类 Reader,然后在 f1() 中当我们尝试 Fruit f = fruitReader.readExact(apples); 编译器会报错,因为 List<Fruit> 与 List<Apple> 之间并没有任何的关系。
public class GenericReading {
static List<Apple> apples = Arrays.asList(new Apple());
static List<Fruit> fruit = Arrays.asList(new Fruit());
static class Reader<T> {
T readExact(List<T> list) {
return list.get(0);
}
}
static void f1() {
Reader<Fruit> fruitReader = new Reader<Fruit>();
// Errors: List<Fruit> cannot be applied to List<Apple>.
// Fruit f = fruitReader.readExact(apples);
}
public static void main(String[] args) {
f1();
}
}
但是按照我们通常的思维习惯,Apple 和 Fruit 之间肯定是存在联系,然而编译器却无法识别,那怎么在泛型代码中解决这个问题呢?我们可以通过使用通配符来解决这个问题:
static class CovariantReader<T> {
T readCovariant(List<? extends T> list) {
return list.get(0);
}
}
static void f2() {
CovariantReader<Fruit> fruitReader = new CovariantReader<Fruit>();
Fruit f = fruitReader.readCovariant(fruit);
Fruit a = fruitReader.readCovariant(apples);
}
public static void main(String[] args) {
f2();
}
这样就相当于告诉编译器, fruitReader 的 readCovariant 方法接受的参数只要是满足 Fruit 的子类就行(包括 Fruit 自身),这样子类和父类之间的关系也就关联上了
<T extends Bounding Type>,表示 T 类型应该是绑定类型及其子类型(subType),T 和绑定类型可以是类或者接口,如果给 T 限定多个类型,则需要使用符号 "&"
<T extends Runnable & Serializable>
给 T 限定类型的时候,限定为某个 class 的时候是有限制的,看看下面几组泛型限定的代码
<T extends Runnable & Serializable & ArrayList> // 错误
<T extends Runnable & ArrayList & Serializable> // 错误
<T extends ArrayList & LinkedList & Serializable> // 错误
<T extends ArrayList & Runnable& Serializable> // 正确
不难看出,如果要限定 T 为 class 的时候,就有一个非常严格的规则,这个 class 只能放在第一个,最多只能有一个 class。这样一来,就能够严格控制 T 类型是单继承的,遵循 Java 规范
通配符的超类型限定
和前面子类型的限定一样,用 "?" 表示通配符,它的存在必须存在泛型类的类型参数中,如:
Pair<? super Fruit>
格式跟通配符限定子类型一样,用了关键字 super,但是这两种方式的通配符存在一个隐蔽的区别:
Pair<? extends Fruit> 定义了 pair 后可以将 getFirst 和 setFirst 想象成如下:
? extends Fruit getFirst() {...}
void setFirst(? extends Fruit) {...}
getFirst 是可以通过的,因为将一个返回值的引用赋给超类 Fruit 是完全可以的,而 setFirst 方法接受的是一个 Fruit 的子类,具体是什么子类,编译器并不知道,所以pair.setFirst(new Apple())
是不能被调用的。通配符的子类型限定适用于读取
再来看看通配符的超类型限定,即 Pair<? super Apple>:
getFirst 和 setFirst 可以想象成:
? super Apple getFirst() {...}
void setFirst(? super Apple) {...}
getFirst 方法的返回值是 Apple 的超类型,而 Apple 的超类型是得不到保证的,虚拟机会将它会给 Object,而 setFirst 方法是需要的是 Apple 的超类型,所以传入任意 Apple 都是允许的。通配符的超类型适用于写入与读取
无限定通配符
无限定通配符去除了超类型和子类型的规则,仅仅用一个 "?" 表示,并且也只能用于指定泛型类的类型参数中。如 Pair<?> 的形式,此时 getFirst 和 setFirst 方法如:
? getFirst() {...}
void setFirst(?) {...}
getFirst 返回值只能赋给 Object,而 setFirst 方法是不允许调用的,除了 setFirst(null)。Pair<?> 和 Pair 本质区别在于:可以以任意对象为参数调用原始 Pair 类的 setFirst 方法
既然这么脆弱,为什么还要引入这种通配符呢?在一些简单的操作中,无限定通配符还是有用武之地的,比如:
public static boolean isPairComplete(Pair<?> pair) {
return pair.getFirst() != null && pair.getSecond() != null;
}
这个方法体中,getFirst 和 getSecond 返回值都是 Object 类型的,此时只需要判断是否为 null 即可,而不需要知道具体的类型是什么,这就发挥了无限定通配符的作用了
通配符代表了泛型类中的参数类型,在方法体中,怎么去捕获这个参数类型呢?这里考虑三种通配符的捕获:
- Pair<? extends Fruit> pair:getFirst 返回 Fruit
- Pair<? super Apple> pair:无法捕获,getFirst 返回 Object
- Pair<?> pair:无法捕获,getFirst 返回 Object
只有第一种能捕获,通配符不是类型变量,因此不能在代码中使用 "?" 作为一种类型。也就是说,下述代码是非法的:
public static void swap(Pair<?> pair) {
? t = pair.getFirst();
pair.setFirst(p.getSecond());
p.setSecond(t);
}
这时可以写一个辅助方法 swapHelper:
public static void swap(Pair<?> pair) {
swapHelper(pair);
}
public static <T> void swapHelper(Pair<T> pair) {
T t = pair.getFirst();
pair.setFirst(p.getSecond());
p.setSecond(t);
}
PECS 原则
上面我们看到了类似 <? extends T> 的用法,利用它我们可以从 list 里面 get 元素,那么我们可不可以往 list 里面 add 元素呢?我们来尝试一下:
public class GenericsAndCovariance {
public static void main(String[] args) {
// Wildcards allow covariance:
List<? extends Fruit> flist = new ArrayList<Apple>();
// Compile Error: can't add any type of object:
// flist.add(new Apple())
// flist.add(new Orange())
// flist.add(new Fruit())
// flist.add(new Object())
flist.add(null); // Legal but uninteresting
// We Know that it returns at least Fruit:
Fruit f = flist.get(0);
}
}
答案是否定,Java 编译器不允许我们这样做,为什么呢?对于这个问题我们不妨从编译器的角度去考虑。因为 List<? extends Fruit> flist 它自身可以有多种含义:
List<? extends Fruit> flist = new ArrayList<Fruit>();
List<? extends Fruit> flist = new ArrayList<Apple>();
List<? extends Fruit> flist = new ArrayList<Orange>();
- 当我们尝试 add 一个 Apple 的时候,flist 可能指向 new ArrayList<Orange>()
- 当我们尝试 add 一个 Orange 的时候,flist 可能指向 new ArrayList<Apple>()
- 当我们尝试 add 一个 Fruit 的时候,这个 Fruit 可以是任何类型的 Fruit,而 flist 可能只想某种特定类型的 Fruit,编译器无法识别所以会报错
所以对于实现了 <? extends T> 的集合类只能将它视为 Producer 向外提供(get)元素,而不能作为 Consumer 来对外获取(add)元素。
如果我们要 add 元素应该怎么做呢?可以使用 <? super T>:
public class GenericWriting {
static List<Apple> apples = new ArrayList<Apple>();
static List<Fruit> fruit = new ArrayList<Fruit>();
static <T> void writeExact(List<T> list, T item) {
list.add(item);
}
static void f1() {
writeExact(apples, new Apple());
writeExact(fruit, new Apple());
}
static <T> void writeWithWildcard(List<? super T> list, T item) {
list.add(item)
}
static void f2() {
writeWithWildcard(apples, new Apple());
writeWithWildcard(fruit, new Apple());
}
public static void main(String[] args) {
f1(); f2();
}
}
这样我们可以往容器里面添加元素了,但是使用 super 的坏处是以后不能 get 容器里面的元素了,原因很简单,我们继续从编译器的角度考虑这个问题,对于 List<? super Apple> list,它可以有下面几种含义:
List<? super Apple> list = new ArrayList<Apple>();
List<? super Apple> list = new ArrayList<Fruit>();
List<? super Apple> list = new ArrayList<Object>();
当我们尝试通过 list 来 get 一个 Apple 的时候,可能会 get 得到一个 Fruit,这个 Fruit 可以是 Orange 等其他类型的 Fruit
根据上面的例子,我们可以总结出一条规律,“Producer Extends, Consumer Super”:
- Producer Extends – 如果你需要一个只读 List,用它来 produce T,那么使用 ? extends T
- Consumer Super – 如果你需要一个只写 List,用它来 consume T,那么使用 ? super T
如果需要同时读取以及写入,那就不能使用通配符了
如何阅读过一些 Java 集合类的源码,可以发现通常我们会将两者结合起来一起用,比如像下面这样:
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++)
dest.set(i, src.get(i));
}
}
类型擦除
Java 泛型只能用于在编译期间的静态类型检查,然后编译器生成的代码会擦除相应的类型信息,这样到了运行期间实际上 JVM 根本就知道泛型所代表的具体类型。这样做的目的是因为 Java 泛型是 1.5 之后才被引入的,为了保持向下的兼容性,所以只能做类型擦除来兼容以前的非泛型代码
说了这么多,那么泛型擦除到底是什么意思呢?先来看一下下面这个简单的例子:
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
}
编译器做完相应的类型检查之后,实际上到了运行期间上面这段代码实际上将转换成:
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() { return data; }
}
这意味着不管我们声明 Node<String> 还是 Node<Integer>,到了运行期间,JVM 统统视为 Node<Object>。有没有什么办法可以解决这个问题呢?这就需要我们自己重新设置 bounds 了,将上面的代码修改成下面这样:
public class Node<T extends Comparable<T>> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
}
这样编译器就会将 T 出现的地方替换成 Comparable 而不再是默认的 Object 了:
public class Node {
private Comparable data;
private Node next;
public Node(Comparable data, Node next) {
this.data = data;
this.next = next;
}
public Comparable getData() { return data; }
}
如有对类型参数有类型限定,擦除类型参数机制告诉我们,使用限定的类型代替,如果有多个,使用第一个代替
public class Period<T extends Comparable<T> & Serializable> {
private T begin;
private T end;
public Period(T one, T two) {
if (one.compareTo(two) > 0) {
begin = two; end = one;
} else {
begin = one; end = two;
}
}
}
类型擦除后,Period 的原始类型如下:
public class Period {
private Comparable begin;
private Comparable end;
public Period(Comparable one, Comparable two) {
if (one.compareTo(two) > 0) {
begin = two; end = one;
} else {
begin = one; end = two;
}
}
}
如果将 Period<T extends Comparable<T> & Serializable> 写成 Period<T extends Serializable & Comparable<T>> 会是怎么样呢?同理,擦除后原始类型用第一个 Serializable 代替,这样进行 compareTo 方法调用的时候,编译器会进行必要的强制类型转换,所以为了提高效率,将标签接口(没有任何方法的接口,也叫 tagging 接口)放在后面
最后看看虚拟机执行表达式的时候发生了什么,如:
Pair<Fruit> pair = ...;
Fruit f = pair.getFirst();
擦除后,getFirst 返回的是 Object 类型,然后虚拟机会插入强制类型转换,将 Object 转换为 Fruit,所以虚拟机实际上执行了两条指令:
- 调用 pair.getFirst() 方法
- 将 Object 转换成 Fruit 类型
桥方法
public class Pair<T> {
private T first = null;
private T second = null;
public Pair(T fir, T sec) {
this.first = fir;
this.second = sec;
}
public T getFirst() {
return this.first;
}
public T getSecond() {
return this.second;
}
public void setFirst(T fir) {
this.first = fir;
}
}
编译阶段类型变量擦除后:
public class Pair {
private Object first = null;
private Object second = null;
public Pair(Object fir,Object sec) {
this.first = fir;
this.second = sec;
}
public Object getFirst() {
return this.first;
}
public void setFirst(Object fir) {
this.first = fir;
}
}
如果这个类被继承:
class SonPair extends Pair<String> {
public void setFirst(String fir) {....}
}
这时,SonPair 中的 setFirst(String fir) 方法根本没有覆盖住 Pair<String> 中的这个方法
原因很简单,Pair<String> 在编译阶段已经被类型擦除为 Pair 了,它的 setFirst 方法变成了 setFirst(Object fir)。那么 SonPair 中 setFirst(String) 当然无法覆盖住父类的 setFirst(Object) 了
编译器会自动在 SonPair 中生成一个桥方法(bridge method ) :
public void setFirst(Object fir){
setFirst((String) fir)
}
这样,SonPair 的桥方法就能覆盖泛型父类的 setFirst(Object) 了。而且桥方法内部其实调用的是子类字节setFirst(String) 方法。对于多态来说就没问题了。
问题还没有完,多态中的方法覆盖是可以了,但是桥方法却带来了一个疑问:
如果还想在 SonPair 中覆盖 getFirst 方法呢?
class SonPair extends Pair<String> {
public String getFirst() {....}
}
由于需要桥方法来覆盖父类中的 getFirst,编译器会自动在 SonPair 中生成一个public Object getFirst()
桥方法
但是,疑问来了,SonPair 中出现了两个方法签名一样的方法(只是返回类型不同):
String getFirst() // 自己定义的方法
Object getFirst() // 编译器生成的桥方法
难道,编译器允许出现方法签名相同的多个方法存在于一个类中吗?
事实上有一个知识点可能大家都不知道:
①方法签名确实只有方法名 + 参数列表
②我们绝对不能编写出方法签名一样的多个方法。如果这样写程序,编译器是不会放过的
③JVM 会用参数类型和返回类型来确定一个方法。一旦编译器通过某种方式自己编译出方法签名一样的两个方法(只能编译器自己来创造这种奇迹,我们却不能人为的编写这种代码)。JVM 还是能够分清楚这些方法的,前提是需要返回类型不一样
看看 SonPair 中的 getFirst 方法:
@Override
public String getFirst() {
return super.getFirst();
}
这里用了 @Override,说明是覆盖了父类的 Object getFirst() 方法,而返回值可以指定为父类中的返回值类型的子类,这就是协变类型,这是 Java 5 以后才可以允许的,允许子类覆盖了方法后指定一个更严格的类型(子类型)
总结:
- 虚拟机中没有泛型,只有普通的类
- 所有泛型的类型参数都用它们限定的类型代替,没有限定则用 Object
- 为了保持类型安全性,虚拟机在有必要时插入强制类型转换
- 桥方法的合成用来保持多态性
- 协变类型允许子类覆盖方法后返回一个更严格的类型
泛型注意事项
不允许创建泛型数组
类似下面这样的做法编译器会报错:
List<Integer>[] arrayOfLists = new List<Integer>[2]; // compile error
为什么编译器不支持上面这样的做法呢?我们站在编译器的角度来考虑这个问题
先看下面这个例子:
Object[] strings = new String[2];
strings[0] = "hi"; // OK
strings[1] = 100; // An ArrayStoreException is thrown.
对于上面这段代码还是很好理解,字符串数组不能存放整型元素,而且这样的错误往往要等到代码运行的时候才能发现,编译器是无法识别的(因为 100 满足 strings 的静态类型 Object[])。接下来我们再来看一下假设 Java 支持泛型数组的创建会出现什么后果:
Object[] stringLists = new List<String>[]; // compiler error, but pretend it's allowed
stringLists[0] = new ArrayList<String>(); // OK
// An ArrayStoreException should be thrown, but the runtime can't detect it.
stringLists[1] = new ArrayList<Integer>();
假设我们支持泛型数组的创建,由于运行时期类型信息已经被擦除,JVM 实际上根本就不知道 new ArrayList<String>() 和 new ArrayList<Integer>() 的区别。类似这样的错误假如出现才实际的应用场景中,将非常难以察觉
如果对上面这一点还抱有怀疑的话,可以尝试运行下面这段代码:
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2); // true
}
}
泛型类并没有自己独有的 Class 类对象。并不存在 List<String>.class 或是 List<Integer>.class,而只有 List.class
可用如下语句声明并初始化一个泛型数组,但是编译器会有警告:
只能创建 new List<?>[10],但没有意义:
不能用基本类型实例化类型参数
也就是说,以下语句是非法的:
Pair<int, int> pair = new Pair<int, int>();
不过可以用相应的包装类型来代替
不能实例化类型参数
Java 泛型只能提供静态类型检查,然后类型的信息就会被擦除。不能以诸如new T(…)
,new T[...]
,T.class
的形式使用类型变量。因为存在类型擦除,所以类似于new T(…)
这样的语句就会变为new Object(…)
,而这通常不是我们的本意。可以用如下语句代替对new T[...]
的调用:
T[] arrays = (T[]) new Object[3]; // Type safety: Unchecked cast from Object[] to T[]
因为 T 会被擦除成 Object,所以
T[] aa = (T[]) new String[1];
也是成立的
但是这种方式在运行的时候可能不通过:
public static <T extends Comparable<T>> T[] maxTwo(T[] array) {
Object[] result = new Object[2];
return (T[]) result; // Type safety: Unchecked cast from Object[] to T[]
}
maxTwo(new String[] { "5", "7", "9" });
运行后,发生了类型转换异常,因为方法在调用的时候将 Object[] 转换为 String[]。同样这里可以使用反射来解决:
public static <T extends Comparable<T>> T[] maxTwo(T[] array) {
// Type safety: Unchecked cast from Object[] to T[]
return (T[]) Array.newInstance(array.getClass().getComponentType(), 2) ;
}
像下面这样利用类型参数创建实例的做法编译器不会通过:
public static <E> void append(List<E> list) {
E elem = new E(); // compile error
list.add(elem);
}
但是如果某些场景需要利用类型参数创建实例,可以使用反射:
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
E elem = cls.newInstance(); // OK
list.add(elem);
}
然后可以像下面这样调用:
List<String> ls = new ArrayList<>();
append(ls, String.class);
泛型参数与静态成员的关系
泛型类的静态上下文中不能使用静态类型变量和静态泛型方法。注意,这里我们强调了泛型类。因为普通类中可以定义静态泛型方法,如:
public class ArrayAlg {
public static <T> T getMiddle(T[] a) {
return a[a.length / 2];
}
}
关于为什么有这样的规定,请考虑下面的代码:
public class People<T> {
public static T name;
public static T getName() {
...
}
}
在创建泛型类的对象时,无论传给泛型类的参数类型是什么,所有该泛型类的对象都会对应内存中的同一个 Class 对象,而类的静态变量与静态方法是所有类实例共享的
- 在创建对象之前先加载对应类的 class 文件,静态成员的初始化是在类的加载、初始化过程中完成的,这时根本就不知道未来要创建的对象的泛型实参是什么,所以不能在静态成员中使用泛型形参
- 在同一时刻,内存中可能存在不只一个 People<T> 类实例。假设现在内存中存在着一个 People<String> 对象和 People<Integer> 对象。那么问题来了,name 究竟是 String 类型还是 Integer 类型呢?所以不允许在泛型类的静态上下文中使用类型变量
泛型类不能继承异常类,泛型类实例也不能被抛出或捕获
因为异常处理是由 JVM 在运行时刻来进行的。由于类型信息被擦除,JVM 无法区分两个异常类型 MyException<String> 和 MyException<Integer>。对于 JVM 来说,它们都是 MyException 类型的。也就无法执行与异常对应的 catch 语句
但在异常声明中使用类型参数是合法的:
public static <T extends Throwable> void doWork(T t) throws T {
try {
...
} catch (Throwable realCause) {
t.initCause(realCause);
throw t;
}
}
泛型数组是不合法的
不能创建这样的数组:
Pair<String, String>[] pairs = new Pair<String, String>[10];
因为实际上 pairs 是 Pair[] 类型的,所以我们添加 Date 类型的元素,编译器是不会发现的。这会产生难以定位的错误。但是可以用下面的方式来定义泛型数组
Pair<String, String>[] pairs = (Pair<String, String>[])(new Pair[10]);
instanceof
无法对泛型代码直接使用 instanceof 关键字,因为 Java 编译器在生成代码的时候会擦除所有相关泛型的类型信息,正如我们上面验证过的 JVM 在运行时期无法识别出 ArrayList<Integer> 和 ArrayList<String> 的之间的区别:
public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>) { // compile error
// ...
}
}
=> { ArrayList<Integer>, ArrayList<String>, ArrayList<Character>, ... }
可以使用通配符重新设置 bounds 来解决这个问题:
public static void rtti(List<?> list) {
if (list instanceof ArrayList<?>) { // OK; instanceof requires a reifiable type
// ...
}
}
如果我们想判断一个对象是否为一个泛型类的实例,则应该使用 obj instanceof Box 或 obj instanceof Box<?>
不能使用 clone 方法
因为 clone() 在 Object 中是 protected 保护访问的,调用 clone() 必须通过将 clone() 改写为 public 公共访问的类方法来完成。但是 T 的 clone() 是否为 public 是无法确定的,因此调用其 clone 也是非法的
public class Test {
public <T> void doSomething(T param) {
T copy = (T) param.clone(); // 编译 Error: clone() 在 java.lang.Object 中访问 protected
}
}
类型擦除后引起的冲突
public class Node<T> {
public T data;
public Node(T data) { this.data = data; }
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node<Integer> {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
对于泛型代码,Java 编译器实际上还会偷偷帮我们实现一个 Bridge method:
class MyNode extends Node {
// Bridge method generated by the compiler
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
// ...
}
下面这段代码运行的时候会抛出 ClassCastException 异常,提示 String 无法转换成 Integer:
Node n = new MyNode(5);
n.setData("Hello"); // Causes a ClassCastException to be thrown
MyNode 中不存在 setData(String data) 方法,所以只能调用从父类 Node 继承下来的 setData(Object data) 方法,setData((Integer) data) 的时候 String 无法转换成 Integer,这就是抛出 ClassCastException 的原因
如果一开始加上 Node<Integer> n = mn 就好了,这样编译器就可以提前帮我们发现错误
T,Class<T>,Class<?> 区别
T 是一种具体的类,例如 String、List、Map ...... 等等,这些都是属于具体的类
Class 也是一个类,但 Class 是存放上面String、List、Map ...... 类信息的一个类
获取 Class 有三种方式:
- 调用 Object 类的 getClass() 方法来得到 Class 对象,这也是最常见的产生 Class 对象的方法
List list = null;
Class clazz = list.getClass();
- 使用 Class 类的中静态 forName() 方法获得与字符串对应的 Class 对象
Class clazz = Class.forName("...");
- 如果 T 是一个 Java 类型,那么 T.class 就代表了匹配的类对象
Class clazz = List.class;
Class<T> 和 Class<?> 适用范围
使用 Class<T> 和 Class<?> 多发生在反射场景下,如果不使用泛型,反射创建一个类需要强转
People people = (People) Class.forName("...").newInstance();
如果反射的类型不是 People 类,就会报 java.lang.ClassCastException 错误
使用 Class<T> 泛型后,不用强转了
public class Test {
public static <T> T createInstance(Class<T> clazz) throws IllegalAccessException, InstantiationException {
return clazz.newInstance();
}
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
Fruit fruit = createInstance(Fruit.class);
People people = createInstance(People.class);
}
}
Class<T> 和 Class<?> 区别
- Class<T> 在实例化的时候,T 要替换成具体类
- Class<?> 它是个通配泛型,? 可以代表任何类型,主要用于声明时的限制情况
例如可以声明一个
public Class<?> clazz;
但不能声明一个
public Class<T> clazz;
因为 T 需要指定类型