Java 泛型

参数类型的好处

在 Java 引入泛型之前,泛型程序设计是用继承实现的。ArrayList 类只维护一个 Object 引用的数组:

public class ArrayList {    // before generic classes
    private Object[] elementData;
    ...
    public Object get(int i) { ... }
    public void add(Object o) { ... }
}
这种方法有两个问题:
  1. 当获取一个值时必须要进行强制类型转换
  2. 添加值时没有错误检查,可以向 ArrayList 中添加任何类的对象
ArrayList al = new ArrayList();  
// 无法进行错误检查,File 对象可以添加进去,编译器和运行期都可以通过  
al.add(new File());   
String first = (String) al.get(0);  // 类型转换失败导致运行失败 
没有泛型的程序导致的后果:
  1. 程序的可读性有所降低,因为可以不受限制往集合中添加任意对象
  2. 程序的安全性遭到质疑,类型转换失败将导致程序运行失败

泛型提供了一个更好的解决方案:类型参数。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,所以虚拟机实际上执行了两条指令:

  1. 调用 pair.getFirst() 方法
  2. 将 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 对象,而类的静态变量与静态方法是所有类实例共享的

  1. 在创建对象之前先加载对应类的 class 文件,静态成员的初始化是在类的加载、初始化过程中完成的,这时根本就不知道未来要创建的对象的泛型实参是什么,所以不能在静态成员中使用泛型形参
  2. 在同一时刻,内存中可能存在不只一个 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 有三种方式:

  1. 调用 Object 类的 getClass() 方法来得到 Class 对象,这也是最常见的产生 Class 对象的方法
List list = null;
Class clazz = list.getClass();
  1. 使用 Class 类的中静态 forName() 方法获得与字符串对应的 Class 对象
Class clazz = Class.forName("...");
  1. 如果 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 需要指定类型

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容