一、包装类:

Java是一种面向对象语言,所有的内容都是对象,即使是Application入口main函数,也是需要封装在一个类内部。但是尽管如此java语言内置还是提供了8种基本的数据类型,不过这8种基本类型,不属于类。因此在很场景下无法直接使用。比如集合类以及泛型编程等。故此JDK标准库,针对这8种基本类型,提供了对应的封装类型。
如图:

基本数据类型 中文名称 长度(字节) 取值范围 包装器类型

boolean | 布尔类型 | 未定 | true/false | Boolean
char | 字符类型 | 2 | 描述UTF-16编码中的一个代码单元 | Character
byte | 字节类型 | 1 | -128~127 | Byte
short| 数值类型 | 2 | -32768~32768 | Short
int | 数值类型 | 4 | -2147483648~
2147483647
(超过20亿) | Integer
long | 数值类型 | 8 | -92233720368
54775808~
922337203
6854775807 | Long
float|数值类型|1|大约+/-3.402823
47E+38f
(有效位数为6~7位|Float
double|数值类型|1|大约+/-1.7976931348
6231570E+308
(有效位数15位)|Double

  • 注 char类型中描述的代码单元和代码点,单独文章分析 *

    相关包装类型的UML图如下:

由图可见,8种基本类型,除了Character和Boolean外,其他的数值类型,都继承自抽象类Number。
并且他们都实现了java.io.Serivalizable和java.lang.Compare接口。这两个接口提供了序列化以及比较功能。

这里需要关注Number抽象类的几个方法:

1
2
3
4
5
6
public abstract int intValue();
public abstract long longValue();
public abstract float floatValue();
public abstract double doubleValue();
public abstract byte doubleValue();
public abstract short shortValue();

和包装类各自实现的方法:
1
valueOf

这些方法会应用在接下来小节阐述的装箱和拆箱中。

二、装箱和拆箱:

包装类的存在是为了弥补基本类型的非对象属性,使得在面向对象的场景中,基本类型也能通过包装以后能使用。比如集合类和泛型编程场景下,基本类型是无法直接使用的,必须转换为对象;但是Java并没有提供运算符重载,包装类不支持数学运算,因此在需要基本类型的场景下,需要把包装类包含的基本类型值,拆解出来。这过程就是“装箱”和“拆箱”。

装箱:就是把基本类型封装到其对应的类对象类型中;
拆箱:就是把类对象类型拆解称为基本类型。

装箱和拆箱是分别通过对应的类对象方法进行操作。在JDK1.5之前,这些操作需要手工完成,比如把整数1封装给Integer对象:Integer I = new Integer(1);而拆箱则需要调用方法:int i = Integer.valueOf(I)。而在JDK1.5之后,这一过程自动由编译器完成,这个过程叫做 “自动装箱”和 “自动拆箱”。装箱的时候可以直接:Integer I = 1; 编译器会自动调用方法构造一个Integer对象,并且初始化为1;拆箱的时候可以直接:int i = I; 编译器自动调用Inteter.intValue()把对象转换为基本类型。比如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
1 public class TestBoxAndUnBoxing01 {
2 public static void main(String[] args) {
3 Integer I = 1;
4 int i = I;
5 int j = I + 2;
6 Integer J = j;
7
8 System.out.println(i);
9 System.out.println(I);
10 System.out.println(j);
11 System.out.println(J);
12 }
13 }

编译后通过
1
javap -c TestBoxAndUnBoxing01

可以看到以下内容:

通过图片可以看出,自动装箱是通过调用Integer.valueOf方法完成,在以上代码存在两次需要进行装箱的操作,分别是第3行赋值、第6行赋值;自动拆箱则是通过Integer.intValue()方法完成,以上代码存在2次需要拆箱的,分别是第4行赋值和第5行的加法运算,对于第5行的加法运算,先是进行拆箱,然后执行数学运算,最后把结果赋值给变量j。

三、装箱缓存:

通过以上类图我们还可以看到Character、Long、Integer、Short、Byte五个类型都分别有一个内部静态类*Cache。那么这些Cache是用来做什么呢?
通过阅读分析JDK源码,发现这些Cache类对于-128~127之间的基本类型,在各自包装类内部都是通过初始化一个对应的对象数组,然后初始化为-128~127之间的值。比如对于Integer类,new一个256个元素的Integer数组,然后分别初始化为-128~127。同样对于Long就new一个256个元素的Long数组,分别初始化为-128~127。接着在进行自动装箱的过程中,判断对应的数值大小,当对应的数值大小介于-128~127之间,则直接使用该缓存中已经建立好的对象,只有超出该范围,才重新new一个对象。

这样做的目的其实就是为了提高效率。

我们不妨看看以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestBoxingCache {
public static void main(String[] args){
Integer i = -128;
Integer j = -128;
Integer k = -129;
Integer l = -129;
Integer m = 127;
Integer n = 127;
Integer o = 128;
Integer p = 128;

System.out.println("i == j? :" + (i == j));
System.out.println("k == l? :" + (k == l));
System.out.println("m == n? :" + (m == n));
System.out.println("o == p? :" + (o == p));
}

}

1
2
3
4
5
6
命令行执行:javac TestBoxingCache.java;java TestBoxingCache 后按到以下结果:

i == j? :true
k == l? :false
m == n? :true
o == p? :false

我们都知道java语言中的类变量是指向一个特定的对象,而==运算符则是判断两个变量是否指向同一个对象。由此可见i和j;m和n分别是指向同一个对象;就是使用了包装类内部预先缓存的对象;而k和l、o和p则分别指向不同的对象,是装箱过程中,动态生成的。

四、总结:

通过以上分析,我们大概可以简单总结。Java语言是一种面向对象编程语言,但是语言内置的基本类型不属于对象,语言通过标准库为期封装了包装类型(Wrapper)。在1.5以前,需要手工的进行装箱和拆箱;而在1.5以后,这一过程大部分情况下,编译器通过调用valueOf和xxxValue函数自动完成装箱和拆箱。而决定编译器执行装箱和拆箱的时机可以理解为:需要对象的时候就装箱和需要基本类型的时候就拆箱:比如把基本类型赋值给对象类型,函数调用参数传递等赋值场景下;使用集合类;使用泛型参数等等需要对象的场景下,装箱;需要基本类型的情况下,比如数学运算,逻辑运算等等,拆箱。
此外,为了提高效率,在Character、Long、Integer、Short、Byte内部缓存了值为-128~127之间的对象,以便复用。