Java 泛型里的 T、E、K、V 总搞混?我用 3 年开发经验,把它讲明白

刚做 Java 开发那年,我写了个泛型工具类,提交代码后被组长批了一顿 —— 代码里 T 和 E 随便混用,别人看半天不知道哪个是集合元素,哪个是普通类型。组长说:“这些字母不是随便写的,是给代码‘贴标签’,你得让别人一眼就懂你的逻辑。”

后来我维护过别人的代码,看到Map<T, E>这种写法,盯着屏幕十分钟才分清哪个是键哪个是值,那一刻才明白组长的话。其实 T、E、K、V 背后藏着 “约定俗成的规矩”,用对了不仅代码不报错,还能让你的代码更 “易读”,别人一看就知道你是 “懂行的”。

今天就用我这几年的开发经验,把 T、E、K、V 讲明白,每个符号都附实际项目里会用到的代码案例,看完你再也不用瞎猜,下次写代码直接上手。

一、先搞懂:泛型是啥?为啥要分 T、E、K、V?

先举个生活里的例子:我家里的衣柜,衬衫放在上层抽屉,裤子挂在衣架上,袜子装在专门的收纳盒里 —— 要是所有衣服都堆在一起,找的时候不仅慢,还容易皱。

泛型就像 “代码里的衣柜”,让数据 “按类型分类存放”。比如:

ArrayList<String> 专门存字符串,不会不小心把整数塞进去;

ArrayList<User> 专门存用户对象,取数据时不用再强转(以前没泛型,得写(User) list.get(0),又麻烦又容易错)。

而 T、E、K、V,就是给衣柜里的 “抽屉” 贴的 “标签”—— 告诉编译器和其他开发者:“这个抽屉专门装哪种类型的数据”。它们本质都是 “类型占位符”,但分工不同:看到 E 就知道是集合里的元素,看到 K 和 V 就知道是键值对,不用再去猜代码逻辑。

二、T:泛型里的 “万金油”,单个不确定类型就用它

含义:Type(类型)

T 是泛型里最常用的 “万能标签”,没有特殊限制,只要你需要 “一个不确定的类型”,比如写通用工具类、单个类型的返回结果,就用 T。

我第一次用 T,是写对象拷贝工具类。当时项目里要把 DTO 转 PO、PO 转 VO,要是每个对象都写一个拷贝方法,得写十几个,后来用 T 做泛型,一个工具类就搞定了。

代码案例:对象拷贝工具类(项目实战版)

// 泛型工具类:<T>表示“任意类型”

public class BeanCopyUtil<T> {

    // 拷贝方法:source和target必须是同一类型T

    public void copy(T source, T target) {

        // 实际项目里,我用的是Spring的BeanUtils,这里用反射演示核心逻辑

        try {

            Class<?> clazz = source.getClass();

            // 获取对象的所有属性(包括private的)

            Field[] fields = clazz.getDeclaredFields();

            for (Field field : fields) {

                field.setAccessible(true); // 突破private权限

                // 把source的属性值赋给target

                Object value = field.get(source);

                field.set(target, value);

            }

        } catch (Exception e) {

            e.printStackTrace();

            throw new RuntimeException("对象拷贝失败,检查属性类型是否匹配");

        }

    }

    // 测试:拷贝User和Order(实际项目里,这部分会写在单元测试里)

    public static void main(String[] args) {

        // 1. 拷贝User对象(DTO转PO)

        BeanCopyUtil<User> userCopy = new BeanCopyUtil<>();

        UserDTO sourceUser = new UserDTO(1, "老王", 35, "13800138000");

        UserPO targetUser = new UserPO();

        userCopy.copy(sourceUser, targetUser);

        System.out.println(targetUser); // 输出UserPO(id=1, name=老王, age=35)

        // 2. 拷贝Order对象(不用改工具类,换泛型类型就行)

        BeanCopyUtil<Order> orderCopy = new BeanCopyUtil<>();

        OrderDTO sourceOrder = new OrderDTO(2001, "20240701001", 199.9, "待支付");

        OrderPO targetOrder = new OrderPO();

        orderCopy.copy(sourceOrder, targetOrder);

        System.out.println(targetOrder); // 输出OrderPO(id=2001, orderNo=20240701001, amount=199.9)

    }

    // 项目里常见的DTO和PO类(简化版)

    static class UserDTO {

        private Integer id;

        private String name;

        private Integer age;

        private String phone; // DTO里有phone,PO里没有,拷贝时会忽略(反射只拷贝存在的属性)

