java 泛型入门 简书_java泛型详解

先来一道经典的测试题。

List l1 = new ArrayList();

List l2 = new ArrayList();

System.out.println(l1.getClass() == l2.getClass());

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

1.泛型是什么?

泛型的英文是 generics,较为准确的说法就是为了参数化类型,或者说可以将类型当作参数传递给一个类或者是方法。

那么,如何解释类型参数化呢?

比如我们需要在Cache里面可以存取任何值,通常我们会这么做

public class Cache {

Object value;

public Object getValue() {

return value;

}

public void setValue(Object value) {

this.value = value;

}

}

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

public class Cache {

T value;

public T getValue() {

return value;

}

public void setValue(T value) {

this.value = value;

}

}

测试代码

Cache cache1 = new Cache();

cache1.setValue("aaa");

// cache1.setValue(1); 这么写会报错,默认类型只能是string

String aString = cache1.getValue();

这就是泛型,它将 value 这个属性的类型也参数化了,这就是所谓的参数化类型

所以,综合上面信息,我们可以得到下面的结论。

1.与普通的 Object 代替一切类型这样简单粗暴而言,泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力

2.当具体的类型确定后,泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。所以说,它是一种类型安全检测机制

3.泛型提高了程序代码的可读性,在定义或者实例化阶段,因为 Cache这个类型显化的效果,程序员能够一目了然猜测出代码要操作的数据类型。

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

2.通配符 ?

在讲类型擦除前先介绍一下通配符 ?

除了用 表示泛型外,还有 >这种形式。?被称为通配符。

class Base{}

class Sub extends Base{}

Sub sub = new Sub();

Base base = sub;

List lsub = new ArrayList<>();

List lbase = lsub;

最后一行代码成立吗?编译会通过吗?答案是否定的。

编译器不会让它通过的。Sub 是 Base 的子类,不代表 List和 List有继承关系。

但是,在现实编码中,确实有这样的需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,对此 Java 引入了通配符这个概念。

所以,通配符的出现是为了指定泛型中的类型范围。

通配符有 3 种形式。

>被称作无限定的通配符。

extends T>被称作有上限的通配符。

super T>被称作有下限的通配符。

2.1无限定通配符 >

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

ArrayList> list = new ArrayList<>();

list.size();

list.isEmpty();

list.get(0);

list.add("aa")//这句要报错

>提供了只读的功能,也就是它删减了增加具体类型元素的能力,只保留与具体类型无关的功能。它不管装载在这个容器内的元素是什么类型,它只关心元素的数量、容器是否为空

如果你看到分函数的参数有无限定通配符的list,而你此时又在查bug,那么这个分函数可以直接跳过,因为这个函数里面,这个list只读,不会对其进行修改

2.2有上限的通配符 extends T>

主要特征是add受限

List extends Number> list = null;//list类型是Number子类,一定能get到数据

list = new ArrayList();

// list.add(new Integer(1));//报错,因为list不能确定实例化的对象具体类型导致add()方法受限

list.get(0);//类型是Number,和无限定通配符的区别就是返回值的类型,无限定通配符返回object

2.3有下限的通配符 super T>

主要特征是get受限

List super Integer> list = null;//list类型是Integer父类,一定能add int型数据

list = new ArrayList();

list.add(1);

Number number = list.get(0);//报错,get获取到的值不确定

3.类型擦除

回顾文章开始时的那段代码,打印的结果为 true 是因为 List和 List在 jvm 中的 Class 都是 List.class。

为啥?这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。

那么类型 String 和 Integer 怎么办?答案是泛型转译

还是以上面Cache为例,看一下Cache类型

Cache 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也不是我们所想要的String.为什么呢?我们有必要来看一下Cache的class文件

MAC下选中对应的Cache,然后cmd+shift+r就可以查看字节码

// Signature: 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.再看一下字节码

// Signature: 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也变了

我们现在可以下结论了,在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 则会被转译成普通的 Object 类型,如果指定了上限如 则类型参数就被替换成类型上限。

(字节码中的descriptor表示返回值,Signature表示泛型信息)

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

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

当泛型遇见重载

4.1反射

还是Cache

Cache 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个方法能同时存在么?

public void method(List list) {

}

public void method(List list) {

}

public int method(List list) {

return 0;

}

public String method(List list) {

return "";

}

先看一下字节码

// Method descriptor #19 (Ljava/util/List;)V

// Signature: (Ljava/util/List;)V

// Stack: 0, Locals: 2

public void method(java.util.List list);

// Method descriptor #19 (Ljava/util/List;)V

// Signature: (Ljava/util/List;)V

// Stack: 0, Locals: 2

public void method(java.util.List list);

// Method descriptor #19 (Ljava/util/List;)I

// Signature: (Ljava/util/List;)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/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更严格,直接爆红(警告和爆红的文字信息都是一样的)

参考文章

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值