Java_包装类与泛型笔记
Java 包装类与泛型复习笔记
1. 学习目标
- 掌握基本数据类型与包装类的关系。
- 理解装箱、拆箱、自动装箱、自动拆箱。
- 理解泛型的作用、语法和使用方式。
- 掌握泛型类、泛型方法、泛型上界的基本写法。
- 理解泛型擦除机制以及不能直接创建泛型数组的原因。
2. 包装类
2.1 为什么需要包装类
Java 的基本数据类型不是 Object 的子类,不能直接作为泛型类型实参使用。为了让基本数据类型也能参与面向对象和泛型相关的代码,Java 为每个基本数据类型提供了对应的包装类。
2.2 基本数据类型与包装类对应关系
| 基本数据类型 | 包装类 |
|---|---|
byte |
Byte |
short |
Short |
int |
Integer |
long |
Long |
float |
Float |
double |
Double |
char |
Character |
boolean |
Boolean |
记忆点:除了 Integer 和 Character,其余包装类基本都是基本类型首字母大写。
3. 装箱与拆箱
3.1 装箱
装箱是指把基本数据类型转换成对应的包装类对象。
int i = 10;
Integer ii = Integer.valueOf(i); // 推荐写法
Integer ij = new Integer(i); // 旧写法,不推荐
3.2 拆箱
拆箱是指把包装类对象中的值取出来,转换成基本数据类型。
Integer ii = Integer.valueOf(10);
int j = ii.intValue();
3.3 自动装箱与自动拆箱
为了减少手动转换的代码量,Java 提供自动装箱和自动拆箱。
int i = 10;
Integer ii = i; // 自动装箱,相当于 Integer.valueOf(i)
Integer ij = (Integer)i; // 自动装箱
int j = ii; // 自动拆箱,相当于 ii.intValue()
int k = (int)ii; // 自动拆箱
自动装箱和自动拆箱只是编译器帮忙补充了转换代码,本质上仍然会调用相关方法,例如:
- 自动装箱:
Integer.valueOf(...) - 自动拆箱:
xxx.intValue()、xxx.doubleValue()等
4. 面试题:Integer 比较问题
4.1 题目
public static void main(String[] args) {
Integer a = 127;
Integer b = 127;
Integer c = 128;
Integer d = 128;
System.out.println(a == b);
System.out.println(c == d);
}
4.2 输出结果
true
false
4.3 原因
Integer a = 127; 会触发自动装箱,本质上调用的是:
Integer.valueOf(127);
Integer.valueOf 对 -128 ~ 127 范围内的整数有缓存。这个范围内的值会复用已有对象,所以:
a == b
比较的是同一个缓存对象的地址,结果是 true。
而 128 超出了默认缓存范围,通常会创建新的 Integer 对象,所以:
c == d
比较的是两个不同对象的地址,结果是 false。
重要结论:
==比较引用类型时,比较的是地址。- 包装类数值比较建议使用
equals。 - 不要依赖包装类缓存范围写业务逻辑。
System.out.println(c.equals(d)); // true
5. 什么是泛型
泛型是 JDK 1.5 引入的语法。通俗理解:泛型就是让类、接口、方法可以适用于多种类型。
从代码角度看,泛型就是把数据类型参数化。
例如:
MyArray<Integer> list = new MyArray<>();
MyArray<String> list2 = new MyArray<>();
同一个 MyArray 类,可以通过传入不同的类型实参,让它分别保存 Integer、String 等不同类型的数据。
6. 为什么要引入泛型
6.1 使用 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);
}
}
问题:
Object[]可以保存任何类型,类型约束太弱。- 取出元素时返回的是
Object,需要强制类型转换。 - 编译器无法提前检查类型错误,容易把问题留到运行时。
正确写法需要强转:
String ret = (String) myArray.getPos(1);
但强转存在风险,如果取出的对象不是 String,运行时会发生 ClassCastException。
6.2 泛型解决的问题
泛型的主要目的:指定当前容器持有什么类型的对象,让编译器在编译阶段进行类型检查。
泛型的优势:
- 数据类型参数化。
- 编译时自动类型检查。
- 取数据时减少强制类型转换。
- 代码复用性更好。
- 类型错误更早暴露。
7. 泛型类
7.1 基本语法
class 泛型类名称<类型形参列表> {
// 可以使用类型参数
}
示例:
class ClassName<T1, T2, ..., Tn> {
}
泛型类也可以继承其他类,并在继承关系中使用类型参数:
class 泛型类名称<类型形参列表> extends 继承类 {
// 可以使用类型参数
}
示例:
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}
7.2 常见类型形参命名规范
| 类型形参 | 常见含义 |
|---|---|
E |
Element,元素 |
K |
Key,键 |
V |
Value,值 |
N |
Number,数字 |
T |
Type,类型 |
S、U、V |
第二、第三、第四个类型 |
这些命名不是强制要求,但遵守规范可以提高代码可读性。
7.3 泛型类示例
class MyArray<T> {
public T[] array = (T[]) new Object[10];
public T getPos(int pos) {
return 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<>();
myArray.setVal(0, 10);
myArray.setVal(1, 12);
int ret = myArray.getPos(1);
System.out.println(ret);
myArray.setVal(2, "bit"); // 编译报错
}
}
分析:
MyArray<Integer>指定当前容器只能保存Integer类型。myArray.getPos(1)返回的是Integer,可以自动拆箱为int。myArray.setVal(2, "bit")试图放入String,编译器会直接报错。
8. 泛型类的使用
8.1 基本语法
泛型类<类型实参> 变量名; // 定义泛型类引用
new 泛型类<类型实参>(构造方法实参); // 实例化泛型类对象
示例:
MyArray<Integer> list = new MyArray<Integer>();
8.2 类型推导
当编译器可以从上下文推导出类型实参时,实例化对象时可以省略右侧尖括号中的类型。
MyArray<Integer> list = new MyArray<>();
这里右侧的 <> 称为钻石操作符,编译器可以推导出它是 Integer。
8.3 泛型不能直接使用基本数据类型
泛型只能接收类类型,不能接收基本数据类型。
错误写法:
MyArray<int> list = new MyArray<>(); // 错误
正确写法:
MyArray<Integer> list = new MyArray<>();
原因:泛型需要使用引用类型,基本数据类型要使用对应的包装类。
9. 裸类型 Raw Type
9.1 什么是裸类型
裸类型是指使用泛型类时没有传入类型实参。
MyArray list = new MyArray();
这里的 MyArray 就是裸类型。
9.2 为什么不建议使用裸类型
裸类型主要是为了兼容老版本 API 保留的机制,不建议在新代码中主动使用。
裸类型的问题:
- 失去泛型的类型检查能力。
- 取数据时通常需要强制类型转换。
- 更容易出现运行时类型转换异常。
推荐写法:
MyArray<Integer> list = new MyArray<>();
10. 泛型的编译机制:类型擦除
10.1 什么是类型擦除
Java 泛型主要在编译阶段发挥作用。编译器会进行类型检查,生成字节码后,运行期间通常不保留具体的泛型类型信息。
在没有指定类型边界时,类型参数 T 通常会被擦除为 Object。
例如:
class MyArray<T> {
public T[] array;
public T getPos(int pos) {
return this.array[pos];
}
public void setVal(int pos, T val) {
this.array[pos] = val;
}
}
编译后,T 会被擦除,字节码层面更接近于使用 Object。
通过 javap -c 查看字节码时,可以看到字段或方法签名中出现 java/lang/Object 相关信息。
10.2 类型擦除的核心结论
- 泛型检查主要发生在编译期。
- 运行期间通常没有完整的泛型类型信息。
- 没有指定边界时,类型参数通常擦除为
Object。 - 指定上界时,类型参数会擦除为对应的上界类型。
示例:
class MyArray<T> {
}
T 默认可以理解为:
T extends Object
如果写成:
class MyArray<E extends Number> {
}
那么 E 会按照 Number 这个上界进行处理。
11. 为什么不能直接创建泛型数组
11.1 错误写法
T[] ts = new T[5]; // 错误
原因:泛型存在类型擦除,运行时无法准确知道 T 到底代表什么具体类型,因此不能直接创建 T[]。
11.2 常见但有风险的写法
class MyArray<T> {
public T[] array = (T[]) new Object[10];
public T getPos(int pos) {
return this.array[pos];
}
public void setVal(int pos, T val) {
this.array[pos] = val;
}
public T[] getArray() {
return array;
}
}
使用:
public static void main(String[] args) {
MyArray<Integer> myArray1 = new MyArray<>();
Integer[] integers = myArray1.getArray();
}
可能出现异常:
Exception in thread "main" java.lang.ClassCastException:
[Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
原因:
array实际上是通过new Object[10]创建的。- 返回时试图把
Object[]当作Integer[]使用。 - 运行时数组类型不匹配,所以可能发生
ClassCastException。
11.3 更安全的处理方式
方式一:返回 Object[]。
public Object[] getArray() {
return array;
}
方式二:通过反射创建指定类型的数组。
import java.lang.reflect.Array;
class MyArray<T> {
public T[] array;
public MyArray() {
}
public MyArray(Class<T> clazz, int capacity) {
array = (T[]) Array.newInstance(clazz, capacity);
}
public T getPos(int pos) {
return this.array[pos];
}
public void setVal(int pos, T val) {
this.array[pos] = val;
}
public T[] getArray() {
return array;
}
}
使用:
public static void main(String[] args) {
MyArray<Integer> myArray1 = new MyArray<>(Integer.class, 10);
Integer[] integers = myArray1.getArray();
}
这种方式通过 Integer.class 明确告诉程序要创建什么类型的数组。
12. 泛型上界
12.1 为什么需要泛型上界
有时候不能让泛型接收任意类型,而是需要限制类型实参的范围。例如,只希望某个泛型类接收 Number 或 Number 的子类。
这时可以使用泛型上界。
12.2 基本语法
class 泛型类名称<类型形参 extends 类型边界> {
// ...
}
注意:这里使用的是 extends,不仅可以表示继承某个类,也可以表示实现某个接口。
12.3 示例:限制为 Number 的子类
public class MyArray<E extends Number> {
// ...
}
使用:
MyArray<Integer> l1; // 正确,Integer 是 Number 的子类
MyArray<String> l2; // 编译错误,String 不是 Number 的子类
编译错误含义:类型实参 String 不在类型变量 E 的边界范围内。
12.4 默认上界
如果没有显式指定类型边界:
class MyArray<E> {
}
可以理解为:
class MyArray<E extends Object> {
}
12.5 示例:限制必须实现 Comparable 接口
public class MyArray<E extends Comparable<E>> {
// ...
}
含义:E 必须具备和同类型对象进行比较的能力。
这种写法常见于需要比较大小、排序、查找最大值或最小值的场景。
13. 泛型方法
13.1 基本语法
方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) {
// ...
}
示例:
public static <E> void swap(E[] array, int i, int j) {
E t = array[i];
array[i] = array[j];
array[j] = t;
}
注意:静态泛型方法需要在 static 后面使用 <E> 声明泛型类型参数。
完整示例:
public class Util {
public static <E> void swap(E[] array, int i, int j) {
E t = array[i];
array[i] = array[j];
array[j] = t;
}
}
13.2 使用类型推导调用泛型方法
Integer[] a = {1, 2, 3, 4, 5};
Util.swap(a, 0, 4);
String[] b = {"a", "b", "c"};
Util.swap(b, 0, 2);
编译器可以根据数组类型自动推导 E 的具体类型。
13.3 不使用类型推导调用泛型方法
Integer[] a = {1, 2, 3, 4, 5};
Util.<Integer>swap(a, 0, 4);
String[] b = {"a", "b", "c"};
Util.<String>swap(b, 0, 2);
这种写法显式指定了泛型方法的类型实参,一般不常写,除非编译器无法推导或为了强调类型。
14. 易错点总结
14.1 泛型不能使用基本数据类型
错误:
MyArray<int> list = new MyArray<>();
正确:
MyArray<Integer> list = new MyArray<>();
14.2 不能直接创建泛型数组
错误:
T[] array = new T[10];
可替代:
T[] array = (T[]) new Object[10]; // 有风险
更安全:
array = (T[]) Array.newInstance(clazz, capacity);
14.3 包装类比较不要随便使用 ==
不推荐:
Integer a = 128;
Integer b = 128;
System.out.println(a == b); // false
推荐:
System.out.println(a.equals(b)); // true
14.4 不建议使用裸类型
不推荐:
MyArray list = new MyArray();
推荐:
MyArray<Integer> list = new MyArray<>();
14.5 泛型只在编译期做主要检查
泛型的类型安全主要由编译器保证。由于类型擦除,运行期间通常无法直接获得完整泛型类型信息。
15. 核心知识速记
- 包装类用于让基本数据类型以对象形式参与泛型和面向对象编程。
- 自动装箱本质是调用包装类的
valueOf方法。 - 自动拆箱本质是调用包装类的
xxxValue方法。 Integer在-128 ~ 127范围内通常会复用缓存对象。- 泛型的本质是数据类型参数化。
- 泛型可以让编译器提前进行类型检查。
- 泛型可以减少强制类型转换。
- 泛型类型实参必须是引用类型,不能是基本数据类型。
- 裸类型是为了兼容旧版本代码保留的机制,不建议主动使用。
- Java 泛型通过类型擦除实现。
- 没有上界时,类型参数默认上界是
Object。 - 有上界时,类型参数会被限制在指定类型或其子类型范围内。
T[] array = new T[10]不合法,因为运行时无法确定T的真实类型。- 泛型方法的类型参数写在返回值类型前面。
更多推荐
所有评论(0)