一、包装类
1.1. 基本类型和对应的包装类
Java 共有 8 种基本数据类型,Java 给这些基本类型都提供了一个类进行表示,来对这些类进行一个封装,这就是包装类。
| 基本类型 | 包装类 |
|---|---|
| byte | Byte |
| short | Short |
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
| char | Character |
| boolean | Boolean |
1.2. 装箱和拆箱
在 Java 当中,提供了一些操作,使包装类和内置类型可以相互转换。内置类型转为包装类型称为装箱,包装类型转为内置类型称为拆箱。但这些代码写法已经过时了,我们需要重点掌握的是自动装箱和自动拆箱。
1.3. 自动装箱和自动拆箱
public class Main {
public static void main(String[] args) {
int i = 10;
Integer ii = i; // 自动装箱
Integer ij = (Integer) i; // 自动装箱,后面的 (Integer) 可有可无
int j = ii; // 自动拆箱
int k = (int)ii; // 自动拆箱,后面的 (int) 可有可无
}
}
我们可以通过 javap -c 查看字节码文件内容,观察装箱和拆箱的操作。我们可以在 IDEA 里面装一个 jclasslib ByteCode Viewer 的插件,然后点击 View,再点击 Show Bytecode With Jclasslib。我们点到 main 方法,点击 code,就可以看到所对应的字节码文件。


我们来看下面的一段代码,此时的 a,b,c,d,e 都是引用类型变量。当赋值相同时,结果就是 true;当赋值不同时,结果就是 false。当赋值超出包装类型的范围时,无论赋值相不相等,结果都是 false。
这是因为 Integer 里面的常量值放在常量池当中,我们进行赋值,相当于在常量池中进行取值,如果超出这个值,那么就是池内与池外的进行比较,结果就是 false。
public class Main {
public static void main(String[] args) {
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false
Integer e = 126;
System.out.println(a == e); // false
}
}
二、泛型的概念
很多编程语言都有泛型这样的语法机制。在 Java 中,写一个类或者是方法,需要声明方法里面的成员或者参数的类型。但也有些情况下,需要一个类或者方法能够多种类型支持。也就是一份代码,支持多种数据类型。
三、引出泛型
3.1. 语法规则
class 泛型名称<参数列表> {
// 这里可以使用类型参数;
}
class ClassName<T1,T2,……,Tn> {}
参数列表中要把类中会用的哪些类型列出来,后续使用这个类,创建实例的时候也要同时指定泛型参数的实参。T1,T2 相当于类型的形参。
private T[] arrays = new T[]; // 这种写法是错误的
因为 T 要表示任何类型,new T[] 的时候就可能会涉及到该类的构造方法,T 是什么类型不知道该怎么办?就得先写成 Object[] 再进行强转。
T[] arrays = (T[]) new Object[10];
class MyArray<T> {
T[] arrays = (T[]) new Object[10];
public T get(int index) {
return arrays[index]; // 获取数组的下标
}
public void set(int index, T value) {
arrays[index] = value; // 对数组进行赋值
}
}
// 上面的 T 不用再进行强转了
// 对方法的实现
public class Main {
public static void main(String[] args) {
MyArray<String> array1 = new MyArray<>(); // 里面可以存放字符。代码是灰色的,表示可以不写
MyArray<Integer> array2 = new MyArray<>(); // 里面可以存放整数
MyArray array4 = new MyArray(); // 裸类型,这种写法是不科学的
// MyArray<int> array3 = new MyArray<>(); // error: Type argument cannot be of primitive type
}
}
3.2. 泛型的优点
- 代码重用,一份代码,支持多种类型;
- 自动地进行类型转化,编译过程中会自动触发一些类型检查。
四、类型擦除
4.1. 擦除的机制
Java 的泛型,本质上是通过 Object 类进行编译的。编译器生成代码的时候,自动进行类型转化。比如下面的代码中的 get 方法,我要对 T 转化成 String 类型,编译器从数组中拿到的是一个 Object 类,然后进行自动转化成 String 类,返回到调用位置。在 set 方法里面,set String 进来,编译器再自动把 String 转化成 Object。
class MyArray<T> {
T[] arrays = (T[]) new Object[10];
public T get(int index) {
return arrays[index];
}
public void set(int index, T value) {
arrays[index] = value;
}
}
下面是一段擦除的代码用例
// 擦除前
class MyArray<T> {
public Object[] arrays = new Object[10];
public T getPos(int pos) {
return (T)this.arrays[pos];
}
public void setVal(int pos, T val) {
this.arrays[pos] = val;
}
}
// 擦除后
class MyArray<T> {
public Object[] arrays = new Object[10];
public Object getPos(int pos) {
return this.arrays[pos];
}
public void setVal(int pos, T val) {
this.arrays[pos] = val;
}
}
五、泛型的上界
5.1. 泛型的上界的定义
描述的是使用泛型,创建泛型实例的时候,传入的参数(类型实参)需要满足什么条件。
5.2. 语法规则
class 泛型名称<类型实参 extends 类型边界> {}
这个类型边界相当于是'父类',后续创建类型实例的类型参数,必须是这个父类的子类。比如我们要写一个算术运算的泛型类,泛型参数必须给数字。
class MyArray<E extends Number> {
MyArray<Integer> l1; // 正常
MyArray<String> l2; // 错误
}
如果没有指定类型边界 E,可以视为 E extends Object。
六、泛型方法
6.1. 定义语法
方法限定符 <类型形参列表> 返回值类型 方法名称 {}
6.2. 交换方法的实例
public class Main {
// 静态的泛型方法需要在 static 后面用<>声明泛型类型参数
public static <E> void swap(E[] array, int i, int j) {
E t = array[i];
array[i] = array[j];
array[j] = t;
}
}
七、通配符
前面的知识都是在定义泛型时涉及到的,通配符是针对泛型实例化的时候涉及到的。
class MyClass<T> {}
public class Main {
public static void main(String[] args) {
MyClass<Integer> obj1 = new MyClass<>();
MyClass<String> obj2 = new MyClass<>();
MyClass<Integer> obj3 = new MyClass<>();
obj1 = obj3; // 正常
obj1 = obj2; // 错误
}
}
因为 Obj1 与 Obj2 类型不相同,所以会报错。那我们能否创建一种引用,能够指向多种泛型参数的对象呢?这时就要用到通配符了。
MyClass<?> obj4 = obj3;
MyClass<? extends Number> obj5 = obj1;
obj5 = obj2; // 这个代码不符合要求,约定 obj5 的通配符,只能匹配到 Number 和它的子类;
// 因为通配符只能在泛型实例化时使用
这里的代码不要和泛型的上界搞混。我们除了可以指定父类,还能指定子类。
MyClass<? super Integer> obj6 = obj1; // 此处的通配符只能匹配到 Integer 和它的父类
obj6 = new MyClass<double>(); // double 并不是 Integer 的子类,所以会报错
obj6 = new MyClass<Number>(); // Number 是 Integer 的父类。


