最近用Java刷算法题的过程中, 很多地方用到了泛型的知识. 快忘完了正好借此机会重新熟悉一下, 在脑子里面建立对泛型的理解.
一、泛型的编译期需要知道的地方
1.1 泛型类型约束机制
泛型的本质是编译器对类型的约束,通过在声明时指定具体类型,编译器会强制检查所有操作是否符合该类型约束。
示例代码:
java
// 声明泛型类型为Integer
List<Integer> list = new ArrayList<>();
// 编译器会检查add方法的参数类型
list.add(100); // 合法:参数为Integer类型
list.add("hello"); // 编译错误:参数必须为Integer类型
// 编译器自动插入类型转换代码
Integer value = list.get(0); // 等价于 (Integer)list.get(0)
1.2 类型擦除后的底层实现
带泛型的对象在实际创建时所有泛型参数都会被替换为Object或上界类型, 如下所示:
java
// 我们编写完的泛型类长这样
public class Box<T> {
private T value;
public T get() { return value; }
public void set(T value) { this.value = value; }
}
// 实际创建对象时, 对象在内存中实际长这样
public class Box {
private Object value;
public Object get() { return value; }
public void set(Object value) { this.value = value; }
}
1.3 菱形语法的类型推断
Java 7引入的菱形语法(<>
)允许省略右侧泛型声明,编译器会根据左侧类型自动推断。
示例代码:
java
// 完整写法(Java 6及以前)
List<String> list1 = new ArrayList<String>();
// 简化写法(Java 7+)
List<String> list2 = new ArrayList<>(); // 自动推断为String类型
// 复杂类型推断示例
Map<Integer, List<String>> map = new HashMap<>();
map.put(1, List.of("a", "b"));
二、JVM层面对理解泛型
2.1 引用类型内存一致性
所有引用变量在内存中占用相同大小的存储空间(64位JVM通常为8字节),因此JVM在内存中创建的对象虽然都一样, 但是给泛型赋上不同类型后还是能正常存储就是这个原因.
内存分配示意图:
java
// 泛型类定义
class Pair<T, U> {
private T first; // 引用类型,占8字节
private U second; // 引用类型,占8字节
}
// 实例化不同类型的Pair
Pair<String, Integer> p1 = new Pair<>();
Pair<Double, List> p2 = new Pair<>();
// 内存布局(简化)
// p1: [first引用(8B) | second引用(8B)]
// p2: [first引用(8B) | second引用(8B)]
2.2 这也解释了为什么泛型T不能是基本类型
泛型参数不能是基本类型,因为基本类型的存储大小不一致(如int占4字节,long占8字节),无法用统一的内存布局处理, 如下假如可以放入int类型的值, JVM在内存中创建的是8字节的数组, 那放进去就会出问题.
错误示例:
java
List<int> errorList = new ArrayList<>(); // 编译错误:不能使用基本类型
// 必须使用包装类
List<Integer> correctList = new ArrayList<>();
2.3 自动装箱与拆箱
使用包装类时,编译器会自动处理基本类型与包装类型之间的转换。
示例代码:
java
List<Integer> list = new ArrayList<>();
list.add(10); // 自动装箱:int → Integer
int value = list.get(0); // 自动拆箱:Integer → int
// 等价的手动写法
list.add(Integer.valueOf(10));
int value = list.get(0).intValue();
三、总结
等号右边创建的均是一个T->object的对象, 我们用的时候只要关注右边是什么对象就行, 不用考虑那么多;
等号左边声明的是一个T->特定类型的引用变量, 编译器在编译时起到了保安的作用, 会检查你输入的元素是否是这个特定类型.
所以见到泛型和用泛型的时候, 例如:
Java
List<Integer> list = new ArrayList<>();
我们的理解应该是, 左边是一个List类型的引用变量, 右边是ArrayList类型的对象, 大类的引用变量接小类的对象. 此外现在还加上了<Integer>
, 代表这个List引用只能加入Integer的类型的元素.