📚 Java泛型
前言
Java 5之前的代码,从集合中取出对象时需要强制类型转换,一不小心就ClassCastException。Java泛型的出现彻底解决了这个问题,让我们的代码更安全、更清晰。但泛型的威力远不止于此------通配符、类型擦除、PECS原则...这些看似复杂的概念背后,隐藏着什么样的设计哲学?
一、为什么需要泛型?
泛型之前的痛点
在Java 5之前,集合只能存储Object类型:
java
// 老式写法的问题
List list = new ArrayList();
list.add("Hello");
list.add(123);
list.add(new Date());
// 取出时需要强制转换,容易出错
String str = (String) list.get(0); // OK
String str2 = (String) list.get(1); // 运行时炸了!ClassCastException
这种方式有三个致命问题:
- 类型不安全:可以往集合里放任何东西
- 需要强制转换:取出时必须手动转型
- 运行时才发现错误:编译器无法提前检查
泛型的解决方案
java
// 使用泛型后
List<String> stringList = new ArrayList<String>();
stringList.add("Hello");
stringList.add("World");
// stringList.add(123); // 编译时就报错,问题提前暴露
String str = stringList.get(0); // 无需强制转换
泛型带来的好处:
- 编译时类型检查:错误提前发现
- 消除强制转换:代码更简洁
- 提高代码可读性:类型信息更明确
二、泛型基础语法
泛型类
java
public class Box<T> {
private T content;
public void set(T content) {
this.content = content;
}
public T get() {
return content;
}
// 可以有多个类型参数
public <U> void inspect(U item) {
System.out.println("T: " + content.getClass().getSimpleName());
System.out.println("U: " + item.getClass().getSimpleName());
}
}
// 使用
Box<String> stringBox = new Box<>();
stringBox.set("Hello Generic");
String content = stringBox.get(); // 无需转换
Box<Integer> intBox = new Box<>();
intBox.set(42);
泛型接口
java
public interface Comparable<T> {
int compareTo(T other);
}
// 实现泛型接口
public class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age);
}
}
泛型方法
java
public class Utility {
// 泛型方法:类型参数在方法级别
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// 多个类型参数
public static <T, U> void printPair(T first, U second) {
System.out.println(first + " - " + second);
}
// 有返回值的泛型方法
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
}
// 使用
String[] names = {"Alice", "Bob", "Charlie"};
Utility.swap(names, 0, 2); // Alice和Charlie交换位置
Utility.printPair("Name", "Alice");
Utility.printPair(1, "First");
String maxName = Utility.max("Alice", "Bob"); // "Bob"
三、通配符的艺术
通配符是泛型中最具挑战性的部分,但也是最强大的特性。
无界通配符 <?>
java
public void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
// 可以接受任何泛型List
printList(Arrays.asList("a", "b", "c"));
printList(Arrays.asList(1, 2, 3));
printList(Arrays.asList(new Date(), new Date()));
上界通配符 <? extends T>
用途 :当你需要从集合中读取数据,且希望支持T的子类型时使用。
java
// 计算数字列表的总和
public double sum(List<? extends Number> numbers) {
double result = 0.0;
for (Number num : numbers) { // 可以安全地读取
result += num.doubleValue();
}
return result;
}
// 这个方法可以处理各种数字类型的列表
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
List<BigDecimal> decimalList = Arrays.asList(
new BigDecimal("10.5"), new BigDecimal("20.7")
);
double sum1 = sum(intList); // 6.0
double sum2 = sum(doubleList); // 6.6
double sum3 = sum(decimalList); // 31.2
限制:不能写入(除了null)
typescript
public void cannotAdd(List<? extends Number> numbers) {
// numbers.add(new Integer(1)); // 编译错误!
// numbers.add(new Double(1.0)); // 编译错误!
numbers.add(null); // 只能加null
}
下界通配符 <? super T>
用途 :当你需要向集合中写入T类型的数据时使用。
java
// 向列表中添加整数
public void addNumbers(List<? super Integer> list) {
list.add(1); // 可以添加Integer
list.add(2); // 可以添加Integer
// list.add(1.5); // 编译错误!不能添加Double
}
// 可以用于不同类型的列表
List<Integer> intList = new ArrayList<>();
List<Number> numberList = new ArrayList<>();
List<Object> objectList = new ArrayList<>();
addNumbers(intList); // 向Integer列表添加
addNumbers(numberList); // 向Number列表添加
addNumbers(objectList); // 向Object列表添加
限制:读取时只能用Object接收
java
public void cannotRead(List<? super Integer> list) {
Object obj = list.get(0); // 只能用Object接收
// Integer i = list.get(0); // 编译错误!
}
四、PECS原则:泛型使用的黄金法则
PECS = Producer Extends, Consumer Super
- Producer Extends :如果你需要从集合中读取数据(集合是数据的生产者),使用
? extends
- Consumer Super :如果你需要向集合中写入数据(集合是数据的消费者),使用
? super
经典应用:Collections.copy()
java
// Collections.copy的简化版本
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T item : src) { // 从src读取(Producer Extends)
dest.add(item); // 向dest写入(Consumer Super)
}
}
// 使用示例
List<Integer> intSource = Arrays.asList(1, 2, 3);
List<Number> numberDest = new ArrayList<>();
copy(numberDest, intSource); // 从Integer列表复制到Number列表
实际开发中的应用
java
// 比较器的使用
public static <T> void sort(List<T> list, Comparator<? super T> comparator) {
// 实现排序逻辑
}
// 可以用父类的比较器来比较子类
class Animal {
String name;
int age;
}
class Dog extends Animal {}
List<Dog> dogs = getDogs();
Comparator<Animal> animalComparator = Comparator.comparing(a -> a.age);
sort(dogs, animalComparator); // 用Animal的比较器比较Dog
五、边界限定
单个边界
java
// 限定T必须是Number的子类
public class NumberBox<T extends Number> {
private T value;
public NumberBox(T value) {
this.value = value;
}
public double getDoubleValue() {
return value.doubleValue(); // 可以调用Number的方法
}
}
NumberBox<Integer> intBox = new NumberBox<>(42);
NumberBox<Double> doubleBox = new NumberBox<>(3.14);
// NumberBox<String> stringBox = new NumberBox<>("hello"); // 编译错误!
多个边界
java
// T必须同时继承Number并实现Comparable接口
public class ComparableNumber<T extends Number & Comparable<T>> {
private T value;
public ComparableNumber(T value) {
this.value = value;
}
public boolean isGreaterThan(T other) {
return value.compareTo(other) > 0; // 可以比较
}
public double asDouble() {
return value.doubleValue(); // 可以转换为double
}
}
ComparableNumber<Integer> intNum = new ComparableNumber<>(5);
ComparableNumber<Double> doubleNum = new ComparableNumber<>(3.14);
System.out.println(intNum.isGreaterThan(3)); // true
六、类型擦除:泛型的实现机制
Java泛型采用类型擦除(Type Erasure)实现,这意味着泛型信息只在编译时存在。
擦除规则
java
// 编译前
List<String> stringList = new ArrayList<String>();
List<Integer> intList = new ArrayList<Integer>();
// 编译后(字节码层面)
List stringList = new ArrayList();
List intList = new ArrayList();
// 两个列表在运行时的类型是一样的
System.out.println(stringList.getClass() == intList.getClass()); // true
擦除的限制
编译后所有泛型信息都被删除
- ❌ 不能创建泛型实例
- ❌ 不能创建泛型数组
- ❌ 不能进行instanceof检查
- ❌ 不能在静态上下文中使用类型参数
1. 不能创建泛型实例 new T()
java
public class GenericClass<T> {
public T createInstance() {
// return new T(); // ❌ 编译器不知道T的构造函数
// 运行时T已经变成Object了,相当于:
// return new Object(); // 这显然不对
}
}
解决方案:使用Class对象
java
public class GenericClass<T> {
private final Class<T> type;
public GenericClass(Class<T> type) {
this.type = type;
}
public T createInstance() throws Exception {
return type.getDeclaredConstructor().newInstance(); // ✅ 可以创建
}
}
// 使用
GenericClass<String> stringClass = new GenericClass<>(String.class);
String instance = stringClass.createInstance(); // 创建String实例
2. 不能创建泛型数组 new T[10]
csharp
java
public class GenericClass<T> {
public T[] createArray() {
// return new T[10]; // ❌ 不知道T的确切类型
// 类型擦除后变成:
// return new Object[10]; // 类型不匹配
}
}
解决方案:使用Class对象或类型擦除
java
public class GenericClass<T> {
private final Class<T> type;
public GenericClass(Class<T> type) {
this.type = type;
}
@SuppressWarnings("unchecked")
public T[] createArray(int size) {
// 方案1:使用Array.newInstance
return (T[]) Array.newInstance(type, size);
}
@SuppressWarnings("unchecked")
public T[] createArrayUnsafe(int size) {
// 方案2:创建Object数组然后强转(不安全但常用)
return (T[]) new Object[size];
}
}
// 使用
GenericClass<String> stringClass = new GenericClass<>(String.class);
String[] stringArray = stringClass.createArray(10); // 创建String数组
3. 不能进行instanceof检查
java
public class GenericClass<T> {
public boolean check(Object obj) {
// return obj instanceof T; // ❌ T在运行时不存在
// 运行时变成:
// return obj instanceof Object; // 总是true,没意义
}
}
解决方案:使用Class对象
java
public class GenericClass<T> {
private final Class<T> type;
public GenericClass(Class<T> type) {
this.type = type;
}
public boolean check(Object obj) {
return type.isInstance(obj); // ✅ 可以检查
}
}
// 使用
GenericClass<String> stringClass = new GenericClass<>(String.class);
System.out.println(stringClass.check("hello")); // true
System.out.println(stringClass.check(123)); // false
4. 不能在静态上下文中使用类型参数
java
public class GenericClass<T> {
// static T staticField; // ❌ 静态成员属于类,不属于实例
// static void staticMethod(T param) { } // ❌ 同样的原因
}
原因:静态成员属于类本身,而泛型类型参数属于类的实例。
解决方案:使用泛型方法
typescript
java
public class GenericClass<T> {
// ✅ 泛型方法有自己的类型参数
public static <U> void staticMethod(U param) {
System.out.println(param);
}
// ✅ 静态泛型方法
public static <U> U staticGenericMethod(U param) {
return param;
}
}
// 使用
GenericClass.staticMethod("hello"); // U推断为String
GenericClass.staticMethod(123); // U推断为Integer
String result = GenericClass.staticGenericMethod("test");
桥接方法
类型擦除可能导致方法签名冲突,编译器会生成桥接方法解决:
java
public class Node<T> {
private T data;
public void setData(T data) {
this.data = data;
}
}
public class StringNode extends Node<String> {
@Override
public void setData(String data) { // 重写的方法
super.setData(data);
}
// 编译器自动生成的桥接方法(符合java规范的重写)
// public void setData(Object data) {
// setData((String) data);
// }
}
类型擦除后,父类的方法签名变成了setData(Object)
,而子类的方法签名是setData(String)
。 这两个方法签名不同,不构成重写关系! 这会破坏多态性:
ini
java
Node node = new StringNode();
node.setData("hello"); // 这会调用哪个方法?
按照Java的方法调用规则,会调用Node.setData(Object)
,而不是StringNode.setData(String)
,这就不是我们期望的行为了。
七、高级技巧与最佳实践
1. 泛型单例工厂
java
public class GenericSingleton {
private static final Map<Class<?>, Object> INSTANCES = new HashMap<>();
@SuppressWarnings("unchecked")
public static <T> T getInstance(Class<T> clazz) {
return (T) INSTANCES.computeIfAbsent(clazz, k -> {
try {
return k.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
}
2. 类型安全的异构容器
typescript
public class TypeSafeMap {
private final Map<Class<?>, Object> map = new HashMap<>();
public <T> void put(Class<T> type, T instance) {
map.put(type, type.cast(instance));
}
public <T> T get(Class<T> type) {
return type.cast(map.get(type));
}
}
// 使用
TypeSafeMap container = new TypeSafeMap();
container.put(String.class, "Hello");
container.put(Integer.class, 42);
String str = container.get(String.class); // 类型安全
Integer num = container.get(Integer.class);
3. 递归类型限定
java
// 枚举类型的经典模式
public class Enum<E extends Enum<E>> implements Comparable<E> {
// ...
}
// 自定义的比较接口
public interface SelfComparable<T extends SelfComparable<T>> {
int compareTo(T other);
}
public class Person implements SelfComparable<Person> {
private String nam
e;
@Override
public int compareTo(Person other) {
return this.name.compareTo(other.name);
}
}
八、常见陷阱与解决方案
1. 数组与泛型的冲突
java
// ❌ 不能创建泛型数组
// List<String>[] stringLists = new List<String>[10]; // 编译错误
// ✅ 解决方案1:使用List代替数组
List<List<String>> stringLists = new ArrayList<>();
// ✅ 解决方案2:使用原始类型(不推荐)
@SuppressWarnings("unchecked")
List<String>[] stringLists2 = new List[10];
// ✅ 解决方案3:使用通配符
List<?>[] wildcardLists = new List<?>[10];
2. 重载与泛型
typescript
public class OverloadExample {
// ❌ 这两个方法在类型擦除后签名相同,不能重载
// public void method(List<String> list) { }
// public void method(List<Integer> list) { }
// ✅ 解决方案:改变方法名或参数
public void processStrings(List<String> strings) { }
public void processIntegers(List<Integer> integers) { }
}
3. 捕获异常与泛型
java
public class ExceptionExample<T extends Exception> {
// ❌ 不能直接捕获泛型异常
public void badMethod() {
try {
// some code
}
// catch (T e) { } // 编译错误
}
// ✅ 解决方案:使用Class对象
private final Class<T> exceptionType;
public ExceptionExample(Class<T> exceptionType) {
this.exceptionType = exceptionType;
}
public void goodMethod() throws T {
try {
// some code
} catch (Exception e) {
if (exceptionType.isInstance(e)) {
throw exceptionType.cast(e);
}
throw new RuntimeException(e);
}
}
}
九、实战案例:构建类型安全的Builder
java
public class SqlQueryBuilder<T> {
private final StringBuilder query = new StringBuilder();
private final Class<T> resultType;
private SqlQueryBuilder(Class<T> resultType) {
this.resultType = resultType;
}
public static <T> SqlQueryBuilder<T> select(Class<T> resultType) {
SqlQueryBuilder<T> builder = new SqlQueryBuilder<>(resultType);
builder.query.append("SELECT * ");
return builder;
}
public SqlQueryBuilder<T> from(String table) {
query.append("FROM ").append(table).append(" ");
return this;
}
public SqlQueryBuilder<T> where(String condition) {
query.append("WHERE ").append(condition).append(" ");
return this;
}
public SqlQueryBuilder<T> orderBy(String column) {
query.append("ORDER BY ").append(column).append(" ");
return this;
}
public Query<T> build() {
return new Query<>(query.toString(), resultType);
}
}
public class Query<T> {
private final String sql;
private final Class<T> resultType;
public Query(String sql, Class<T> resultType) {
this.sql = sql;
this.resultType = resultType;
}
public List<T> execute() {
// 执行查询并返回类型安全的结果
System.out.println("Executing: " + sql);
return new ArrayList<>(); // 简化实现
}
}
// 使用
Query<User> userQuery = SqlQueryBuilder
.select(User.class)
.from("users")
.where("age > 18")
.orderBy("name")
.build();
List<User> users = userQuery.execute(); // 类型安全的结果
十、性能考虑
装箱与拆箱
java
// ❌ 频繁装箱拆箱影响性能
List<Integer> numbers = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
numbers.add(i); // 自动装箱
}
int sum = 0;
for (Integer num : numbers) {
sum += num; // 自动拆箱
}
// ✅ 使用原始类型数组提高性能
int[] numbers2 = new int[1000000];
for (int i = 0; i < numbers2.length; i++) {
numbers2[i] = i;
}
int sum2 = 0;
for (int num : numbers2) {
sum2 += num; // 无装箱拆箱
}
选择合适的集合类型
scss
// 针对特定场景选择专门的集合
TIntList intList = new TIntArrayList(); // Trove库,避免装箱
IntList fastUtilIntList = new IntArrayList(); // FastUtil库
// 或使用Java 8+的原始类型流
IntStream.range(0, 1000000)
.filter(i -> i % 2 == 0)
.sum(); // 无装箱操作
总结
Java泛型是一个强大而复杂的特性,它不仅提供了编译时类型安全,还通过通配符和边界限定提供了灵活的类型系统。掌握泛型的关键在于:
- 理解基本概念:泛型类、接口、方法的语法和用法
- 掌握通配符:extends用于读取,super用于写入,遵循PECS原则
- 了解限制:类型擦除带来的限制和解决方案
- 应用最佳实践:在实际开发中合理使用泛型提高代码质量
记住,泛型不仅仅是语法糖,它是Java类型系统的重要组成部分。正确使用泛型可以让你的代码更安全、更清晰、更易维护。虽然学习曲线陡峭,但掌握后会让你的Java编程能力上升一个台阶。
最后,泛型的精髓在于在编译时捕获更多错误,在运行时减少类型转换。当你开始思考"这个方法应该接受什么类型的参数"时,你就已经在泛型的道路上了。