ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Java 泛型:类型参数、通配符与类型擦除

Java 泛型:类型参数、通配符与类型擦除

目录

  • 一、泛型要解决的问题
  • 二、集合中的泛型
  • 三、自定义泛型结构
  • 四、自定义泛型方法
  • 五、泛型与继承关系
  • 六、类型通配符
  • 七、有限制的通配符
  • 八、泛型的综合应用
  • 九、要点回顾

示例约定。标注「完整示例」的代码块可保存为.java文件编译运行;标注「片段」的代码放入方法或类体。示例以 JDK 8 为基准。文中出现的T、E、K、V均为类型变量的惯用名,可以替换为任意合法标识符。


一、泛型要解决的问题

一.1 一个直观的类比

中药店的药柜有许多抽屉,每个抽屉外面贴着标签,写明里面装的是什么。取药时按标签定位,不必逐个拉开确认。

Java 的泛型起的是同样的作用:给容器贴上类型标签,让编译器在写入和取出时都知道里面装的是什么类型,而不必等到运行时再判断。

一.2 从方法形参到类型参数

声明方法时,如果完成功能需要一项未知的数据参与,而该项数据要到调用时才能确定,做法是把它声明为形参:方法体中使用形参名代表这个未知值,调用者传入实参即可。

泛型沿用了同样的思路,只是把「值的未知」提升为「类型的未知」。方法参数传递的是值,类型参数传递的是类型。

JDK 1.5 引入泛型,其本质是参数化类型:在声明类、接口或方法时,用一个标识符代表未知的某种通用类型;在真正使用时(继承、实现接口、创建对象、调用方法)再传入具体类型。

一.3 引入泛型的两个动因

动因一:集合无法预知元素类型。

集合类在设计阶段无法确定容器里究竟存放什么类型的对象。JDK 5.0 之前只能把元素类型设计为Object,代价是两重问题:写入时任何类型都能进入,类型约束形同虚设;取出时必须强制转换,一旦类型判断错误,编译期毫无提示,运行时才抛ClassCastException。

JDK 5.0 引入参数化类型后,可以在创建集合时指定元素类型:

// 片段 List<String> list = new ArrayList<>();

这条声明表明该List只能保存String。

这里有两点值得注意。第一,用集合存储数据时,元素类型不确定,但其他部分是确定的:元素如何保存、如何扩容、如何遍历,都与元素类型无关。泛型要参数化的正是那个不确定的部分。第二,类型检查前移到编译期,代价是写法变长,收益是错误提前暴露。

动因二:比较器接口无法约束比较对象。

java.lang.Comparable与java.util.Comparator用于比较对象大小。两个接口只约定了语义:a 大于 b 返回正整数,小于返回负整数,相等返回 0。但接口本身并不确定参与比较的是什么类型。

JDK 5.0 之前只能用Object表示,实现类中不得不写成compareTo(Object o),再自行强转,既繁琐又不安全。JDK 5.0 为它们增加了泛型:

// 片段 public interface Comparable<T> { int compareTo(T o); } public interface Comparator<T> { int compare(T o1, T o2); }

其中的T就是类型参数。实现Comparable<Person>之后,compareTo的形参类型固定为Person,传入其他类型直接编译失败。

一.4 泛型的定义

泛型允许在定义类、接口时,用一个标识符表示类中某个属性的类型,或某个方法返回值、参数的类型。这个类型参数在使用时确定,传入的实际类型称为类型实参。


二、集合中的泛型

JDK 5.0 引入泛型后,核心类库做了大幅改写:集合框架中的全部接口与类、Comparable、Comparator、Class等均增加了泛型支持,从而可以在声明变量、创建对象时传入类型实参。

二.1 使用泛型前后的差别

不使用泛型时,集合中的元素一律按Object处理:

// 完整示例 NoGenericDemo.java import java.util.ArrayList; import java.util.Iterator; public class NoGenericDemo { public static void main(String[] args) { ArrayList list = new ArrayList(); list.add(56); list.add(76); list.add("Tom"); // 编译通过,运行时才可能出问题 Iterator iterator = list.iterator(); while (iterator.hasNext()) { Object obj = iterator.next(); if (obj instanceof Integer) { Integer score = (Integer) obj; // 必须强转 System.out.println(score); } } } }

同一段逻辑使用泛型后:

