作为一名从Java 1.4时代走过来的老程序员,我清楚地记得没有泛型的日子有多痛苦。每次从集合中取出对象都要做强制类型转换,就像在雷区行走一样提心吊胆。2004年Java 5引入泛型后,这种局面才得到根本改变。
泛型的核心价值在于类型安全和代码复用。举个例子,假设我们要处理一个只能存放String的列表:
java复制// 非泛型时代的写法
List rawList = new ArrayList();
rawList.add("hello");
String str = (String) rawList.get(0); // 必须强制转换
// 泛型写法
List<String> genericList = new ArrayList<>();
genericList.add("hello");
String str = genericList.get(0); // 自动类型推断
关键提示:泛型在编译期就会进行类型检查,这意味着如果你尝试向
genericList添加一个Integer,编译器会立即报错。这种编译期检查可以避免90%以上的ClassCastException。
虽然日常开发中直接定义泛型类的情况不多,但在框架设计中非常常见。比如我们熟悉的ArrayList<E>就是一个典型的泛型类。来看一个自定义泛型类的例子:
java复制public class Box<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
// 使用示例
Box<String> stringBox = new Box<>();
stringBox.setContent("Java Generics");
String value = stringBox.getContent(); // 无需强制转换
类型参数命名惯例(这些约定俗成的规则能让代码更易读):
泛型接口在Java集合框架中随处可见,比如Comparable<T>、Iterator<E>等。我们来看一个自定义的泛型接口:
java复制public interface Repository<T, ID> {
T findById(ID id);
void save(T entity);
}
// 实现示例
public class UserRepository implements Repository<User, Long> {
@Override
public User findById(Long id) {
// 实现具体逻辑
}
@Override
public void save(User entity) {
// 实现具体逻辑
}
}
为什么泛型接口使用更广泛? 因为接口通常定义的是行为契约,而这些行为往往需要处理多种数据类型。通过泛型接口,我们可以保持接口的通用性,同时在实现时指定具体类型。
泛型方法比泛型类更加灵活,它们可以独立于类存在。来看几个典型场景:
1. 静态工具方法:
java复制public class CollectionUtils {
public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
List<T> result = new ArrayList<>();
for (T item : list) {
if (predicate.test(item)) {
result.add(item);
}
}
return result;
}
}
2. 非静态方法中的泛型:
java复制public class JsonParser {
public <T> T parse(String json, Class<T> clazz) {
// 使用Gson或Jackson等库实现
}
}
重要区别:泛型类在实例化时就确定了类型参数,而泛型方法在每次调用时都可以有不同的类型参数。这就是为什么泛型方法可以是静态的——它们不依赖于类的类型参数。
当你需要读取一个集合,但不关心具体是什么子类型时,这是最佳选择:
java复制public double sum(List<? extends Number> numbers) {
double total = 0.0;
for (Number num : numbers) {
total += num.doubleValue();
}
return total;
}
// 可以这样调用
List<Integer> ints = Arrays.asList(1, 2, 3);
sum(ints); // 合法
关键限制:使用上界通配符的集合,你不能往里面添加元素(null除外),因为编译器无法确定具体类型是否匹配。
当你想写入集合而不关心父类型时使用:
java复制public void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
// 可以这样调用
List<Number> numbers = new ArrayList<>();
addNumbers(numbers); // 合法
典型应用场景:Java的Collections.copy()方法就使用了这种模式:
java复制public static <T> void copy(List<? super T> dest, List<? extends T> src)
当你真的不关心具体类型时使用,通常用于方法参数:
java复制public void printList(List<?> list) {
for (Object elem : list) {
System.out.println(elem);
}
}
与原始类型的区别:虽然List<?>和List看起来相似,但前者是类型安全的,编译器会阻止你向其中添加任何元素(除了null)。
Java泛型是通过类型擦除实现的,这意味着在运行时,所有泛型类型信息都会被擦除。例如:
java复制List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
// 运行时两者类型相同
System.out.println(strings.getClass() == integers.getClass()); // 输出true
带来的限制:
new List<String>[10]是非法的list instanceof List<String>会编译错误new T()是不允许的泛型类中的静态方法或静态变量不能使用类的类型参数:
java复制public class GenericClass<T> {
// 错误!静态成员不能使用类的类型参数
// private static T staticField;
// 正确!这是一个独立的泛型方法
public static <E> E staticMethod(E input) {
return input;
}
}
原因:静态成员属于类本身,而不是类的实例。而泛型类的类型参数是在实例化时确定的。
为了保持与老版本Java的兼容性,编译器会生成一些"桥接方法"。例如对于以下接口实现:
java复制interface Comparable<T> {
int compareTo(T other);
}
class String implements Comparable<String> {
public int compareTo(String other) { ... }
}
编译器实际上会生成两个方法:
public int compareTo(String) - 你实现的方法public int compareTo(Object) - 桥接方法,内部调用第一个方法Java集合框架是泛型的最佳示范:
java复制// 传统写法(容易出错)
Map rawMap = new HashMap();
rawMap.put("key", "value");
String value = (String) rawMap.get("key");
// 泛型写法(类型安全)
Map<String, String> genericMap = new HashMap<>();
genericMap.put("key", "value");
String value = genericMap.get("key");
建议:永远不要使用原始类型的集合,即使你要处理多种类型,也应该使用通配符。
设计通用工具类时,泛型能大幅提升API的灵活性:
java复制public class Response<T> {
private boolean success;
private T data;
private String error;
// 静态工厂方法
public static <T> Response<T> success(T data) {
Response<T> response = new Response<>();
response.success = true;
response.data = data;
return response;
}
// 省略其他方法
}
// 使用示例
Response<User> userResponse = Response.success(new User());
虽然泛型很强大,但也要避免过度设计。以下情况可能不需要泛型:
问题:为什么不能创建泛型数组?
java复制T[] array = new T[10]; // 编译错误
解决方案:
ArrayList代替数组java复制T[] array = (T[]) new Object[10];
问题:当泛型遇到可变参数时会有警告:
java复制public static <T> void printAll(T... elements) {
for (T element : elements) {
System.out.println(element);
}
}
解决方案:
@SafeVarargs注解(确保方法内部不会错误处理数组)List<T>代替可变参数问题:有时编译器无法推断泛型类型:
java复制// 编译错误:无法推断类型
Collections.emptyList().add("string");
解决方案:明确指定类型参数:
java复制Collections.<String>emptyList().add("string");
java复制// Java 6及以前
Map<String, List<String>> map = new HashMap<String, List<String>>();
// Java 7+
Map<String, List<String>> map = new HashMap<>();
java复制// Java 7需要这样写
process(Collections.<String>emptyList());
// Java 8可以简写
process(Collections.emptyList());
java复制List<String> names = people.stream()
.map(Person::getName)
.collect(Collectors.toList());
java复制// 传统写法
Map<String, List<String>> map = new HashMap<>();
// Java 10+
var map = new HashMap<String, List<String>>();
虽然var看起来像是弱化了类型,但实际上它仍然保持强类型特性,编译器会根据右侧表达式推断出完整类型(包括泛型参数)。
在我参与的一个电商平台项目中,我们使用泛型设计了一个通用的数据访问层:
java复制public interface GenericRepository<T, ID> {
Optional<T> findById(ID id);
List<T> findAll();
T save(T entity);
void deleteById(ID id);
}
// 具体实现
public class UserRepository implements GenericRepository<User, Long> {
// 实现接口方法
}
public class ProductRepository implements GenericRepository<Product, String> {
// 实现接口方法
}
这种设计带来了几个好处:
性能考虑:有人担心泛型会影响性能,实际上由于类型擦除,泛型在运行时不会产生任何额外开销。所有的类型检查都在编译期完成。