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); // 2147483647
System.out.println(Integer.MIN_VALUE); // -2147483648
System.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 -> Integer Integer 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); // true Integer c = 128; Integer d = 128; System.out.println(c == d); // false Integer 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返回truec和d都是 128,超出默认缓存范围,所以是两个不同的对象,c == d返回falsee和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); // true Character 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); // false Double 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 } }当包装类与基本类型进行
==比较时,包装类会自动拆箱为基本类型,然后比较值的大小,这时候使用==是安全的。
更多推荐



所有评论(0)