java泛型-看这一篇就够了

java泛型-看这一篇就够了

泛型,JDK1.5添加的特性,这都0202年了,还没整明白?今天我们就来再回顾下,本篇主要介绍以下几个方面:

  • 泛型接口、泛型类、泛型方法的定义。
  • 泛型擦除和泛型检测
  • 泛型擦除和多态的冲突

希望能给你带来一点点帮助或者欢乐~

一、定义

  • 泛型接口
public interface GenericInterface<T>
{
    public T get(T t);
}
  • 泛型类
public class GenericClass<T>
{
    public T add(T t)
    {
        return t;
    }

    /**
     * 泛型方法,单独的泛型声明擦除为Object
     */
    public static <V> V add(V v1, V v2)
    {
        return v1;
    }

    /**
     * 有继承上限说明的擦除为父类类型
     */
    public <A extends Number> void get(A a)
    {}
}
  • 泛型方法
// 泛型接口
public <V> V add(V v);

二、泛型擦除和泛型检测

我们知道Java泛型是伪泛型,原因在于在编译期所有的泛型都会被擦除,在生成的字节码中只保留最基本的原始类型,那么如何定义最基本的原始类型呢?

  • 单独的泛型声明擦除为Object:< T > -> Object

  • 有继承上限说明的擦除为父类类型:< T extends Number> -> Number

下面通过javap -s 获取的字节码充分说明了这个问题:

Compiled from "GenericClass.java"
public class com.kevin.genericity.GenericClass<T> {
  public com.kevin.genericity.GenericClass();
    descriptor: ()V

  public T add(T);
    //单独的泛型声明擦除为Object:<T>  -> Object
    descriptor: (Ljava/lang/Object;)Ljava/lang/Object;

  public static <V> V add(V, V);
    descriptor: (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;

  public <A extends java.lang.Number> void get(A);
    //有继承上限说明的擦除为父类类型:<T extends Number> -> Number
    descriptor: (Ljava/lang/Number;)V
}

以上是编译后的类型擦除,而编译前是需要进行检查的,检查的规则又是怎么样的?

        // 这两个参数都是Integer,所以V为Integer
        int res = GenericClass.add(1, 2);
        // 一个参数为Integer,一个参数为Double 返回类型取同一父类Number
        Number res2 = GenericClass.add(1, 2.0d);
        // 这两个参数一个是Integer,一个是String,所以取同一父类,为Object
        Object res3 = GenericClass.add(1, "2");

以上只是说明,能通过编译检查了,那真正到编译器后类型消除后又是什么样子呢?

Compiled from "GenericClass.java"
public class com.kevin.genericity.GenericClass<T> {
  public com.kevin.genericity.GenericClass();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

  public T add(T);
    Code:
       0: aload_1
       1: areturn

  public static <V> V add(V, V);
    Code:
       0: aload_0
       1: areturn

  public <A extends java.lang.Number> void get(A);
    Code:
       0: return

  public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: invokestatic  #37                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       4: iconst_2
       5: invokestatic  #37                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       8: invokestatic  #43                 // Method add:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
      11: checkcast     #38                 // class java/lang/Integer
      14: invokevirtual #45                 // Method java/lang/Integer.intValue:()I
      17: istore_1
      18: iconst_1
      19: invokestatic  #37                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      22: ldc2_w        #49                 // double 2.0d
      25: invokestatic  #51                 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
      28: invokestatic  #43                 // Method add:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
      31: checkcast     #56                 // class java/lang/Number
      34: astore_2
      35: iconst_1
      36: invokestatic  #37                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      39: ldc           #58                 // String 2
      41: invokestatic  #43                 // Method add:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
      44: astore_3
      45: return
}

我们发现全部都是Object类型的,那就很疑问了,如果都是Object为啥编译前还要进行类型检查呢? 再次顺着思路的猜测是这样的:编译前的类型检查时为了运行时能将Object类型强转为正确的类型,编译期的检查保证了代码的正常运行,避免了编译期不报错,运行时却各种错误的情况。

进而我们可以推断出,类型检查是针对引用的,针对引用的目的是Object类型能正确强转为引用类型,let`s try it~

        ArrayList<String> list1 = new ArrayList<String>();  
        list1.add("1"); //编译通过  
        list1.add(1); //编译错误  
        String str1 = list1.get(0); //返回类型为String  

        ArrayList list2 = new ArrayList<String>();  
        list2.add("1"); //编译通过  
        list2.add(1); //编译通过  
        Object object = list2.get(0); //返回类型为Object  

        new ArrayList<String>().add("11"); //String 编译通过  
        new ArrayList<>().add(22); //Object 编译通过 
        new ArrayList<String>().add(11); //String 编译错误

amusing !!! 简直太好玩了。

泛型擦除和多态的冲突

对于代码:

public class GenericClass<T>
{
    public T add(T t)
    {
        return t;
    }
}

public class SubGenericClass extends GenericClass<String>
{
    @Override
    public String add(String t)
    {
        return t;
    }
}

我们知道,子类SubGenericClass 重写了父类add方法,一点问题都没有,不管是理解还是运行,和我们的理解都没有任何冲突。那编译后的字节码又是怎样的呢?

  public com.kevin.genericity.SubGenericClass();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method com/kevin/genericity/GenericClass."<init>":()V
       4: return

  public java.lang.String add(java.lang.String);
    Code:
       0: aload_1
       1: areturn

  public java.lang.Object add(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #20                 // class java/lang/String
       5: invokevirtual #22                 // Method add:(Ljava/lang/String;)Ljava/lang/String;
       8: areturn
}

哦吼~ 怎么有两个add方法呢? Why?

Jvm大喊:臣妾做不到啊!

对于父类GenericClass的泛型参数T被子类确定为String,jvm是知道的,但是它做不到,泛型消除后,父类的T成为了Object,那对于子类来说,我本来是重写一个参数为String的方法,你却让我重载了一个参数为Object类型的方法,嗯?掉我包?

为了解决这个问题,Jvm不得不生成一个中间方法public Object add(Object);来调用你重写的public String add(String); 说实话jvm也是挺不容易的,不过问题总归解决了。对表层用户来说一点感觉都没有,世界观也不会崩塌。

谢天谢地,世界还在。

最后的一点小细节

  • 泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数,原因就是泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象没有创建,如何确定这个泛型参数是何种类型?
  • 使用?代替具体的类型实参,此处’?’是类型实参,而不是类型形参,我的理解就是 ?是一个和任何类如Number、String一样的,而不是如泛型 T U 这样的未指定类型。
public void get(GenericClass<?> t)
{
    t.get();
}

public <U> void get(GenericClass<U> t)
{
    t.get();
}

End

觉得还行的大哥三连一波,小弟博客有点惨淡,写文不易,你的点赞就是我最大的动力!✨

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值