Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。

为什么会引入泛型

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

引入泛型的意义在于:

  • 适用于多种数据类型执行相同的代码(代码复用)
  • 泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型)

泛型的基本使用

泛型类

  • 简单泛型类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Point<T>{         // 此处可以随便写标识符号,T是type的简称  
    private T var ; // var的类型由T指定,即:由外部指定
    public T getVar(){ // 返回值的类型由外部决定
    return var ;
    }
    public void setVar(T var){ // 设置的类型也由外部决定
    this.var = var ;
    }
    }
    public class GenericsDemo06{
    public static void main(String args[]){
    Point<String> p = new Point<String>() ; // 里面的var类型为String类型
    p.setVar("it") ; // 设置字符串
    System.out.println(p.getVar().length()) ; // 取得字符串的长度
    }
    }
  • 多元泛型
    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
    class Notepad<K,V>{       // 此处指定了两个泛型类型  
    private K key ; // 此变量的类型由外部决定
    private V value ; // 此变量的类型由外部决定
    public K getKey(){
    return this.key ;
    }
    public V getValue(){
    return this.value ;
    }
    public void setKey(K key){
    this.key = key ;
    }
    public void setValue(V value){
    this.value = value ;
    }
    }
    public class GenericsDemo09{
    public static void main(String args[]){
    Notepad<String,Integer> t = null ; // 定义两个泛型类型的对象
    t = new Notepad<String,Integer>() ; // 里面的key为String,value为Integer
    t.setKey("汤姆") ; // 设置第一个内容
    t.setValue(20) ; // 设置第二个内容
    System.out.print("姓名;" + t.getKey()) ; // 取得信息
    System.out.print(",年龄;" + t.getValue()) ; // 取得信息

    }
    }

泛型接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface Info<T>{        // 在接口上定义泛型  
public T getVar() ; // 定义抽象方法,抽象方法的返回值就是泛型类型
}
class InfoImpl<T> implements Info<T>{ // 定义泛型接口的子类
private T var ; // 定义属性
public InfoImpl(T var){ // 通过构造方法设置属性内容
this.setVar(var) ;
}
public void setVar(T var){
this.var = var ;
}
public T getVar(){
return this.var ;
}
}

public class GenericsDemo24{
public static void main(String arsg[]){
Info<String> i = null; // 声明接口对象
i = new InfoImpl<String>("汤姆") ; // 通过子类实例化对象
System.out.println("内容:" + i.getVar()) ;
}
}

泛型方法

泛型方法,是在调用方法的时候指明泛型的具体类型。

  • 定义泛型方法
  • 调用泛型方法

定义泛型方法时,必须在返回值前边加一个<T>,来声明这是一个泛型方法,持有一个泛型T,然后才可以用泛型T作为方法的返回值。
Class<T>的作用就是指明泛型的具体类型,而Class<T>类型的变量c,可以用来创建泛型类的对象。

泛型的上下限

先看下如下的代码,很明显是会报错的 (具体错误原因请参考后文)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A{}
class B extends A {}

// 如下两个方法不会报错
public static void funA(A a) {
// ...
}
public static void funB(B b) {
funA(b);
// ...
}

// 如下funD方法会报错
public static void funC(List<A> listA) {
// ...
}
public static void funD(List<B> listB) {
funC(listB); // Unresolved compilation problem: The method doPrint(List<A>) in the type test is not applicable for the arguments (List<B>)
// ...
}

那么如何解决呢?
为了解决泛型中隐含的转换问题,Java泛型加入了类型参数的上下边界机制<? extends A>表示该类型参数可以是A(上边界)或者A的子类类型编译时擦除到类型A,即用A类型代替类型参数
这种方法可以解决开始遇到的问题,编译器知道类型参数的范围,如果传入的实例类型B是在这个范围内的话允许转换,这时只要一次类型转换就可以了,运行时会把对象当做A的实例看待。

1
2
3
4
5
6
7
public static void funC(List<? extends A> listA) {
// ...
}
public static void funD(List<B> listB) {
funC(listB); // OK
// ...
}

泛型上下限的引入

在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。

  • 上限
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Info<T extends Number>{    // 此处泛型只能是数字类型
    private T var ; // 定义泛型变量
    public void setVar(T var){
    this.var = var ;
    }
    public T getVar(){
    return this.var ;
    }
    public String toString(){ // 直接打印
    return this.var.toString() ;
    }
    }
    public class demo1{
    public static void main(String args[]){
    Info<Integer> i1 = new Info<Integer>() ; // 声明Integer的泛型对象
    }
    }
  • 下限
    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
    class Info<T>{
    private T var ; // 定义泛型变量
    public void setVar(T var){
    this.var = var ;
    }
    public T getVar(){
    return this.var ;
    }
    public String toString(){ // 直接打印
    return this.var.toString() ;
    }
    }
    public class GenericsDemo21{
    public static void main(String args[]){
    Info<String> i1 = new Info<String>() ; // 声明String的泛型对象
    Info<Object> i2 = new Info<Object>() ; // 声明Object的泛型对象
    i1.setVar("hello") ;
    i2.setVar(new Object()) ;
    fun(i1) ;
    fun(i2) ;
    }
    public static void fun(Info<? super String> temp){ // 只能接收String或Object类型的泛型,String类的父类只有Object类
    System.out.print(temp + ", ") ;
    }
    }

