Java 泛型

Java 泛型的有关内容

在引入 Java 泛型之前,使用继承来实现泛型程序设计,即使用 Object 类来替代具体的类型。这种方法存在两个问题:一是集合中的元素可以是任意类型,而没有进行错误检查;二是当获取一个值的时候必须要进行强制类型转换。将其从 Object 类型转换成目标类型。

泛型的本质就是参数化类型,使用时由程序员指定类型,这样集合就只能容纳该类型的元素。泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型,避免插入错误类型的对象;同时消除了许多的强制类型转换。参数化类型使程序具有更好的可读性和安全性。

使用泛型的好处:

  • 代码重用:我们可以编写一次方法/类/接口,然后将其用于我们想要的任何类型。
  • 类型安全:泛型使错误在编译时出现而不是在运行时出现(在编译时知道代码中的问题总是比让代码在运行时失败更好)。
  • 不需要单独的类型转换:如果我们不使用泛型,那么每次我们从 ArrayList 中检索数据(比如)时,我们都必须对其进行类型转换。

#泛型类

泛型类就是具有一个或多个类型变量的类,多个类型变量间用逗号隔开。

1
2
3
4
5
6
7
// 多个泛型类型之间用逗号隔开
public class Generic<T, U> {
    ...
}

// 实例化泛型类
Generic<String, Integer> generic = new Generic<>();

在java 7之后,构造函数中可以省略泛型类型,省略的类型可以从变量的类型推断出来。

一般使用变量E表示集合中的元素类型,使用KV表示键和值的类型,使用T(或者US)表示任意类型。

#泛型方法

泛型方法可以定义在普通类中,也可以定义在泛型类中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Array {
    // 类型变量放在修饰符之后,返回类型之前
    public static <E> void printArray(E[] inputArray) {
        for (E element : inputArray) {
            System.out.printf("%s ", element);
        }
        System.out.println();
    }

    public static void main(String[] args) {
        Array a = new Array();
        Integer[] intArray = {1, 2, 3};
        // 可以省略<Integer>类型参数,可以通过传入的参数推断类型
        a.<Integer>printArray(intArray);
        // 如果传入多个参数,参数的类型不同,编译器会寻找共同的最小超类型(不能是泛型类)
    }
}

#类型变量限定

有时候需要泛型类型实现了一定的接口,比如有一个比较大小的泛型方法,那么参数的泛型类型必须是实现 Comparable 接口。

1
2
3
4
// 使用 extends 进行限定
public static <T extends Comparable> T min(T[] a) {...}
// 多个限定之间用 & 隔开
public static <T extends Comparable & Serializable> T min(T[] a) {...}

#类型擦除

Java 的泛型是伪泛型,因为 Java 在编译期间,所有的泛型信息都会被擦掉。Java 的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会替换为原始类型,这个过程成为类型擦除。原始类型用第一个限定的类型变量来替换,如果没有限定就用 Object 替换。如在代码中定义 ArrayList<Integer>ArrayList<String> 等类型,在编译后都会变成 ArrayList,JVM 看到的只是 ArrayList

#原始类型

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

泛型类

1
2
3
4
5
6
7
8
9
public class Pair<T> {  
    private T value;  
    public T getValue() {  
        return value;  
    }  
    public void setValue(T value) {  
        this.value = value;  
    }  
}  

的原始类型为(类型擦除之后为),因为 T 是无限定的,所以替换为 Object

1
2
3
4
5
6
7
8
9
public class Pair {  
    private Object value;  
    public Object getValue() {  
        return value;  
    }  
    public void setValue(Object  value) {  
        this.value = value;  
    }  
}

使用限定类型替换的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Interval<T extends Comparable & Serializable> implements Serializable {
    private T lower;
    Private T upper;

    public Interval(T first, T second) {
        if (first.compareTo(second) <= 0) {
            lower = first;
            upper = second;
        } else {
            lower = second;
            upper = first;
        }
    }
}

