Java 泛型完全解读

阅读目录

一、为什么会出现泛型

二、泛型会带来什么样的问题

1. 不能用基本类型实例化类型参数

2. 不能用于运行时类型检查

3. 不能创建类型实例

4. 不能静态化

5. 不能抛出或捕获泛型类的实例

6. 不允许作为参数进行重载

7. 不能创建泛型数组

三、边界拓展

1. 无界通配符

2. 上界

3. 下界

4. PECS 原则

5. 自限定类型

总结

对于泛型的使用我想大家都非常熟悉,但是对于类型擦除,边界拓展等细节问题,可能不是很清楚,所以本文会重点讲解一下;另外对泛型的了解其实可以看出,一个语言特性的产生逻辑,这对我们平时的开发也是非常有帮助的;

一、为什么会出现泛型

首先泛型并不是Java的语言特性,是直到 JDK1.5 才支持的特性(具体区别后面会讲到);那么在泛型出现之前是怎么做的呢?

List list = new ArrayList(); list.add("123"); String s = (String) list.get(0);

如上面代码所示,在集合里面需要我们自己记住放进去的是什么,取出来的时候再强转; 也就将这种类型转换的错误推迟到了运行时,即麻烦还不安全,所以才出现了泛型;

使用场景:泛型类,泛型接口,泛型方法;

public class Test<T>public interface Test<T>public <T> void test(T t)

二、泛型会带来什么样的问题

正如上面所讲泛型并不是 Java 一开始就具有的特性,所以在后来想要增加泛型的时候,就必须要兼容以前的版本,Sun 他们想到的折中解决方案就是类型擦除;意思就是泛型的信息只存在于编译期,在运行时期所有的泛型信息都被擦除了,就想没有一样;

List<String> list1 = new ArrayList<>(); List<Integer> list2 = new ArrayList<>(); System.out.println(list1.getClass()); System.out.println(list2.getClass() == list1.getClass());

// 打印:

class java.util.ArrayList

true

可以看到 List<String> 和 List<String> 在运行时其实都是一样的,都是class java.util.ArrayList;所以在使用泛型的时候需要牢记,在运行时期没有泛型信息,也无法获取任何有关参数类型的信息;所以凡是需要获取运行时类型的操作,泛型都不支持!

1. 不能用基本类型实例化类型参数

new ArrayList<int>(); // errornew ArrayList<Integer>(); // correct

因为类型擦除,会擦除到他的上界也就是 Object;而 Java 的8个基本类型的直接父类是 Number,所以基本类型不不能用基本类型实例化类型参数,而必须使用基本类型的包装类;

2. 不能用于运行时类型检查

t instanceof T // errort instanceof List<T> // errort instanceof List<String> // errort instanceof List // correct

但是可以使用 clazz.isInstance(); 进行补偿;

3. 不能创建类型实例

T t = new T(); // error

同样可以使用 clazz.newInstance(); 进行补偿;

4. 不能静态化

private static T t; // error private T t; // correct private static List<T> list; // error private static List<?> list; // correct private static List<String> list; // correct // e.g. class Test<T> { private T t; public void set(T arg) { t = arg; } public T get() { return t; } }

因为静态变量在类中共享,而泛型类型是不确定的,所以泛型不能静态化;但是非静态的时候,编译期可以根据上下文推断出T是什么,例如:

Test l = new Test(); System.out.println(l.get()); l.set("123"); System.out.println(l.get()); // javap -v 反编译 12: invokevirtual #15 // Method JDK/Test14_genericity$Test.get:()Ljava/lang/Object; 15: invokevirtual #16 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 18: aload_1 19: ldc #17 // String 123 21: invokevirtual #18 // Method JDK/Test14_genericity$Test.set:(Ljava/lang/Object;)V 24: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; // --------------------------- Test l = new Test(); System.out.println(l.get()); l.set("123"); System.out.println(l.get()); // javap -v 反编译 12: invokevirtual #15 // Method JDK/Test14_genericity$Test.get:()Ljava/lang/Object; 15: invokevirtual #16 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 18: aload_1 19: bipush 123 21: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;

根据上面的代码,可以很清楚的看到,编译器对非静态类型的推导;

另外 List<?> 和 List<String> 之所以是正确的,仍然是因为编译器可以在编译期间就能确定类型转换的正确性;

5. 不能抛出或捕获泛型类的实例

catch (T t) // errorclass Test<T> extends Throwable // error

因为在捕捉异常时候需要运行时类信息,并且判断异常的继承关系,所以不能抛出或捕获泛型类的实例;

6. 不允许作为参数进行重载

void test(List<Integer> list)void test(List<String> list)

因为在运行时期泛型信息被擦除,重载的两个方法签名就完全一样了;

7. 不能创建泛型数组

对于一点我觉得是最重要的,关于数组的介绍可以参考,Array 相关 ;

List<String>[] lists = new ArrayList<String>[10]; // error List<String>[] lists1 = (List<String>[]) new ArrayList[10]; // correct

