前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >java泛型详解

java泛型详解

作者头像
提莫队长
发布2020-06-02 15:26:55
6670
发布2020-06-02 15:26:55
举报
文章被收录于专栏:刘晓杰

先来一道经典的测试题。

代码语言:javascript
复制
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());

输出是啥?正确答案是true。上面的代码中涉及到了泛型,而输出的结果缘由是类型擦除。

1.泛型是什么?

泛型的英文是 generics,较为准确的说法就是为了参数化类型,或者说可以将类型当作参数传递给一个类或者是方法。 那么,如何解释类型参数化呢? 比如我们需要在Cache里面可以存取任何值,通常我们会这么做

代码语言:javascript
复制
public class Cache {
    Object value;

    public Object getValue() {
        return value;
    }

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

但是这么做缺点就是每次getValue以后必须要强制转化才行.但是如果用泛型的话就是另外的画面

代码语言:javascript
复制
public class Cache<T> {
    T value;

    public T getValue() {
        return value;
    }

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

测试代码

代码语言:javascript
复制
Cache<String> cache1 = new Cache<String>();
cache1.setValue("aaa");
// cache1.setValue(1); 这么写会报错,默认类型只能是string
String aString = cache1.getValue();

这就是泛型,它将 value 这个属性的类型也参数化了,这就是所谓的参数化类型 所以,综合上面信息,我们可以得到下面的结论。

  • 1.与普通的 Object 代替一切类型这样简单粗暴而言,泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力
  • 2.当具体的类型确定后,泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。所以说,它是一种类型安全检测机制
  • 3.泛型提高了程序代码的可读性,在定义或者实例化阶段,因为 Cache<String>这个类型显化的效果,程序员能够一目了然猜测出代码要操作的数据类型。

当然,并不意味着 cache1.setValue(1) 这种操作完全不能执行,后面会说明

2.通配符 ?

在讲类型擦除前先介绍一下通配符 ? 除了用 <T>表示泛型外,还有 <?>这种形式。?被称为通配符。

代码语言:javascript
复制
class Base{}
class Sub extends Base{}
Sub sub = new Sub();
Base base = sub;        
List<Sub> lsub = new ArrayList<>();
List<Base> lbase = lsub;

最后一行代码成立吗?编译会通过吗?答案是否定的。 编译器不会让它通过的。Sub 是 Base 的子类,不代表 List<Sub>和 List<Base>有继承关系。 但是,在现实编码中,确实有这样的需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,对此 Java 引入了通配符这个概念。 所以,通配符的出现是为了指定泛型中的类型范围。 通配符有 3 种形式。

  • <?>被称作无限定的通配符。
  • <? extends T>被称作有上限的通配符。
  • <? super T>被称作有下限的通配符。

2.1无限定通配符 <?>

无限定通配符经常与容器类配合使用,它其中的 ? 其实代表的是未知类型,所以涉及到 ? 时的操作,一定与具体类型无关。

代码语言:javascript
复制
ArrayList<?> list = new ArrayList<>();
list.size();
list.isEmpty();
list.get(0);
list.add("aa")//这句要报错

<?>提供了只读的功能,也就是它删减了增加具体类型元素的能力,只保留与具体类型无关的功能。它不管装载在这个容器内的元素是什么类型,它只关心元素的数量、容器是否为空 如果你看到分函数的参数有无限定通配符的list,而你此时又在查bug,那么这个分函数可以直接跳过,因为这个函数里面,这个list只读,不会对其进行修改

2.2有上限的通配符<? extends T>

主要特征是add受限

代码语言:javascript
复制
List<? extends Number> list = null;//list类型是Number子类,一定能get到数据
list = new ArrayList<Integer>();
// list.add(new Integer(1));//报错,因为list不能确定实例化的对象具体类型导致add()方法受限
list.get(0);//类型是Number,和无限定通配符的区别就是返回值的类型,无限定通配符返回object

2.3有下限的通配符<? super T>

主要特征是get受限

代码语言:javascript
复制
List<? super Integer> list = null;//list类型是Integer父类,一定能add int型数据
list = new ArrayList<Number>();
list.add(1);
Number number = list.get(0);//报错,get获取到的值不确定

3.类型擦除

回顾文章开始时的那段代码,打印的结果为 true 是因为 List<String>和 List<Integer>在 jvm 中的 Class 都是 List.class。 为啥?这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。 那么类型 String 和 Integer 怎么办?答案是泛型转译 还是以上面Cache<T>为例,看一下Cache类型

代码语言:javascript
复制
Cache<String> cache = new Cache<>();
System.out.println(cache.getClass());
        
Field[] fs = cache.getClass().getDeclaredFields();
for ( Field f:fs) {
    System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
}

打印的结果是Cache和Object 明显,并不是 Cache<T>这种形式,同时所谓的T也不是我们所想要的String.为什么呢?我们有必要来看一下Cache<T>的class文件 MAC下选中对应的Cache,然后cmd+shift+r就可以查看字节码

代码语言:javascript
复制
// Signature: <T:Ljava/lang/Object;>Ljava/lang/Object;
public class fanxing.Cache {
  