在擦除变量时,使用第一个限定的类型变量来替换:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Interval implements Serializable {
    private Comparable lower;
    Private Comparable upper;

    public Interval(Comparable first, Comparable second) {
        if (first.compareTo(second) <= 0) {
            lower = first;
            upper = second;
        } else {
            lower = second;
            upper = first;
        }
    }
}

#原始类型相等

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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());  // class java.util.ArrayList
    System.out.println(list1.getClass() == list2.getClass());  // true
}

上面代码定义了两个 ArrayList 数组,一个是 ArrayList<String> 泛型类型的,只能存储字符串;一个是 ArrayList<Integer> 泛型类型的,只能存储整。最后,通过 list1 对象和 list2 对象的 getClass() 方法获取他们的类的信息,最后发现结果为 true。说明泛型类型 String 和 Integer 都被擦除掉了,只剩下原始类型(这里是Object)。

可以使用 javap 命令及相关的参数可以查看类的信息以及反汇编class文件:

  • javap xxx.class 将显示类的最小构造,不会包含带有私有访问修饰符的字段/方法。
  • javap -p xxx.class 将显示所有的类和成员。
  • javap -v xxx.class 可以查看详细信息,例如堆栈大小和类方法的参数。
  • javap -c xxx.class 反汇编整个 Java 类。

#通过反射添加其它类型元素

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    ArrayList<Integer> list = new ArrayList<Integer>();
    // 这样调用 add 方法只能存储整型,因为泛型类型的实例为 Integer
    list.add(1);
    // 通过反射添加字符串类型
    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 泛型实例在编译之后被擦除掉了,只保留了原始类型。

#翻译泛型表达式

如果擦除返回类型或者存取泛型域,编译器则会插入强制类型转换。

1
2
3
4
5
6
// 擦除返回类型
Pair<Employee> budddies = ...;
Employee buddy = buddies.getFirst;

// 存取泛型域
Employee buddy = buddies.first;

编译器把这个方法调用翻译成两条虚拟机调用:

  • 对原始方法 Pair.getFirst 的调用

  • 将返回的 Object 类型强制转换为 Employee 类型

#翻译泛型方法

一个泛型方法

1
public static <T extends ComparableT min(T[] a)

擦除泛型后变为一个方法

1
public static Comparable min(Comparable[] a)

泛型方法擦除泛型存在的问题:类型擦除与多态的冲突。

假设存在泛型类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Pair<T> {  
    private T value;  

    public T getValue() {  
        return value;  
    }  

    public void setValue(T value) {  
        this.value = value;  
    }  
}

有一个子类继承它:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class DateInter extends Pair<Date> {  

    @Override
    public void setValue(Date value) {  
        super.setValue(value);  
    }  

    @Override
    public Date getValue() {  
        return super.getValue();  
    }  
}

本意是将父类的泛型类型限定为 Date,那么父类里面的两个方法的参数都为 Date 类型。所以上面的写法没有问题,@override 标签也没有问题,但是内部处理上使用了桥方法

实际上,类型擦除后,父类的的泛型类型全部变为了原始类型 Object,所以父类编译之后会变成下面的样子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Pair {  
    private Object value;  

    public Object getValue() {  
        return value;  
    }  

    public void setValue(Object  value) {  
        this.value = value;  
    }  
}  

在子类重写的setValue方法中,参数是Date类型,而父类的setValue方法参数是Object类型,在普通的继承关系中,这个方法是一个重载,而不是重写。

但在测试

1
2
3
4
5
public static void main(String[] args) throws ClassNotFoundException {  
    DateInter dateInter = new DateInter();  
    dateInter.setValue(new Date());                  
    dateInter.setValue(new Object()); //编译错误  
}

中,可以看到子类并没有参数是 Object 类型的 setValue 方法,说明确实是重写了,而不是重载了。

原因在于,我们传入父类的泛型类型是 DatePair<Date>,我们的本意是将泛型类变为如下:

1
2
3
4
5
6
7
8
9
class Pair {  
    private Date value;  
    public Date getValue() {  
        return value;  
    }  
    public void setValue(Date value) {  
        this.value = value;  
    }  
}

