泛型

什么是泛型

泛型就是类型参数化,用尖括号 <T> 给类或方法”留个坑”,用的时候再填上具体类型

打个比方:泛型就像快递柜,柜子本身不在乎你放什么东西,但你可以贴个标签说明里面放的是什么(书、水果、衣服),这样取的时候不会拿错

没有泛型之前(Java5以前),集合里啥都能放,取出来要强制转型,一不小心就 ClassCastException

为什么需要泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testWithoutGenerics() {
// 没有泛型的黑暗年代
List list = new ArrayList();
list.add("hello");
list.add(123); // 编译不报错,什么都能塞

// 取出来要强制转型
String s = (String) list.get(0); // OK
// String s2 = (String) list.get(1); // 💥 ClassCastException!123不是String

// 有了泛型之后
List<String> safeList = new ArrayList<>();
safeList.add("hello");
// safeList.add(123); // ❌ 编译就报错!编译器帮你守门

String str = safeList.get(0); // 不用强制转型,编译器知道里面是String
}

泛型的好处:

编译期类型检查:类型不对编译就报错,不用等到运行时才炸

不用强制转型:编译器自动帮你转

代码复用:一套逻辑适用于多种类型

泛型类

在类名后面加 <T>,这个T就是类型参数,用的时候指定具体类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 定义泛型类:T是类型参数,随便叫什么都行,习惯用T(Type)
class Box<T> {
private T data;

public void set(T data) {
this.data = data;
}

public T get() {
return data;
}
}

@Test
public void testGenericClass() {
// 用的时候指定类型
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String s = stringBox.get(); // 不用转型

Box<Integer> intBox = new Box<>();
intBox.set(42);
int n = intBox.get(); // 自动拆箱

// 同一个Box类,装不同类型的东西,这就是泛型的威力
}

常见的类型参数命名惯例:

字母 含义 典型使用
T Type 通用类型
E Element 集合元素
K Key
V Value
N Number 数字
R Result 返回值

可以有多个类型参数:class Pair<K, V> { K key; V value; }

泛型方法

在方法返回值前面加 <T>,这个方法就有了自己的类型参数,不依赖类的泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class GenericMethodDemo {

// 泛型方法:<T> 写在返回值前面
public <T> void printArray(T[] array) {
for (T item : array) {
System.out.print(item + " ");
}
System.out.println();
}

// 泛型方法也可以有返回值
public <T> T getFirst(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
}

@Test
public void testGenericMethod() {
GenericMethodDemo demo = new GenericMethodDemo();

// 编译器自动推断T的类型
Integer[] intArr = {1, 2, 3};
String[] strArr = {"A", "B", "C"};

demo.printArray(intArr); // 1 2 3
demo.printArray(strArr); // A B C

List<String> names = Arrays.asList("张三", "李四");
String first = demo.getFirst(names); // 自动推断T是String
System.out.println(first); // 张三
}

泛型接口

接口也可以定义泛型,实现类指定具体类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 泛型接口
interface DataStore<T> {
void save(T data);
T load(int id);
}

// 实现时指定类型
class StringStore implements DataStore<String> {
@Override
public void save(String data) {
System.out.println("保存字符串:" + data);
}

@Override
public String load(int id) {
return "数据" + id;
}
}

// 也可以继续保持泛型,让子类决定
class CacheStore<T> implements DataStore<T> {
private Map<Integer, T> cache = new HashMap<>();

@Override
public void save(T data) {
cache.put(cache.size(), data);
}

@Override
public T load(int id) {
return cache.get(id);
}
}

@Test
public void testGenericInterface() {
DataStore<String> store = new StringStore();
store.save("Hello");
System.out.println(store.load(1)); // 数据1

CacheStore<Integer> intCache = new CacheStore<>();
intCache.save(100);
System.out.println(intCache.load(0)); // 100
}

通配符

通配符 ? 用在使用泛型的地方(不是定义泛型的地方),表示”我不确定具体类型”

无界通配符 <?>

表示”任何类型都行”,一般只读不写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testUnbounded() {
// <?> 表示可以接受任何类型的List
List<String> strings = Arrays.asList("A", "B");
List<Integer> integers = Arrays.asList(1, 2);

printList(strings); // OK
printList(integers); // OK
}

// 参数用 <?> 可以接受任何泛型类型
public void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
// list.add("X"); // ❌ 编译报错!不知道具体类型,不能往里加东西
}

上界通配符 <? extends Number> —— 只读

表示”Number或Number的子类”,可以安全读取(当Number用),但不能写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testUpperBound() {
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);

System.out.println("整数之和:" + sumOfList(intList)); // 6.0
System.out.println("小数之和:" + sumOfList(doubleList)); // 6.6
}

// <? extends Number> 表示接受Number及其子类(Integer, Double等)
public double sumOfList(List<? extends Number> list) {
double sum = 0;
for (Number n : list) { // 安全地当Number读取
sum += n.doubleValue();
}
// list.add(1); // ❌ 编译报错!不能往里写
return sum;
}

下界通配符 <? super Integer> —— 只写

表示”Integer或Integer的父类”,可以安全写入Integer,但读出来只能当Object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void testLowerBound() {
List<Number> numberList = new ArrayList<>();
addNumbers(numberList);
System.out.println(numberList); // [1, 2, 3]
}

// <? super Integer> 表示接受Integer及其父类(Number, Object)
public void addNumbers(List<? super Integer> list) {
list.add(1); // ✅ 可以安全写入Integer
list.add(2);
list.add(3);
// Integer n = list.get(0); // ❌ 读出来只能是Object
Object obj = list.get(0); // ✅ 只能当Object读
}

PECS原则:Producer Extends, Consumer Super

