第18章 泛型入门
集合里能装任何类型的对象,但取出时要强转,容易出错。泛型(Generics)解决了"类型安全"问题:在编译期就限定集合装什么类型。本节掌握泛型的基本用法。
一、为什么需要泛型

不使用泛型的集合:可以放任何对象,取出必须强转,运行期才可能报错:
java
List list = new ArrayList();
list.add("Java");
list.add(123); // 什么都能放
String s = (String) list.get(0); // 要强转
String t = (String) list.get(1); // 运行期报错:ClassCastException!
// Integer 强转 String 编译期发现不了
使用泛型的集合:编译期就限定类型,编译报错而不是运行崩溃:
java
List<String> list = new ArrayList<>();
list.add("Java");
// list.add(123); // 编译错误:String 列表不能放 Integer
String s = list.get(0); // 不用强转,自动就是 String
泛型也不只用在集合上:Map<String, Integer> 取出来就是 Integer,不用猜类型、不用强转,类型一目了然。
泛型的好处:
- 编译期类型检查,把错误提前
- 省去强转代码,更简洁
- 代码可复用:一套代码处理多种类型
二、泛型类

自己定义带类型的类:
java
// T 是类型参数,可以是任意标识(惯例用 T、E、K、V)
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
java
Box<String> strBox = new Box<>();
strBox.set("hello");
String s = strBox.get(); // 自动是 String
Box<Integer> intBox = new Box<>();
intBox.set(100);
int n = intBox.get(); // 自动是 Integer
细心的你可能注意到
new Box<>()只写了一次类型,后面的空尖括号叫钻石操作符 (diamond)。这个写法在 JDK 8、11、17 三个版本里都完全一样,不用担心差异;老项目里也常看到new Box<String>()这种写全类型的版本。
同一个类,不同类型参数就是不同类型,但它们内部都是同一个类 (泛型的擦除机制,第七节细讲)。一个类可以有多个类型参数,如 Pair<K, V>(两元组),写法与 Box 相同。
三、泛型方法