  // Field descriptor #6 Ljava/lang/Object;
  // Signature: TT;
  java.lang.Object value;
  
  // Method descriptor #10 ()V
  // Stack: 1, Locals: 1
  public Cache();
  
  // Method descriptor #21 ()Ljava/lang/Object;
  // Signature: ()TT;
  // Stack: 1, Locals: 1
  public java.lang.Object getValue();
  
  // Method descriptor #26 (Ljava/lang/Object;)V
  // Signature: (TT;)V
  // Stack: 2, Locals: 2
  public void setValue(java.lang.Object value);
}

看到了没?getValue返回的是Object,无论T是什么,字节码都是Object,进入JVM自然都是Object咯 顺带说一句,前文提到 cache1.setValue(1) 可以执行的奥秘就在这里 那怎么样获取T的具体类型呢?改一下,改成Cache<T extends String>.再看一下字节码

代码语言:javascript
复制
// Signature: <T:Ljava/lang/String;>Ljava/lang/Object;
public class fanxing.Cache {
  
  // Field descriptor #6 Ljava/lang/String;
  // Signature: TT;
  java.lang.String value;
  
  // Method descriptor #10 ()V
  // Stack: 1, Locals: 1
  public Cache();
  
  // Method descriptor #21 ()Ljava/lang/String;
  // Signature: ()TT;
  // Stack: 1, Locals: 1
  public java.lang.String getValue();
  
  // Method descriptor #26 (Ljava/lang/String;)V
  // Signature: (TT;)V
  // Stack: 2, Locals: 2
  public void setValue(java.lang.String value);
}

getValue返回string,Signature也变了 我们现在可以下结论了,在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T>则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String>则类型参数就被替换成类型上限。 (字节码中的descriptor表示返回值,Signature表示泛型信息)

4.类型擦除带来的局限性

  • 利用类型擦除的原理,用反射的手段就绕过了正常开发中编译器不允许的操作限制
  • 当泛型遇见重载

4.1反射

还是Cache<T>

代码语言:javascript
复制
        Cache<String> cache = new Cache<>();
        cache.setValue("aaa");
        // cache.setValue(123);正常不允许
        try {
            Method method = cache.getClass().getDeclaredMethod("setValue", Object.class);// 字节码是object
            method.invoke(cache, 123);
        } catch (NoSuchMethodException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (SecurityException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        System.out.println(cache.getValue());//这句话会报错,int无法转化为string,内部已经存在123这个int值,但无法输出

4.2重载

一下4个方法能同时存在么?

代码语言:javascript
复制
    public void method(List<String> list) {
    }

    public void method(List<Integer> list) {
    }

    public int method(List<String> list) {
        return 0;
    }

    public String method(List<Integer> list) {
        return "";
    }

先看一下字节码

代码语言:javascript
复制
  // Method descriptor #19 (Ljava/util/List;)V
  // Signature: (Ljava/util/List<Ljava/lang/String;>;)V
  // Stack: 0, Locals: 2
  public void method(java.util.List list);

  // Method descriptor #19 (Ljava/util/List;)V
  // Signature: (Ljava/util/List<Ljava/lang/Integer;>;)V
  // Stack: 0, Locals: 2
  public void method(java.util.List list);

  // Method descriptor #19 (Ljava/util/List;)I
  // Signature: (Ljava/util/List<Ljava/lang/String;>;)I
  // Stack: 1, Locals: 2
  public int method(java.util.List list);

  // Method descriptor #19 (Ljava/util/List;)Ljava/lang/String;
  // Signature: (Ljava/util/List<Ljava/lang/Integer;>;)Ljava/lang/String;
  // Stack: 1, Locals: 2
  public java.lang.String method(java.util.List list);

方法重载要求方法具备不同的特征签名,返回值并不包含在方法的特征签名之中,所以返回值不参与重载选择.但是在 Class 文件格式之中,只要描述符不是完全一致的两个方法就可以共存。 从方法重载的要求来看,看Signature一行,首先把返回值去掉,然后类型擦除,整个Signature就剩下相同的Ljava/util/List,所以以上四个方法都不能共存 但是在class文件格式中,3和4的Method descriptor不同,导致在低版本的jdk里面可以共存.后两个方法jdk1.6是警告,jdk1.8更严格,直接爆红(警告和爆红的文字信息都是一样的)

参考文章

https://blog.csdn.net/briblue/article/details/76736356 https://blog.csdn.net/itmyhome1990/article/details/78872403

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.泛型是什么?
  • 2.通配符 ?
    • 2.1无限定通配符 <?>
      • 2.2有上限的通配符<? extends T>
        • 2.3有下限的通配符<? super T>
        • 3.类型擦除
        • 4.类型擦除带来的局限性
          • 4.1反射
            • 4.2重载
            • 参考文章
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档