本文是 Java 集合框架前置知识的整理笔记,覆盖包装类、包装类的自动装箱/拆箱机制,以及泛型的语法、擦除机制、上界与通配符。整理自课程讲义《3. 包装类&泛型》,代码做了格式化与勘误,并补充了讲义中留作思考的面试题答案。
本节目标
- 以能阅读 Java 集合源码为目标学习泛型
- 了解包装类
- 了解泛型
一、包装类
1.1 基本数据类型和对应的包装类
在 Java 中,由于基本类型不是继承自 Object,为了在泛型代码中可以支持基本类型,Java 给每个基本类型都对应了一个包装类型。
| 基本数据类型 | 包装类 |
|---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
除了 Integer 和 Character,其余基本类型的包装类都是首字母大写。
1.2 装箱和拆箱
装箱:把基本类型的数据放进一个包装类对象里。
拆箱:把包装类对象里的值取出来,还原成基本数据类型。
int i = 10;
// 装箱操作:新建一个 Integer 类型对象,将 i 的值放入对象的某个属性中
Integer ii = Integer.valueOf(i);
Integer ij = new Integer(i);
// 拆箱操作:将 Integer 对象中的值取出,放到一个基本数据类型中
int j = ii.intValue();
勘误与提醒:
new Integer(i)这种写法从 Java 9 起已被标记为废弃(deprecated),永远应该使用Integer.valueOf(i)。原因是valueOf会复用缓存对象,而new每次都强制创建新对象。
1.3 自动装箱和自动拆箱
可以看到,使用过程中装箱和拆箱会带来不少代码量。为了减少开发者的负担,Java 提供了自动机制。
int i = 10;
Integer ii = i; // 自动装箱
Integer ij = (Integer) i; // 自动装箱
int j = ii; // 自动拆箱
int k = (int) ii; // 自动拆箱
自动装箱/拆箱是编译期的语法糖,本质仍然是调用 valueOf 和 xxxValue()。可以通过 javap -c 查看字节码文件内容,观察装箱和拆箱的操作。
1.4【面试题】下面代码输出什么,为什么?
public static void main(String[] args) {
Integer a = 127;
Integer b = 127;
Integer c = 128;
Integer d = 128;
System.out.println(a == b); // true
System.out.println(c == d); // false
}
答案:a == b 为 true,c == d 为 false。
原因:Integer 内部维护了一个静态的整数缓存数组 IntegerCache,默认缓存范围是 -128 ~ 127。
a和b都是 127,在缓存范围内,Integer.valueOf(127)直接返回缓存中的同一个对象,所以==比较引用相等,结果为true。c和d都是 128,超出了缓存范围,valueOf会各自new一个对象,两个引用不同,所以结果为false。
想改变上界可以设置 JVM 参数 -XX:AutoBoxCacheMax=<size>(只影响上界,下界固定为 -128)。
结论:包装类比较值的时候,请用
equals(),不要用==。
二、泛型
2.1 什么是泛型
一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。
—— 来源:《Java 编程思想》对泛型的介绍
泛型是在 JDK 1.5 引入的新语法。通俗讲,泛型就是适用于许多许多类型。从代码上讲,就是对类型实现了参数化。
2.2 引出泛型:一个"万能数组"的困境
需求:实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值。
既然所有类的父类默认都是 Object,那我们能不能把数组声明为 Object[]?
class MyArray {
public Object[] array = new Object[10];
public Object getPos(int pos) {
return this.array[pos];
}
public void setVal(int pos, Object val) {
this.array[pos] = val;
}
}
public class TestDemo {
public static void main(String[] args) {
MyArray myArray = new MyArray();
myArray.setVal(0, 10);
myArray.setVal(1, "hello"); // 字符串也可以存放
String ret = myArray.getPos(1); // 编译报错
System.out.println(ret);
}
}
问题:以上代码实现后发现
- 任何类型数据都可以存放
- 1 号下标本身就是字符串,但是却编译报错,必须进行强制类型转换
虽然在这种情况下,当前数组任何数据都可以存放,但是更多情况下,我们还是希望它只能够持有一种数据类型,而不是同时持有这么多类型。所以:
泛型的主要目的:就是指定当前的容器要持有什么类型的对象,让编译器去做检查。 此时就需要把类型作为参数传递,需要什么类型,就传入什么类型。
2.3 泛型类的语法
基础写法:
class 泛型类名称<类型形参列表> {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> {
}
其他写法(带继承):
class 泛型类名称<类型形参列表> extends 继承类 /* 这里可以使用类型参数 */ {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}
把上面的 MyArray 改写为泛型版本:
class MyArray<T> {
public Object[] array = new Object[10];
public T getPos(int pos) {
return (T) this.array[pos];
}
public void setVal(int pos, T val) {
this.array[pos] = val;
}
}
public class TestDemo {
public static void main(String[] args) {
MyArray<Integer> myArray = new MyArray<>(); // 1
myArray.setVal(0, 10);
myArray.setVal(1, 12);
int ret = myArray.getPos(1); // 2
System.out.println(ret);
myArray.setVal(2, "bit"); // 3
}
}
代码解释:
- 类名后的
<T>代表占位符,表示当前类是一个泛型类 - 【规范】类型形参一般使用一个大写字母表示,常用名称有:
E表示 ElementK表示 KeyV表示 ValueN表示 NumberT表示 TypeS、U、V等等 —— 第二、第三、第四个类型
- 注释 1 处,类型后加入
<Integer>指定当前类型 - 注释 2 处,不需要进行强制类型转换
- 注释 3 处,代码编译报错。因为在注释 2 处已经指定了类当前的类型,所以编译器会在存放元素的时候帮助我们进行类型检查
2.4 泛型类的使用
语法:
泛型类<类型实参> 变量名; // 定义一个泛型类引用
new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象
示例:
MyArray<Integer> list = new MyArray<Integer>();
注意:泛型只能接受类,所有的基本数据类型必须使用包装类!
类型推导(Type Inference):
当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写。
MyArray<Integer> list = new MyArray<>(); // 可以推导出实例化需要的类型实参为 Integer
2.5 裸类型(Raw Type)(了解)
裸类型是一个泛型类但没有带着类型实参,例如 MyArray 就是一个裸类型。
MyArray list = new MyArray();
注意:我们不要自己去使用裸类型。 裸类型是为了兼容老版本的 API 而保留的机制。下面的类型擦除部分也会讲到,编译器是如何使用裸类型的。
2.6 泛型如何编译:擦除机制
那么,泛型到底是怎么编译的?
1. 基本概念
- 在编译时,Java 编译器会将泛型类型信息从代码中移除,这个过程就叫做类型擦除。
- 擦除后,泛型类型会被替换为其边界类型(通常是
Object)或者指定的类型。
2. 擦除过程
- 将泛型参数替换为其边界或
Object - 在必要的地方插入类型转换以保持类型安全
- 生成桥接方法以保持多态性
3. 示例
擦除前:
class MyArray<T> {
public Object[] array = new Object[10];
public T getPos(int pos) {
return (T) this.array[pos];
}
public void setVal(int pos, T val) {
this.array[pos] = val;
}
}
擦除后:
class MyArray {
public Object[] array = new Object[10];
public Object getPos(int pos) {
return this.array[pos];
}
public void setVal(int pos, Object val) {
this.array[pos] = val;
}
}
重要推论:因为擦除后
T变成了Object,所以泛型里不能new T[]、不能new T(),静态方法中也不能直接使用类的类型形参。
2.7 关于桥接方法
- 泛型类型擦除可能导致子类方法和父类方法的签名不一致
- 为了维护 Java 的多态性,需要桥接方法来确保子类方法能够正确覆盖父类方法
public class Node<T> {
T data;
public void setData(T data) {
this.data = data;
}
}
public class StringNode extends Node<String> {
@Override
public void setData(String data) {
super.setData(data);
}
}
类型擦除后代码变为:
public class Node {
Object data;
public void setData(Object data) {
this.data = data;
}
}
public class StringNode extends Node {
public void setData(String data) {
super.setData(data);
}
}
此时 StringNode 的 setData 方法并没有真正覆盖父类的 setData 方法(参数类型不同)。为了解决这个问题,编译器会在 StringNode 中生成一个桥接方法:
// 编译器生成的桥接方法
public void setData(Object data) {
setData((String) data);
}
2.8 泛型的上界
在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。
语法:
class 泛型类名称<类型形参 extends 类型边界> {
...
}
示例:
public class MyArray<E extends Number> {
...
}
只接受 Number 的子类型作为 E 的类型实参。
MyArray<Integer> l1; // 正常,因为 Integer 是 Number 的子类型
MyArray<String> l2; // 编译错误,因为 String 不是 Number 的子类型
了解:没有指定类型边界
E,可以视为E extends Object。
复杂示例:
public class MyArray<E extends Comparable<E>> {
...
}
E必须是实现了Comparable接口的
2.9 泛型方法
定义语法:
方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) { ... }
示例:
public class Util {
// 静态的泛型方法,需要在 static 后用 <> 声明泛型类型参数
public static <E> void swap(E[] array, int i, int j) {
E t = array[i];
array[i] = array[j];
array[j] = t;
}
}
使用示例 —— 可以类型推导:
Integer[] a = { ... };
swap(a, 0, 9);
String[] b = { ... };
swap(b, 0, 9);
使用示例 —— 不使用类型推导:
Integer[] a = { ... };
Util.<Integer>swap(a, 0, 9);
String[] b = { ... };
Util.<String>swap(b, 0, 9);
三、通配符
? 用于在泛型的使用中,即为通配符。
3.1 通配符解决什么问题
示例:
package www.bit.java.test;
class Message<T> {
private T message;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class TestDemo {
public static void main(String[] args) {
Message<String> message = new Message<>();
message.setMessage("比特就业课欢迎您");
fun(message);
}
public static void fun(Message<String> temp) {
System.out.println(temp.getMessage());
}
}
以上程序会带来新的问题:如果现在泛型的类型设置的不是 String,而是 Integer:
public class TestDemo {
public static void main(String[] args) {
Message<Integer> message = new Message();
message.setMessage(99);
fun(message); // 出现错误,只能接收 String
}
public static void fun(Message<String> temp) {
System.out.println(temp.getMessage());
}
}
我们需要的解决方案:可以接收所有的泛型类型,但是又不能够让用户随意修改。这种情况就需要使用通配符 ? 来处理。
范例:使用通配符
public class TestDemo {
public static void main(String[] args) {
Message<Integer> message = new Message<>();
message.setMessage(55);
fun(message);
}
// 此时使用通配符 "?" 描述的是它可以接收任意类型
public static void fun(Message<?> temp) {
System.out.println(temp.getMessage());
}
}
在 ? 的基础上又产生了两个子通配符:
? extends 类:设置通配符上限? super 类:设置通配符下限
3.2 通配符上界
语法:
<? extends 上界>
<? extends Number> // 可以传入的实参类型是 Number 或者 Number 的子类
示例:
class Food {
}
class Fruit extends Food {
}
class Apple extends Fruit {
}
class Banana extends Fruit {
}
class Plate<T> { // 设置泛型
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
public class TestDemo {
public static void main(String[] args) {
Plate<Apple> plate = new Plate<>();
plate.setData(new Apple());
fun(plate);
Plate<Banana> plate2 = new Plate<>();
plate2.setData(new Banana());
fun(plate2);
}
// 此时使用通配符 "?" 描述的是它可以接收任意类型,但是需要是 Fruit 或者 Fruit 的子类
public static void fun(Plate<? extends Fruit> temp) {
// temp.setData(new Banana()); // error
// temp.setData(new Apple()); // error
System.out.println(temp.getData());
}
}
此时无法在 fun 函数中对 temp 进行添加元素,因为 temp 接收的是 Fruit 和它的子类,此时存储的元素应该是哪个子类无法确定,所以添加会报错!
记忆要点:上界通配符
? extends T—— 能读不能写。
3.3 通配符下界
语法:
<? super 下界>
<? super Integer> // 代表可以传入的实参的类型是 Integer 或者 Integer 的父类类型
示例:
class Food {
}
class Fruit extends Food {
}
class Apple extends Fruit {
}
class Plate<T> {
private T plate;
public T getData() {
return plate;
}
public void setData(T plate) {
this.plate = plate;
}
}
public class TestDemo {
public static void main(String[] args) {
Plate<Fruit> plate1 = new Plate<>();
plate1.setData(new Fruit());
fun(plate1);
Plate<Food> plate2 = new Plate<>();
plate2.setData(new Food());
fun(plate2);
}
public static void fun(Plate<? super Fruit> temp) {
// 此时可以修改!!添加的是 Fruit 或者 Fruit 的子类
temp.setData(new Apple()); // 这个是 Fruit 的子类
temp.setData(new Fruit()); // 这个是 Fruit 本身
// Fruit fruit = temp.getData(); 不能接收,这里无法确定是哪个父类
System.out.println(temp.getData()); // 只能直接输出
}
}
记忆要点:下界通配符
? super T—— 能写不能读(读出来只能当Object)。
3.4 补充:PECS 原则
Java 泛型通配符的选取可以记成 PECS:Producer Extends, Consumer Super。
- 如果这个容器是生产者(你主要从它读数据),用
? extends T - 如果这个容器是消费者(你主要往它写数据),用
? super T
四、小结
- 包装类:每个基本类型都有对应包装类;除
Integer和Character外都是首字母大写。 - 装箱拆箱:是编译期语法糖;包装类的值比较请用
equals();Integer缓存范围为 -128 ~ 127。 - 泛型:将数据类型参数化,进行传递;使用
<T>表示当前类是一个泛型类。 - 泛型的优点:数据类型参数化,编译时自动进行类型检查和转换。
- 类型擦除:编译期泛型信息被擦除,
T被替换为边界类型或Object,并可能生成桥接方法维持多态。 - 上界:
<E extends Number>约束类型实参必须是Number的子类型。 - 通配符:
?表示任意类型,? extends是上界(能读不能写),? super是下界(能写不能读)。
本文整理自课程讲义《3. 包装类&泛型》,代码已重新排版并做了少量勘误与补充说明,仅供个人学习记录使用。
3432



被折叠的 条评论
为什么被折叠?