然后在子类中重写参数类型为 Date 的两个方法,实现继承中的多态。可是虚拟机并不能将泛型类型变为 Date,只能将类型擦除掉,变为原始类型 Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。这样,类型擦除就和多态有了冲突。JVM 知道你的本意但不能直接实现!因此虚拟机通过在子类中生成桥方法来解决这个问题。

子类在编译反汇编之后存在四个方法:

 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
28
29
30
31
32
33
34
35
36
37
38
class DateInter extends Pair<java.util.Date> {
  DateInter();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method Pair."<init>":()V
       4: return

  public void setValue(java.util.Date);
    Code:
       0: aload_0
       1: aload_1
       2: invokespecial #2                  // Method Pair.setValue:(Ljava/lang/Object;)V
       5: return

  public java.util.Date getValue();
    Code:
       0: aload_0
       1: invokespecial #3                  // Method Pair.getValue:()Ljava/lang/Object;
       4: checkcast     #4                  // class java/util/Date
       7: areturn

  public void setValue(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #4                  // class java/util/Date
       5: invokevirtual #5                  // Method setValue:(Ljava/util/Date;)V
       8: return

  public java.lang.Object getValue();
    Code:
       0: aload_0
       1: invokevirtual #6                  // Method getValue:()Ljava/util/Date;
       4: areturn
}


// invokespecial选择方法基于引用声明的类型,而不是对象实际的类型。但invokevirtual则选择当前引用的对象的类型。

从编译的结果来看,我们本意重写 setValuegetValue 方法的子类,竟然有 4 个方法,其实不用惊奇,最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是 Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而在我们自己定义的 setvaluegetValue 方法上面的 @Oveerride 只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。

#泛型的问题

  • 不能使用基本数据类型实例化类型参数

    基本数据类型八种:byteshortintlongfloatdoublecharboolean。没有 ArrayList<int>,只有 ArrayList<Integer>,因为类型擦除之后,ArrayList 存在的字段是 Object,而 Object 显然无法存储 int

  • 运行时类型查询只适用于原始类型

    试图查询一个对象是否属于某个泛型类型将会获得一个编译器错误(使用 instanceof),或者得到一个警告(使用强制类型转换)。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    ArrayList<String> list = new ArrayList<>();
    if (list instanceof ArrayList<String>) {  // ERROR: Illegal generic type for instanceof
        ...
    }
    
