第18章 泛型入门

📅 2026/8/27 10:01:32
第18章 泛型入门
第18章 泛型入门集合里能装任何类型的对象但取出时要强转容易出错。泛型Generics解决了类型安全问题在编译期就限定集合装什么类型。本节掌握泛型的基本用法。一、为什么需要泛型不使用泛型的集合可以放任何对象取出必须强转运行期才可能报错ListlistnewArrayList();list.add(Java);list.add(123);// 什么都能放Strings(String)list.get(0);// 要强转Stringt(String)list.get(1);// 运行期报错ClassCastException// Integer 强转 String 编译期发现不了使用泛型的集合编译期就限定类型编译报错而不是运行崩溃ListStringlistnewArrayList();list.add(Java);// list.add(123); // 编译错误String 列表不能放 IntegerStringslist.get(0);// 不用强转自动就是 String泛型也不只用在集合上MapString, Integer取出来就是 Integer不用猜类型、不用强转类型一目了然。泛型的好处编译期类型检查把错误提前省去强转代码更简洁代码可复用一套代码处理多种类型二、泛型类自己定义带类型的类// T 是类型参数可以是任意标识惯例用 T、E、K、VpublicclassBoxT{privateTvalue;publicvoidset(Tvalue){this.valuevalue;}publicTget(){returnvalue;}}BoxStringstrBoxnewBox();strBox.set(hello);StringsstrBox.get();// 自动是 StringBoxIntegerintBoxnewBox();intBox.set(100);intnintBox.get();// 自动是 Integer细心的你可能注意到new Box()只写了一次类型后面的空尖括号叫钻石操作符diamond。这个写法在 JDK 8、11、17 三个版本里都完全一样不用担心差异老项目里也常看到new BoxString()这种写全类型的版本。同一个类不同类型参数就是不同类型但它们内部都是同一个类泛型的擦除机制第七节细讲。一个类可以有多个类型参数如PairK, V两元组写法与 Box 相同。三、泛型方法方法级别使用泛型类型参数写在返回值前面publicclassUtils{// 泛型方法打印数组任何类型都能用publicstaticTvoidprintArray(T[]arr){for(Titem:arr){System.out.print(item );}System.out.println();}}String[]names{张三,李四};Integer[]nums{1,2,3};Utils.printArray(names);// 张三 李四Utils.printArray(nums);// 1 2 3调用时类型由实参自动推断不用显式指定。再看一个更实用的例子——交换数组中的两个元素publicclassUtils2{publicstaticTvoidswap(T[]arr,inti,intj){Ttemparr[i];arr[i]arr[j];arr[j]temp;}}String[]words{甲,乙,丙};Utils2.swap(words,0,2);// 类型由实参推断System.out.println(Arrays.toString(words));// [丙, 乙, 甲]Utils2.Stringswap(words,0,1);// 也可以显式指定一般不用写四、泛型接口// 泛型接口interfaceGeneratorT{Tnext();}// 实现类指定具体类型classNumberGeneratorimplementsGeneratorInteger{OverridepublicIntegernext(){return(int)(Math.random()*100);}}GeneratorIntegergennewNumberGenerator();System.out.println(gen.next());// 随机数实现类也可以继续保留泛型由使用方决定具体类型写法就是class MyGeneratorT implements GeneratorT。五、类型通配符 ?通配符用于方法参数解决泛型类型之间的继承问题// 只能接收 BoxNumberstaticvoidprintNum(BoxNumberbox){...}// 可以接收 BoxInteger、BoxDouble 等所有 Number 子类staticvoidprintAny(Box?extendsNumberbox){...}BoxIntegerintBoxnewBox();intBox.set(100);// printNum(intBox); // 编译错误BoxInteger 不是 BoxNumberprintAny(intBox);// 正确? extends Number三个通配符写法含义?任意类型? extends TT 或 T 的子类上限? super TT 或 T 的父类下限上限通配符的应用// 计算任意数字集合的和staticdoublesum(List?extendsNumbernums){doubletotal0;for(Numbern:nums){totaln.doubleValue();}returntotal;}sum(List.of(1,2,3));// ListInteger6.0sum(List.of(1.5,2.5));// ListDouble4.0上面例子里的List.of是 JDK 11 和 17 的快捷写法需 JDK 11/17 才能运行JDK 8 里用Arrays.asList(1, 2, 3)效果一样。下限通配符的应用? super T常用于往里写的场景// 接收所有能装 Integer 的集合ListInteger、ListNumber、ListObject 都行staticvoidaddOne(List?superIntegerlist){list.add(1);// 可以安全地写入 Integer}ListNumbernumbersnewArrayList();addOne(numbers);System.out.println(numbers);// [1]注意读写限制? extends只能读不能写null 除外? super只能写、读出来只能当 Object易错点 4 有完整对照。六、泛型与继承的注意点高频考点BoxIntegerintBoxnewBox();BoxNumbernumBox;// numBox intBox; // 编译错误BoxInteger 与 BoxNumber 没有继承关系即使Integer extends NumberBoxInteger也不是BoxNumber的子类——泛型参数之间没有继承关系所以才需要? extends。对比数组的协变——恰恰相反能编译通过代价是运行期才报错String[]strsnewString[3];Object[]objsstrs;// ✅ 数组协变String[] 是 Object[] 的子类objs[0]123;// 运行期才报错ArrayStoreExceptionListStringlistnewArrayList();// ListObject objs2 list; // ❌ 编译错误泛型不变七、泛型的擦除了解原理Java 泛型是编译期机制编译后类型参数被擦除替换为 Object 或边界类型生成的字节码和普通类一样。ListStringanewArrayList();ListIntegerbnewArrayList();System.out.println(a.getClass()b.getClass());// true运行时都是 ArrayList所以泛型信息在运行期不存在不能 new T()、不能 T[] 数组直接创建、不能 instanceof T// 以下写法全部编译错误擦除后 T 在运行期没有具体类型// T obj new T(); // ❌ 不能 new 类型参数// T[] arr new T[10]; // ❌ 不能直接创建泛型数组// if (x instanceof T) { } // ❌ 不能 instanceof 类型参数有边界时擦除成边界类型如T extends Number擦除成 Number。八、扩展知识1. 擦除带来的重载冲突既然ListString和ListInteger运行时都是List它们不能构成方法重载// ❌ 编译错误擦除后签名都是 print(List)冲突// void print(ListString list) { }// void print(ListInteger list) { }// ✅ 改方法名即可printString / printInt同理泛型类不能继承 Throwable异常机制在运行期工作而运行期没有泛型。2. 通配符进阶PECS 原则什么时候用? extends、什么时候用? super记住口诀PECSProducer Extends, Consumer Super生产者用 extends消费者用 super。方法只读取元素 → 参数用? extends T生产者方法只写入元素 → 参数用? super T消费者经典例子——把 src 全部拷到 dest// src 只读生产者用 extendsdest 只写消费者用 superstaticTvoidcopy(List?extendsTsrc,List?superTdest){for(Titem:src){dest.add(item);}}ListIntegersrcArrays.asList(1,2,3);// JDK 8 写法JDK 11/17 可用 List.ofListNumberdestnewArrayList();copy(src,dest);System.out.println(dest);// [1, 2, 3]copy能同时接收ListInteger和ListNumber读写都安全——这就是通配符的意义。3. 泛型方法 vs 泛型类怎么选泛型类整个类的多个字段/方法共用同一个 T比如BoxT、PairK, V泛型方法只有这一个方法用泛型。static 方法拿不到类的泛型只能用泛型方法classBoxT{privateTvalue;// ❌ static 成员不能用类上的泛型// static T count; // 编译错误// static void show(T v) { } // 编译错误// ✅ 静态方法用自己的泛型换个字母避免混淆staticEvoidshow(Ev){System.out.println(v);}}类型参数可有多个上限用连接如T extends Number ComparableT。九、易错点1. 基本类型不能做类型参数// ❌ Listint list new ArrayList(); // 编译错误// ✅ 用包装类自动装箱/拆箱ListIntegerlistnewArrayList();list.add(10);// int → Integer装箱intxlist.get(0);// Integer → int拆箱System.out.println(x);// 10包装类对照int→Integer、long→Long、double→Double、boolean→Boolean、char→Character。2. 泛型数组创建报错// ❌ 以下都编译错误Generic array creation// ListString[] arr new ListString[10];// T[] arr new T[10];// ✅ 用 List 代替数组ListListStringlistnewArrayList();list.add(newArrayList());System.out.println(list.size());// 13. 静态成员不能用类泛型classBoxT{privateTvalue;// ✅ 实例字段可用类泛型// ❌ static T count; // 编译错误// ❌ static void f(T v) {} // 编译错误静态成员不能用类泛型// ✅ 静态方法要自己声明泛型}4. 通配符的读写限制搞反List?extendsNumbernumsnewArrayListInteger();// ❌ nums.add(1); // 编译错误? extends 只能读不能写null 除外Numbernnums.get(0);// ✅ 读出来当 NumberList?superIntegerintsnewArrayListNumber();ints.add(1);// ✅ 可以写 Integer// ❌ Integer x ints.get(0); // 编译错误? super 读出来只能当 ObjectObjectoints.get(0);// ✅记住extends 偏读上限super 偏写下限。5. instanceof 不能用于泛型类型ListStringlistnewArrayList();// ❌ list instanceof ListString // 编译错误运行期没有泛型// ✅ 用无界通配符if(listinstanceofList?){System.out.println(是一个 List);// 是一个 List}十、小结泛型限定集合/类的类型编译期检查免强转泛型类class BoxT、泛型方法T 返回类型 方法名、泛型接口? extends T上限偏读? super T下限偏写PECS 记读写方向BoxInteger不是BoxNumber的子类数组协变但泛型不变泛型运行期擦除不能 new T()、不能建泛型数组、不能 instanceof T基本类型用包装类static 成员不能用类泛型要用泛型方法下一节学习 IO 流。下一篇第19章 IO 流