        // 构造器、getter、setter、toString省略

    }

    static class UserPO {

        private Integer id;

        private String name;

        private Integer age;

        // 构造器、getter、setter、toString省略

    }

    static class OrderDTO {

        private Integer id;

        private String orderNo;

        private Double amount;

        private String status;

        // 构造器、getter、setter、toString省略

    }

    static class OrderPO {

        private Integer id;

        private String orderNo;

        private Double amount;

        // 构造器、getter、setter、toString省略

    }

}

我的经验总结:

T 代表 “一个固定类型”,比如BeanCopyUtil<User>里,T 就固定是 User,source 和 target 必须都是 User(不能一个是 User,一个是 Order);

要是需要 “多个不同类型”,比如方法里要传两个不同类型的参数,光靠 T 不够,得用 K、V 这种成对的符号。

三、E:集合的 “专属标签”,存元素就用它

含义:Element(元素)

E 是集合的 “专属符号”,专门表示 “集合里装的元素类型”。我第一次注意到 E,是看 ArrayList 的源码,里面全是 E,当时就纳闷 “为啥不用 T?” 后来才明白,用 E 能让代码更易读 —— 别人一看 E,就知道是 “集合里的元素”,不用再去猜。

比如项目里要写个 “过滤集合空元素” 的方法,用 E 做泛型,别人一看就知道是处理集合元素的。

代码案例:集合工具类(过滤空元素)

// 泛型集合工具类:E代表“集合里的元素类型”

public class CollectionUtil<E> {

    // 过滤集合里的空元素(比如null、空字符串)

    public List<E> filterEmpty(List<E> list) {

        List<E> result = new ArrayList<>();

        if (list == null || list.isEmpty()) {

            return result;

        }

        for (E element : list) {

            if (element != null) {

                // 若是字符串,还要过滤空字符串

                if (element instanceof String) {

                    String str = (String) element;

                    if (!str.trim().isEmpty()) {

                        result.add(element);

                    }

                } else {

                    result.add(element);

                }

            }

        }

        return result;

    }

    // 测试:过滤字符串集合和User集合

    public static void main(String[] args) {

        CollectionUtil<String> strUtil = new CollectionUtil<>();

        List<String> strList = Arrays.asList("苹果", "", null, "香蕉", "  ");

        List<String> filteredStrList = strUtil.filterEmpty(strList);

        System.out.println(filteredStrList); // 输出[苹果, 香蕉]

        CollectionUtil<User> userUtil = new CollectionUtil<>();

        List<User> userList = Arrays.asList(

                new User(1, "小李"),

                null,

                new User(2, ""),

                new User(3, "小赵")

        );

        List<User> filteredUserList = userUtil.filterEmpty(userList);

        System.out.println(filteredUserList); // 输出[User(id=1, name=小李), User(id=3, name=小赵)]

    }

    // 简化的User类

    static class User {

        private Integer id;

        private String name;

        // 构造器、toString省略

    }

}

我的经验总结:

只要跟集合相关,不管是自定义集合,还是方法里的集合参数,都用 E,比如List<E>、Set<E>,这是 Java 源码里的规范,别自己改成 T;

我之前见过有人写List<T>,虽然语法没错,但别人看的时候得反应一下 “这个 T 是啥”,用 E 更直观。

四、K 和 V:键值对的 “黄金搭档”,成对出现才好用

含义:Key(键)和 Value(值)

K 和 V 是泛型里的 “搭档组合”,专门用在 “键值对” 场景,比如 Map、缓存、字典。我第一次用 K 和 V,是写项目里的缓存工具类,当时要存 “用户 ID→用户信息”“商品编码→商品详情”,用 K 和 V 刚好能区分键和值的类型。

代码案例:通用缓存工具(项目实战版)

// 泛型缓存工具:K是键类型,V是值类型

public class CacheManager<K, V> {

    // 用ConcurrentHashMap,线程安全(项目里要考虑并发)

    private Map<K, V> cache = new ConcurrentHashMap<>();

    // 缓存过期时间(默认30分钟)

    private long expireMinutes = 30;

    // 带过期时间的构造器

    public CacheManager(long expireMinutes) {

        this.expireMinutes = expireMinutes;

    }

    // 存缓存:key是K类型,value是V类型

    public void put(K key, V value) {

        cache.put(key, value);

        // 实际项目里,会用定时任务清理过期缓存,这里简化

        System.out.println("存缓存:key=" + key + ", value=" + value + ", 过期时间=" + expireMinutes + "分钟");

    }

