Java泛型


一、泛型介绍

1. 背景

在JDK5之前,还没有泛型。在使用集合时,需要构建一个元素类型为Object的集合,集合能够存储任意的数据类型对象,在使用该集合的过程中,需要明确知道存储每个元素的数据类型,否则很容易引发ClassCastException异常。

2. 概念

JDK5中引入Java泛型这个新特性,泛型提供了编译时类型安全监测机制,该机制允许我们在编译时检测到非法的类型数据结构。

  • 泛型主要是方便了程序员的代码编写,以及更好的安全性检测。
  • 泛型是一种运用于编译时期的技术,泛型的出现,实现了把运行阶段的错误提前暴露到了编译阶段

在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类泛型接口泛型方法

参数化类型

泛型的本质就是参数化类型。在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型。

参数化类型,顾名思义就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

简单理解:泛型就是把类型当作是参数一样进行传递,数据类型只能是引用类型。

3. 好处

  • 编译时类型安全监测
  • 消除强制类型转换

二、泛型声明

泛型的使用需要先声明,声明通过<符号>的方式,符号可以任意,编译器通过识别尖括号和尖括号内的字母来解析泛型。

  • 泛型的类型只能为引用类型,不能为基本类型
  • 尖括号的位置也是固定的,只能在类名之后或方法返回值之前

泛型类型符号

一般约定的类型符号:

  • E:Element (表示集合元素,在集合中使用)
  • T:Type(表示Java类)
  • K:Key(表示键,比如Map中的key)
  • V:Value(表示值,比如Map中的key)
  • N:Number(表示数值类型)
  • ?:泛型通配符(表示不确定的Java类型)

泛型声明方式

常见声明方式:

  • <T>:普通声明
  • <?>:无边界声明
  • <? extends 类>:上边界声明
  • <? super 类>:下边界声明

需要注意:通配符?不能在泛型类(接口)的声明上使用。

三、类型擦除

1. 什么是类型擦除

Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。

官方描述:
泛型被引入Java语言,以在编译时提供更严格的类型检查,并支持泛型编程。

为了实现泛型,Java编译器将类型擦除应用于:

  • 如果类型参数是无界的,则将泛型类型中的所有类型参数替换为其边界或Object。因此,生成的字节码只包含普通类、接口和方法。
  • 如果需要,请插入类型强制转换以保持类型安全。
  • 生成桥接方法以保留扩展泛型类型中的多态性。

类型擦除确保了不会为参数化类型创建新类;所以泛型是没有运行时开销的。

对官方描述的理解:

  1. 泛型的使用,使得编译器在编译时进行了更严格类型检查,避免运行时引发ClassCastException异常
  2. 泛型应用于编译阶段,用泛型定义的类型参数,会在编译时会去掉,这称之为“类型擦除”。编译后生成的字节码class文件不包含泛型类型信息,运行时虚拟机并不知道泛型。
    1. 编译时,泛型的类型参数会被替换为其边界或Object
    2. 编译时,使用泛型的地方会插入强制类型转换
    3. 在泛型父类、泛型接口的场景中,会生成桥接方法以保留泛型类型中的多态性。
      • 编译时,如果子类复写的父类中的方法使用了泛型类型,子类会自动生成一个该方法的桥接方法
      • 编译时,如果实现类实现接的接口方法使用了泛型类型,实现类会自动生成一个该方法的桥接方法
public class Test {

    public static void main(String[] args) {
        ArrayList<Integer> list1 = new ArrayList<>();

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

        System.out.println(list1.getClass() == list2.getClass());
    }
}

打印结果为true,因为list1和list2的Class对象是同一个Class。

特别注意:对于编译后生成的字节码class文件不包含泛型类型信息这句话,网上普遍都说这个说法,但都没去仔细解释,理解的时候可能会有不解,因为你发现现实中的class文件中时是包含了泛型类型信息的。下文会解释这个问题。

桥接方法

桥接方法是JDK1.5引入泛型后,为使java泛型方法生成的字节码与JDK1.5版本之前的字节码兼容由编译器自动生成的。

子类继承父类(实现接口)实现泛型方法的情况,父类经过编译后方法的泛型类型入参和返回值类型都为Object(或上边界类型),而子类的实现方法的入参和返回值类型为具体的泛型类型(如String),此时子类并没有重写父类的方法了(返回值和形参与父类完全相同才是重写方法),所以需要编译器生成一个桥接方法达到重写父类方法的目的。

因此当子类继承父类(实现接口)实现泛型方法的时候,编译器会为子类自动生成桥接方法。

举例说明:

public interface TestInterface<T> {

    T get();

    void set(T t);
}

class Test3 implements TestInterface<Integer> {

    @Override
    public Integer get() {
        return null;
    }

    @Override
    public void set(Integer s) {

    }
}
class MainClass {

    public static void main(String[] args) {
        Method[] methods = Test3.class.getDeclaredMethods();
        for (Method method : methods) {
            System.out.println((method.isBridge() ? "桥接方法:" : "普通方法:") + method.toGenericString());
        }
    }
}

打印结果:

