引言
在 Java 编程中,泛型是一项强大的特性,它能够让我们编写更安全、更简洁、更具复用性的代码。如果你曾经被类型转换错误困扰,或者希望代码能适应多种数据类型而不必重复编写,那么泛型正是你需要掌握的技术。本文将从基础到进阶,全面解析 Java 泛型的方方面面,让你一文通关。
一、泛型基础:为什么需要泛型?
1.1 泛型的核心作用
泛型主要解决两个核心问题:类型安全和避免强制转型。
没有泛型之前,我们使用集合时需要进行大量强制类型转换,既繁琐又不安全:
// 没有泛型的时代
List list = new ArrayList();
list.add("Hello");
String str = (String) list.get(0); // 需要强制转型
list.add(123); // 可以添加任何类型,编译不会报错
String str2 = (String) list.get(1); // 运行时会抛出ClassCastException
使用泛型后,代码变得更安全、更简洁:
// 使用泛型
List<String> list = new ArrayList<>();
list.add("Hello");
String str = list.get(0); // 无需强制转型
list.add(123); // 编译时直接报错,杜绝类型错误1.2 泛型类
泛型类是在类定义时引入类型参数的类。语法格式为:class 类名<T>,其中 T 是类型参数,可自定义名称(通常使用单个大写字母)。
// 定义泛型类
public class Box<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
public static void main(String[] args) {
// 使用泛型类
Box<String> stringBox = new Box<>();
stringBox.setContent("Hello");
String str = stringBox.getContent(); // 无需转型
Box<Integer> intBox = new Box<>();
intBox.setContent(123);
int num = intBox.getContent(); // 无需转型
}
}
泛型类可以有多个类型参数:
// 多参数泛型类
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
// getters and setters
}
1.3 泛型方法
泛型方法是在方法定义时声明类型参数的方法,它可以在普通类或泛型类中定义。
public class GenericMethodExample {
// 泛型方法
public <T> T getFirstElement(T[] array) {
if (array != null && array.length > 0) {
return array[0];
}
return null;
}
public static void main(String[] args) {
GenericMethodExample example = new GenericMethodExample();
String[] strArray = {"Apple", "Banana", "Cherry"};
String firstStr = example.getFirstElement(strArray); // 无需转型
Integer[] intArray = {1, 2, 3, 4};
Integer firstInt = example.getFirstElement(intArray); // 无需转型
}
}
静态方法要使用泛型,必须定义为泛型方法,不能使用类的泛型参数:
public class StaticGenericExample {
// 静态泛型方法
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
}
1.4 泛型接口
泛型接口与泛型类类似,在接口定义时声明类型参数。
// 定义泛型接口
public interface Generator<T> {
T generate();
}
// 实现泛型接口时指定具体类型
public class NumberGenerator implements Generator<Integer> {
@Override
public Integer generate() {
return (int) (Math.random() * 100);
}
}
// 实现泛型接口时保留泛型参数
public class ArrayGenerator<T> implements Generator<T> {
private T[] array;
public ArrayGenerator(T[] array) {
this.array = array;
}
@Override
public T generate() {
int index = (int) (Math.random() * array.length);
return array[index];
}
}
二、泛型通配符:灵活处理未知类型
通配符 (?) 用于表示未知类型,主要有三种形式:无界通配符、上界通配符和下界通配符。
2.1 无界通配符(?)
无界通配符表示任意类型,主要用于读取操作:
public class UnboundedWildcardExample {
// 打印任何类型的列表
public static void printList(List<?> list) {
for (Object element : list) {
System.out.print(element + " ");
}
System.out.println();
}
public static void main(String[] args) {
List<String> strList = Arrays.asList("Apple", "Banana", "Cherry");
List<Integer> intList = Arrays.asList(1, 2, 3, 4);
printList(strList); // 可以传入任何类型的List
printList(intList);
}
}
注意:使用无界通配符的集合,不能添加任何元素(除了 null):
List<?> list = new ArrayList<String>();
list.add(null); // 允许
list.add("hello"); // 编译错误,无法确定具体类型
2.2 上界通配符(? extends T)
上界通配符表示 "T 及其子类",适用于读取操作,提供了类型安全的最大灵活性:
public class UpperBoundedWildcardExample {
// 计算数字列表的总和
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number n : list) {
sum += n.doubleValue(); // 可以安全调用Number的方法
}
return sum;
}
public static void main(String[] args) {
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.5, 2.5, 3.5);
System.out.println(sumOfList(intList)); // 6.0
System.out.println(sumOfList(doubleList)); // 7.5
}
}
注意:使用上界通配符的集合,不能添加任何元素(除了 null):
List<? extends Number> list = new ArrayList<Integer>();
list.add(null); // 允许
list.add(10); // 编译错误,无法确定具体类型
2.3 下界通配符(? super T)
下界通配符表示 "T 及其父类",适用于写入操作:
public class LowerBoundedWildcardExample {
// 向列表添加整数
public static void addIntegers(List<? super Integer> list) {
for (int i = 1; i <= 5; i++) {
list.add(i); // 可以安全添加Integer
}
}
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
List<Number> numList = new ArrayList<>();
List<Object> objList = new ArrayList<>();
addIntegers(intList); // 允许
addIntegers(numList); // 允许
addIntegers(objList); // 允许
System.out.println(intList); // [1, 2, 3, 4, 5]
}
}
读取下界通配符集合时,只能得到 Object 类型:
List<? super Integer> list = new ArrayList<Number>();
Object obj = list.get(0); // 只能得到Object类型
2.4 通配符使用原则:PECS
记住一个简单原则:PECS(Producer Extends, Consumer Super)
- 如果集合是生产者(主要用于读取),使用? extends T
- 如果集合是消费者(主要用于写入),使用? super T
// 生产者示例:从列表中读取数据
public static <T> T getLastElement(List<? extends T> list) {
if (!list.isEmpty()) {
return list.get(list.size() - 1);
}
return null;
}
// 消费者示例:向列表中写入数据
public static <T> void addElements(List<? super T> list, T... elements) {
for (T element : elements) {
list.add(element);
}
}
三、泛型擦除:泛型的底层实现
Java 的泛型是在编译期实现的,在运行时会被 "擦除",这就是所谓的类型擦除。
3.1 擦除原理
- 编译时,所有泛型信息被擦除,替换为其边界类型(若无边界则替换为 Object)
- 编译器会自动插入类型转换代码
- 生成桥接方法以保持多态性
例如,对于Box<T>,编译后会变成:
public class Box {
private Object content;
public Object getContent() {
return content;
}
public void setContent(Object content) {
this.content = content;
}
}
当我们使用Box<String>时,编译器会自动帮我们添加类型转换:
Box<String> box = new Box<>();
box.setContent("Hello");
String content = (String) box.getContent(); // 编译器自动添加
3.2 泛型的限制
由于类型擦除,Java 泛型存在一些限制:
- 不能使用基本类型作为类型参数
List<int> list = new ArrayList<>(); // 编译错误
List<Integer> list = new ArrayList<>(); // 正确,使用包装类
- 不能实例化泛型类型的对象
public class Box<T> {
public Box() {
T obj = new T(); // 编译错误
}
}
解决方法:使用反射
public class Box<T> {
private Class<T> clazz;
public Box(Class<T> clazz) {
this.clazz = clazz;
}
public T createInstance() throws InstantiationException, IllegalAccessException {
return clazz.newInstance(); // 可以通过反射创建实例
}
}
- 不能创建泛型数组
List<String>[] stringLists = new List<String>[10]; // 编译错误
List<?>[] lists = new List<?>[10]; // 允许使用通配符创建泛型数组
- 不能在静态上下文中使用类的泛型参数
public class Box<T> {
private static T content; // 编译错误
public static T getContent() { // 编译错误
return content;
}
}
- 不能捕获泛型类型的异常
public class GenericException<T extends Exception> {
public void handle() {
try {
// 一些代码
} catch (T e) { // 编译错误
// 处理异常
}
}
}
四、实战练习:泛型的实际应用
4.1 泛型工具类
创建一个通用的集合工具类,提供常用的集合操作:
import java.util.*;
import java.util.stream.Collectors;
public class CollectionUtils {
// 将一个列表分割成多个指定大小的子列表
public static <T> List<List<T>> partition(List<T> list, int size) {
if (list == null || size <= 0) {
return Collections.emptyList();
}
List<List<T>> result = new ArrayList<>();
int total = list.size();
int batches = (total + size - 1) / size; // 计算总批次
for (int i = 0; i < batches; i++) {
int start = i * size;
int end = Math.min(start + size, total);
result.add(list.subList(start, end));
}
return result;
}
// 提取列表中对象的某个属性,形成新的列表
public static <T, R> List<R> extractProperty(List<T> list, PropertyExtractor<T, R> extractor) {
if (list == null || extractor == null) {
return Collections.emptyList();
}
return list.stream()
.map(extractor::extract)
.collect(Collectors.toList());
}
// 过滤列表中符合条件的元素
public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
if (list == null || predicate == null) {
return Collections.emptyList();
}
return list.stream()
.filter(predicate)
.collect(Collectors.toList());
}
// 函数式接口:用于提取属性
@FunctionalInterface
public interface PropertyExtractor<T, R> {
R extract(T t);
}
// 测试
public static void main(String[] args) {
// 测试partition方法
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
List<List<Integer>> partitions = CollectionUtils.partition(numbers, 3);
System.out.println("分割结果: " + partitions); // [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
// 创建测试对象列表
List<User> users = Arrays.asList(
new User("Alice", 25),
new User("Bob", 30),
new User("Charlie", 35)
);
// 测试extractProperty方法
List<String> names = CollectionUtils.extractProperty(users, User::getName);
System.out.println("提取的姓名: " + names); // [Alice, Bob, Charlie]
// 测试filter方法
List<User> adults = CollectionUtils.filter(users, user -> user.getAge() >= 30);
System.out.println("年龄大于等于30的用户: " + adults.stream()
.map(User::getName)
.collect(Collectors.toList())); // [Bob, Charlie]
}
// 测试用的User类
static class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
}
}
4.2 泛型集合封装
创建一个通用的分页结果封装类:
import java.util.List;
import java.util.Objects;
/**
* 分页结果封装类
* @param <T> 数据类型
*/
public class PageResult<T> {
// 当前页码
private int pageNum;
// 每页条数
private int pageSize;
// 总记录数
private long total;
// 总页数
private int pages;
// 当前页数据
private List<T> list;
public PageResult(int pageNum, int pageSize, long total, List<T> list) {
this.pageNum = pageNum;
this.pageSize = pageSize;
this.total = total;
this.list = list;
// 计算总页数
this.pages = (int) (total % pageSize == 0 ? total / pageSize : total / pageSize + 1);
}
// 静态工厂方法
public static <T> PageResult<T> of(int pageNum, int pageSize, long total, List<T> list) {
return new PageResult<>(pageNum, pageSize, total, list);
}
// 判断是否有下一页
public boolean hasNextPage() {
return pageNum < pages;
}
// 判断是否有上一页
public boolean hasPreviousPage() {
return pageNum > 1;
}
// getter方法
public int getPageNum() { return pageNum; }
public int getPageSize() { return pageSize; }
public long getTotal() { return total; }
public int getPages() { return pages; }
public List<T> getList() { return list; }
@Override
public String toString() {
return "PageResult{" +
"pageNum=" + pageNum +
", pageSize=" + pageSize +
", total=" + total +
", pages=" + pages +
", list=" + list +
'}';
}
// 测试
public static void main(String[] args) {
// 模拟查询结果
List<String> data = List.of("Item1", "Item2", "Item3", "Item4", "Item5");
PageResult<String> pageResult = PageResult.of(1, 2, 10, data);
System.out.println(pageResult);
System.out.println("是否有下一页: " + pageResult.hasNextPage()); // true
System.out.println("是否有上一页: " + pageResult.hasPreviousPage()); // false
}
}
4.3 通用缓存工具类
通用缓存工具类用于简化各类数据的缓存管理,通常需要支持数据的添加、获取、删除、过期清理等功能。以下是一个基于 Java 的通用缓存工具类实现,结合泛型设计以支持多种数据类型:
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Function;
/**
* 通用缓存工具类,支持泛型和过期时间设置
* @param <K> 缓存键类型
* @param <V> 缓存值类型
*/
public class GenericCache<K, V> {
// 核心缓存容器,使用ConcurrentHashMap保证线程安全
private final Map<K, CacheEntry> cacheMap = new ConcurrentHashMap<>();
// 定时清理过期缓存的线程池
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
// 缓存条目内部类,存储值和过期时间
private static class CacheEntry {
Object value; // 实际存储的值(因泛型擦除,运行时为Object)
long expireTime; // 过期时间戳(毫秒)
CacheEntry(Object value, long expireTime) {
this.value = value;
this.expireTime = expireTime;
}
// 判断是否过期
boolean isExpired() {
return expireTime > 0 && System.currentTimeMillis() > expireTime;
}
}
/**
* 初始化缓存并启动定时清理任务
* @param cleanupInterval 清理间隔(秒)
*/
public GenericCache(int cleanupInterval) {
// 定时清理过期条目
scheduler.scheduleAtFixedRate(this::cleanupExpired,
cleanupInterval,
cleanupInterval,
TimeUnit.SECONDS);
}
/**
* 添加缓存(无过期时间)
*/
public void put(K key, V value) {
put(key, value, 0); // 0表示永不过期
}
/**
* 添加缓存(带过期时间)
* @param key 缓存键
* @param value 缓存值
* @param expireSeconds 过期时间(秒,0表示永不过期)
*/
public void put(K key, V value, long expireSeconds) {
if (key == null) return;
long expireTime = expireSeconds > 0
? System.currentTimeMillis() + expireSeconds * 1000
: 0;
cacheMap.put(key, new CacheEntry(value, expireTime));
}
/**
* 获取缓存,如果不存在则通过loader加载并缓存
* @param key 缓存键
* @param loader 数据加载器(当缓存不存在时调用)
* @param expireSeconds 过期时间(秒)
* @return 缓存值
*/
@SuppressWarnings("unchecked")
public V getOrLoad(K key, Function<K, V> loader, long expireSeconds) {
// 先尝试获取缓存
V value = get(key);
if (value != null) {
return value;
}
// 缓存不存在则加载数据
value = loader.apply(key);
if (value != null) {
put(key, value, expireSeconds);
}
return value;
}
/**
* 获取缓存值
*/
@SuppressWarnings("unchecked")
public V get(K key) {
if (key == null) return null;
CacheEntry entry = cacheMap.get(key);
if (entry == null || entry.isExpired()) {
cacheMap.remove(key); // 移除过期条目
return null;
}
// 泛型擦除后需要强制转换,编译器会生成checked cast
return (V) entry.value;
}
/**
* 移除缓存
*/
public void remove(K key) {
cacheMap.remove(key);
}
/**
* 清理所有过期缓存
*/
private void cleanupExpired() {
cacheMap.entrySet().removeIf(entry -> entry.getValue().isExpired());
}
/**
* 关闭缓存,释放资源
*/
public void close() {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(1, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
}
cacheMap.clear();
}
}
4.3.1 缓存工具类关键特性说明
- 泛型支持通过K(键类型)和V(值类型)泛型参数,使缓存可适用于任意键值类型组合,解决了类型转换的繁琐问题。
- 线程安全设计使用ConcurrentHashMap作为底层存储,配合单线程定时任务,既保证了并发访问安全性,又避免了复杂的同步逻辑。
- 过期机制
- 支持设置过期时间,通过时间戳判断条目是否有效
- 双重清理策略:获取时检查过期 + 定时批量清理
- 延迟加载提供getOrLoad方法,支持缓存穿透时自动加载数据,简化 "缓存 - 数据库" 联动逻辑。
- 泛型擦除处理由于运行时泛型类型信息被擦除,内部使用Object存储值,取出时通过(V)强制转换(编译器会生成类型检查代码)。
4.3.2 使用示例
public class CacheDemo {
public static void main(String[] args) {
// 创建缓存实例(每30秒清理一次过期数据)
GenericCache<String, User> userCache = new GenericCache<>(30);
// 添加缓存(2分钟过期)
userCache.put("user1", new User(1, "Alice"), 120);
// 获取缓存
User user = userCache.get("user1");
// 延迟加载示例(缓存不存在时从数据库加载)
User lazyLoadedUser = userCache.getOrLoad("user2",
userId -> fetchUserFromDB(userId), 300);
}
// 模拟从数据库加载用户
private static User fetchUserFromDB(String userId) {
System.out.println("Loading user from DB: " + userId);
return new User(Integer.parseInt(userId.substring(4)), "User" + userId);
}
static class User {
private int id;
private String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
}
}
4.3.3 注意事项
- 泛型类型限制若需要限制泛型类型(如只允许引用类型),可使用GenericCache<K, V extends SomeType>语法。
- 序列化支持如需持久化缓存,缓存的键值类型需实现Serializable接口。
- 缓存穿透防护实际使用中可在getOrLoad方法中添加互斥锁,防止缓存失效时的并发穿透问题。
- 内存管理对于大容量缓存,建议添加最大容量限制和淘汰策略(如 LRU),避免内存溢出。
以上实现提供了一个基础的通用缓存框架,可根据实际需求扩展功能,如添加统计监控、分布式支持等特性。