    // 取缓存:根据K类型的key,返回V类型的value

    public V get(K key) {

        V value = cache.get(key);

        System.out.println("取缓存:key=" + key + ", value=" + value);

        return value;

    }

    // 测试:两种不同的键值对缓存

    public static void main(String[] args) {

        // 1. 缓存用户:键是Integer(用户ID),值是UserVO

        CacheManager<Integer, UserVO> userCache = new CacheManager<>(60); // 1小时过期

        userCache.put(1, new UserVO(1, "小张", "13900139000", "普通用户"));

        UserVO user = userCache.get(1);

        System.out.println(user); // 输出UserVO(id=1, name=小张, phone=13900139000, role=普通用户)

        // 2. 缓存商品:键是String(商品编码),值是ProductVO

        CacheManager<String, ProductVO> productCache = new CacheManager<>(30); // 30分钟过期

        productCache.put("PROD202407", new ProductVO("PROD202407", "笔记本电脑", 4999, "库存充足"));

        ProductVO product = productCache.get("PROD202407");

        System.out.println(product); // 输出ProductVO(code=PROD202407, name=笔记本电脑, price=4999, stock=库存充足)

    }

    // 项目里的VO类(简化版)

    static class UserVO {

        private Integer id;

        private String name;

        private String phone;

        private String role;

        // 构造器、toString省略

    }

    static class ProductVO {

        private String code;

        private String name;

        private Integer price;

        private String stock;

        // 构造器、toString省略

    }

}

我的经验总结:

K 和 V 必须成对用,比如CacheManager<K, V>,不能只写CacheManager<K>;

别搞反顺序,比如Map<V, K>,我之前见过有人这么写,别人看的时候得反过来想 “哪个是键哪个是值”,很麻烦;

键和值的类型可以不一样,比如用户缓存(K=Integer,V=UserVO)、商品缓存(K=String,V=ProductVO),灵活适配不同场景。

五、记不住?3 个口诀帮你秒分清

我刚开始记不住的时候,自己编了 3 个口诀,后来用得多了,就再也没混过:

1. 单个类型用 T(Type)

只要是 “一个不确定的类型”,比如工具类、单个返回值,就用 T,比如BeanCopyUtil<T>、public <T> T getResult()。

2. 集合元素用 E(Element)

跟集合相关的,不管是自定义集合,还是方法里的集合参数,只要涉及 “集合里的元素”,就用 E,比如List<E>、public void processList(List<E> list)。

3. 键值对用 K-V(Key-Value)

存键值对的时候,比如 Map、缓存,就用 K 代表键,V 代表值,成对出现,比如Map<K, V>、CacheManager<K, V>。

六、这些坑别再踩了!我当年踩过的错

1. 坑 1:T、E、K、V 随便换着用

我刚学的时候,写 Map 用Map<T, E>,结果同事 review 代码时问我 “哪个是键哪个是值?” 后来才知道,正确的写法是Map<K, V>,一看就懂,别自己瞎改。

2. 坑 2:泛型里用基本类型

泛型只能用 “引用类型”,不能用 int、double 这些基本类型。比如ArrayList<int>会报错,要写成ArrayList<Integer>(用包装类);List<double>要写成List<Double>。我第一次写的时候报了错,查了半天才知道是这个原因。

3. 坑 3:泛型方法没写<T>

比如想写个返回任意类型的方法,直接写public T getValue()会报错,必须在方法名前面加<T>,正确写法是public <T> T getValue()。这个<T>是 “声明泛型”,告诉编译器 “这个方法要用泛型 T”,我之前漏写过,报了错还不知道为啥。

最后:一张表总结,收藏起来随时看

符号含义适用场景代码示例

TType(类型)通用工具类、单个不确定类型BeanCopyUtil<T>、public <T> T getResult()

EElement(元素)集合类、集合相关方法ArrayList<E>、List<E> list、CollectionUtil<E>

KKey(键)键值对场景(与 V 成对)Map<K, V>、CacheManager<K, V>中的键

VValue(值)键值对场景(与 K 成对)Map<K, V>、CacheManager<K, V>中的值

其实泛型里的这些符号,本质是 “让代码更易读的规范”。我现在写代码,都会下意识用对符号,不是因为必须这么写,而是因为 “好的代码,不仅要能跑通,还要让别人能看懂”。

你之前有没有混用 T、E、K、V 踩过坑?或者有其他泛型相关的疑问?欢迎在评论区分享,咱们一起交流进步~

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容