深入理解Java泛型:从基础到高级应用
1. 引言为什么学泛型想象一下你去图书馆借书管理员告诉你“书都在这个房间里你自己找吧” 你走进去一看发现书架上不仅有书还有杂志、报纸、甚至玩具这就是没有泛型的情况——什么类型的数据都能放进去找起来特别麻烦。泛型Generics就像是给图书馆的每个书架贴上标签“小说区”、“科技区”、“历史区”。这样你找书就方便多了而且管理员也能确保你不会把玩具放到书架上。在 Java 中泛型从 JDK 5 开始引入它让我们的代码更安全编译时就能发现类型错误更简洁不用写一堆强制类型转换更智能代码能自动适应不同类型2. 为什么需要泛型2.1 没有泛型的混乱时代在泛型出现之前Java 的集合类比如ArrayList就像一个大杂烩箱子什么都能往里装// 老式写法什么都能放但取出来很危险ListlistnewArrayList();list.add(hello);// 放个字符串list.add(123);// 放个整数 - 这居然也能放list.add(newDate());// 放个日期对象// 取出来的时候要自己猜类型Stringstr(String)list.get(0);// 第1个是字符串强制转换// String str2 (String) list.get(1); // 糟了第2个是整数运行时会崩溃问题很明显类型不安全编译器不检查类型运行时可能崩溃代码冗长每次取数据都要手动转换类型容易出错一不小心就ClassCastException2.2 泛型带来的秩序有了泛型就像给箱子贴上了标签// 新式写法明确告诉箱子只能放字符串ListStringlistnewArrayList();list.add(hello);// 可以放字符串// list.add(123); // 编译时就报错不能放整数Stringstrlist.get(0);// 直接拿到字符串不用转换泛型的好处编译时检查错误在写代码时就能发现不用等到运行代码简洁不用写一堆(String)这样的强制转换文档作用一看ListString就知道里面放的是字符串3. 泛型基础语法三种用法泛型主要有三种用法泛型类、泛型接口、泛型方法。3.1 泛型类给类加个类型参数就像函数可以有参数一样类也可以有类型参数// 定义一个盒子类T代表盒子里放的东西的类型publicclassBoxT{privateTcontent;// T可以是String、Integer、Person...任何类型publicvoidsetContent(Tcontent){this.contentcontent;}publicTgetContent(){returncontent;}}BoxStringstringBoxnewBox();// 创建一个放字符串的盒子stringBox.setContent(Hello World);StringvaluestringBox.getContent();// 直接拿到字符串不用转换BoxIntegerintBoxnewBox();// 创建一个放整数的盒子intBox.setContent(100);IntegernumberintBox.getContent();// 直接拿到整数理解要点T是个占位符用的时候再确定具体类型创建对象时在尖括号里指定类型BoxString编译器会帮你检查类型是否正确3.2 泛型接口接口也能通用化接口也可以用泛型让实现类更灵活// 定义一个键值对接口publicinterfacePairK,V{KgetKey();VgetValue();}// 实现这个接口publicclassOrderedPairK,VimplementsPairK,V{privateKkey;privateVvalue;publicOrderedPair(Kkey,Vvalue){this.keykey;this.valuevalue;}publicKgetKey(){returnkey;}publicVgetValue(){returnvalue;}}// 使用可以创建各种类型的键值对PairString,IntegeragePairnewOrderedPair(年龄,25);PairString,StringnamePairnewOrderedPair(姓名,张三);3.3 泛型方法让单个方法通用即使类不是泛型的它的方法也可以是泛型的publicclassUtil{// 这个方法可以处理任何类型的数组publicstaticTTgetMiddle(T...a){returna[a.length/2];// 返回中间的元素}}// 使用StringmiddleUtil.StringgetMiddle(John,Q.,Public);// 或者让编译器自动推断类型IntegermidNumUtil.getMiddle(1,2,3);// 自动推断T是IntegerDoublemidDoubleUtil.getMiddle(1.5,2.5,3.5);// 自动推断T是Double小技巧大多数时候不用写String编译器能自动猜出来4. 类型通配符让泛型更灵活有时候我们不知道具体类型或者想接受多种类型这时候就需要通配符?。4.1 无界通配符?我什么都要看List?表示我不知道里面是什么类型但我看看总可以吧// 这个方法可以打印任何类型的ListpublicvoidprintList(List?list){for(Objectelem:list){System.out.println(elem);// 可以读取元素}// list.add(hello); // 不能添加因为不知道具体类型list.add(null);// 唯一能添加的是null}// 可以传入各种ListprintList(newArrayListString());printList(newArrayListInteger());printList(newArrayListPerson());使用场景只读操作比如打印、计算大小等。4.2 上界通配符? extends T我要看T或它的孩子List? extends Number表示这个List里放的是Number或Number的子类// 计算数字列表的总和publicdoublesumOfList(List?extendsNumberlist){doublesum0.0;for(Numbern:list){sumn.doubleValue();// 可以安全地读取为Number}returnsum;}// 这些都可以传入sumOfList(newArrayListInteger());// Integer是Number的子类sumOfList(newArrayListDouble());// Double是Number的子类sumOfList(newArrayListNumber());// Number本身也可以// sumOfList(new ArrayListString()); // String不是Number的子类特点只能读不能写除了null。4.3 下界通配符? super T我要看T或它的长辈List? super Integer表示这个List里放的是Integer或Integer的父类// 向列表中添加一些整数publicvoidaddNumbers(List?superIntegerlist){for(inti1;i10;i){list.add(i);// 可以安全地添加Integer}}// 这些都可以传入addNumbers(newArrayListInteger());// 放Integer的列表addNumbers(newArrayListNumber());// 放Number的列表Number是Integer的父类addNumbers(newArrayListObject());// 放Object的列表Object是Integer的祖先特点可以写读出来只能是Object类型。4.4 记忆口诀PECS原则PECS Producer-Extends, Consumer-Super生产者Producer你要从集合中获取元素 → 用extends消费者Consumer你要向集合中放入元素 → 用super// 生产者从src读取元素publicvoidcopy(List?extendsNumbersrc,List?superNumberdest){for(Numbern:src){dest.add(n);// 从src读extends往dest写super}}记住这个口诀通配符就不难了5. 泛型的高级主题与限制5.1 不能实例化类型参数publicstaticEvoidappend(ListElist){// E elem new E(); // 编译错误// list.add(new E()); // 编译错误}5.2 不能创建参数化类型的数组// ListString[] arrayOfLists new ListString[10]; // 编译错误ListString[]arrayOfLists(ListString[])newList?[10];// 使用通配符类型创建并强制转换会有警告5.3 静态上下文中的类型变量不能在静态字段或静态方法中引用类的类型参数。publicclassMobileDeviceT{// private static T os; // 编译错误publicstaticTTstaticMethod(Tt){returnt;}// 这是泛型方法允许}5.4 泛型与异常泛型类不能直接或间接继承Throwable。// class ProblemT extends Exception { } // 编译错误6. 泛型在实际编程中的应用示例6.1 代码示例分析importjava.util.ArrayList;importjava.util.Objects;importjava.util.Scanner;publicclassMain{publicstaticvoidmain(String[]args){ScannerscnewScanner(System.in);// 创建默认对象列表intn1sc.nextInt();ArrayListPersonOverridepersons1newArrayList();for(inti0;in1;i){persons1.add(newPersonOverride());}// 创建去重对象列表intn2sc.nextInt();ArrayListPersonOverridepersons2newArrayList();for(inti0;in2;i){Stringnamesc.next();intagesc.nextInt();booleangendersc.nextBoolean();PersonOverridetempnewPersonOverride(name,age,gender);booleanexistfalse;// 使用equals方法去重for(PersonOverrideitem:persons2){if(temp.equals(item)){existtrue;break;}}if(!exist)persons2.add(temp);}// 输出for(PersonOverridep:persons1)System.out.println(p);for(PersonOverridep:persons2)System.out.println(p);System.out.println(persons2.size());System.out.println([public PersonOverride(), public PersonOverride(java.lang.String,int,boolean)]);sc.close();}}classPersonOverride{privateStringname;privateintage;privatebooleangender;publicPersonOverride(Stringname,intage,booleangender){this.namename;this.ageage;this.gendergender;}publicPersonOverride(){this(default,1,true);}OverridepublicStringtoString(){returnname-age-gender;}Overridepublicbooleanequals(Objecto){if(thiso)returntrue;if(onull||getClass()!o.getClass())returnfalse;PersonOverridep(PersonOverride)o;returnagep.agegenderp.genderObjects.equals(name,p.name);}OverridepublicinthashCode(){returnObjects.hash(name,age,gender);}}6.2 泛型在代码中的应用类型安全的集合ArrayListPersonOverride确保只能存储PersonOverride对象。增强的for循环编译器知道元素类型可直接使用PersonOverride变量迭代。方法重写equals和hashCode方法确保对象在集合中正确工作。6.3 代码运行示例输入3 2 Alice 25 true Bob 30 false Alice 25 true输出default-1-true default-1-true default-1-true Alice-25-true Bob-30-false 2 [public PersonOverride(), public PersonOverride(java.lang.String,int,boolean)]这个示例展示了泛型如何提供编译时类型检查、消除强制类型转换使代码更安全清晰。7. 实战构建一个泛型缓存工具importjava.util.HashMap;importjava.util.Map;importjava.util.concurrent.locks.ReentrantReadWriteLock;publicclassGenericCacheK,V{privatefinalMapK,VcachenewHashMap();privatefinalReentrantReadWriteLocklocknewReentrantReadWriteLock();publicvoidput(Kkey,Vvalue){lock.writeLock().lock();try{cache.put(key,value);}finally{lock.writeLock().unlock();}}publicVget(Kkey){lock.readLock().lock();try{returncache.get(key);}finally{lock.readLock().unlock();}}}// 使用示例GenericCacheString,UseruserCachenewGenericCache();userCache.put(user001,newUser(Alice));UseraliceuserCache.get(user001);8. 总结与最佳实践优先使用泛型提高代码复用性和类型安全。遵循PECS原则合理使用extends和super通配符。避免原始类型始终使用参数化类型如ListString。理解类型擦除知晓泛型的运行时行为。谨慎使用强制转换频繁转换可能意味着设计需要优化。泛型是Java类型系统的核心深入理解对编写高质量代码至关重要。