// 完整示例 GenericDemo.java import java.util.ArrayList; import java.util.Iterator; public class GenericDemo { public static void main(String[] args) { ArrayList<Integer> list = new ArrayList<>(); list.add(56); // 自动装箱 list.add(76); // list.add("Tom"); // 编译报错,类型不符 Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()) { Integer score = iterator.next(); // 无需强转 System.out.println(score); } } }

两次差异可以归结为三点:

对比项无泛型有泛型
类型约束任意类型都可写入写入时即校验
取出元素必须强转直接得到声明类型
错误暴露时机运行时抛ClassCastException编译期报错

由此得出一条判断:如果程序在编译期没有产生泛型警告,运行时就不会因类型不符抛出ClassCastException。不安全的因素被提前到编译期排除,代码同时变得更简洁、更健壮。

把一个集合的内容限制为特定数据类型,这就是泛型的核心思想。

二.2 List 中使用泛型

// 完整示例 ListGenericTest.java import java.util.ArrayList; import java.util.Iterator; public class ListGenericTest { public static void main(String[] args) { // 将学生成绩保存在 ArrayList 中 // 标准写法:ArrayList<Integer> list = new ArrayList<Integer>(); // JDK 7 起可用菱形语法,由编译器推断类型 ArrayList<Integer> list = new ArrayList<>(); list.add(56); // 自动装箱 list.add(76); list.add(88); list.add(89); // list.add("Tom"); // 编译报错 Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()) { Integer score = iterator.next(); System.out.println(score); } } }

二.3 Map 中使用泛型

Map有两个类型参数,分别对应键与值,遍历时三种集合视图的类型也随之确定。

// 完整示例 MapGenericTest.java import java.util.*; public class MapGenericTest { public static void main(String[] args) { HashMap<String, Integer> map = new HashMap<>(); map.put("Tom", 67); map.put("Jim", 56); map.put("Rose", 88); // map.put(67, "Jack"); // 编译不通过 // 遍历 key 集 Set<String> keySet = map.keySet(); for (String str : keySet) { System.out.println(str); } // 遍历 value 集 Collection<Integer> values = map.values(); Iterator<Integer> iterator = values.iterator(); while (iterator.hasNext()) { Integer value = iterator.next(); System.out.println(value); } // 遍历 entry 集 Set<Map.Entry<String, Integer>> entrySet = map.entrySet(); Iterator<Map.Entry<String, Integer>> iterator1 = entrySet.iterator(); while (iterator1.hasNext()) { Map.Entry<String, Integer> entry = iterator1.next(); String key = entry.getKey(); Integer value = entry.getValue(); System.out.println(key + ":" + value); } } }

二.4 使用说明

  1. 创建集合对象时可以指明泛型类型,格式为List<String> list = new ArrayList<String>();
  2. JDK 7.0 起可以简写为List<String> list = new ArrayList<>();,右侧由编译器推断,称为菱形语法;
  3. 泛型参数只能使用引用数据类型。基本类型不能作为类型实参,需用对应的包装类替代,例如int写作Integer;
  4. 声明集合时可声明泛型参数,使用时再具体指明。一旦指明,类或接口内部凡是使用该类型参数的位置都替换为具体类型;如果没有指明,则一律按Object处理。

二.5 练习:集合与泛型的配合

要求:

  1. 创建ArrayList集合对象,指定泛型为Integer;
  2. 添加 5 个[0, 100)以内的整数;
  3. 使用 foreach 遍历输出这 5 个整数;
  4. 使用removeIf删除偶数,为Predicate接口指定泛型Integer;
  5. 使用Iterator输出剩余元素,为Iterator接口指定泛型Integer。
// 完整示例 TestList.java import java.util.ArrayList; import java.util.Iterator; import java.util.Random; public class TestList { public static void main(String[] args) { // (1) 创建集合 ArrayList<Integer> list = new ArrayList<>(); // (2) 添加 5 个 [0, 100) 的随机整数 Random rand = new Random(); for (int i = 0; i < 5; i++) { list.add(rand.nextInt(100)); } System.out.println("原始集合:" + list); // (3) foreach 遍历 System.out.println("--- foreach 遍历 ---"); for (Integer num : list) { System.out.println(num); } // (4) 删除偶数,Predicate 的泛型由 Lambda 推断为 Integer list.removeIf(num -> num % 2 == 0); System.out.println("删除偶数后:" + list); // (5) Iterator 遍历 System.out.println("--- Iterator 遍历 ---"); Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()) { Integer num = iterator.next(); System.out.println(num); } } }

