Java基础-泛型详解

3,947 阅读7分钟

我的博客 转载请注明原创出处。

之所以会想来写泛型相关的内容,是因为看到这样的一段代码:

当时我的内心是这样的:

所以就赶紧去复习了下,记录下来。基础不扎实,源码看不懂啊。

泛型介绍

Java 泛型(generics)是 JDK 5 中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数,在Java集合框架里使用的非常广泛。

定义的重点是提供了编译时类型安全检测机制。比如有这样的一个泛型类:

public class Generics <T> {

    private T value;

    public T getValue() {
        return value;
    }

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

然后写这样一个类:

public class Generics  {

    private Object value;

    public Object getValue() {
        return value;
    }

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

它们同样都能存储所有值,但是泛型类有编译时类型安全检测机制:

泛型类

一个类定义了一个或多个类型变量,那么就是泛型类。语法是在类名后用尖括号括起来,类型变量写在里面用逗号分开。然后就可以在方法的返回类型、参数和域、局部变量中使用类型变量了,但是不能在有static修饰符修饰的方法或域中使用。例子:

类定义参考上文例子
使用形式
Generics<String> generics = new Generics<String>();
后面尖括号内的内容在Jdk7以后可以省略
Generics<String> generics = new Generics<>();

泛型方法

一个方法定义了一个或多个类型变量,那么就是泛型方法。语法是在方法修饰符后面、返回类型前面用尖括号括起来,类型变量写在里面用逗号分开。泛型方法可以定义在普通类和泛型类中,泛型方法可以被static修饰符修饰。 例子:

    private <U> void out(U u) {
        System.out.println(u);
    }
    
    调用形式,
    Test.<String>out("test");
    大部分情况下<String>都可以省略,编译器可以推断出来类型
    Test.out("test");
    

类型变量的限定

有时候我们会有希望限定类型变量的情况,比如限定指定的类型变量需要实现List接口,这样我们就可以在代码对类型变量调用List接口里的方法,而不用担心会没有这个方法。

public class Generics <T extends List> {

    private T value;

    public T getValue() {
        return value;
    }

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

    public void add(Object u) {
        value.add(u);
    }

    public static void main(String[] args) {
        Generics<List> generics = new Generics<>();
        generics.setValue(new ArrayList<>());
        generics.add("ss");
        System.out.println(generics.getValue());
    }
    
}

限定的语法是在类型变量的后面加extends关键字,然后加限定的类型,多个限定的类型要用&分隔。类型变量和限定的类型可以是类也可以是接口,因为Java中类只能继承一个类,所以限定的类型是类的话一定要在限定列表的第一个。

类型擦除

类型擦除是为了兼容而搞出来的,大意就是在虚拟机里是没有泛型类型,泛型只存在于编译期间。泛型类型变量会在编译后被擦除,用第一个限定类型替换(没有限定类型的用Object替换)。上文中的Generics <T>泛型类被擦除后会产生对应的一个原始类型:

public class Generics {

    private Object value;

    public Object getValue() {
        return value;
    }

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

}

之所以我们能设置和返回正确的类型是因为编译器自动插入了类型转换的指令。

    public static void main(String[] args) {
        Generics<String> generics = new Generics<>();
        generics.setValue("ss");
        System.out.println(generics.getValue());
    }
    
    javac Generics.java
    javap -c Generics
    编译后的代码
     public static void main(java.lang.String[]);
        Code:
       0: new           #3                  // class generics/Generics
       3: dup
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #5                  // String ss
      11: invokevirtual #6                  // Method setValue:(Ljava/lang/Object;)V
      14: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      17: aload_1
      18: invokevirtual #8                  // Method getValue:()Ljava/lang/Object;
      21: checkcast     #9                  // class java/lang/String
      24: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      27: return

我们可以看到在21行插入了一条类型转换的指令。

类型擦除还带来了另一个问题,如果我们有一个类继承了泛型类并重写了父类的方法:

public class SubGenerics extends Generics<String> {

    @Override
    public void setValue(String value) {
        System.out.println(value);
    }

