Java的泛型理解与如何在运行时获取泛型类型

有个朋友是学C#的,我们经常一起讨论这两种语言的一些特性,发现这两种语言的一些特性还挺相似的,后来才发现C#是微软在Java上与sun公司有理念分歧转而开发的语言,所以这两种语言有天生的一些相似处,比如GC,堆对象存储等。扯得有点远,说回主题,Java中的泛型理解。

泛型我们用的最多的可能是Collection相关的容器接口,如List,Map等。它们经常用来存储一类相同对象,还有就是相似功能的代码可能需要接收不同的入参。
使用场景
1、 泛型类,定义一个可以处理多种类型的容器,如:Collection等容器,Set<E>, List<E>等,这类容器存放相同类型的对象,便于操作与遍历等,在获取的时候也不用加类型强转。

// 定义一个泛型类 T是类型参数
public class Box<T> {
    private T content;

    public void set(T content) {
        this.content = content;
    }

    public T get() {
        return content;
    }
}

// 使用泛型类(钻石运算符 <>)
Box<String> stringBox = new Box<>(); // 这里的String是实际类型参数
stringBox.set("Hello");
String value = stringBox.get(); // 无需强制转换
Box<Integer> intBox = new Box<>();
intBox.set(123);
// intBox.set("abc"); // 编译错误!类型不匹配

2、相似功能代码集成,如泛型方法与泛型接口:

// 这里定义一个将String序列化成指定对象的方法
public static <TEMPLATE> TEMPLATE parseObject(String origin, Class<TEMPLATE> clazz) {
    return (TEMPLATE) new Gson().fromJson(origin, clazz);
}

// 这里定义一个通用比较器的接口
public interface Comparable<T> {
    int compareTo(T other);
}

// 实现接口时指定具体类型
public class Person implements Comparable<Person> {
    @Override
    public int compareTo(Person other) { ... }
}

我们为什么需要泛型?和没有泛型对比有啥区别?

好处 说明 对比非泛型
更强的类型安全 将运行时错误提前到编译时发现 避免 ClassCastException
消除强制转换 代码更简洁、可读性更好 String s = (String) box.get();
代码复用 一个泛型类可以用于无数种类型 避免为每种类型编写重复代码
更好的设计 明确表达“这个容器装什么类型”的意图 Object 语义模糊

如果没有泛型会怎么样?:

如果没有泛型:
        List<Object> temp = new ArrayList<>();
        temp.add("123");
        Integer o = (Integer) temp.get(0); // 运行时崩溃;
或者使用如下代码,每次获取时都需要先判断类型
        if (temp.get(0) instanceof String) {
            String o1 = (String) temp.get(0);
        }

这些都是通用泛型的一些定义。对于Java里面的泛型的特性来说,Java的泛型是编译时特性,运行时会执行类型擦除,所有类型参数会被替换为 Object。因此:

  • new T() 是不允许的(因为不知道T的具体类型,编译后被擦除)。
  • instanceof Box<String> 是不允许的(运行时只有 Box)。
  • 基本类型不支持:类型参数不能是 int、double,必须是包装类 Integer、Double。
  • 静态上下文中的泛型:静态成员不能使用类的泛型参数(因为静态属于类,而泛型参数属于实例)。

这一点相比C#语言特性是不一样的,因为Java中的泛型是Java5后面出的语言特性,为了兼容老版本的代码,让其能够在新的jvm上跑起来,所以设计成编译时特性,新版本有泛型的代码编译出的字节码和老版本是一致的,这样就无需大改jvm而实现兼容。
C#是后出的语言,没有历史包袱,所以在设计之初就设计好了一套泛型规范以支持运行时泛型
运行时的差别是什么?
Java中:

List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass()); // true
// 下面的代码编译错误: 'instanceof' 不能与泛型一起使用
 boolean b1 = list1 instanceof List; // 可以执行
// boolean b2 = list2 instanceof List<Integer>;  会编译报错
}