第 4 步中removeIf的形参是Predicate<? super E>,E已由ArrayList<Integer>确定为Integer,因此 Lambda 参数无需显式声明类型。


三、自定义泛型结构

三.1 语法形式

<类型>这种写法就是泛型。其中尖括号内的标识符称为类型参数,习惯上用T表示,取自 Type 的首字母;也可以使用K、V、E等任意合法标识符,K与V分别对应 Key 与 Value,E对应 Element。

类型参数代表一个未知的数据类型,使用时可以指定为Integer、String、Person等具体类型。类比方法参数,声明处的T称为类型形参,传入处的Integer称为类型实参。

类型变量可以声明在两个位置:

位置一:类名或接口名之后。这样声明的类或接口称为泛型类、泛型接口。

// 片段 [修饰符] class 类名<类型变量列表> [extends 父类] [implements 接口们] { } [修饰符] interface 接口名<类型变量列表> [extends 接口们] { }
// 片段:JDK 中的例子 public class ArrayList<E> public interface Map<K, V>

位置二:修饰符与返回值类型之间。这样声明的方法称为泛型方法。

// 片段 [修饰符] <类型变量列表> 返回值类型 方法名([形参列表]) [throws 异常列表] { // ... }
// 片段:java.util.Arrays 中的例子 public static <T> List<T> asList(T... a)

三.2 自定义泛型类与泛型接口

当一个类或接口中某个成员的类型不确定,且该类型需要在使用时才确定,就适合定义为泛型类或泛型接口。

三.2.1 使用规则
  1. 声明完自定义泛型类后,可以在类内部使用该泛型:属性类型、方法参数与返回值、构造器参数均可使用。
  2. 创建泛型类对象时可以指明类型实参。一旦指明,内部所有使用该类型参数的位置都具体化为该类型。
  3. 创建对象时若未指明类型实参,泛型将被擦除,相应位置按Object处理,但不等价于Object——这一点在反射与重载解析中会体现。
  4. 泛型指定必须使用引用数据类型,不能使用基本类型,此时只能用包装类替换。
  5. 除创建对象外,子类继承泛型类、实现类实现泛型接口时,同样可以确定泛型参数。
  6. 若子类本身也不确定类型,可以继续沿用泛型参数;也可以在父类已有泛型参数的基础上新增类型参数。

关于第 3 条有一条实践经验:泛型要么一路都用,要么一路都不用。混用时部分位置按具体类型处理、部分位置按Object处理,读写规则随之不一致,排查成本远高于统一写法。

三.2.2 六条限制
  1. 泛型类可以有多个参数,一并写在尖括号内,如<E1, E2, E3>。
  2. JDK 7.0 起可用菱形语法简化创建过程,如ArrayList<String> flist = new ArrayList<>();。
  3. 若泛型结构是接口或抽象类,则不能直接创建其对象。
  4. 不能写new E[]。需要泛型数组时只能绕道:E[] elements = (E[]) new Object[capacity];。ArrayList源码中声明的正是Object[] elementData,而非泛型参数类型数组,原因同上——运行时无法得知E的具体类型,JVM 无法为其分配确定大小的数组。
  5. 在类或接口上声明的泛型,在本类或本接口中代表某种确定类型,但不能在静态方法中使用类上声明的泛型。静态成员随类加载而存在,此时类型实参尚未传入。
  6. 异常类不能带泛型。catch子句需要运行时确定异常类型,而泛型在运行时已擦除。
三.2.3 举例一:泛型类的基本写法
// 完整示例 Person.java public class Person<T> { private T info; // 用 T 定义属性 public Person() { } public Person(T info) { // 用 T 定义构造器参数 this.info = info; } public T getInfo() { // 用 T 定义返回值 return info; } public void setInfo(T info) { // 用 T 定义参数 this.info = info; } // 编译错误:静态方法中不能使用类上声明的泛型 // public static void show(T t) { } // 编译错误:catch 的参数不能是泛型类型 // public void test() { // try { // } catch (MyException<T> ex) { // } // } }
三.2.4 举例二:子类如何确定父类泛型

