ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

泛型擦除、通配符与 PECS 一次讲透

泛型擦除、通配符与 PECS 一次讲透 1. 引言泛型Generics是 Java 语言中非常重要但又容易让人困惑的特性。很多开发者在使用ListString、MapString, Integer时得心应手但一旦遇到类型擦除、通配符? extends/? super以及 PECS 原则就开始犯迷糊。这篇文章将围绕三个核心问题展开类型擦除到底是什么它如何影响我们的代码extends和super通配符到底该怎么选PECS 原则Producer Extends, Consumer Super如何帮助我们写出更健壮的泛型代码最后我会给出几道常见的泛型面试题和可运行的代码示例帮助你彻底掌握这些概念。2. 类型擦除是什么2.1 泛型的本质编译期检查Java 泛型是在 JDK 5 中引入的。它的核心设计思想是在编译期进行类型检查在运行期擦除类型信息。也就是说泛型类型参数如T、E、K、V在编译完成后会被擦除为它的上界通常是Object或者被替换为具体的类型当使用extends限定上界时。2.2 擦除的规则类型擦除遵循以下规则若类型参数无上界如T则擦除为Object。若类型参数有上界如T extends Number则擦除为该上界类型。泛型方法、泛型类的类型参数都会被擦除。擦除后编译器会插入必要的类型转换cast来保证类型安全。2.3 一个直观的例子// 编译前的代码publicclassBoxT{privateTvalue;publicvoidset(Tvalue){this.valuevalue;}publicTget(){returnvalue;}}编译后T被擦除为Object等价于// 编译后的代码概念上等价publicclassBox{privateObjectvalue;publicvoidset(Objectvalue){this.valuevalue;}publicObjectget(){returnvalue;}}当你在使用BoxString时编译器会在调用get()后自动插入(String)的强制类型转换。2.4 为什么需要类型擦除类型擦除的设计主要有两个原因向后兼容Java 泛型是在 JDK 5 才引入的为了让旧代码JDK 1.4 及之前无需修改就能与泛型代码共存JVM 层面并不感知泛型泛型只是编译器的把戏。避免运行时开销如果 JVM 为每种泛型类型都生成一份字节码会导致类数量爆炸增加内存开销。2.5 类型擦除带来的限制类型擦除带来了一些众所周知的限制不能new T()无法创建泛型类型的实例。不能创建泛型数组new T[10]是不允许的。不能使用instanceof判断泛型类型if (obj instanceof ListString)编译报错。静态字段不能使用类型参数static T field;编译报错。泛型类不能继承Throwable及其子类。// 编译报错的例子publicclassGenericArrayT{// 错误不能创建泛型数组// private T[] array new T[10];// 错误不能使用 instanceof 判断泛型类型publicbooleanisStringList(Objectobj){// return obj instanceof ListString; // 编译错误returnobjinstanceofList?;// 正确写法}}3. extends 和 super 怎么选3.1 通配符Wildcard简介通配符用?表示用于表示未知类型。它有三种形式?无界通配符表示任意类型。? extends T有上界通配符表示T或T的子类型。? super T有下界通配符表示T或T的父类型。3.2 为什么需要通配符先看一个经典问题。假设我们有两个方法publicstaticdoublesum(ListNumbernumbers){doubletotal0;for(Numbern:numbers){totaln.doubleValue();}returntotal;}如果我们传入ListInteger会编译报错。因为ListInteger并不是ListNumber的子类型——泛型是不变的invariant。这时就需要通配符来放宽限制publicstaticdoublesum(List?extendsNumbernumbers){doubletotal0;for(Numbern:numbers){totaln.doubleValue();}returntotal;}现在ListInteger、ListDouble、ListNumber都可以传入。3.3 什么时候用 extends? extends T适用于只读场景即我们只想从集合中读取元素而不想往里写入。原因当我们把集合声明为List? extends Number时编译器只知道集合中的元素是Number的某个子类型但具体是Integer还是Double并不知道。因此不能向其中添加任何元素除了null否则可能破坏类型安全。List?extendsNumberlistnewArrayListInteger();// list.add(1); // 编译错误无法确定具体类型// list.add(3.14); // 编译错误Numbernlist.get(0);// 可以读取安全3.4 什么时候用 super? super T适用于只写场景即我们主要想向集合中添加元素。原因当我们把集合声明为List? super Integer时编译器知道集合中的元素是Integer或其某个父类型如Number、Object。向其中添加Integer是安全的因为Integer一定是该集合元素类型的子类型。List?superIntegerlistnewArrayListNumber();list.add(1);// 可以写入安全list.add(2);// 可以写入// Integer i list.get(0); // 编译错误读取到的可能是 ObjectObjectobjlist.get(0);// 只能以 Object 读取3.5 无界通配符 ??表示未知类型常用于只使用Object提供的方法。泛型类中不依赖类型参数的方法。publicstaticvoidprintList(List?list){for(Objectobj:list){System.out.println(obj);}}4. PECS 原则Producer Extends, Consumer Super4.1 PECS 是什么PECS 是Producer Extends, Consumer Super的缩写由 Joshua Bloch 在《Effective Java》中提出。它是一条指导我们选择通配符的经验法则Producer Extends如果参数化类型表示一个生产者只往外提供数据即我们只读取使用? extends T。Consumer Super如果参数化类型表示一个消费者只往里接收数据即我们只写入使用? super T。4.2 一个经典例子Collections.copyjava.util.Collections.copy方法的签名完美诠释了 PECSpublicstaticTvoidcopy(List?superTdest,List?extendsTsrc)src是生产者我们从中读取元素所以用? extends T。dest是消费者我们向其中写入元素所以用? super T。ListIntegersrcArrays.asList(1,2,3);ListNumberdestnewArrayList(Arrays.asList(0.5,0.6,0.7));Collections.copy(dest,src);System.out.println(dest);// 输出 [1, 2, 3]4.3 另一个例子自定义 copy 方法我们自己实现一个类似的拷贝方法publicstaticTvoidcopyElements(List?superTdest,List?extendsTsrc){for(Titem:src){dest.add(item);}}调用时src可以是ListIntegerdest可以是ListNumberListIntegersrcArrays.asList(1,2,3);ListNumberdestnewArrayList();copyElements(dest,src);System.out.println(dest);// 输出 [1, 2, 3]4.4 记忆技巧一个简单的记忆方法读取Read→extends→ 上界通配符。写入Write→super→ 下界通配符。或者记住口诀“读 extends写 super”。4.5 什么时候不需要通配符如果方法内部既读又写且类型完全一致就不需要通配符直接使用类型参数即可publicstaticTvoidswap(ListTlist,inti,intj){Ttemplist.get(i);list.set(i,list.get(j));list.set(j,temp);}5. 常见泛型面试题与代码示例5.1 面试题 1ListObject和ListString有什么关系答案它们没有任何父子关系。泛型是不变的invariantListString不是ListObject的子类型反之亦然。ListStringstringsnewArrayList();// ListObject objects strings; // 编译错误5.2 面试题 2List?和ListObject有什么区别答案List?是未知类型的列表可以接收ListString、ListInteger等任何类型的列表但只能读取不能写入除null外。ListObject只能接收ListObject但可以写入任意Object及其子类。List?wildcardListnewArrayListString();ListObjectobjectListnewArrayList();// wildcardList.add(hello); // 编译错误objectList.add(hello);// 正确5.3 面试题 3如何创建一个泛型数组答案不能直接创建泛型数组但可以通过Array.newInstance或类型转换的方式间接实现。// 方式一通过反射创建SuppressWarnings(unchecked)publicstaticTT[]createArray(ClassTclazz,intsize){return(T[])java.lang.reflect.Array.newInstance(clazz,size);}// 使用String[]arrcreateArray(String.class,5);5.4 面试题 4什么是桥方法Bridge Method答案当泛型类在继承或实现时发生类型擦除编译器会自动生成桥方法来保持多态性。classParentT{publicTget(){returnnull;}}classChildextendsParentString{OverridepublicStringget(){returnchild;}}编译后Child类中会生成一个桥方法Object get()它调用String get()从而保证多态正常工作。5.5 面试题 5泛型方法中的类型推断publicstaticTTpick(Ta,Tb){returnb;}// 类型推断Stringresultpick(a,b);// 推断为 StringNumbernumpick(1,2.0);// 推断为 Number共同父类型5.6 面试题 6PECS 实战——合并两个列表publicstaticTvoidmerge(List?superTtarget,List?extendsTsource){target.addAll(source);}// 使用ListIntegersourceArrays.asList(1,2,3);ListNumbertargetnewArrayList();merge(target,source);System.out.println(target);// 输出 [1, 2, 3]6. 总结本文围绕泛型的三个核心主题进行了深入讲解类型擦除Java 泛型在编译期做类型检查运行期擦除类型信息这是为了向后兼容和减少运行时开销。擦除带来了一些限制如不能创建泛型数组、不能使用instanceof判断泛型类型等。extends 和 super 的选择? extends T用于只读场景生产者? super T用于只写场景消费者?用于不关心具体类型的场景。PECS 原则Producer Extends, Consumer Super。读取用extends写入用super。这是编写健壮泛型代码的核心指导原则。掌握这些概念不仅能帮助你写出更优雅、更安全的泛型代码还能在面试中从容应对相关提问。希望这篇文章能帮你彻底理解 Java 泛型的精髓。
返回列表