    // java.util.ArrayList.java
    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }
    

    如果去掉@SuppressWarnings("unchecked")注释,将会产生一个unchecked cast的警告,因为编译器不知道这个转型是否是安全的。但在这个例子中,elementData是一个Object的数组,泛型类型E没有任何的限制,因此在擦除的时候是Object,因此实际上是从Object转到Object,不应该产生警告,因此加上注解防止警告的产生。但如果泛型类型E是有限制的,那么这个警告就是必要的了。

  • 不能实例化类型变量

    不能使用new T()new T[]或者T.class这样的表达式,一方面的原因是类型擦除,另一方面的原因是编译器无法验证T是否具有默认的(无参)构造函数。解决的方法之一是通过传入一个工厂对象来创建一个新实例。

     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
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    
    class Foo<T> {
        private T x;
    
        public Foo(){
            x = new T();  // ERROR
        }
    
        public Foo(Supplier<T> x){
            // 使用工厂方法获得一个 T 类型的实例
            this.x = x.get();
        }
    
        public void setValue(T value){
            x = value;
        }
    
        public T getValue(){
            return x;
        }
    }
    
    class Apple {
        String name;
        public Apple(){
            name = "Apple";
        }
    }
    
    class AppleFactory implements Supplier<Apple> {
        @Override
        public Apple get(){
            return new Apple();
        }
    }
    
    public class Generic {
        public static void main(String[] args) {
            Foo<String> foo = new Foo<>(String::new);
            foo.setValue("hello");
            System.out.println(foo.getValue());   // hello
    
            Foo<Apple> foo3 = new Foo<>(new AppleFactory());
            System.out.println(foo3.getValue().getClass());  // Class Apple
    
            Foo<Apple> foo2 = new Foo<>(Apple::new);
            System.out.println(foo2.getValue().getClass());  // Class Apple
    
            // APPle没有类似于AppleFactory的工厂操作,但是Apple::new会产生工厂行为
            // ::new 类似的方法引用语法在Java 8中引入
        }
    }
    

    另外一个方法是模板方法设计模式。

     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
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    
    abstract class Foo<T> {
        private T x;
    
        public Foo() {
            x = create();
        }
    
        public T getX(){
            return x;
        }
    
        abstract T create();
    }
    
    class Apple {
        String name;
    
        public Apple() {
            name = "Apple";
        }
    }
    
    class AppleFoo extends Foo<Apple> {
        @Override
        Apple create() {
            return new Apple();
        }
    
        public void printClass() {
            System.out.println(getX().getClass().getSimpleName());
        }
    }
    
    public class Factory {
        public static void main(String[] args) {
            AppleFoo af = new AppleFoo();
            af.printClass();  // Class
        }
    }
    

    Foo有字段x,通过无参构造函数强制初始化,在初始化函数中调用抽象方法create()。这个方法在子类中实现,从而返回希望的类型。

  • 不能构造泛型数组

    没有办法通过T[] tArray = new T[];创建一个泛型数组。通用解决方案是在试图创建泛型数组的时候使用ArrayList

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    public class TemplateArray<T> {
        private List<T> array;
    
        public TemplateArray(){
            array = new ArrayList<>();
        }
    
        public void add(T item){
            array.add(item);
        }
    
        public T get(int index){
            return array.get(index);
        }
    }
    

    使用这种方法可以获得数组的行为,并且还具有泛型提供的编译时类型安全性。有时仍需创建泛型数组,比如在ArrayList中使用数组。编译器可以接受ArrayList<String>[] arrayLists;但是永远无法创建具有该确切类型(包括类型参数)的数组,不能使用new ArrayList<String>[10];来初始化这个变量。

     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
    28
    29
    30
    31
    32
    33
    34
    
    public class GenericArray<T> {
        private T[] array;
    
        @SuppressWarnings("unchecked")
        public GenericArray(int sz) {
            array = (T[]) new Object[sz];
        }
    
        public void put(int index, T item) {
            array[index] = item;
        }
    
        public T get(int index) {
            return array[index];
        }
    
        // 获得底层表示的方法
        public T[] rep() {
            return array;
        }
    
        public static void main(String[] args) {
            GenericArray<Integer> gai = new GenericArray<>(10);
            try {
                Integer[] ia = gai.rep();
            } catch (ClassCastException e) {
                System.out.println(e.getMessage());
            }
            // This is OK:
            Object[] oa = gai.rep();
        }
    }
    
    // class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.Integer;
    

    rep()方法返回一个T[] ,在主方法中它应该是gaiInteger[],但是如果调用它并尝试将结果转换为Integer[]引用,则会得到ClassCastException,这是因为实际的运行时类型为Object[]。数组会跟踪它的实际类型,类型在创建数组时建立,因此即使将Object[]强制转换成T[],在运行时,仍是一个Object数组。

    由于擦除,数组的运行时类型只能是Object[]。如果我们立即将其转换为T[],则在编译时会丢失数组的实际类型,并且编译器可能会错过一些潜在的错误检查。因此,最好在集合中使用Object[],并在使用数组元素时向T添加强制类型转换。在ArrayList的实现中,存储类型就是Object[]

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

    1
    2
    3
    4
    
    public class Test<T> {
        public static T field;        // ERROR
        public static T getField(){}  // ERROR
    }
    

    但是,可以定义泛型静态方法,例如,在ArrayList类中:

    1
    2
    3
    4
    
    @SuppressWarnings("unchecked")
    static <E> E elementAt(Object[] es, int index) {
        return (E) es[index];
    }
    

    这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的T,而不是泛型类中的T

#参考

Java泛型类型擦除以及类型擦除带来的问题

generics in java

On Java 8

Java核心技术·卷 I(原书第11版)

updatedupdated2022-07-182022-07-18