父类带有两个类型参数,子类有四种处理方式。

// 完整示例 FatherSon.java class Father<T1, T2> { } // 子类不保留父类的泛型 // 1) 不写类型:擦除,等价于 extends Father<Object, Object> class Son1 extends Father { } // 2) 写具体类型 class Son2 extends Father<Integer, String> { } // 子类保留父类的泛型 // 1) 全部保留 class Son3<T1, T2> extends Father<T1, T2> { } // 2) 部分保留 class Son4<T2> extends Father<Integer, T2> { }

子类也可以在自己新增类型参数的同时继承父类的泛型:

// 完整示例 FatherSon2.java class Father<T1, T2> { } // 子类新增 A、B 两个类型参数,父类泛型被擦除 class Son<A, B> extends Father { // 等价于 extends Father<Object, Object> } // 新增类型参数,同时为父类指定具体类型 class Son2<A, B> extends Father<Integer, String> { } // 新增类型参数,同时全部保留父类泛型 class Son3<T1, T2, A, B> extends Father<T1, T2> { } // 新增类型参数,同时部分保留父类泛型 class Son4<T2, A, B> extends Father<Integer, T2> { }

四、自定义泛型方法

如果定义类或接口时没有使用类型参数,而某个方法的形参类型不确定,可以只在该方法上定义类型参数。

四.1 格式与规则

// 片段 [访问权限] <泛型> 返回值类型 方法名([泛型标识 参数名称]) [抛出的异常] { }

四条规则:

  1. 方法与所在类是否为泛型类无关,普通类中同样可以定义泛型方法;
  2. 泛型方法中的类型参数在方法被调用时确定;
  3. 泛型方法可以声明为static,此时类型参数来自方法自身,与类无关;
  4. 泛型方法的类型参数应写在返回值类型之前,否则编译器会将其视为普通返回值类型而报错。

四.2 举例一:返回类型由调用方决定

// 完整示例 DAO.java public class DAO { public <E> E get(int id, E e) { E result = null; return result; } }

调用dao.get(1, "abc")时,E被推断为String;调用dao.get(1, 100)时,E被推断为Integer。同一个方法对不同调用返回不同类型,且无需强转。

四.3 举例二:数组到集合的搬运

// 完整示例 CollectionCopy.java import java.util.*; public class CollectionCopy { public static <T> void fromArrayToCollection(T[] a, Collection<T> c) { for (T o : a) { c.add(o); } } public static void main(String[] args) { Object[] ao = new Object[100]; Collection<Object> co = new ArrayList<Object>(); fromArrayToCollection(ao, co); // T 推断为 Object String[] sa = new String[20]; Collection<String> cs = new ArrayList<>(); fromArrayToCollection(sa, cs); // T 推断为 String Collection<Double> cd = new ArrayList<>(); // fromArrayToCollection(sa, cd); // 编译错误:T 无法同时为 String 与 Double fromArrayToCollection(sa, co); // T 推断为 Object,String[] 可赋给 Object[] } }

最后一行是关键:String[]是Object[]的子类型,因此T可以被推断为Object;但Collection<String>不是Collection<Object>的子类型,所以fromArrayToCollection(sa, co)只是借用数组的协变关系,集合一侧的实参类型仍然是Collection<Object>。

四.4 举例三:泛型方法实现排序

// 完整示例 MyArrays.java import java.util.Arrays; class MyArrays { public static <T> void sort(T[] arr) { for (int i = 1; i < arr.length; i++) { for (int j = 0; j < arr.length - i; j++) { if (((Comparable<T>) arr[j]).compareTo(arr[j + 1]) > 0) { T temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } } } public class MyArraysTest { public static void main(String[] args) { int[] arr = {3, 2, 5, 1, 4}; // MyArrays.sort(arr); // 错误:int[] 不是对象数组,T 无法绑定 String[] strings = {"hello", "java", "song"}; MyArrays.sort(strings); System.out.println(Arrays.toString(strings)); Circle[] circles = {new Circle(2.0), new Circle(1.2), new Circle(3.0)}; MyArrays.sort(circles); // 编译通过,运行抛出 ClassCastException } }

