有个朋友是学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吧,或者fastjson 和 jackson 中的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;
}
}