参考文献

泛型

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

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

  • 引入泛型的意义在于:

    • 适用于多种数据类型执行相同的代码(代码复用)

泛型类

1
2
3
4
5
6
7
8
9
public class Test<T>{          
private T var ;
public T getVar(){
return var ;
}
public void setVar(T var){
this.var = var ;
}
}

泛型接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface Test<T>{       
public T getVar() ;
}
public class TestImpl<T> implements Test<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 ;
}
}

泛型方法

1
2
3
4
5
6
7
8
9
public <T> T method(Class<T> c) throws InstantiationException, IllegalAccessException{
T t = c.newInstance();
return t;
}

public static <T> T method(Class<T> c) throws InstantiationException, IllegalAccessException{
T t = c.newInstance();
return t;
}

泛型的上下限

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

上限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 此处泛型只能是数字类型
class Test<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() ;
}
}

下限

1
2
3
4
// 只能接收String或Object类型的泛型,String类的父类只有Object类
public static void fun(Info<? super String> temp){
System.out.print(temp + ", ") ;
}

泛型的类型擦除

类型擦除的目的

  • 主要是为了向下兼容,因为JDK5之前没有泛型的,为了让JVM保持向下兼容,就出了类型擦除这个策略.

类型擦除原则

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

类型擦除分类

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

泛型的多态

  • 类型擦除会造成多态的冲突,而JVM解决方法就是桥接方法.

使用泛型的注意点

  • 泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class Test2<T> {    
    public static T one; //编译错误
    public static T show(T one){ //编译错误
    return null;
    }
    }


    public class Test2<T> {

    public static <T >T show(T one){ //这是正确的
    return null;
    }
    }
    • 因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的 T,而不是泛型类中的T.

桥方法(Bridge Method)

  • 桥方法(Bridge Method)是指在泛型类型中,由编译器自动生成的一种特殊方法,用于在继承体系中解决类型擦除(Type Erasure)所带来的重载冲突问题.

  • 在Java中,泛型类型的类型参数信息只存在于代码编译阶段,在实际运行时,泛型类型的实例只保留了原始类型(Raw Type)的信息.这种类型擦除的特性,导致了以下问题:

    • 在继承体系中,当泛型类型的父类和子类都实现了相同的泛型方法时,类型擦除会导致它们实际上变成了两个不同的方法,编译器无法确定应该调用哪一个方法.
    • 在继承体系中,子类的泛型类型参数不一定与父类相同,但是由于类型擦除,子类继承自父类的泛型方法的方法签名可能不一致,这也会导致编译错误.
  • 为了解决上述问题,Java编译器会自动为泛型类型中涉及到类型擦除的情况生成桥方法,它们的作用如下:

    • 桥方法能够确保在继承体系中调用正确的方法.例如,当子类中定义了一个重载方法,在父类中已经定义了相同的方法,但是参数类型与子类中不同,这时编译器会自动为子类生成一个桥方法,用于调用父类中的方法.

    • 桥方法能够确保在继承体系中泛型方法的方法签名一致.例如,在父类中定义了一个泛型方法,在子类中使用与父类不同的泛型参数类型定义同名方法,这时编译器会自动为子类生成一个桥方法,用于调用父类中的泛型方法.

  • 桥方法的生成过程由编译器完成,开发者不需要手动编写桥方法.在Java的字节码中,桥方法的特征是:方法名以bridge开头,修饰符为synthetic(表示该方法不是由用户代码直接定义的),方法参数类型和返回值类型都会做特殊处理.