Java 包装类:自动拆箱 / 装箱与 128 陷阱
一、为什么需要包装类?
Java 作为面向对象编程语言,万物皆对象的设计理念深入人心。但 8 种基本数据类型(byte
、short
、int
、long
、float
、double
、char
、boolean
)却不具备对象特性,这在很多场景下会带来不便:
- 集合框架(如
ArrayList
、HashMap
)只能存储对象,不能直接存储基本数据类型 - 面向对象编程中,需要将数据以对象形式传递和处理
- 一些类库方法只接收对象作为参数
为了解决这个矛盾,Java 为每种基本数据类型都提供了对应的包装类(Wrapper Class):
基本数据类型 | 包装类 | 父类 |
---|---|---|
byte | Byte | Number |
short | Short | Number |
int | Integer | Number |
long | Long | Number |
float | Float | Number |
double | Double | Number |
char | Character | Object |
boolean | Boolean | Object |
包装类的主要作用是将基本数据类型封装为对象,使其具备对象的特性,同时提供了大量处理数据的实用方法。
// 包装类的基本使用
public class WrapperDemo {public static void main(String[] args) {// 将基本类型封装为对象Integer num = new Integer(100);// 获取包装类中的基本类型值int value = num.intValue();// 包装类提供的实用方法System.out.println(Integer.MAX_VALUE); // 2147483647System.out.println(Integer.MIN_VALUE); // -2147483648System.out.println(Integer.parseInt("123")); // 字符串转整数System.out.println(Integer.toBinaryString(10)); // 1010}
}
二、自动装箱与拆箱机制
Java 5 引入了自动装箱(Autoboxing)和自动拆箱(Unboxing)机制,极大简化了基本类型与包装类之间的转换操作。
2.1 自动装箱(Autoboxing)
自动装箱指的是 Java 编译器自动将基本数据类型转换为对应的包装类对象的过程。
// 自动装箱:int -> Integer
Integer a = 100; // 等价于 Integer a = Integer.valueOf(100);// 集合中的自动装箱
List<Integer> list = new ArrayList<>();
list.add(200); // 自动将int类型的200装箱为Integer对象
2.2 自动拆箱(Unboxing)
自动拆箱则是指 Java 编译器自动将包装类对象转换为对应的基本数据类型的过程。
// 自动拆箱:Integer -> int
Integer b = new Integer(300);
int c = b; // 等价于 int c = b.intValue();// 运算中的自动拆箱
Integer d = 400;
int e = d + 100; // d自动拆箱为int,与100相加
2.3 自动装箱 / 拆箱的实现原理
通过反编译 class 文件可以发现,自动装箱其实是编译器在后台调用了valueOf()
方法,而自动拆箱则是调用了xxxValue()
方法(如intValue()
、doubleValue()
等)。
// 反编译前
Integer a = 100;
int b = a;// 反编译后
Integer a = Integer.valueOf(100);
int b = a.intValue();
理解这一点对于我们后面分析 "128 陷阱" 至关重要。
2.4 自动装箱 / 拆箱的使用场景
- 赋值操作:基本类型与包装类之间直接赋值
- 方法调用:实参与形参类型不匹配时(基本类型与包装类)
- 运算操作:包装类对象参与算术运算时会自动拆箱
- 集合操作:向集合中添加基本类型元素时自动装箱
// 方法调用中的自动装箱/拆箱 public class BoxDemo {public static void print(Integer num) {System.out.println(num);}public static int calculate(int a, int b) {return a + b;}public static void main(String[] args) {int x = 10;print(x); // 自动装箱:int -> IntegerInteger y = 20;Integer z = 30;int result = calculate(y, z); // 自动拆箱:Integer -> int} }
三、深入理解 128 陷阱
在使用包装类时,最容易遇到的问题就是 "128 陷阱",也称为 Integer 缓存机制导致的比较问题。
3.1 什么是 128 陷阱?
先看一个经典的示例
public class IntegerTrap {public static void main(String[] args) {Integer a = 127;Integer b = 127;System.out.println(a == b); // trueInteger c = 128;Integer d = 128;System.out.println(c == d); // falseInteger e = new Integer(127);Integer f = new Integer(127);System.out.println(e == f); // false} }
为什么同样是赋值,127 时
==
比较返回true
,而 128 时返回false
?为什么使用new
关键字创建的对象即使值相同,==
比较也返回false
?3.2 陷阱背后的原因:Integer 缓存机制
要理解这个现象,我们需要查看
Integer.valueOf()
方法的源码(基于 JDK 8):public static Integer valueOf(int i) {if (i >= IntegerCache.low && i <= IntegerCache.high)return IntegerCache.cache[i + (-IntegerCache.low)];return new Integer(i); }
可以看到,
valueOf()
方法并非总是创建新的Integer
对象,而是先检查该值是否在缓存范围内。如果在范围内,则直接返回缓存中的对象;否则,才创建新对象。再看
IntegerCache
的源码:private static class IntegerCache {static final int low = -128;static final int high;static final Integer cache[];static {// 高值可以通过属性配置int h = 127;String integerCacheHighPropValue =sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");if (integerCacheHighPropValue != null) {try {int i = parseInt(integerCacheHighPropValue);i = Math.max(i, 127);// 最大不超过Integer.MAX_VALUE - (-low)h = Math.min(i, Integer.MAX_VALUE - (-low) -1);} catch( NumberFormatException nfe) {// 如果配置无效,则使用默认值}}high = h;cache = new Integer[(high - low) + 1];int j = low;for(int k = 0; k < cache.length; k++)cache[k] = new Integer(j++);// 断言缓存范围正确assert IntegerCache.high >= 127;}private IntegerCache() {} }
从源码可知:
IntegerCache
是Integer
类的私有静态内部类- 默认缓存范围是
-128
到127
- 缓存中的对象在类加载时就已创建并存储在
cache
数组中 - 缓存的上限
high
可以通过 JVM 参数-XX:AutoBoxCacheMax=<size>
进行调整
这解释了前面的示例:
a
和b
都是 127,在缓存范围内,所以引用的是同一个对象,a == b
返回true
c
和d
都是 128,超出默认缓存范围,所以是两个不同的对象,c == d
返回false
e
和f
是通过new
关键字创建的,即使值在缓存范围内,也会创建新对象,所以e == f
返回false
3.3 其他包装类的缓存机制
不仅Integer
有缓存机制,其他包装类也有类似的实现,但缓存范围有所不同:
- Byte:缓存范围固定为
-128
到127
,没有配置选项 - Short:缓存范围固定为
-128
到127
- Long:缓存范围固定为
-128
到127
- Character:缓存范围为
0
到127
- Boolean:缓存
TRUE
和FALSE
两个对象// 其他包装类的缓存示例 public class WrapperCache {public static void main(String[] args) {// Byte缓存测试Byte b1 = 127;Byte b2 = 127;System.out.println(b1 == b2); // true// Character缓存测试Character c1 = 127;Character c2 = 127;System.out.println(c1 == c2); // trueCharacter c3 = 128;Character c4 = 128;System.out.println(c3 == c4); // false// Boolean缓存测试Boolean bool1 = true;Boolean bool2 = true;System.out.println(bool1 == bool2); // true} }
Float
和Double
没有实现缓存机制,因为它们是浮点类型,可能的取值范围太大,缓存不切实际。// Float和Double没有缓存 public class FloatDoubleCache {public static void main(String[] args) {Float f1 = 1.0f;Float f2 = 1.0f;System.out.println(f1 == f2); // falseDouble d1 = 1.0;Double d2 = 1.0;System.out.println(d1 == d2); // false} }
四、包装类比较的正确方式
了解了缓存机制后,我们应该明确:包装类对象比较应该使用
equals()
方法,而不是==
运算符。==
运算符比较的是对象的引用(内存地址),而equals()
方法比较的是对象的值。public class WrapperComparison {public static void main(String[] args) {Integer a = 128;Integer b = 128;// 错误:使用==比较包装类System.out.println(a == b); // false// 正确:使用equals()比较包装类System.out.println(a.equals(b)); // true// 注意:基本类型比较可以安全使用==int c = 128;int d = 128;System.out.println(c == d); // true// 混合比较:包装类会自动拆箱System.out.println(a == c); // true} }
当包装类与基本类型进行
==
比较时,包装类会自动拆箱为基本类型,然后比较值的大小,这时候使用==
是安全的。