泛型基础
本教程共 100 篇 · 第 83 篇 · 更新于 2026-08-05 · 约 5 分钟阅读
83. 泛型基础
本节目标:理解泛型为什么存在——用类型参数在编译期锁死容器元素类型,免去强转、杜绝类型错配,学完能写简单的泛型类和方法。
没有泛型的日子
回到集合还没泛型的时候,List 只能装 Object,取出来是个 Object,你得自己强转成真正类型。
// 历史兼容写法(别在主线上用)
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // 必须强转,且容易转错
```bash
问题在哪?编译器不知道你往里塞了什么。你塞个整数进去,取的时候按字符串转,编译期一声不吭,运行到那行才崩 `ClassCastException`。这种 bug 藏得很深。
## 泛型把类型锁在编译期
泛型用一个「类型参数」告诉编译器:这个容器只装某一种类型。从那以后,塞错类型编译就报错,取出来也不用强转。
```java
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(123); // 编译期直接红字,根本跑不到运行
String s = list.get(0); // 自动就是 String,不用强转
类型安全从「运行时才崩」提前成了「写代码时就被拦」,这正是泛型最大的价值。
Note泛型只存在于编译期。编译完成后,类型参数会被「擦除」,运行时其实还是 Object 在底层跑。这叫「类型擦除」,初学不必深究,只要享受它带来的编译期检查即可。
写一个泛型类
你也可以给自己写的类加类型参数,用尖括号 <T> 声明,T 代表「某种类型」。
class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> box = new Box<>();
box.set("礼物");
String gift = box.get(); // 直接是 String
```bash
`Box<String>` 表示这个盒子里只装 String。编译器会保证你不会往里塞别的类型,取出来的也一定是 String。
## 类型参数的命名约定
类型参数名是可以随便起的,但圈子里有约定俗成的字母,读起来一眼懂:
- `E`:Element,集合里的元素(如 `ArrayList<E>`)。
- `T`:Type,通用类型。
- `K` / `V`:Key / Value,键值对(如 `Map<K, V>`)。
- `N`:Number,数字。
- `?`:通配符,下一章专门讲。
> [!TIP]
> 用约定字母不是为了炫技,是让读代码的人秒懂这个参数代表什么。你写 `<T>` 和写 `<苹果>` 编译器都认,但前者大家一看就明白。
## 菱形运算符省去重复
Java 7 起有了「菱形运算符」`<>`:右边可以省略类型参数,编译器从左边推断。
```java
Map<String, List<Integer>> map = new HashMap<>(); // 右边 <> 自动推断
// 旧写法要写成 new HashMap<String, List<Integer>>(),啰嗦
这行如果不用菱形,右侧那一长串类型参数得重写一遍,又臭又长。主线代码一律用 <>,干净利落。
泛型方法
不光类能泛型,单个方法也能。类型参数写在返回类型前面:
static <T> T first(T[] arr) {
return arr.length > 0 ? arr[0] : null;
}
String s = first(new String[]{"a", "b"}); // T 被推断成 String
Integer n = first(new Integer[]{1, 2}); // T 被推断成 Integer
```bash
泛型方法的好处是:不管你传什么类型的数组,返回的就同类型,不用强转。
> [!WARNING]
> 裸类型(如 `List list = new ArrayList()` 不写类型参数)只在历史兼容时出现,它等于放弃了所有类型检查。主线代码永远写全 `<类型>`,别图省事省略,那是给自己埋雷。
## 一个误区:泛型不能是基本类型
你写不了 `List<int>`,泛型参数必须是引用类型。要装数字就用包装类:`List<Integer>`、`List<Double>`,靠自动装箱装进去。
```java
List<Integer> nums = new ArrayList<>();
nums.add(10); // 自动装箱成 Integer
泛型接口也一样
接口也能带类型参数,比如 List 本身就是 interface List<E>。你实现时指定具体类型即可:class MyList implements List<String>。集合框架里几乎所有接口都是泛型的,这正是你写 List<String> 的底层原因——接口早就把类型参数铺好了。
class MyStack<E> implements Iterable<E> {
private final List<E> data = new ArrayList<>();
public void push(E e) { data.add(e); }
public Iterator<E> iterator() { return data.iterator(); }
}
```bash
上面这个简单栈既泛型又 Iterable,能直接 for-each 遍历,展示了泛型 + 迭代器的配合。
## 边界的雏形:限定类型参数
泛型还能给参数设上下界,比如 `<T extends Number>` 表示 T 必须是 Number 或其子类:
```java
static <T extends Number> double toDouble(T n) {
return n.doubleValue(); // 编译器保证 T 一定有 doubleValue 方法
}
这样在方法里就能调用 Number 的方法。这是下一章通配符边界的近亲(? extends Number),先有个印象:泛型不仅能「任意类型」,还能「限定到某类及其子类的范围」。
为什么有了泛型还要擦除
类型擦除是为了「老代码兼容」:泛型是 Java 5 才加的,之前写了二十年的非泛型代码不能废。擦除让新旧字节码能共存。代价是运行时拿不到泛型类型信息(比如 new T() 不行),但换来的是平稳过渡。知道有这回事,遇到相关限制不慌,高级话题留到以后深挖。
类型推断让泛型方法更省心
调用泛型方法时,编译器会从实参推断类型参数,你通常不用手写。比如前面的 first 方法,传 String[] 就推断成 String,传 Integer[] 就推断成 Integer,写代码时完全无感。菱形 <> 同理:右侧类型靠左侧推断。泛型把「类型」变成了可以流动、可以推断的信息,而不是每处都手写一遍。
Tip当你觉得泛型「啰嗦」时,多半是没用上推断和菱形。该省略的一律省略,泛型就能既安全又清爽。比如
Map<String, List<Integer>> m = new HashMap<>(),右侧的<>让代码短一大截。
小结
泛型用类型参数在编译期锁死元素类型,换来类型安全、免去强转。你会写泛型类 Box<T>、泛型方法 <T> T foo(...),类型参数约定用 E/T/K/V,日常用菱形 <> 省略右侧类型。裸类型只作历史兼容。下一章看泛型的通配符与边界。