小结

<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类

// 使用原则《Effictive Java》
// 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限

  1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
  2. 如果它表示一个 T 的消费者,就使用 < ? super T>;
  3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。

加深印象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private  <E extends Comparable<? super E>> E max(List<? extends E> e1) {
if (e1 == null){
return null;
}
//迭代器返回的元素属于 E 的某个子类型
Iterator<? extends E> iterator = e1.iterator();
E result = iterator.next();
while (iterator.hasNext()){
E next = iterator.next();
if (next.compareTo(result) > 0){
result = next;
}
}
return result;
}

上述代码中的类型参数 E 的范围是<E extends Comparable<? super E>>,我们可以分步查看:

  • 要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>(注意这里不要和继承的 extends 搞混了,不一样)
  • Comparable< ? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super
  • 而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大

多个限制

使用&符号

1
2
3
4
5
6
7
8
9
10
11
public class Client {
//工资低于2500元的上斑族并且站立的乘客车票打8折
public static <T extends Staff & Passenger> void discount(T t){
if(t.getSalary()<2500 && t.isStanding()){
System.out.println("恭喜你!您的车票打八折!");
}
}
public static void main(String[] args) {
discount(new Me());
}
}

泛型数组

1
2
3
4
5
6
List<String>[] list11 = new ArrayList<String>[10]; //编译错误,非法创建 
List<String>[] list12 = new ArrayList<?>[10]; //编译错误,需要强转类型
List<String>[] list13 = (List<String>[]) new ArrayList<?>[10]; //OK,但是会有警告
List<?>[] list14 = new ArrayList<String>[10]; //编译错误,非法创建
List<?>[] list15 = new ArrayList<?>[10]; //OK
List<String>[] list6 = new ArrayList[10]; //OK,但是会有警告

如何使用?
我们在使用到泛型数组的场景下应该尽量使用列表集合替换,此外也可以通过使用 java.lang.reflect.Array.newInstance(Class componentType, int length) 方法来创建一个具有指定类型和维度的数组,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ArrayWithTypeToken<T> {
private T[] array;

public ArrayWithTypeToken(Class<T> type, int size) {
array = (T[]) Array.newInstance(type, size);
}

public void put(int index, T item) {
array[index] = item;
}

public T get(int index) {
return array[index];
}

public T[] create() {
return array;
}
}
//...

ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100);
Integer[] array = arrayToken.create();

深入理解泛型

如何理解Java中的泛型是伪泛型?
泛型的类型擦除原则是:

  • 消除类型参数声明,即删除<>及其包围的部分。
  • 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:
    如果类型参数是无限制通配符或没有上下界限定,则替换为Object;
    如果存在上下界限定,则根据子类替换原则取类型参数的最左边限定类型(即父类)。
  • 为了保证类型安全,必要时插入强制类型转换代码。
  • 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。

如何进行擦除的?

  • 擦除类定义中的类型参数 - 无限制类型擦除
    当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如<T><?>的类型参数都被替换为Object。
  • 擦除类定义中的类型参数 - 有限制类型擦除
    当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number><? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object。
  • 擦除方法定义中的类型参数
    擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的。

如何证明类型的擦除呢?

  • 原始类型相等
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class Test {

    public static void main(String[] args) {

    ArrayList<String> list1 = new ArrayList<String>();
    list1.add("abc");

    ArrayList<Integer> list2 = new ArrayList<Integer>();
    list2.add(123);

    System.out.println(list1.getClass() == list2.getClass()); // true
    }
    }
    在这个例子中,我们定义了两个ArrayList数组,不过一个是<String>泛型类型的,只能存储字符串;一个是<Integer>泛型类型的,只能存储整数,最后,我们通过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下原始类型。
  • 通过反射添加其它类型元素
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class Test {

    public static void main(String[] args) throws Exception {

    ArrayList<Integer> list = new ArrayList<Integer>();

    list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer

    list.getClass().getMethod("add", Object.class).invoke(list, "asd");

    for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
    }
    }

    }
    在程序中定义了一个ArrayList泛型类型实例化为Integer对象,如果直接调用add()方法,那么只能存储整数数据,不过当我们利用反射调用add()方法的时候,却可以存储字符串,这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型。

如何理解类型擦除后保留的原始类型?
原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。

如何理解泛型的编译器检查?

如何理解泛型的多态?泛型的桥接方法
类型擦除会造成多态的冲突,而JVM解决方法就是桥接方法。

如何理解基本类型不能作为泛型类型?

如何理解泛型类型不能实例化?
不能实例化泛型类型, 这本质上是由于类型擦除决定的。

可以看到如下代码会在编译器中报错:

1
T test = new T(); // ERROR

因为在 Java 编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了,此外由于T 被擦除为 Object,如果可以 new T() 则就变成了 new Object(),失去了本意。

如果我们确实需要实例化一个泛型,应该如何做呢?可以通过反射实现:

1
2
3
4
static <T> T newTclass (Class < T > clazz) throws InstantiationException, IllegalAccessException {
T obj = clazz.newInstance();
return obj;
}

如何理解泛型类中的静态方法和静态常量?
泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数

如何理解异常中使用泛型?
如何获取泛型的参数类型?