【Java】泛型详解
Java 泛型详解
参数化类型、类型擦除、边界与通配符完整教程
目录
一、泛型概述
1.1 什么是泛型
泛型(Generics)是JDK 5引入的特性,实现了参数化类型,使代码可以适用于多种类型。
核心优势:
- ✅ 类型安全:编译期检查类型错误
- ✅ 消除强制转换:自动类型转换
- ✅ 代码复用:一套代码适用多种类型
- ✅ 更好的可读性:明确表达意图
1.2 为什么需要泛型
没有泛型的问题:
// JDK 5之前:使用Object存储classBox{privateObject obj;publicvoidset(Object obj){this.obj = obj;}publicObjectget(){return obj;}}// 使用时的问题Box box =newBox(); box.set("Hello");String str =(String) box.get();// 需要强制转换 box.set(123);String error =(String) box.get();// 运行时错误!ClassCastException使用泛型的解决方案:
// JDK 5之后:使用泛型classBox<T>{privateT obj;publicvoidset(T obj){this.obj = obj;}publicTget(){return obj;}}// 使用时的优势Box<String> box =newBox<>(); box.set("Hello");String str = box.get();// 无需强制转换// box.set(123); // 编译错误!类型安全二、泛型类
2.1 基本语法
// 单个类型参数classGenericClass<T>{privateT data;publicGenericClass(T data){this.data = data;}publicTgetData(){return data;}}// 多个类型参数classPair<K,V>{privateK key;privateV value;publicPair(K key,V value){this.key = key;this.value = value;}publicKgetKey(){return key;}publicVgetValue(){return value;}}2.2 常用类型参数命名
| 符号 | 含义 | 示例 |
|---|---|---|
| E | Element(元素) | List |
| K | Key(键) | Map<K, V> |
| V | Value(值) | Map<K, V> |
| T | Type(类型) | Class |
| N | Number(数字) | - |
| ? | 通配符 | List<?> |
2.3 实战示例:元组类
// 二元组publicclassTuple2<A,B>{publicfinalA first;publicfinalB second;publicTuple2(A first,B second){this.first = first;this.second = second;}@OverridepublicStringtoString(){return"("+ first +", "+ second +")";}}// 三元组publicclassTuple3<A,B,C>extendsTuple2<A,B>{publicfinalC third;publicTuple3(A first,B second,C third){super(first, second);this.third = third;}@OverridepublicStringtoString(){return"("+ first +", "+ second +", "+ third +")";}}// 使用示例publicclassTupleExample{publicstaticvoidmain(String[] args){// 返回多个值Tuple2<String,Integer> person =newTuple2<>("张三",25);System.out.println(person);// (张三, 25)// 不同类型组合Tuple3<String,Integer,Boolean> data =newTuple3<>("Java",95,true);System.out.println(data);// (Java, 95, true)}}三、泛型方法
3.1 基本语法
泛型方法的类型参数放在返回值之前:
publicclassGenericMethod{// 泛型方法public<T>voidprint(T item){System.out.println(item.getClass().getName()+": "+ item);}// 多个类型参数public<K,V>voidprintPair(K key,V value){System.out.println(key +" = "+ value);}// 返回泛型类型public<T>TgetFirst(T[] array){return array.length >0? array[0]:null;}}3.2 类型推断
publicclassTypeInference{publicstatic<T>Tidentity(T item){return item;}publicstaticvoidmain(String[] args){// 显式指定类型String s1 =GenericMethod.<String>identity("Hello");// 类型推断(推荐)String s2 =identity("Hello");// 编译器自动推断为StringInteger i =identity(123);// 自动推断为Integer}}3.3 实战示例:通用工具方法
importjava.util.*;importjava.util.function.Function;publicclassGenericUtils{// 交换数组元素publicstatic<T>voidswap(T[] array,int i,int j){T temp = array[i]; array[i]= array[j]; array[j]= temp;}// 查找最大值publicstatic<TextendsComparable<T>>Tmax(T[] array){if(array ==null|| array.length ==0){returnnull;}T max = array[0];for(int i =1; i < array.length; i++){if(array[i].compareTo(max)>0){ max = array[i];}}return max;}// 转换Listpublicstatic<T,R>List<R>map(List<T> list,Function<T,R> mapper){List<R> result =newArrayList<>();for(T item : list){ result.add(mapper.apply(item));}return result;}publicstaticvoidmain(String[] args){// 交换示例String[] names ={"Alice","Bob","Charlie"};swap(names,0,2);System.out.println(Arrays.toString(names));// [Charlie, Bob, Alice]// 查找最大值Integer[] numbers ={3,7,2,9,1};System.out.println("最大值: "+max(numbers));// 9// 转换ListList<String> words =Arrays.asList("hello","world");List<Integer> lengths =map(words,String::length);System.out.println(lengths);// [5, 5]}}四、类型擦除
4.1 什么是类型擦除
Java泛型采用**类型擦除(Type Erasure)**机制实现,这是Java泛型与C++模板的本质区别。编译器在编译时检查类型安全,但在生成字节码时会擦除所有泛型类型信息。
擦除规则:
- 无边界类型参数(
<T>)→ 擦除为Object - 有边界类型参数(
<T extends Number>)→ 擦除为第一个边界类型Number List<String>和List<Integer>→ 运行时都是原始类型List- 编译器会自动插入类型转换代码,保证类型安全
importjava.util.*;publicclassErasureExample{publicstaticvoidmain(String[] args){List<String> list1 =newArrayList<>();List<Integer> list2 =newArrayList<>();// 运行时类型相同System.out.println(list1.getClass()== list2.getClass());// trueSystem.out.println(list1.getClass());// class java.util.ArrayList// 泛型信息在运行时不存在System.out.println(list1.getClass().getTypeParameters().length);// 1System.out.println(list1.getClass().getTypeParameters()[0].getName());// E}}4.2 擦除的影响
类型擦除带来的限制:
❌ 限制1:无法创建泛型数组
classContainer<T>{// ❌ 编译错误:Cannot create a generic array of T// private T[] array = new T[10];}❌ 限制2:无法使用 instanceof 检查泛型类型
classContainer<T>{// ❌ 编译错误:Cannot perform instanceof check against type parameter T// public boolean check(Object obj) {// return obj instanceof T;// }}❌ 限制3:无法创建泛型类型实例
classContainer<T>{// ❌ 编译错误:Cannot instantiate the type T// public T create() {// return new T();// }}❌ 限制4:静态字段不能使用类型参数
classContainer<T>{// ❌ 编译错误:Cannot make a static reference to the non-static type T// private static T staticItem;}✅ 解决方案:传递 Class 对象
通过传递 Class<T> 对象,可以在运行时获取类型信息:
importjava.lang.reflect.Array;classContainer<T>{privateClass<T> type;publicContainer(Class<T> type){this.type = type;}// ✅ 使用反射创建数组@SuppressWarnings("unchecked")publicT[]createArray(int size){return(T[])Array.newInstance(type, size);}// ✅ 使用 Class.isInstance() 检查类型publicbooleancheck(Object obj){return type.isInstance(obj);}// ✅ 使用反射创建实例publicTcreate()throwsException{return type.getDeclaredConstructor().newInstance();}}// 使用示例Container<String> container =newContainer<>(String.class);String[] array = container.createArray(5);System.out.println(container.check("Hello"));// trueSystem.out.println(container.check(123));// falseString instance = container.create();// 创建新的String实例4.3 边界与擦除的关系
类型参数的边界决定了擦除后的类型:
// 示例1:无边界 → 擦除为 ObjectclassBox<T>{privateT item;// 擦除后:private Object item;publicvoidset(T item){this.item = item;}// 擦除后:public void set(Object item) { ... }}// 示例2:单个边界 → 擦除为边界类型classNumberBox<TextendsNumber>{privateT item;// 擦除后:private Number item;publicvoidprocess(){// ✅ 可以调用 Number 的方法double value = item.doubleValue();int intValue = item.intValue();}}// 示例3:多个边界 → 擦除为第一个边界interfaceReadable{Stringread();}interfaceWritable{voidwrite(String data);}classDataBox<TextendsNumber&Readable&Writable>{privateT item;// 擦除后:private Number item;// 注意:第一个边界必须是类(如果有类的话),接口放在后面publicvoidprocess(){// ✅ 可以调用 Number 的方法 item.doubleValue();// ✅ 也可以调用接口方法(编译器会插入强制转换) item.read(); item.write("data");}}重要规则:
- 多个边界时,类必须放在第一位
- 正确:
<T extends Number & Serializable> - 错误:
<T extends Serializable & Number>(编译错误)
五、边界与通配符
5.1 类型参数边界(extends)
使用 extends 关键字限制类型参数的范围,使其必须是某个类的子类或实现某个接口。
// 上界:T 必须是 Number 或其子类classNumberProcessor<TextendsNumber>{privateT number;publicNumberProcessor(T number){this.number = number;}publicdoublegetDoubleValue(){return number.doubleValue();// 可以调用 Number 的方法}publicintcompare(T other){returnDouble.compare(number.doubleValue(), other.doubleValue());}}// 使用示例NumberProcessor<Integer> p1 =newNumberProcessor<>(100);NumberProcessor<Double> p2 =newNumberProcessor<>(3.14);System.out.println(p1.getDoubleValue());// 100.0System.out.println(p2.compare(2.5));// 1// ❌ 编译错误:String 不是 Number 的子类// NumberProcessor<String> p3 = new NumberProcessor<>("error");5.2 通配符详解
通配符(Wildcard)用于表示未知类型,主要用于方法参数,提供更灵活的类型匹配。
| 通配符 | 含义 | 读操作 | 写操作 | 使用场景 |
|---|---|---|---|---|
<?> | 任意类型 | ✅ 只能读取为 Object | ❌ 不能写入(null除外) | 只读取,不关心具体类型 |
<? extends T> | T 或 T 的子类 | ✅ 可以读取为 T 类型 | ❌ 不能写入(null除外) | 从集合中读取数据(生产者) |
<? super T> | T 或 T 的父类 | ❌ 只能读取为 Object | ✅ 可以写入 T 及其子类 | 向集合中写入数据(消费者) |
PECS 原则:Producer Extends, Consumer Super
- 如果你需要从集合中读取数据(生产者),使用
<? extends T> - 如果你需要向集合中写入数据(消费者),使用
<? super T> - 如果既要读又要写,不要使用通配符
importjava.util.*;publicclassWildcardExample{// 示例1:使用 extends 读取数据(生产者)publicstaticdoublesum(List<?extendsNumber> list){double total =0;for(Number num : list){ total += num.doubleValue();// ✅ 可以读取为 Number}// list.add(1); // ❌ 编译错误:不能写入return total;}// 示例2:使用 super 写入数据(消费者)publicstaticvoidaddNumbers(List<?superInteger> list){ list.add(1);// ✅ 可以写入 Integer list.add(2); list.add(3);// Integer num = list.get(0); // ❌ 编译错误:只能读取为 ObjectObject obj = list.get(0);// ✅ 可以读取为 Object}// 示例3:无界通配符publicstaticvoidprintList(List<?> list){for(Object obj : list){// ✅ 只能读取为 ObjectSystem.out.println(obj);}// list.add("test"); // ❌ 编译错误:不能写入(null 除外) list.add(null);// ✅ 可以写入 null}publicstaticvoidmain(String[] args){// extends 示例:可以接受 Number 的任何子类List<Integer> integers =Arrays.asList(1,2,3);List<Double> doubles =Arrays.asList(1.1,2.2,3.3);System.out.println("整数求和: "+sum(integers));// 6.0System.out.println("浮点数求和: "+sum(doubles));// 6.6// super 示例:可以接受 Integer 的任何父类List<Number> numbers =newArrayList<>();List<Object> objects =newArrayList<>();addNumbers(numbers);// ✅ Number 是 Integer 的父类addNumbers(objects);// ✅ Object 是 Integer 的父类System.out.println("numbers: "+ numbers);// [1, 2, 3]System.out.println("objects: "+ objects);// [1, 2, 3]// 无界通配符示例printList(integers);printList(Arrays.asList("A","B","C"));}}5.3 多重边界
一个类型参数可以有多个边界,用 & 连接。类型必须同时满足所有边界条件。
interfaceFlyable{voidfly();}interfaceSwimmable{voidswim();}// 多重边界:T 必须同时实现 Flyable 和 SwimmableclassAnimal<TextendsFlyable&Swimmable>{privateT creature;publicAnimal(T creature){this.creature = creature;}publicvoidmove(){ creature.fly();// ✅ 可以调用 Flyable 的方法 creature.swim();// ✅ 可以调用 Swimmable 的方法}}// 实现类必须同时实现两个接口classDuckimplementsFlyable,Swimmable{@Overridepublicvoidfly(){System.out.println("鸭子飞行");}@Overridepublicvoidswim(){System.out.println("鸭子游泳");}}classFishimplementsSwimmable{@Overridepublicvoidswim(){System.out.println("鱼游泳");}}// 使用示例Animal<Duck> duck =newAnimal<>(newDuck()); duck.move();// 输出:鸭子飞行 \n 鸭子游泳// ❌ 编译错误:Fish 只实现了 Swimmable,没有实现 Flyable// Animal<Fish> fish = new Animal<>(new Fish());多重边界的规则:
- 最多只能有一个类边界,且必须放在第一位
- 可以有多个接口边界
- 格式:
<T extends 类 & 接口1 & 接口2 & ...>
// ✅ 正确:类在前,接口在后classExample<TextendsNumber&Comparable<T>&Serializable>{}// ❌ 错误:接口不能放在类前面// class Example<T extends Comparable<T> & Number> {}// ❌ 错误:不能有多个类边界// class Example<T extends Number & Integer> {}六、常见问题与陷阱
6.1 基本类型不能作为类型参数
Java 泛型不支持基本类型(primitive types),必须使用对应的包装类。
// ❌ 错误:不能使用基本类型// List<int> list = new ArrayList<>();// Map<double, boolean> map = new HashMap<>();// ✅ 正确:使用包装类List<Integer> list =newArrayList<>();Map<Double,Boolean> map =newHashMap<>();// 自动装箱和拆箱 list.add(1);// 自动装箱:int → Integerint value = list.get(0);// 自动拆箱:Integer → int// 注意:频繁装箱拆箱会影响性能List<Integer> numbers =newArrayList<>();for(int i =0; i <1000; i++){ numbers.add(i);// 每次循环都会装箱,性能开销}性能优化建议:
- 对于大量基本类型数据,考虑使用数组
int[]而不是List<Integer> - 或使用第三方库如 Trove、FastUtil 提供的基本类型集合
6.2 不能实现同一泛型接口的不同变体
由于类型擦除,一个类不能同时实现同一个泛型接口的不同参数化版本。
interfaceProcessor<T>{voidprocess(T item);}// ✅ 正确:实现单一类型classStringProcessorimplementsProcessor<String>{@Overridepublicvoidprocess(String item){System.out.println("处理字符串: "+ item);}}// ❌ 错误:不能同时实现 Processor<String> 和 Processor<Integer>// 因为类型擦除后,两个接口都变成了 Processor,方法签名冲突// class MultiProcessor implements Processor<String>, Processor<Integer> {// @Override// public void process(String item) { } // 擦除后:process(Object)//// @Override// public void process(Integer item) { } // 擦除后:process(Object) - 冲突!// }// ✅ 解决方案:使用不同的方法名或包装类classMultiProcessor{publicvoidprocessString(String item){System.out.println("处理字符串: "+ item);}publicvoidprocessInteger(Integer item){System.out.println("处理整数: "+ item);}}6.3 不能创建泛型数组
由于类型擦除,Java 不允许直接创建泛型数组。
importjava.lang.reflect.Array;importjava.util.*;// ❌ 错误:不能创建泛型数组// T[] array = new T[10];// List<String>[] lists = new ArrayList<String>[10]; // 编译错误// List<Integer>[] intLists = new List<Integer>[5]; // 编译错误// ✅ 解决方案1:使用 ArrayList 代替数组List<List<String>> lists =newArrayList<>(); lists.add(newArrayList<>()); lists.add(newArrayList<>());// ✅ 解决方案2:使用原始类型数组(会有警告)@SuppressWarnings("unchecked")List<String>[] lists2 =(List<String>[])newList[10];for(int i =0; i < lists2.length; i++){ lists2[i]=newArrayList<>();}// ✅ 解决方案3:使用 Array.newInstance()(需要 Class 对象)classGenericArray<T>{privateT[] array;@SuppressWarnings("unchecked")publicGenericArray(Class<T> type,int size){ array =(T[])Array.newInstance(type, size);}publicvoidset(int index,T value){ array[index]= value;}publicTget(int index){return array[index];}}// 使用示例GenericArray<String> stringArray =newGenericArray<>(String.class,10); stringArray.set(0,"Hello");System.out.println(stringArray.get(0));// Hello为什么不能创建泛型数组?
- 数组在运行时需要知道元素的确切类型(用于类型检查)
- 泛型类型在运行时被擦除,无法提供这个信息
- 如果允许创建泛型数组,会破坏类型安全
6.4 泛型方法重载问题
由于类型擦除,某些看似合理的方法重载实际上会导致编译错误。
importjava.util.*;// ❌ 错误:擦除后方法签名相同// class Overload {// void method(List<String> list) { } // 擦除后:method(List)// void method(List<Integer> list) { } // 擦除后:method(List) - 冲突!// }// ✅ 解决方案1:使用不同的方法名classOverload{voidprocessStrings(List<String> list){for(String s : list){System.out.println("字符串: "+ s);}}voidprocessIntegers(List<Integer> list){for(Integer i : list){System.out.println("整数: "+ i);}}}// ✅ 解决方案2:使用泛型方法classGenericOverload{<T>voidprocess(List<T> list,Class<T> type){System.out.println("处理类型: "+ type.getSimpleName());for(T item : list){System.out.println(item);}}}// 使用示例GenericOverload processor =newGenericOverload(); processor.process(Arrays.asList("A","B"),String.class); processor.process(Arrays.asList(1,2,3),Integer.class);6.5 原始类型的危险
使用原始类型(Raw Type)会绕过泛型的类型检查,导致运行时错误。
importjava.util.*;// ❌ 危险:使用原始类型List rawList =newArrayList();// 原始类型,没有类型参数 rawList.add("String"); rawList.add(123); rawList.add(newObject());// 运行时错误!String s =(String) rawList.get(1);// ClassCastException// ✅ 正确:始终使用泛型List<String> stringList =newArrayList<>(); stringList.add("String");// stringList.add(123); // 编译错误,类型安全// ⚠️ 警告:原始类型与泛型混用List<String> genericList =newArrayList<>();List rawList2 = genericList;// 警告:unchecked conversion rawList2.add(123);// 编译通过,但破坏了类型安全String str = genericList.get(0);// 可能导致 ClassCastException最佳实践:
- 永远不要使用原始类型(除非与遗留代码交互)
- 如果不确定类型,使用
List<?>而不是List - 使用
@SuppressWarnings("unchecked")时要格外小心
七、实战示例
7.1 泛型栈
importjava.util.*;publicclassGenericStack<T>{privateList<T> items =newArrayList<>();publicvoidpush(T item){ items.add(item);}publicTpop(){if(isEmpty()){thrownewEmptyStackException();}return items.remove(items.size()-1);}publicTpeek(){if(isEmpty()){thrownewEmptyStackException();}return items.get(items.size()-1);}publicbooleanisEmpty(){return items.isEmpty();}publicintsize(){return items.size();}}// 使用GenericStack<String> stack =newGenericStack<>(); stack.push("A"); stack.push("B");System.out.println(stack.pop());// B7.2 泛型缓存
importjava.util.*;publicclassCache<K,V>{privateMap<K,V> cache =newHashMap<>();privateint maxSize;publicCache(int maxSize){this.maxSize = maxSize;}publicvoidput(K key,V value){if(cache.size()>= maxSize){// 简单的LRU:删除第一个K firstKey = cache.keySet().iterator().next(); cache.remove(firstKey);} cache.put(key, value);}publicVget(K key){return cache.get(key);}publicbooleancontainsKey(K key){return cache.containsKey(key);}}// 使用Cache<String,User> userCache =newCache<>(100); userCache.put("user1",newUser("张三"));User user = userCache.get("user1");7.3 泛型构建器
importjava.util.function.BiConsumer;publicclassBuilder<T>{privateT object;publicBuilder(Class<T> clazz)throwsException{ object = clazz.getDeclaredConstructor().newInstance();}public<V>Builder<T>set(BiConsumer<T,V> setter,V value){ setter.accept(object, value);returnthis;}publicTbuild(){return object;}}// 使用classPerson{privateString name;privateint age;publicvoidsetName(String name){this.name = name;}publicvoidsetAge(int age){this.age = age;}}Person person =newBuilder<>(Person.class).set(Person::setName,"张三").set(Person::setAge,25).build();快速参考
泛型语法速查表
| 语法 | 说明 | 示例 | 使用场景 |
|---|---|---|---|
<T> | 类型参数 | class Box<T> | 定义泛型类或方法 |
<T extends X> | 上界约束 | <T extends Number> | 限制类型必须是 X 或其子类 |
<T super X> | 下界约束 | <T super Integer> | 限制类型必须是 X 或其父类 |
<?> | 无界通配符 | List<?> | 只读取,不关心具体类型 |
<? extends X> | 上界通配符 | List<? extends Number> | 读取数据(生产者) |
<? super X> | 下界通配符 | List<? super Integer> | 写入数据(消费者) |
<T, U> | 多个类型参数 | Map<K, V> | 需要多个类型参数 |
<T extends A & B> | 多重边界 | <T extends Number & Comparable<T>> | 类型必须满足多个约束 |
常用类型参数命名约定
| 符号 | 含义 | 常见用法 |
|---|---|---|
E | Element(元素) | List<E>, Set<E> |
K | Key(键) | Map<K, V> |
V | Value(值) | Map<K, V> |
T | Type(类型) | Class<T>, 通用类型 |
N | Number(数字) | 数值类型 |
R | Result(结果) | 返回类型 |
S, U | 第2、3个类型 | 多个类型参数时使用 |
核心原则与最佳实践
PECS 原则(Producer Extends, Consumer Super)
// Producer(生产者):从集合中读取数据 → 使用 extendspublicstaticdoublesum(List<?extendsNumber> numbers){double total =0;for(Number n : numbers){ total += n.doubleValue();// ✅ 可以读取}return total;}// Consumer(消费者):向集合中写入数据 → 使用 superpublicstaticvoidaddIntegers(List<?superInteger> list){ list.add(1);// ✅ 可以写入 list.add(2);}使用建议
- ✅ 优先使用泛型而非原始类型
- ✅ 优先使用泛型方法而非泛型类(更灵活)
- ✅ 使用 PECS 原则选择通配符
- ✅ 避免使用原始类型(Raw Type)
- ✅ 传递
Class<T>对象解决类型擦除问题 - ✅ 使用
@SuppressWarnings("unchecked")时要谨慎,并添加注释说明原因 - ❌ 不要忽略编译器的泛型警告
- ❌ 不要在新代码中使用原始类型
常见错误与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 不能创建泛型数组 | 类型擦除 | 使用 ArrayList 或 Array.newInstance() |
| 不能使用基本类型 | 泛型只支持引用类型 | 使用包装类(Integer, Double 等) |
| 不能 instanceof 泛型 | 类型擦除 | 传递 Class<T> 对象 |
| 方法重载冲突 | 擦除后签名相同 | 使用不同方法名或泛型方法 |
| 静态字段不能用类型参数 | 类型参数属于实例 | 使用静态泛型方法 |
参考资源
- Java官方文档 - Generics
- 《Java编程思想》第15章 - 泛型
- 《Effective Java》第5章 - 泛型