// 方法重载
public class OverloadExample {
    // 编译错误: 方法擦除后与另一个方法具有相同的签名
    // public void process(List<String> list) { ... }
    // public void process(List<Integer> list) { ... }

    // 只能通过不同的参数名或个数来重载
    public void process(List<String> list) { ... }
    public void process(List<Integer> list, String flag) { ... } // 可以,参数个数不同
}

C#中:

List<string> stringList = new List<string>();
List<int> intList = new List<int>();
// 运行时,两者是完全不同的类型
Console.WriteLine(stringList.GetType() == intList.GetType()); // 输出: False 
 // 可以精确判断
Console.WriteLine(stringList is List<string>); // 输出: True
Console.WriteLine(stringList is List<int>);    // 输出: False

// 方法重载
public class OverloadExample {
    // 完全合法,运行时它们是两个不同的方法
    public void Process(List<string> list) { 
        Console.WriteLine("Strings"); 
    }
    public void Process(List<int> list) { 
        Console.WriteLine("Ints"); 
    }
}

// 调用时,C# 会根据运行时类型精确选择
var example = new OverloadExample();
example.Process(new List<string>()); // 输出 "Strings"
example.Process(new List<int>());    // 输出 "Ints"

那运行时泛型对比编译时泛型的区别是什么?