桥接方法:public java.lang.Object com.joker.test.generic.Test3.get()
普通方法:public java.lang.Integer com.joker.test.generic.Test3.get()
桥接方法:public void com.joker.test.generic.Test3.set(java.lang.Object)
普通方法:public void com.joker.test.generic.Test3.set(java.lang.Integer)

以set方法作分析:

  • 编译时,经过类型擦除后,TestInterface接的set方法变成了void set(Object t)
  • 而实现类Test3原本的实现方法是void set(Integer s),明显已经不再是实现方法了
  • 为了解决这个问题,Java编译器通过桥接的生成了一个**void set(Object t)**方法,保证了实现方法

2. 为何需要类型擦除

为什么Java不像C#一样实现真正的泛型呢?而要用类型擦除的方式实现了个伪泛型。

其实JDK1.5引入的泛型采用类型擦除式实现的根本原因是兼容性上的取舍,而不是因为实现不了真正意义上的泛型。

为了确保JDK1.5之前的和JDK1.5能使用同一个类加载器,所以Java通过类型擦除的方式实现的泛型支持。经过编译阶段的泛型类型擦除后,与JDK1.5之前是基本没有变动。

3. 类型信息并未完全擦除

下面举例说明:

定义一个Demo泛型类和一个Test测试类。

public class Demo<T> {

    private T id;


    public T getId() {
        return id;
    }
}
public class Test {

    public static void main(String[] args) {
        Demo<Integer> demo = new Demo<>();
        Integer id = demo.getId();
    }
}

1. 查看IDEA编译后的class文件
Demo.calss

public class Demo<T> {
    private T id;

    public Demo() {
    }

    public T getId() {
        return this.id;
    }
}

Test.class

public class Test {
    public Test() {
    }

    public static void main(String[] args) {
        Demo<Integer> demo = new Demo();
        Integer id = (Integer)demo.getId();
    }
}

从class文件可见:

  • 使用泛型的地方进行了强制类型转换
  • 但泛型的类型参数并未替换为Object

原因:在编译过程中,泛型信息是被擦除了,但是声明侧的泛型信息会被class文件以Signature的形式保留在Class文件的Constant pool中。

2. 使用javap命令反编译Demo.class文件和Test.class文件
javap -v Demo.class

Classfile /D:/work/my/springboot/target/classes/com/joker/test/generic/Demo.class
  Last modified 2023-2-14; size 610 bytes
  MD5 checksum 4851ec541c05f1d29bee93edb79085f0
  Compiled from "Demo.java"
public class com.joker.test.generic.Demo<T extends java.lang.Object> extends java.lang.Object
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#24         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#25         // com/joker/test/generic/Demo.id:Ljava/lang/Object;
   #3 = Class              #26            // com/joker/test/generic/Demo
   #4 = Class              #27            // java/lang/Object
   #5 = Utf8               id
   #6 = Utf8               Ljava/lang/Object;
   #7 = Utf8               Signature
   #8 = Utf8               TT;
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               Lcom/joker/test/generic/Demo;
  #16 = Utf8               LocalVariableTypeTable
  #17 = Utf8               Lcom/joker/test/generic/Demo<TT;>;
  #18 = Utf8               getId
  #19 = Utf8               ()Ljava/lang/Object;
  #20 = Utf8               ()TT;
  #21 = Utf8               <T:Ljava/lang/Object;>Ljava/lang/Object;
  #22 = Utf8               SourceFile
  #23 = Utf8               Demo.java
  #24 = NameAndType        #9:#10         // "<init>":()V
  #25 = NameAndType        #5:#6          // id:Ljava/lang/Object;
  #26 = Utf8               com/joker/test/generic/Demo
  #27 = Utf8               java/lang/Object
{
  public com.joker.test.generic.Demo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/joker/test/generic/Demo;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/joker/test/generic/Demo<TT;>;

  public T getId();
    descriptor: ()Ljava/lang/Object;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field id:Ljava/lang/Object;
         4: areturn
      LineNumberTable:
        line 13: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/joker/test/generic/Demo;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/joker/test/generic/Demo<TT;>;
    Signature: #20                          // ()TT;
}
Signature: #21                          // <T:Ljava/lang/Object;>Ljava/lang/Object;
SourceFile: "Demo.java"

javap -v Test.class

Classfile /D:/work/my/springboot/target/classes/com/joker/test/generic/Test.class
  Last modified 2023-2-14; size 739 bytes
  MD5 checksum a82bd374c2bff99147acd130f3819415
  Compiled from "Test.java"
