Java 泛型全解析:从基础到实战,一篇通关掌握

引言

在 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 泛型存在一些限制:

  1. 不能使用基本类型作为类型参数
List<int> list = new ArrayList<>(); // 编译错误
List<Integer> list = new ArrayList<>(); // 正确,使用包装类
  1. 不能实例化泛型类型的对象
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(); // 可以通过反射创建实例
    }
}
  1. 不能创建泛型数组
List<String>[] stringLists = new List<String>[10]; // 编译错误
List<?>[] lists = new List<?>[10]; // 允许使用通配符创建泛型数组
  1. 不能在静态上下文中使用类的泛型参数
public class Box<T> {
    private static T content; // 编译错误
    
    public static T getContent() { // 编译错误
        return content;
    }
}
  1. 不能捕获泛型类型的异常
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 缓存工具类关键特性说明

  1. 泛型支持通过K(键类型)和V(值类型)泛型参数,使缓存可适用于任意键值类型组合,解决了类型转换的繁琐问题。
  2. 线程安全设计使用ConcurrentHashMap作为底层存储,配合单线程定时任务,既保证了并发访问安全性,又避免了复杂的同步逻辑。
  3. 过期机制
  4. 支持设置过期时间,通过时间戳判断条目是否有效
  5. 双重清理策略:获取时检查过期 + 定时批量清理
  6. 延迟加载提供getOrLoad方法,支持缓存穿透时自动加载数据,简化 "缓存 - 数据库" 联动逻辑。
  7. 泛型擦除处理由于运行时泛型类型信息被擦除,内部使用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 注意事项

  1. 泛型类型限制若需要限制泛型类型(如只允许引用类型),可使用GenericCache<K, V extends SomeType>语法。
  2. 序列化支持如需持久化缓存,缓存的键值类型需实现Serializable接口。
  3. 缓存穿透防护实际使用中可在getOrLoad方法中添加互斥锁,防止缓存失效时的并发穿透问题。
  4. 内存管理对于大容量缓存,建议添加最大容量限制和淘汰策略(如 LRU),避免内存溢出。

以上实现提供了一个基础的通用缓存框架,可根据实际需求扩展功能,如添加统计监控、分布式支持等特性。

原文链接:,转发请注明来源!