对比维度 编译时泛型 (Java风格) 运行时泛型 (C#风格)
类型信息保留时机 仅编译时 编译时 + 运行时
字节码/IL中是否保留 ❌ 不保留,被擦除为原始类型 ✅ 完整保留
运行时能否区分不同参数类型 ❌ 不能。List<String>List<Integer>运行时都是List ✅ 能。它们是CLR眼中的不同类型
值类型作为参数时 必须装箱为包装类(如Integer),有额外内存和性能开销 无需装箱,直接存储,CLR生成特化代码
能否 new T() ❌ 不能。运行时不知道T是什么 ✅ 能。需约束where T : new()
能否 new T[10] ❌ 不能。数组运行时需知道确切类型 ✅ 能
能否用 instanceof/is 判断泛型参数 ❌ 不能。list instanceof List<String>是编译错误 ✅ 能。list is List<string>返回正确结果
方法重载(仅泛型参数不同) ❌ 不支持。擦除后签名相同 ✅ 支持
静态成员归属 所有参数化类型共享同一份静态成员 不同参数类型有各自独立的静态成员
性能 引用类型无额外开销;值类型有装箱开销 值类型特化,性能更高;引用类型代码共享
向后兼容性 ✅ 极好。旧代码无需修改即可运行 ⚠️ 需要修改CLR/框架,旧代码需重新编译
设计哲学 兼容性优先:宁愿牺牲部分能力,也要保证生态平滑演进 功能/性能优先:为了更好的能力,愿意改动底层

如何在运行时获取Java泛型类型?

既然Java的泛型是编译时特性,如果就想在运行时获取泛型类型就真的没有办法了吗?有的兄弟,有的。
Java通过类型擦除实现泛型,但为了支持反射,编译器会将泛型信息以Signature属性的形式保留在类文件的元数据中。

.class 文件(磁盘)                          方法区 / 元空间(Metaspace)                     堆内存(Heap)
┌─────────────────────┐                      ┌─────────────────────────────────┐            ┌─────────────────────────────────┐
│ 常量池               │                      │ Klass 元数据(类蓝图)           │            │ 对象实例数据区                  │
│ 方法表(字节码)      │                      │ ┌─────────────────────────────┐ │            │ ┌─────────────────────────────┐ │
│ 字段表(声明)        │                      │ │ 1. 已解析常量池              │ │            │ │ 1. 实例字段真实值            │ │
│ 属性表               │   类加载、解析        │ │ 2. 方法字节码(可执行)       │ │            │ │    Person<String>            │ │
│   ├── Code           │ ───────────────────→ │ │ 3. 字段布局/内存偏移量       │ │            │ │    - name: "张三"             │ │
│   ├── LineNumberTable│                      │ │ 4. 虚方法表 vtable           │ │  指针指向   │ │    - age: 20                 │ │
│   └──Signature     │ 【泛型关键】            │ │ 5. 泛型签名(Signature)      │ │◄───────────│ │ 2. 类型指针(指向Klass)      │ │
└─────────────────────┘                       │ └─────────────────────────────┘ │             │ │    ↓ 运行时仅存原始类型       │ │
                                              └─────────────────────────────────┘             │ │ 3. 无泛型类型(已擦除)       │ │
                                                                                               │ └─────────────────────────────┘ │
                                                                                               └─────────────────────────────────┘
                                             
                                                                                            ┌─────────────────────────────────┐
                                                                                            │ java.lang.Class 对象           │
                                                                                            │ (类对象,存储静态变量)        │
                                                                                            │  - 持有 Klass 指针             │
                                                                                            └─────────────────────────────────┘

【泛型核心说明】
1. .class 文件:保存 Signature 属性 → 存储完整泛型信息(供编译器/反射使用)
2. 方法区Klass:保留泛型签名,但**运行时字节码已完成泛型擦除**
3. 堆实例对象:**不存储任何泛型类型**,运行时只有原始类型(Person 而非 Person<String>)

所以基于上面这个特性我们可以在运行时获取泛型类型。

    class User {
        public int age;
        public String name;
        @Override
        public String toString() { return "User{age=" + age + ", name='" + name + "'}"; }
    }

    // 泛型基类,同时包含字段和方法,用于演示多种泛型获取方式
    class Base<T> {
        List<String> stringList = new ArrayList<>();
        Map<Integer, User> userMap = new HashMap<>();

        public List<User> getUsers() { return null; }
        public void setNames(List<String> names) {}
    }

    // 子类明确指定泛型为 User
    class Derived extends Base<User> {}

    // ============================================================
    // Java 泛型是"编译期"特性,运行时会"类型擦除"
    // Base<T> 运行时 T 变成 Object,Base<User> 运行时就是 Base
    // 但!通过反射可以拿到子类声明时、字段/方法签名上写入的泛型信息
    // ============================================================
    public static void main(String[] args) {
        // 方式1:通过子类的 getGenericSuperclass() 获取父类泛型参数
        Type genericSuperclass = Derived.class.getGenericSuperclass();
        System.out.println("genericSuperclass: " + genericSuperclass); // Base<User>
        if (genericSuperclass instanceof ParameterizedType) {
            Class<?> actualType = (Class<?>) ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];
            System.out.println("泛型参数类名: " + actualType.getSimpleName()); // User
        }
        // 对比:Base 自身没指定泛型父类,拿到的就是原始类型
        System.out.println("Base 的父类: " + Base.class.getGenericSuperclass()); // class java.lang.Object
        // 方式2:通过字段的 getGenericType() 获取字段泛型参数
        try {
            Field stringListField = Base.class.getDeclaredField("stringList");
            Type listType = stringListField.getGenericType();
            System.out.println("stringList 泛型: " + listType); // java.util.List<java.lang.String>
            if (listType instanceof ParameterizedType) {
                System.out.println("List 元素类型: " + ((ParameterizedType) listType).getActualTypeArguments()[0]); // class java.lang.String
            }
            Field userMapField = Base.class.getDeclaredField("userMap");
            Type mapType = userMapField.getGenericType();
            System.out.println("userMap 泛型: " + mapType); // java.util.Map<java.lang.Integer, User>
            if (mapType instanceof ParameterizedType) {
                System.out.println("Map Key: " + ((ParameterizedType) mapType).getActualTypeArguments()[0]);   // class java.lang.Integer
                System.out.println("Map Value: " + ((ParameterizedType) mapType).getActualTypeArguments()[1]); // class User
            }
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
        // 方式3:通过方法的 getGenericReturnType() / getGenericParameterTypes() 获取
        try {
            Method getUsers = Base.class.getMethod("getUsers");
            Type returnType = getUsers.getGenericReturnType();
            System.out.println("getUsers 返回值泛型: " + returnType); // java.util.List<User>
            if (returnType instanceof ParameterizedType) {
                System.out.println("返回值元素类型: " + ((ParameterizedType) returnType).getActualTypeArguments()[0]); // User
            }
            Method setNames = Base.class.getMethod("setNames", List.class);
            Type paramType = setNames.getGenericParameterTypes()[0];
            System.out.println("setNames 参数泛型: " + paramType); // java.util.List<java.lang.String>
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

上面是一个获取声明好的泛型参数,这个特性在各个框架中被广泛应用,比如Spring @Autowired

@Service
public class UserService {
    @Autowired
    private List<BaseService> baseServiceList;  
    
    @Autowired
    private List<DerivedService> derivedServiceList; 
}

底层原理类似如下:
public void processAutowired(Object target) throws Exception {
    Class<?> targetClass = target.getClass();
    // 遍历所有字段
    for (Field field : targetClass.getDeclaredFields()) {
        // 检查是否有@Autowired注解
        if (field.isAnnotationPresent(Autowired.class)) {
            // 步骤1:获取字段的完整泛型类型信息(关键!)
            ResolvableType fieldType = ResolvableType.forField(field);
            // 步骤2:根据字段类型查找依赖
            Object dependency = resolveDependency(fieldType, field);
            // 步骤3:注入依赖
            if (dependency != null) {
                field.setAccessible(true);
                field.set(target, dependency);
            }
        }
    }
}

private Object resolveDependency(ResolvableType requiredType, Field field) {
    Class<?> rawType = requiredType.getRawClass();
    if (List.class.isAssignableFrom(rawType)) {
        return resolveListDependency(requiredType);
    }
    // 。。。。处理其他type
}

/**
 * 处理List类型依赖,找到所有类型匹配泛型参数的Bean,组装成List返回
 */
private List<Object> resolveListDependency(ResolvableType listType) {
    // 获取List的泛型参数(第0个)
    ResolvableType elementType = listType.getGeneric(0);
    System.out.println("  解析List<" + elementType.getType() + "> 依赖");
    // 查找所有匹配elementType的Bean
    return beanContainer.values().stream()
            .filter(bean -> isTypeMatch(bean.getClass(), elementType))
            .collect(Collectors.toList());
}

上面的例子虽然展示了如何在运行时获取泛型类型,但是好像漏掉了一种情况,上面都是声明好的泛型,如果没有声明具体泛型的泛型方法呢,这种情况下还是没有办法获取的,因为泛型信息是写在类class中的,运行时才能确定的泛型就没有办法了。

// 比如这种就没有办法在运行时获取
public static <TEMPLATE> TEMPLATE parseObject(String origin, Class<TEMPLATE> clazz) {
    return (TEMPLATE) new Gson().fromJson(origin, clazz);
}

但是在使用Gson序列化的时候,大家应该写过这个类吧 TypeToken吧,或者fastjsonjackson 中的TypeReference。原理都是一样的,既然运行时获取不到,就创建一个能获取到的类进去,在程序真正运行时通过这个类去获取需要的泛型信息。

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.util.List;

public class GsonDemo {
    public static void main(String[] args) throws Exception {
        String json = "[{\"name\":\"张三\"},{\"name\":\"李四\"}]";
        Gson gson = new Gson();
        // 方式1:使用TypeToken匿名内部类
        List<User> users = gson.fromJson(json, new TypeToken<List<User>>() {}.getType());
        // 原理:new TypeToken<List<User>>() {} 创建了匿名子类
        // 该匿名子类的父类是 TypeToken<List<User>>,签名中固定了 List<User>
        System.out.println(users.get(0).getName()); // 张三
        // 方式2:直接传Class无法获取泛型参数(会报错或丢失泛型)
        // List<User> wrong = gson.fromJson(json, List.class); // 编译通过但运行时类型丢失
    }
}

class User {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容