之所以不能创建泛型数组的主要原因:

数组是协变的,而泛型的不变的;

数组的Class信息是在运行时动态创建的,而运行时不能获取泛型的类信息;

根据上面的讲解可以看出所谓的擦除补偿或者擦除后的修正,其大体思路都是用额外的方法告知运行时的类型信息,可以是记录到局部变量,也可以是指定参数的确切类型(Array.newInstance(Class<?> componentType, int length));

三、边界拓展

基于安全的考虑 Java 泛型是不变的(避免取出数据时的类型转换错误);

List<Object> list = new ArrayList<String>(); // error

所以在使用集合类的时候,每个集合都需要强制指定确切类型就有点不方便,比如我想指定一个集合存放 A 以及 A 的子类;在这种情况下就引入了 extends,super,? 来拓展和管理泛型的边界;

1. 无界通配符 <?>

通配符主要用于泛型的使用场景(泛型一般有“声明”和“使用”两种场景);

通常情况下 <?> 和原生类型大致相同,就像 List 和 List<?> 的表现大部分都是一样的;但是要注意他们其实是有本质去别的,<?> 代表了某一特定的类型,但是编译器不知道这种类型是什么;而原生的表示可以是任何 Object,其中并没有类型限制;

List<?> list = new ArrayList<String>(); // correct list.add("34"); // error String s = list.get(0); // error Object o = list.get(0); // correct boolean add(E e);

上面的代码很明确的反应了这一点(<?> 代表了某一特定的类型,但是编译器不知道这种类型是什么),

因为编译器不知道这种类型是什么,所以在添加元素的时候,当然也就不能确认添加的这个类型是否正确;当使用<?>的时候,代码中的 add(E e) 方法,此时的 E 会被替换为 <?>,实际上编译器为了安全起见,会直接拒绝参数列表中涉及通配符的方法调用;就算这个方法没有向集合中添加元素,也会被直接拒绝;

当 List<?> 取出元素的时候,同样因为不知道这个特定的类型是什么,所以只能将取出的元素放在Object中;或者在取出后强转;

2. 上界 <extends>

extends,主要用于确定泛型的上界;

<T extends Test> // 泛型声明<T extends Test & interface1 & interface2> // 声明泛型是可以确定多个上界<? extends T>

界定的范围如图所示:


应当注意的是当extends用于参数类型限定时:

List<? extends List> list = new ArrayList<ArrayList>(); // correct list.add(new ArrayList()); // error List l = list.get(0); // correct ArrayList l = list.get(0); // error

上面的分析同无界通配符类似,只是 List l = list.get(0); 是正确的,是因为 <? extends List> 界定了放入的元素一定是 List 或者 list 的子类,所以取出的元素能放入 List 中,但是不能放入 ArrayList 中;

3. 下界 <super>

super,主要用于确定泛型的下界;如图所示:



List<? super HashMap> list = new ArrayList<>(); // correct LinkedHashMap m = new LinkedHashMap(); // correct HashMap m1 = m; // correct Map m2 = m; // correct list.add(m); // correct list.add(m1); // correct list.add(m2); // error Map mm = list.get(0); // error LinkedHashMap mm1 = list.get(0); // error

根据图中的范围对照代码,就能很快发现Map在List<? super HashMap>的范围之外;而编辑器为了安全泛型下界集合取出的元素只能放在 Object里面;

4. PECS 原则

PECS原则是对上界和下界使用的归纳,即producer-extends, consumer-super;结合上面的两幅图,表示:

extends,只能读,相当于生产者,向外产出;

super,只能写,相当于消费者,只能接收消费;

同时边界不能同时规定上界和下界,正如图所示,他们的范围其实是一样的,只是开口不一样;

5. 自限定类型

对于上面讲的泛型边界拓展,有一个很特别的用法,

class Test<T extends Test<T>> {}public <T extends Comparable<T>> T max(List<T> list) {}

自限定类型可以通俗的解释,就是用自己限定自己,即自和自身相同的类进行某操作;如上面的 max 方法,就表示可以和自身进行比较的类型;

那么如果想要表达只要是同一祖先就能相互比较呢?

public>T max(List<? extends T> list) {}<T extends Comparable<? super>>:

表明只要是同一祖先就能相互比较,<? extends T>表明集合中装的都是同一祖先的元素;(出至《Effective Java》第 28 条)

总结

对于泛型的时候首先要很清楚的知道,在运行时没有任何泛型的信息,全部都被擦除掉了;

需要知道 Java 泛型做不到的事情;

需要知道怎么拓展边界,让泛型更加灵活;

另外所有的 Comparator、Comparable 都是消费者,应用 super;

加java高级架构师群获取Java工程化、高性能及分布式、高性能、深入浅出。高架构。

性能调优、Spring,MyBatis,Netty源码分析和大数据等多个知识点高级进阶干货的直播免费学习权限

都是大牛带飞 让你少走很多的弯路的 群号是: 798891710对了 小白勿进 最好是有开发经验

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容