public class com.joker.test.generic.Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#28         // java/lang/Object."<init>":()V
   #2 = Class              #29            // com/joker/test/generic/Demo
   #3 = Methodref          #2.#28         // com/joker/test/generic/Demo."<init>":()V
   #4 = Methodref          #2.#30         // com/joker/test/generic/Demo.getId:()Ljava/lang/Object;
   #5 = Class              #31            // java/lang/Integer
   #6 = Class              #32            // com/joker/test/generic/Test
   #7 = Class              #33            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lcom/joker/test/generic/Test;
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               args
  #18 = Utf8               [Ljava/lang/String;
  #19 = Utf8               demo
  #20 = Utf8               Lcom/joker/test/generic/Demo;
  #21 = Utf8               id
  #22 = Utf8               Ljava/lang/Integer;
  #23 = Utf8               LocalVariableTypeTable
  #24 = Utf8               Lcom/joker/test/generic/Demo<Ljava/lang/Integer;>;
  #25 = Utf8               MethodParameters
  #26 = Utf8               SourceFile
  #27 = Utf8               Test.java
  #28 = NameAndType        #8:#9          // "<init>":()V
  #29 = Utf8               com/joker/test/generic/Demo
  #30 = NameAndType        #34:#35        // getId:()Ljava/lang/Object;
  #31 = Utf8               java/lang/Integer
  #32 = Utf8               com/joker/test/generic/Test
  #33 = Utf8               java/lang/Object
  #34 = Utf8               getId
  #35 = Utf8               ()Ljava/lang/Object;
{
  public com.joker.test.generic.Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/joker/test/generic/Test;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class com/joker/test/generic/Demo
         3: dup
         4: invokespecial #3                  // Method com/joker/test/generic/Demo."<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method com/joker/test/generic/Demo.getId:()Ljava/lang/Object;
        12: checkcast     #5                  // class java/lang/Integer
        15: astore_2
        16: return
      LineNumberTable:
        line 9: 0
        line 10: 8
        line 11: 16
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      17     0  args   [Ljava/lang/String;
            8       9     1  demo   Lcom/joker/test/generic/Demo;
           16       1     2    id   Ljava/lang/Integer;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            8       9     1  demo   Lcom/joker/test/generic/Demo<Ljava/lang/Integer;>;
    MethodParameters:
      Name                           Flags
      args
}
SourceFile: "Test.java"

由反编译的文件可知:

  • 声明侧的泛型被记录在Class文件的Constant pool中以Signature的形式保存

对于Java中泛型类型的获取可参考:Java中如何获取泛型类型信息

四、泛型使用

1. 泛型类

泛型类型用于类的定义中,被称为泛型类。用户在使用该类的时候,才把类型明确下来。

在类上定义的泛型,在实例方法中可以直接使用,不需要定义,但不能在静态方法中使用。

原因:Java中泛型只是一个占位符,必须在传递类型后才能使用。类实例化时才能正真的的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数静态的方法就已经加载完成了。

语法:

public class 类名<泛型表示符号> {
}

示例:

public class Test<T> {

    private T data;

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

如果使用时没指定具体的泛型类型,则泛型类型为Object。

    public static void main(String[] args) {
        Test<String> test1 = new Test<>();
        String data1 = test1.getData();

        Test test2 = new Test();
        Object data2 = test2.getData();
    }

2. 泛型接口

泛型接口和泛型类的声明方式一致。泛型接口的具体类型需要在实现类中进行声明。
语法:
public interface 接口名<泛型标识符号> {
}
示例:

public interface TestInterface<T> {
    
    public T get();
    
    public void set(T t);
}
public class Test2 implements TestInterface<String> {

    @Override
    public String get() {
        return null;
    }

    @Override
    public void set(String s) {

    }
}

如果实现类未指定具体的泛型类型,则泛型类型为Object。

public class Test2 implements TestInterface{

    @Override
    public Object get() {
        return null;
    }

    @Override
    public void set(Object s) {

    }
}

3. 泛型方法

泛型类型声明在方法上,叫做泛型方法。

需要注意:只是在方法中使用类定义的泛型,该方法不是泛型方法。
语法:

public <泛型表示符号> void 方法名(泛型表示符号 参数名){
}

public <泛型表示符号> 泛型表示符号 方法名(泛型表示符号 参数名){
}......

示例:

public class Test {

    public static <T> void aaa(T t) {
        
    }

    public <T> void bbb(T t) {
    }
}

五、泛型扩展

1. 泛型的上下边界

泛型的上边界

泛型类型必须为指定类型的子类型。

格式:<? extends 类>

示例:

public class Test {

    public void aaa(List<? extends Number> t) {
        Number number = t.get(0);
    }

    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        Test test = new Test();
        test.aaa(list);
    }
}

泛型的下边界

泛型类型必须为指定类型的父类型。
格式:<? super 类>

示例:

public class Test {

    public void aaa(List<? super Number> t) {
        Object object = t.get(0);
    }

    public static void main(String[] args) {
        ArrayList<Object> list = new ArrayList<>();
        list.add(1);
        Test test = new Test();
        test.aaa(list);
    }
}

2. 泛型中使用&(并且)操作符

  • 当需要多重约束的时候,可以使用&操作符
  • &操作符只能放在泛型的声明上(泛型类、泛型接口、方法方法的声明上)
  • &操作符后面只能是接口,不能是具体的类型,即使是Object也不行
  • &操作符不能用于super上 ,因为java有规定

使用方式:

    // 泛型类上申明,约束泛型类变量
    class WildcardTypeT<T extends Comparable<T> & List<T> & Serializable> {
        
    }
    // 方法上申明
    public <R extends Enum<R> & Serializable> List<R> parse2Enums(){}

https://blog.csdn.net/tianzhonghaoqing/article/details/119705014

  • 3
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值