Java基础-泛型详解

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

Java基础-泛型详解

当时我的内心是这样的:

Java基础-泛型详解

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

泛型介绍

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;
    }
}
复制代码

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

Java基础-泛型详解

泛型类

一个类定义了一个或多个类型变量,那么就是泛型类。语法是在类名后用尖括号括起来,类型变量写在里面用逗号分开。然后就可以在方法的返回类型、参数和域、局部变量中使用类型变量了,但是不能在有 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 的方法也发生了变化,变成了

Java基础-泛型详解

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

Java基础-泛型详解

getValue 方法是正常的

Java基础-泛型详解

超类型限定

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

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

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

Java基础-泛型详解

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

Java基础-泛型详解

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

Java基础-泛型详解
Java基础-泛型详解

反射和泛型

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

类似地,看一下泛型方法

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

这是擦除后

public static Comparable min(Coniparable[] a)

可以使用反射 API 来确定:

T

后记

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

原文 

https://juejin.im/post/5b61545151882569fd2886bd

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » Java基础-泛型详解

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址