如果集合是生产者(你从里面读数据)→ 用 extends

如果集合是消费者(你往里面写数据)→ 用 super

记忆口诀:读extends,写super

1
2
3
4
5
6
7
8
// Java标准库的经典例子:Collections.copy
// src是生产者(读数据),用extends
// dest是消费者(写数据),用super
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++) {
dest.set(i, src.get(i));
}
}

通配符对比表

通配符 含义 能读? 能写? 使用场景
<?> 任意类型 读为Object 不能写 只需遍历不需修改
<? extends T> T或T的子类 读为T 不能写 从集合读数据
<? super T> T或T的父类 读为Object 能写T 往集合写数据

类型擦除

泛型是编译期的语法糖,编译后泛型信息会被擦除,运行时不存在

也就是说 List<String>List<Integer> 在运行时其实是同一个类 List

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testTypeErasure() {
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

// 运行时类型是一样的!
System.out.println(stringList.getClass() == intList.getClass()); // true
System.out.println(stringList.getClass().getName()); // java.util.ArrayList

// 编译后 List<String> 变成了 List(Raw Type)
// 所有的T变成了Object(或者上界类型)
}

类型擦除带来的限制

不能 new T():编译后T变成Object,JVM不知道你要创建什么类型的对象

不能 new T[]:同理

不能 instanceof List<String>:运行时没有泛型信息

不能用基本类型:List<int> 不行,要用 List<Integer>

1
2
3
4
5
// 这些都不行:
// T obj = new T(); // ❌ 不能实例化类型参数
// T[] arr = new T[10]; // ❌ 不能创建泛型数组
// if (list instanceof List<String>) // ❌ 不能运行时检查泛型类型
// List<int> intList; // ❌ 不能用基本类型

泛型的实际应用

看看 Collection体系Map体系 的源码声明就知道了:

public interface List<E> extends Collection<E>

public class ArrayList<E> implements List<E>

public interface Map<K, V>

public class HashMap<K, V> implements Map<K, V>

自己写一个泛型工具方法的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void testPracticalGeneric() {
// 写一个通用的"转换List"方法
List<String> names = Arrays.asList("张三", "李四", "王五");

// 把每个名字加上"同学"后缀
List<String> result = mapList(names, name -> name + "同学");
System.out.println(result); // [张三同学, 李四同学, 王五同学]

// 把字符串转成长度
List<Integer> lengths = mapList(names, String::length);
System.out.println(lengths); // [2, 2, 2]
}

// 泛型方法:T是输入类型,R是输出类型
public <T, R> List<R> mapList(List<T> list, Function<T, R> mapper) {
List<R> result = new ArrayList<>();
for (T item : list) {
result.add(mapper.apply(item));
}
return result;
}

常见坑

坑1:泛型不能用基本类型

List<int> ❌,要写 List<Integer>,自动装箱拆箱会帮你搞定

坑2:泛型类的静态方法不能用类的类型参数

1
2
3
4
5
6
class MyClass<T> {
// static T data; // ❌ 静态成员不能用T
// static void foo(T t) {} // ❌ 静态方法不能用类的T

static <E> void bar(E e) {} // ✅ 静态方法可以自己声明泛型
}

坑3:泛型数组不能直接创建

1
2
3
// List<String>[] arr = new List<String>[10];  // ❌ 编译报错
List<String>[] arr = new List[10]; // ⚠️ 可以但有警告
// 原因:数组会记住元素类型(运行时检查),但泛型在运行时被擦除了,冲突

坑4:List<Object> 不是 List<String> 的父类

1
2
3
4
// String是Object的子类,但 List<String> 不是 List<Object> 的子类!
// List<Object> list = new ArrayList<String>(); // ❌ 编译报错
// 原因:如果允许的话,你可以往list里放Integer,就类型不安全了
// 要实现多态用通配符:List<? extends Object> list = new ArrayList<String>();

练习题

题1:实现一个泛型Pair类 —— 存储两个可能不同类型的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 实现这个类
class Pair<A, B> {
private A first;
private B second;

public Pair(A first, B second) {
this.first = first;
this.second = second;
}

public A getFirst() { return first; }
public B getSecond() { return second; }
}

@Test
public void exercise1() {
Pair<String, Integer> pair = new Pair<>("张三", 90);
System.out.println(pair.getFirst() + " : " + pair.getSecond());
// 张三 : 90
}

题2:写一个泛型方法 —— 找出List中的最大值(要求元素实现Comparable)

提示:方法签名 <T extends Comparable<T>> T findMax(List<T> list)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public <T extends Comparable<T>> T findMax(List<T> list) {
if (list.isEmpty()) throw new IllegalArgumentException("空列表");
T max = list.get(0);
for (T item : list) {
if (item.compareTo(max) > 0) {
max = item;
}
}
return max;
}

@Test
public void exercise2() {
List<Integer> nums = Arrays.asList(3, 1, 4, 1, 5, 9);
System.out.println(findMax(nums)); // 9

List<String> words = Arrays.asList("banana", "apple", "cherry");
System.out.println(findMax(words)); // cherry
}

题3:理解PECS —— 下面哪些代码能编译通过?为什么?

1
2
3
4
5
6
7
8
List<? extends Number> list1 = new ArrayList<Integer>();
// list1.add(1); // 能编译吗?
// Number n = list1.get(0); // 能编译吗?

List<? super Integer> list2 = new ArrayList<Number>();
// list2.add(1); // 能编译吗?
// Integer i = list2.get(0); // 能编译吗?
// Object o = list2.get(0); // 能编译吗?

答案:extends的不能add,能读为Number;super的能add Integer,读只能当Object


上一章 目录 下一章
Collections工具类 java基础 异常体系