最后一个调用暴露了T无界的代价:编译期只保证元素是Object,强转成Comparable的动作放到运行时,Circle未实现该接口便直接抛异常。若把签名改为<T extends Comparable<T>>,这类错误会在编译期被拦下。


五、泛型与继承关系

如果 B 是 A 的子类型(子类或子接口),G 是带泛型声明的类或接口,那么G 与 G之间不存在父子类型关系。

String是Object的子类,但List<String>不是List<Object>的子类。数组则相反,String[]是Object[]的子类型。这一差异是理解泛型不变性的起点。

// 完整示例 GenericSubClass.java import java.util.List; public class GenericSubClass { public void testGenericAndSubClass() { Person[] persons = null; Man[] mans = null; persons = mans; // 合法:Person[] 是 Man[] 的父类型 Person p = mans[0]; // 合法 List<Person> personList = null; List<Man> manList = null; // personList = manList; // 编译错误:List<Person> 与 List<Man> 无继承关系 } } class Person { } class Man extends Person { }

数组的协变看似方便,实则埋了隐患:Object[] arr = new String[1]; arr[0] = 100;能通过编译,运行时抛ArrayStoreException。泛型选择不变性,正是为了把这类错误提前到编译期。

五.1 两道对比题

两段代码的形参声明不同,接受实参的范围也不同。

// 片段一:原始类型形参 public void printCollection(Collection c) { Iterator i = c.iterator(); for (int k = 0; k < c.size(); k++) { System.out.println(i.next()); } }
// 片段二:Collection<Object> 形参 public void printCollection(Collection<Object> c) { for (Object e : c) { System.out.println(e); } }

片段一使用的是原始类型,任何Collection都能传入,但元素按Object处理,编译期不产生警告;片段二的形参是Collection<Object>,只能接受Collection<Object>及其子类型,List<String>、List<Integer>都无法传入。

实际开发中两者都不是好选择:前者放弃了类型检查,后者适用范围过窄。正确做法是使用通配符Collection<?>,既能接受任意类型集合,又不放弃类型安全。


六、类型通配符

声明变量或形参时,如果类型是泛型类或泛型接口,但其中的类型实参仍无法确定,可以使用类型通配符?。

六.1 通配符的含义

使用形式为?,例如List<?>、Map<?, ?>。

List<?>是List<String>、List<Object>、List<Integer>等各种泛型List的父类型。注意这里的措辞:List<?>与它们之间存在父子关系,而List<Object>与List<String>之间没有。

六.2 读与写的不对称

写操作受限。向Collection<?>中添加元素不是类型安全的:

// 片段 Collection<?> c = new ArrayList<String>(); c.add(new Object()); // 编译时错误

原因在于add的形参类型是类型参数E,编译器无法得知E到底是什么,因此无法确认传入的对象是E的实例。唯一可以插入的是null,因为它是所有引用类型的合法值。

读操作安全。读取List<?>中的元素永远是安全的:无论实际类型是什么,元素都可以用Object接收。

操作Collection<?>Collection<T>(泛型方法中)Collection<Object>
接收任意类型集合可以,适用范围最广可以,调用时推断T不可以,只能接收Collection<Object>
写入元素除null外均不可可以,必须是T类型可以,但通常需要强转
读取元素可以,只能用Object接收可以,用T接收可以,用Object接收

这张表可以概括为一句:通配符适合「只读」,泛型方法适合「读写」,Object具体类型适合「确定就是 Object」。

六.3 举例

// 完整示例 TestWildcard.java import java.util.*; public class TestWildcard { public static void m4(Collection<?> coll) { for (Object o : coll) { System.out.println(o); } } public static void main(String[] args) { List<?> list = null; list = new ArrayList<String>(); list = new ArrayList<Double>(); // list.add(3); // 编译不通过 list.add(null); // 唯一允许的写入 List<String> l1 = new ArrayList<String>(); List<Integer> l2 = new ArrayList<Integer>(); l1.add("尚硅谷"); l2.add(15); read(l1); read(l2); } public static void read(List<?> list) { for (Object o : list) { System.out.println(o); } } }

六.4 三条使用限制

  1. 不能用于泛型方法声明。返回值类型前的<>中不能写?:public static <?> void test(ArrayList<?> list)编译错误。类型参数必须是可命名的标识符,?表示「未知」,无法在方法体中引用。
  2. 不能用于泛型类的声明。class GenericTypeClass<?> { }编译错误,理由同上。
  3. 不能用于创建对象。ArrayList<?> list2 = new ArrayList<?>();编译错误,右侧创建对象时必须给出确定的类型实参。