方法级别使用泛型,类型参数写在返回值前面:
java
public class Utils {
// 泛型方法:打印数组,任何类型都能用
public static <T> void printArray(T[] arr) {
for (T item : arr) {
System.out.print(item + " ");
}
System.out.println();
}
}
java
String[] names = {"张三", "李四"};
Integer[] nums = {1, 2, 3};
Utils.printArray(names); // 张三 李四
Utils.printArray(nums); // 1 2 3
调用时类型由实参自动推断,不用显式指定。再看一个更实用的例子------交换数组中的两个元素:
java
public class Utils2 {
public static <T> void swap(T[] arr, int i, int j) {
T temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
java
String[] words = {"甲", "乙", "丙"};
Utils2.swap(words, 0, 2); // 类型由实参推断
System.out.println(Arrays.toString(words)); // [丙, 乙, 甲]
Utils2.<String>swap(words, 0, 1); // 也可以显式指定(一般不用写)
四、泛型接口

java
// 泛型接口
interface Generator<T> {
T next();
}
// 实现类指定具体类型
class NumberGenerator implements Generator<Integer> {
@Override
public Integer next() {
return (int) (Math.random() * 100);
}
}
java
Generator<Integer> gen = new NumberGenerator();
System.out.println(gen.next()); // 随机数
实现类也可以继续保留泛型 ,由使用方决定具体类型,写法就是 class MyGenerator<T> implements Generator<T>。
五、类型通配符 ?
通配符用于方法参数,解决"泛型类型之间的继承"问题:
java
// 只能接收 Box<Number>
static void printNum(Box<Number> box) { ... }
// 可以接收 Box<Integer>、Box<Double> 等所有 Number 子类
static void printAny(Box<? extends Number> box) { ... }
java
Box<Integer> intBox = new Box<>();
intBox.set(100);
// printNum(intBox); // 编译错误!Box<Integer> 不是 Box<Number>
printAny(intBox); // 正确:? extends Number
三个通配符:
| 写法 | 含义 |
|---|---|
<?> |
任意类型 |
<? extends T> |
T 或 T 的子类(上限) |
<? super T> |
T 或 T 的父类(下限) |
上限通配符的应用
java
// 计算任意数字集合的和
static double sum(List<? extends Number> nums) {
double total = 0;
for (Number n : nums) {
total += n.doubleValue();
}
return total;
}
java
sum(List.of(1, 2, 3)); // List<Integer>:6.0
sum(List.of(1.5, 2.5)); // List<Double>:4.0
上面例子里的
List.of是 JDK 11 和 17 的快捷写法(需 JDK 11/17 才能运行),JDK 8 里用Arrays.asList(1, 2, 3)效果一样。
下限通配符的应用
? super T 常用于"往里写"的场景:
java
// 接收所有能装 Integer 的集合:List<Integer>、List<Number>、List<Object> 都行
static void addOne(List<? super Integer> list) {
list.add(1); // 可以安全地写入 Integer
}
List<Number> numbers = new ArrayList<>();
addOne(numbers);
System.out.println(numbers); // [1]
注意读写限制:? extends 只能读不能写(null 除外),? super 只能写、读出来只能当 Object(易错点 4 有完整对照)。
六、泛型与继承的注意点(高频考点)
java
Box<Integer> intBox = new Box<>();
Box<Number> numBox;
// numBox = intBox; // 编译错误!Box<Integer> 与 Box<Number> 没有继承关系
即使 Integer extends Number,Box<Integer> 也不是 Box<Number> 的子类 ------泛型参数之间没有继承关系,所以才需要 ? extends。
对比数组的协变------恰恰相反,能编译通过,代价是运行期才报错:
java
String[] strs = new String[3];
Object[] objs = strs; // ✅ 数组协变:String[] 是 Object[] 的子类
objs[0] = 123; // 运行期才报错:ArrayStoreException
List<String> list = new ArrayList<>();
// List<Object> objs2 = list; // ❌ 编译错误:泛型不变
七、泛型的擦除(了解原理)

Java 泛型是编译期机制:编译后类型参数被擦除(替换为 Object 或边界类型),生成的字节码和普通类一样。
java
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true:运行时都是 ArrayList
所以:泛型信息在运行期不存在,不能 new T()、不能 T\[\] 数组直接创建、不能 instanceof T:
java
// 以下写法全部编译错误(擦除后 T 在运行期没有具体类型):
// T obj = new T(); // ❌ 不能 new 类型参数
// T[] arr = new T[10]; // ❌ 不能直接创建泛型数组
// if (x instanceof T) { } // ❌ 不能 instanceof 类型参数
有边界时擦除成边界类型(如 <T extends Number> 擦除成 Number)。
八、扩展知识
1. 擦除带来的"重载冲突"
既然 List<String> 和 List<Integer> 运行时都是 List,它们不能构成方法重载:
java
// ❌ 编译错误:擦除后签名都是 print(List),冲突!
// void print(List<String> list) { }
// void print(List<Integer> list) { }
// ✅ 改方法名即可:printString / printInt
同理,泛型类不能继承 Throwable(异常机制在运行期工作,而运行期没有泛型)。
2. 通配符进阶:PECS 原则
什么时候用 ? extends、什么时候用 ? super?记住口诀 PECS:Producer Extends, Consumer Super(生产者用 extends,消费者用 super)。
- 方法只读取 元素 → 参数用
? extends T(生产者) - 方法只写入 元素 → 参数用
? super T(消费者)
经典例子------把 src 全部拷到 dest:
java
// src 只读(生产者)用 extends,dest 只写(消费者)用 super
static <T> void copy(List<? extends T> src, List<? super T> dest) {
for (T item : src) {
dest.add(item);
}
}
java
List<Integer> src = Arrays.asList(1, 2, 3); // JDK 8 写法(JDK 11/17 可用 List.of)
List<Number> dest = new ArrayList<>();
copy(src, dest);
System.out.println(dest); // [1, 2, 3]
copy 能同时接收 List<Integer> 和 List<Number>,读写都安全------这就是通配符的意义。
3. 泛型方法 vs 泛型类,怎么选
- 泛型类 :整个类的多个字段/方法共用同一个 T,比如
Box<T>、Pair<K, V> - 泛型方法 :只有这一个方法用泛型。static 方法拿不到类的泛型,只能用泛型方法:
java
class Box<T> {
private T value;
// ❌ static 成员不能用类上的泛型
// static T count; // 编译错误
// static void show(T v) { } // 编译错误
// ✅ 静态方法用"自己的"泛型(换个字母避免混淆)
static <E> void show(E v) {
System.out.println(v);
}
}
类型参数可有多个上限,用 & 连接,如 <T extends Number & Comparable<T>>。
九、易错点
1. 基本类型不能做类型参数
java
// ❌ List<int> list = new ArrayList<>(); // 编译错误
// ✅ 用包装类:自动装箱/拆箱
List<Integer> list = new ArrayList<>();
list.add(10); // int → Integer(装箱)
int x = list.get(0); // Integer → int(拆箱)
System.out.println(x); // 10
包装类对照:int→Integer、long→Long、double→Double、boolean→Boolean、char→Character。
2. 泛型数组创建报错
java
// ❌ 以下都编译错误(Generic array creation)
// List<String>[] arr = new List<String>[10];
// T[] arr = new T[10];
// ✅ 用 List 代替数组
List<List<String>> list = new ArrayList<>();
list.add(new ArrayList<>());
System.out.println(list.size()); // 1
3. 静态成员不能用类泛型
java
class Box<T> {
private T value; // ✅ 实例字段可用类泛型
// ❌ static T count; // 编译错误
// ❌ static void f(T v) {} // 编译错误:静态成员不能用类泛型
// ✅ 静态方法要自己声明泛型
}
4. 通配符的读写限制搞反
java
List<? extends Number> nums = new ArrayList<Integer>();
// ❌ nums.add(1); // 编译错误:? extends 只能读不能写(null 除外)
Number n = nums.get(0); // ✅ 读出来当 Number
List<? super Integer> ints = new ArrayList<Number>();
ints.add(1); // ✅ 可以写 Integer
// ❌ Integer x = ints.get(0); // 编译错误:? super 读出来只能当 Object
Object o = ints.get(0); // ✅
记住:extends 偏读(上限),super 偏写(下限)。
5. instanceof 不能用于泛型类型
java
List<String> list = new ArrayList<>();
// ❌ list instanceof List<String> // 编译错误:运行期没有泛型
// ✅ 用无界通配符
if (list instanceof List<?>) {
System.out.println("是一个 List"); // 是一个 List
}
十、小结
- 泛型限定集合/类的类型,编译期检查,免强转
- 泛型类
class Box<T>、泛型方法<T> 返回类型 方法名、泛型接口 ? extends T上限(偏读),? super T下限(偏写),PECS 记读写方向Box<Integer>不是Box<Number>的子类,数组协变但泛型不变- 泛型运行期擦除:不能 new T()、不能建泛型数组、不能 instanceof T
- 基本类型用包装类;static 成员不能用类泛型,要用泛型方法
下一节学习 IO 流。
下一篇:第19章 IO 流