    public static void main(String[] args) {
        Generics<String> generics = new SubGenerics();
        generics.setValue("ss");
    }

}

因为类型擦除所以SubGenerics实际上有两个setValue方法,SubGenerics自己的setValue(String value)方法和从Generics继承来的setValue(Object value)方法。例子中的generics引用的是SubGenerics对象,所以我们希望调用的是SubGenerics.setValue。为了保证正确的多态性,编译器在SubGenerics类中生成了一个桥方法:

public void setValue(Object value) {
    setValue((String) value);
}

我们可以编译验证下:

Compiled from "SubGenerics.java"
public class generics.SubGenerics extends generics.Generics<java.lang.String> {
  public generics.SubGenerics();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method generics/Generics."<init>":()V
       4: return

  public void setValue(java.lang.String);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: aload_1
       4: invokevirtual #3                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       7: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #4                  // class generics/SubGenerics
       3: dup
       4: invokespecial #5                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #6                  // String ss
      11: invokevirtual #7                  // Method generics/Generics.setValue:(Ljava/lang/Object;)V
      14: return

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

引用《Java核心技术 卷一》

总之,需要记住有关 Java 泛型转换的事实: 1.虚拟机中没有泛型,只有普通的类和方法。 2.所有的类型参数都用它们的限定类型替换。 3.桥方法被合成来保持多态。 4.为保持类型安全性,必要时插人强制类型转换。

约束与局限性

  • 类型变量不能是基本变量,比如int,double等。应该使用它们的包装类Integer,Double
  • 虚拟机中没有泛型,所以不能使用运行时的类型查询,比如 if (generics instanceof Generics<String>) // Error
  • 因为类型擦除的原因,不能创建泛型类数组,比如Generics<String>[] generics = new Generics<String>[10]; // Error
  • 不能实例化类型变量,比如new T(...) new T[...] 或 T.class

通配符类型

通配符类型和上文中的类型变量的限定有些类似,区别是通配符类型是运用在声明的时候而类型变量的限定是在定义的时候。比如通配符类型Generics<? extends List>代表任何泛型Generics类型的类型变量是ListList的子类。

Generics<? extends List> generics = new Generics<ArrayList>();

不过这样声明之后Generics的方法也发生了变化,变成了

这样就导致了不能调用setValue方法

getValue方法是正常的

超类型限定

通配符限定还可以限定超类,比如通配符类型Generics<? super ArrayList>代表任何泛型Generics类型的类型变量是ArrayListArrayList的超类。

Generics<? super ArrayList> generics = new Generics<List>();

同样的,Generics的方法也发生了变化,变成了

调用getValue方法只能赋值给Object变量

调用setValue方法只能传入ArrayListArrayList的子类,超类List,Object等都不行

反射和泛型

虽然因为类型擦除,在虚拟机里是没有泛型的。不过被擦除的类还是保留了一些关于泛型的信息,可以使用反射相关的Api来获取。

类似地,看一下泛型方法

public static <T extends Comparable<? super T>> T min(T[] a)

这是擦除后

public static Comparable min(Coniparable[] a)

可以使用反射 API 来确定:

  • 这个泛型方法有一个叫做T的类型参数。
  • 这个类型参数有一个子类型限定, 其自身又是一个泛型类型。
  • 这个限定类型有一个通配符参数。
  • 这个通配符参数有一个超类型限定。
  • 这个泛型方法有一个泛型数组参数。

后记

周一就建好的草稿,到了星期天才写好,还是删掉了一些小节情况下,怕是拖延症晚期了......不过也是因为泛型的内容够多,虽然日常业务里很少自己去写泛型相关的代码,但是在阅读类库源码时要是不懂泛型就寸步难行了,特别是集合相关的。这次的大部分内容都是《Java核心技术 卷一》里的,这可是本关于Java基础的好书。不过还是老规矩,光读可不行,还是要用自己的语言记录下来。众所周知,人类的本质是复读机,把好书里的内容重复一遍,就等于我也有责任了!