七、有限制的通配符

七.1 三种形式

形式含义范围
<?>允许所有泛型的引用调用全集
<? extends 类或接口>类型必须是该类或其子类,或该接口的实现类上限,<=
<? super 类或接口>类型必须是该类或其父类,或该接口的父接口下限,>=
// 片段 <? extends Number> // (无穷小, Number],只允许 Number 及其子类 <? super Number> // [Number, 无穷大),只允许 Number 及其父类 <? extends Comparable> // 只允许 Comparable 的实现类

七.2 举例一:类型参数的上限

// 完整示例 PersonTest.java class Creature { } class Person extends Creature { } class Man extends Person { } class PersonTest { public static <T extends Person> void test(T t) { System.out.println(t); } public static void main(String[] args) { test(new Person()); test(new Man()); // test(new Creature()); // 编译错误:T 的上限是 Person,Creature 不满足 } }

<T extends Person>与<? extends Person>写法相近,语义位置不同:前者用于声明类型参数,后者用于描述已有泛型的实参范围。

七.3 举例二:集合形参的上限与下限

// 完整示例 WildcardBound.java import java.util.*; public class WildcardBound { public static void main(String[] args) { Collection<Integer> list1 = new ArrayList<Integer>(); Collection<String> list2 = new ArrayList<String>(); Collection<Number> list3 = new ArrayList<Number>(); Collection<Object> list4 = new ArrayList<Object>(); getElement1(list1); // 合法:Integer 是 Number 的子类 // getElement1(list2); // 报错:String 不是 Number 的子类 getElement1(list3); // 合法:Number 本身 // getElement1(list4); // 报错:Object 不是 Number 的子类 // getElement2(list1); // 报错:Integer 不是 Number 的父类 // getElement2(list2); // 报错 getElement2(list3); // 合法:Number 本身 getElement2(list4); // 合法:Object 是 Number 的父类 } // 上限:泛型必须是 Number 或 Number 的子类 public static void getElement1(Collection<? extends Number> coll) { } // 下限:泛型必须是 Number 或 Number 的父类 public static void getElement2(Collection<? super Number> coll) { } }

七.4 举例三:遍历带界的通配符集合

// 片段 public static void printCollection1(Collection<? extends Person> coll) { // 迭代器的类型参数同样受上限约束,用 Iterator<?> 最稳妥 Iterator<?> iterator = coll.iterator(); while (iterator.hasNext()) { Person per = (Person) iterator.next(); System.out.println(per); } } public static void printCollection2(Collection<? super Person> coll) { Iterator<?> iterator = coll.iterator(); while (iterator.hasNext()) { Object obj = iterator.next(); System.out.println(obj); } }

两段代码体现了同一个规律:上限通配符可以读成具体类型,下限通配符只能读成Object。

七.5 举例四:带界通配符的读写规则

// 完整示例 BoundReadWrite.java import java.util.*; public class BoundReadWrite { public void test1() { List<Person> list2 = new ArrayList<Person>(); List<? extends Person> list4 = null; list2.add(new Person()); list4 = list2; // 合法:List<Person> 是 List<? extends Person> 的子类型 Person p1 = list4.get(0); // 读取:可以,返回值按上限 Person 处理 list4.add(null); // 写入:只允许 null // list4.add(new Person()); // 编译错误 // list4.add(new Student()); // 编译错误 } public void test2() { List<Person> list2 = new ArrayList<Person>(); List<? super Person> list5 = null; list2.add(new Person()); list5 = list2; // 合法 Object obj = list5.get(0); // 读取:只能按 Object 接收 list5.add(new Person()); // 写入:可以写入 Person list5.add(new Student()); // 可以写入 Person 的子类 } } class Person { } class Student extends Person { }

test1不能写入,是因为编译器只知道元素类型「至多是Person」,实际可能是Student,写入Person会破坏集合;
test2可以写入Person及其子类,是因为元素类型「至少是Person」,任何Person及其子类都能安全存入。

这条规律通常概括为 PECS 原则:Producer Extends, Consumer Super。只读取(生产数据)时用extends,只写入(消费数据)时用super,既读又写则不应使用通配符。


