8.1 学会了用和写单个占位符的泛型类;这节进阶三步:多个类型参数(Pair\<K, V>)、泛型接口、泛型类的继承两姿势——学完你会发现 HashMap 的骨架你自己就能搭出来。
1. 多个类型参数:Pair\<K, V>
一个占位符不够用?逗号隔开,各管各的:
public class Pair<K, V> { // K 管键,V 管值
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
使用:
Pair<String, Integer> p = new Pair<>("波仔", 95);
String name = p.getKey(); // K 出来是 String
Integer score = p.getValue(); // V 出来是 Integer
HashMap 就是 Pair 思想的放大版——每个键值对就是一间 K→V 的格子。7.6 学 Map 时看到的 Map<K, V> 尖括号里那两个字母,源头上就是这里的多参数占位符。
2. 泛型接口
接口也能带占位符,最典型的模样是"生产者":
public interface Generator<T> {
T next(); // 每次要一个 T 类型的东西
}
实现它有两种姿势(和继承泛型类完全同款):
// 姿势一:当场拍板——定死 String
class StrGen implements Generator<String> {
@Override
public String next() { return "波仔" + new Random().nextInt(100); }
}
// 姿势二:继续传下去——保持泛型(类也要跟着声明 <T>)
class UserGen<T> implements Generator<T> {
private Random r = new Random();
@Override
public T next() { return null; } // 实际项目里返回真实数据
}
3. 泛型类的继承:两种姿势二选一
父类留着占位符 T,子类接手时必须表态:
| 姿势 | 写法 | 结果 |
|---|---|---|
| 当场拍板 | class StringBox extends Box<String> |
子类是普通类,用的时候不用写 <> |
| 继续传下去 | class SubBox<T> extends Box<T> |
子类还是泛型类,T 继续由使用者决定 |
class StringBox extends Box<String> { } // 定死:StringBox 只装 String
StringBox sb = new StringBox(); // 不用写 <>
class SubBox<T> extends Box<T> { } // 续传:还是泛型
SubBox<Integer> sb2 = new SubBox<>(); // 使用时才定类型
class SubBox extends Box —— 编译器不报错(顶多给个 raw types 警告),但 Box 从此退化成裸 Object 盒子:set(Integer) 也不拦、get() 出来全要强转——安检全失效,8.1 的价值归零。这是泛型章最阴的坑:不炸,但悄悄变差。
4. 两颗雷:static 与 new T()
| 雷区 | 代码 | 为什么 |
|---|---|---|
| static 成员用 T | private static T shared; ✗ |
static 在类加载时就存在,那时 T 还没定——类的每个实例可能 T 不同,static 只有一份,没法伺候 |
| new T() | value = new T(); ✗ |
类型擦除后 T 只是占位符,编译后不知道 T 是谁、有没有无参构造——要对象从外面传进来(构造方法参数) |
记住结论就行:T 的活儿全靠"外面传进来",泛型类自己造不出 T 的对象。
5. 动手时间(Demo02)
import java.util.*;
// 泛型接口
interface Generator<T> {
T next();
}
// 多参数泛型类
class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) { this.key = key; this.value = value; }
public K getKey() { return key; }
public V getValue() { return value; }
}
// 姿势一:定死类型
class ScoreGen implements Generator<Integer> {
private Random r = new Random();
@Override
public Integer next() { return r.nextInt(101); }
}
// 姿势二:继续泛型
class Box<T> {
private T value;
public void set(T v) { this.value = v; }
public T get() { return value; }
}
class SubBox<T> extends Box<T> { }
public class Demo02 {
public static void main(String[] args) {
// Pair:学生 → 成绩
Pair<String, Integer> p = new Pair<>("波仔", 95);
System.out.println(p.getKey() + " → " + p.getValue());
// 泛型接口姿势一:定死 Integer
Generator<Integer> g = new ScoreGen();
System.out.println("随机分: " + g.next());
// 姿势二:续传
SubBox<String> sb = new SubBox<>();
sb.set("墨池");
System.out.println(sb.get());
}
}
6. 30 秒实验
实验 1:class SubBox extends Box(故意丢 <T>)——编译能过但有 raw types 警告;接着 sb.set(42) 也不报错——安检悄悄失效,亲眼见最阴的坑。
实验 2:在 Pair 里写 private static K count;——编译报错 non-static type variable K cannot be referenced from a static context。static 用不了占位符。
实验 3:在 Box 里写 value = new T();——编译报错。类型擦除的代价。
实验 4:Pair<String, Integer> 上调 p.setValue("高的")——编译报错,V 的座位已被 Integer 占了,别想塞 String。
实验 5:Pair p2 = new Pair("甲", 1);(裸用)——能跑,但 p2.getKey() 出来是 Object,什么都要强转——和 8.1 万能袋子一个下场。
7. 常见误区
- 以为泛型类的子类必须也是泛型类——两种姿势都合法:定死类型(变普通类)或续传(保持泛型)。
- 子类忘写
<T>——编译不报错但安检全失效(raw types),比报错更危险。 - 在 static 成员里用类的占位符——static 先于 T 存在,用不了;static 方法想泛型要自己单独声明
<T>(8.1 的泛型方法)。 - 想在泛型类里 new T()——不行,对象从外面传进来。
- 以为多参数必须同名规则——
Pair<K, V>、Pair<A, B>都行,惯例用 K/V 是为了望文生义;顺序也不能乱(K 填第一个参数位)。 - 把泛型接口当成只能被泛型类实现——普通类定死类型照样实现(姿势一),
ArrayList<String>实现的就是泛型接口List<E>的定死版。
8. 折叠自测题
能。Pair<String, String> 完全合法——两个占位符只是两个"座位",填同一个类型没问题。泛型不要求互不相同。
定死类型:这个子类天生只服务一种类型(如 StringBox 只装字符串)——用起来简洁;续传:子类要继续保持灵活,让使用者决定类型——工具类、容器类几乎都选这条(ArrayList\<E> 继承 AbstractList\<E>)。
这叫 raw types(原始类型):编译器放行(向后兼容老代码),只给警告。但占位符没交代,Box 退化成 Object 盒子——set 什么都能塞、get 全要强转,8.1 的安检价值归零,错误重新推迟到运行时。
static 成员随类加载就存在,那时占位符 T 还没被任何具体类型替换;而且每个 Box<String>、Box<Integer> 共享同一个 static——它没法同时是 String 又是 Integer。static 方法想泛型,自己声明 <T>。
class Triple<A, B, C> {
private A first; private B second; private C third;
public Triple(A a, B b, C c) { first=a; second=b; third=c; }
public A getFirst() { return first; }
public B getSecond() { return second; }
public C getThird() { return third; }
}
要点:占位符数量随便加,逗号隔开;getter 返回值各归各位。
9. 下一步学什么
- ☑ 8.3 泛型接口 ✅ —— 点此复习:T 的两个岗位、实现两姿势决定权链、泛型接口当类型用(多态主场)、JDK 真身 Comparable/Iterator 收网(7.7 sort 炸的解药)。
相关笔记
- 第八章 泛型/8.1 泛型基础:占位符、钻石操作符、三条铁规矩——本节的地基
- 第七章 集合/7.6 Map接口:Pair\<K, V> 的放大版成品——HashMap 骨架认亲
- 第四章 面向对象(下)/4.3 抽象类和接口:接口实现的两种姿势出处——本节 Generator 的本体课
- 第六章 Java API/6.3 System类:static"全世界一份"的脾气——static 用不了 T 的根子