泛型的核心作用
为什么需要泛型?
写代码时,你一定被这个问题折磨过:
java
// 没有泛型:往 List 里塞什么,全靠约定
List list = new ArrayList();
list.add("hello");
list.add(123); // 鬼知道这里会放什么
// 取出来?强制类型转换
String s = (String) list.get(1); // 运行时报错:ClassCastException编译时不报错,运行时才崩。这种「记错类型」的代价,往往在生产环境才能发现。
泛型的出现,就是为了把这个问题提前到编译期。
泛型做了什么
用泛型重写上面的代码:
java
List<String> list = new ArrayList<>();
list.add("hello");
list.add(123); // 编译错误! IDE 直接告诉你类型不匹配
String s = list.get(0); // 不需要强制转换,类型已经确定编译器像是一个严格的检查员:在你写代码的时候就拦住错误,而不是等到用户用的时候才崩溃。
三个核心作用
1. 类型安全
编译器负责类型检查,把 ClassCastException 从运行时搬到编译时。
java
// 错误在编译期暴露
List<String> list = new ArrayList<>();
list.add(123); // 编译错误:incompatible types2. 消除强制类型转换
没有泛型时,每次取数据都要转型:
java
// 没有泛型
Map map = new HashMap();
map.put("name", "张三");
String name = (String) map.get("name"); // 需要转型
// 有泛型
Map<String, String> map = new HashMap<>();
map.put("name", "张三");
String name = map.get("name"); // 直接用,类型已经确定3. 代码复用
泛型让同一套逻辑可以作用于不同类型:
java
// 不是写三个方法,而是写一个通用的
public <T> void print(T item) {
System.out.println(item);
}
print("hello"); // T = String
print(123); // T = Integer
print(true); // T = Boolean泛型的使用场景
泛型类
java
// 创建一个可以装任何东西的盒子,但取出来时类型是明确的
public class Box<T> {
private T content;
public void put(T item) {
this.content = item;
}
public T get() {
return content;
}
}
Box<String> stringBox = new Box<>();
stringBox.put("物品");
// String item = stringBox.get(); // 类型安全泛型接口
java
// 定义一个可以生成任意类型结果的接口
public interface Generator<T> {
T generate();
}
// 实现时指定具体类型
public class StringGenerator implements Generator<String> {
@Override
public String generate() {
return "generated string";
}
}泛型方法
java
// 方法级别的泛型,不影响整个类
public class Utils {
// 交换任意类型数组的两个元素
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
String[] names = {"Alice", "Bob"};
Utils.swap(names, 0, 1); // 编译时确定 T = String泛型限定
限制泛型可以接受的类型范围:
java
// 只接受 Number 及其子类
public class NumberBox<T extends Number> {
private T value;
public void printDouble() {
// 确定 T 是 Number,可以调用 doubleValue()
System.out.println(value.doubleValue());
}
}
NumberBox<Integer> intBox = new NumberBox<>(); // OK
NumberBox<String> strBox = new NumberBox<>(); // 编译错误泛型的局限性
泛型不是银弹,它有一些本质限制:
基本类型不能作为类型参数
java
List<int> list; // 编译错误
List<Integer> list; // 正确,需要使用包装类原因:泛型是为了类型安全,而基本类型和对象在 JVM 里有本质区别。
泛型类型不能实例化
java
T item = new T(); // 编译错误因为 T 在运行时会被擦除,new T() 在运行时根本不知道该 new 什么。
泛型数组不能直接创建
java
T[] array = new T[10]; // 编译错误泛型类型不能用于静态上下文
java
public class Container<T> {
private static T instance; // 编译错误
public static T getInstance() { // 编译错误
return instance;
}
}静态成员属于类,不属于实例,而泛型是实例级别的概念,两者天然冲突。
泛型与多态
一个常见的困惑:
java
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs; // 编译错误!List<Dog> 不是 List<Animal> 的子类。这叫类型系统的不变性。
为什么?假设可以赋值:
java
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs; // 假设可以
animals.add(new Cat()); // 往 Dog 列表里塞了 Cat!
Dog dog = dogs.get(0); // 运行时崩溃这是为了类型安全付出的代价。具体怎么突破这个限制,后面讲通配符时会详细介绍。
总结
泛型干了一件什么事?
让编译器帮你记住类型,把运行时的错误提前到编译时。
记住这个核心,理解泛型的各种规则就会顺畅很多:
| 特性 | 原因 |
|---|---|
| 类型参数不能是基本类型 | 泛型只作用于对象,运行时需要类型信息 |
| 不能 new T() | 类型擦除后 T 在运行时不存在 |
List<Dog> 不是 List<Animal> | 不变性保证类型安全 |
| 不能用 static T | 静态成员属于类,泛型属于实例 |