八、泛型的综合应用

八.1 泛型嵌套

// 完整示例 NestedGeneric.java import java.util.*; public class NestedGeneric { public static void main(String[] args) { HashMap<String, ArrayList<Citizen>> map = new HashMap<String, ArrayList<Citizen>>(); ArrayList<Citizen> list = new ArrayList<Citizen>(); list.add(new Citizen("赵又廷")); list.add(new Citizen("高圆圆")); list.add(new Citizen("瑞亚")); map.put("赵又廷", list); Set<Map.Entry<String, ArrayList<Citizen>>> entrySet = map.entrySet(); Iterator<Map.Entry<String, ArrayList<Citizen>>> iterator = entrySet.iterator(); while (iterator.hasNext()) { Map.Entry<String, ArrayList<Citizen>> entry = iterator.next(); String key = entry.getKey(); ArrayList<Citizen> value = entry.getValue(); System.out.println("户主:" + key); System.out.println("家庭成员:" + value); } } } class Citizen { private String name; public Citizen(String name) { this.name = name; } @Override public String toString() { return name; } }

嵌套泛型的类型声明会随层数迅速变长。JDK 7 之后的菱形语法只能简化创建一侧,声明一侧仍需写全;若出现三层以上嵌套,通常应抽出中间类型或改用局部变量分步声明。

八.2 个人信息设计:用泛型表达关联

设计类时常用关联关系表达「一个对象持有另一个对象」。若这个被持有的对象有多个变体(联系方式、基本信息等),可以把属性的类型声明为泛型,再为每种变体提供实现类。

// 完整示例 GenericPerson.java interface Info { } // 只有此接口的子类才表示人的信息 class Contact implements Info { // 联系方式 private String address; private String telephone; private String zipcode; public Contact(String address, String telephone, String zipcode) { this.address = address; this.telephone = telephone; this.zipcode = zipcode; } public void setAddress(String address) { this.address = address; } public void setTelephone(String telephone) { this.telephone = telephone; } public void setZipcode(String zipcode) { this.zipcode = zipcode; } public String getAddress() { return this.address; } public String getTelephone() { return this.telephone; } public String getZipcode() { return this.zipcode; } @Override public String toString() { return "Contact [address=" + address + ", telephone=" + telephone + ", zipcode=" + zipcode + "]"; } } class Introduction implements Info { // 基本信息 private String name; private String sex; private int age; public Introduction(String name, String sex, int age) { this.name = name; this.sex = sex; this.age = age; } public void setName(String name) { this.name = name; } public void setSex(String sex) { this.sex = sex; } public void setAge(int age) { this.age = age; } public String getName() { return this.name; } public String getSex() { return this.sex; } public int getAge() { return this.age; } @Override public String toString() { return "Introduction [name=" + name + ", sex=" + sex + ", age=" + age + "]"; } } class Person<T extends Info> { private T info; public Person(T info) { // 通过构造器设置信息属性 this.info = info; } public void setInfo(T info) { this.info = info; } public T getInfo() { return info; } @Override public String toString() { return "Person [info=" + info + "]"; } } public class GenericPerson { public static void main(String[] args) { Person<Contact> per = new Person<Contact>( new Contact("北京市", "01088888888", "102206")); System.out.println(per); Person<Introduction> per2 = new Person<Introduction>( new Introduction("李雷", "男", 24)); System.out.println(per2); } }

T extends Info的作用是限定边界:Person只能接收Info的实现类,从而保证getInfo()的返回值至少具备Info的契约,而不会退化为Object。


九、要点回顾

主题关键结论
泛型本质参数化类型,把「类型的未知」延迟到使用时确定
主要收益类型检查前移到编译期,免去强转
类型实参只能使用引用类型,基本类型需包装类
擦除未指定类型实参时按Object处理,但不等价于Object
泛型类类型参数写在类名后,静态方法中不可使用该参数
泛型方法类型参数写在返回值类型前,与所在类是否为泛型类无关
数组不能new E[],需(E[]) new Object[n]
继承G<A>与G<B>无父子关系,数组则有协变
通配符?是各种具体泛型的父类型;只读安全,写入仅允许null
有界通配符extends定上限,super定下限,遵循 PECS
异常类不能带泛型参数
返回列表