Java 泛型使用上下边界通配符解决泛型擦除问题

Posted 韩曙亮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 泛型使用上下边界通配符解决泛型擦除问题相关的知识,希望对你有一定的参考价值。

前言

上一篇博客 【Java 泛型】泛型用法 ( 泛型编译期擦除 | 上界通配符 <? extends T> | 下界通配符 <? super T> ) 一、泛型擦除 章节中 , 讲到了泛型擦除问题 , 泛型只保留到了编译阶段 , 运行时就没有泛型的限制了 ;

本篇博客中介绍一种方法 , 使用上下边界通配符解决泛型擦除问题 ;





一、使用上边界通配符示例



接口类 :

public interface Data <T>{
    void set(T t);
    T get();
}

实现类 :

public class DataImpl<T extends Person> implements Data<T>{
    private T t;

    @Override
    public void set(T t) {

    }

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

反编译查看 实现类的 字节码的信息 : 发现分别有 2 2 2 个 get 和 set 方法 ;

使用

javap -p DataImpl.class

命令 , 反编译 DataImpl.class 字节码文件 , 查看类中的主要方法 ;


D:\\002_Project\\004_Java_Learn\\Main\\out\\production\\Main>javap -p DataImpl.class
Compiled from "DataImpl.java"
public class DataImpl<T extends Person> implements Data<T> {
  private T t;
  public DataImpl();
  public void set(T);
  public T get();
  public java.lang.Object get();
  public void set(java.lang.Object);
}

下面的 2 2 2 个方法 , 明显不符合 Java 语法规范 , 方法名和参数一样 ;

  public T get();
  public java.lang.Object get();




二、分析字节码的附加信息



下面分析字节码详细信息 ;

使用

javap -v DataImpl.class

命令 , 查看详细的字节码附加信息 ;

D:\\002_Project\\004_Java_Learn\\Main\\out\\production\\Main>javap -v DataImpl.class
Classfile /D:/002_Project/004_Java_Learn/Main/out/production/Main/DataImpl.class
  Last modified 2021-9-7; size 907 bytes
  MD5 checksum 90421d2a83f40d38de81c4c7f3cf341b
  Compiled from "DataImpl.java"
public class DataImpl<T extends Person> extends java.lang.Object implements Data<T>
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#32         // java/lang/Object."<init>":()V
   #2 = Methodref          #5.#33         // DataImpl.get:()LPerson;
   #3 = Class              #34            // Person
   #4 = Methodref          #5.#35         // DataImpl.set:(LPerson;)V
   #5 = Class              #36            // DataImpl
   #6 = Class              #37            // java/lang/Object
   #7 = Class              #38            // Data
   #8 = Utf8               t
   #9 = Utf8               LPerson;
  #10 = Utf8               Signature
  #11 = Utf8               TT;
  #12 = Utf8               <init>
  #13 = Utf8               ()V
  #14 = Utf8               Code
  #15 = Utf8               LineNumberTable
  #16 = Utf8               LocalVariableTable
  #17 = Utf8               this
  #18 = Utf8               LDataImpl;
  #19 = Utf8               LocalVariableTypeTable
  #20 = Utf8               LDataImpl<TT;>;
  #21 = Utf8               set
  #22 = Utf8               (LPerson;)V
  #23 = Utf8               (TT;)V
  #24 = Utf8               get
  #25 = Utf8               ()LPerson;
  #26 = Utf8               ()TT;
  #27 = Utf8               ()Ljava/lang/Object;
  #28 = Utf8               (Ljava/lang/Object;)V
  #29 = Utf8               <T:LPerson;>Ljava/lang/Object;LData<TT;>;
  #30 = Utf8               SourceFile
  #31 = Utf8               DataImpl.java
  #32 = NameAndType        #12:#13        // "<init>":()V
  #33 = NameAndType        #24:#25        // get:()LPerson;
  #34 = Utf8               Person
  #35 = NameAndType        #21:#22        // set:(LPerson;)V
  #36 = Utf8               DataImpl
  #37 = Utf8               java/lang/Object
  #38 = Utf8               Data
{
  public DataImpl();
    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 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LDataImpl;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LDataImpl<TT;>;

  public void set(T);
    descriptor: (LPerson;)V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   LDataImpl;
            0       1     1     t   LPerson;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   LDataImpl<TT;>;
            0       1     1     t   TT;
    Signature: #23                          // (TT;)V

  public T get();
    descriptor: ()LPerson;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aconst_null
         1: areturn
      LineNumberTable:
        line 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       2     0  this   LDataImpl;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       2     0  this   LDataImpl<TT;>;
    Signature: #26                          // ()TT;

  public java.lang.Object get();
    descriptor: ()Ljava/lang/Object;
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #2                  // Method get:()LPerson;
         4: areturn
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LDataImpl;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LDataImpl<TT;>;

  public void set(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #3                  // class Person
         5: invokevirtual #4                  // Method set:(LPerson;)V
         8: return
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   LDataImpl;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   LDataImpl<TT;>;
}
Signature: #29                          // <T:LPerson;>Ljava/lang/Object;LData<TT;>;
SourceFile: "DataImpl.java"

主要分析 下面 2 2 2 个方法的详细字节码数据 ;

public void set(T);
public void set(java.lang.Object);

public void set(T) 方法的字节码详细数据如下 :

  public void set(T);
    descriptor: (LPerson;)V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   LDataImpl;
            0       1     1     t   LPerson;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   LDataImpl<TT;>;
            0       1     1     t   TT;
    Signature: #23                          // (TT;)V

public void set(java.lang.Object) 的字节码详细数据如下 : 该方法是桥接方法 ;

  public void set(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #3                  // class Person
         5: invokevirtual #4                  // Method set:(LPerson;)V
         8: return
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   LDataImpl;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   LDataImpl<TT;>;

分析 public void set(java.lang.Object) 方法 :

该方法传入 Object 类型 , 所有的类都是 Object 子类 ;

descriptor: (Ljava/lang/Object;)V 说明该方法的参数是 Ljava/lang/Object; 类型 , 返回值是 void 类型 ;

ACC_BRIDGE 标识 标明 该该方法是一个桥接方法 ;

0: aload_0 从局部变量 0 装载引用类型值到操作数栈 ;

1: aload_1 从局部变量 1 装载引用类型值到操作数栈 ;

2: checkcast #3 检查该值是否是常量值 #3 的引用 , 也就是检查参数中传入的 Object 参数是否是 Person 类型 ;

Constant pool:
   #3 = Class              #34            // Person

5: invokevirtual #4 如果上一步检查 , 传入的参数是 Person 类型 , 就调用常量池中的 #4 常量对应的方法 , 也就是实际的 public void set(T) 方法 ;

Constant pool:
   #4 = Methodref          #5.#35         // DataImpl.set:(LPerson;)V

通过 上下边界 通配符 解决 泛型擦除问题 ;

以上是关于Java 泛型使用上下边界通配符解决泛型擦除问题的主要内容,如果未能解决你的问题,请参考以下文章

Java 泛型泛型用法 ( 泛型编译期擦除 | 上界通配符 <? extends T> | 下界通配符 <? super T> )

Java中泛型区别以及泛型擦除详解

Java核心知识泛型

Java泛型擦除

Java 之 泛型擦除

27.Android架构-泛型擦除机制