深入理解Java包装类:自动装箱拆箱与缓存池机制
对象包装器
Java中的数据类型可以分为两类:基本类型和引用类型。作为一门面向对象编程语言, 一切皆对象是Java语言的设计理念之一。但基本类型不是对象,无法直接参与面向对象操作,为了解决这个问题,Java让每个基本类型都有一个与之对应的包装器类型。
Java中有8种不可变的基本类型,分别为:
- 整型:
byte
、short
、int
、long
- 浮点类型:
float
、double
- 字符类型:
char
- 布尔类型:
boolean
类型 | 大小 | 默认值 | 示例 |
---|---|---|---|
byte | 1字节 | 0 | byte b = 10 |
short | 2字节 | 0 | short s = 200 |
int | 4字节 | 0 | int i = 1000 |
long | 8字节 | 0L | long l = 5000L |
float | 4字节 | 0.0f | float f = 3.14f |
double | 8字节 | 0.0d | double d = 2.718 |
char | 2字节 | ‘\u0000’ | char c = 'A' |
boolean | 未明确定义 | false | boolean flag = true |
这八种基本类型都有对应的包装类分别为:Byte
、Short
、Integer
、Long
、Float
、Double
、Character
、Boolean
(前6个派生于公共的超类Number
)。包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时,包装器类还是final
,因此不能派生它们的子类。
Java不是“一切皆对象”吗,为什么还要保留基本数据类型?
包装类是引用类型,对象的引用存储在栈中,对象本身存储在堆中;而对于基本数据类型,变量对应的内存块直接存储数据本身(栈中)。因此,基本数据类型读写效率更高效。在64位JVM上,在开启引用压缩的情况下,一个Integer对象占用16个字节的内存空间,而一个int类型数据只占用4字节的内存空间,前者对空间的占用是后者的4倍。也就是说,不管是读写效率还是存储效率,基本类型都更高效。尽管Java强调面向对象,但为了性能做了妥协。
自动装箱与拆箱
装箱和拆箱是实现基本数据类型与包装类之间相互转换的特性。Java 5引入自动装箱/拆箱功能,进一步简化了包装类的使用。
- 装箱:将基本数据类型转化为对应的包装类对象。
- 拆箱:将包装类对象转化为对应的基本数据类型值。
示例:
Integer a = 100; // 自动装箱 -> Integer.valueOf(100)int b = a; // 自动拆箱 -> a.intValue()// 自动装箱和拆箱也适用于算术表达式
Integer n = 3;
n++; // 编译器将自动插入一条对象拆箱的指令,然后进行自增运算,最后再将结果装箱
装箱其实就是调用了包装类的valueOf()
方法,拆箱其实就是调用了 xxxValue()
方法。
API
java.lang.Integer
int intValue()
将这个
Integer
对象的值作为一个int
返回(覆盖Number
类中的intValue
方法)。
static Integer valueOf(String s)
返回一个新的
Integer
对象,用字符串s
表示的整数初始化。指定字符串必须表示一个十进制整数。
关于自动装箱还有几点需要注意:
高频装箱拆箱(如循环)会产生大量临时对象,消耗内存和GC资源:
// 错误示例:每次循环触发装箱 Long sum = 0L; for (long i = 0; i < 1e6; i++) {sum += i; // sum = Long.valueOf(sum.longValue() + i) }// 正确优化:使用基本类型 long sum = 0L; for (long i = 0; i < 1e6; i++) {sum += i; }
由于包装器类引用可以为
null
,所以自动装箱有可能会抛出一个NullPointerException
异常:Integer n = null; System.out.println(2 * n) // throws NullPointerException
如果在一个表达式中混合使用
Integer
和Double
类型,Integer
值就会拆箱,提升为double
,再装箱为Double
:Integer n = 1; Double x = 2.0; System.out.println(true ? n : x); // 1.0
装箱和拆箱是编译器要做的工作,而不是虚拟机。编译器在生成类的字节码时会插入必要的方法调用。虚拟机只是执行这些字节码。
缓存池机制
缓存池是 Java 为优化包装类对象创建和内存消耗而设计的核心机制,通过预创建和复用常用数值的包装类对象,减少重复对象创建的开销。Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回 TRUE
or FALSE
。
示例:
Integer a = 100; // Integer.valueOf(100)
Integer b = 100; // Integer.valueOf(100)
Integer c = 200; // Integer.valueOf(200)
Integer d = 200; // Integer.valueOf(200)System.out.println(a == b); // true
System.out.println(c == d); // false
System.out.println(c.equals(d)); //true
Integer.valueOf()
的缓存逻辑:
public static Integer valueOf(int i) {if (i >= IntegerCache.low && i <= IntegerCache.high)return IntegerCache.cache[i + (-IntegerCache.low)];return new Integer(i); // 超出缓存范围时创建新对象
}
缓存池机制:Java对
-128 ~ 127
范围内的Integer
对象预先生成并缓存,a
和b
指向同一个缓存对象,a == b
比较对象地址,返回true
。200
超出默认缓存范围(-128 ~ 127
),Integer.valueOf(200)
每次会创建新对象,c
和d
指向不同对象,c == d
比较对象地址,返回false
。
对于 Integer
,可以通过 JVM 参数 -XX:AutoBoxCacheMax=<size>
修改缓存上限,但不能修改下限 -128。实际使用时,并不建议设置过大的值,避免浪费内存,甚至是 OOM(全称Out Of Memory, 即内存溢出)。
- 内存溢出:申请的内存超出了JVM能提供的内存大小,此时称之为溢出。
- 内存泄漏:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分配给别人用。
对于Byte
,Short
,Long
,Character
没有类似 -XX:AutoBoxCacheMax
参数可以修改,因此缓存范围是固定的,无法通过 JVM 参数调整。Boolean
则直接返回预定义的 TRUE
和 FALSE
实例,没有缓存范围的概念。
Character
的缓存逻辑:
public static Character valueOf(char c) {if (c <= 127) { // must cachereturn CharacterCache.cache[(int)c];}return new Character(c);
}
Boolean
的缓存逻辑:
public static Boolean valueOf(boolean b) {return (b ? TRUE : FALSE);
}
两种浮点数类型的包装类 Float
,Double
并没有实现缓存机制:
Float a = 3f;
Float b = 3f;
System.out.println(a == b);// 输出 falseDouble c = 1.2;
Double d = 1.2;
System.out.println(c == d);// 输出 false
下面这段代码的输出结果是什么?
Integer a = 40;
Integer b = new Integer(40);
System.out.println(a == b);
Integer a = 40
自动装箱等价于Integer a = Integer.valueOf(40)
,40
在默认缓存范围内,所以a
直接使用的是缓存中的对象,而Integer b = new Integer(40)
会直接创建新的对象。因此,答案是false
。