一、为什么需要包装类?

Java 作为面向对象编程语言,万物皆对象的设计理念深入人心。但 8 种基本数据类型(byteshortintlongfloatdoublecharboolean)却不具备对象特性,这在很多场景下会带来不便:

  • 集合框架(如ArrayListHashMap)只能存储对象,不能直接存储基本数据类型
  • 面向对象编程中,需要将数据以对象形式传递和处理
  • 一些类库方法只接收对象作为参数

为了解决这个矛盾,Java 为每种基本数据类型都提供了对应的包装类(Wrapper Class):

基本数据类型包装类父类
byteByteNumber
shortShortNumber
intIntegerNumber
longLongNumber
floatFloatNumber
doubleDoubleNumber
charCharacterObject
booleanBooleanObject

包装类的主要作用是将基本数据类型封装为对象,使其具备对象的特性,同时提供了大量处理数据的实用方法。

// 包装类的基本使用
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 自动装箱 / 拆箱的使用场景

  1. 赋值操作:基本类型与包装类之间直接赋值
  2. 方法调用:实参与形参类型不匹配时(基本类型与包装类)
  3. 运算操作:包装类对象参与算术运算时会自动拆箱
  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() {}
    }

从源码可知:

  1. IntegerCacheInteger类的私有静态内部类
  2. 默认缓存范围是-128127
  3. 缓存中的对象在类加载时就已创建并存储在cache数组中
  4. 缓存的上限high可以通过 JVM 参数-XX:AutoBoxCacheMax=<size>进行调整

这解释了前面的示例:

  • ab都是 127,在缓存范围内,所以引用的是同一个对象,a == b返回true
  • cd都是 128,超出默认缓存范围,所以是两个不同的对象,c == d返回false
  • ef是通过new关键字创建的,即使值在缓存范围内,也会创建新对象,所以e == f返回false

3.3 其他包装类的缓存机制

不仅Integer有缓存机制,其他包装类也有类似的实现,但缓存范围有所不同:

  1. Byte:缓存范围固定为-128127,没有配置选项
  2. Short:缓存范围固定为-128127
  3. Long:缓存范围固定为-128127
  4. Character:缓存范围为0127
  5. Boolean:缓存TRUEFALSE两个对象
    // 其他包装类的缓存示例
    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
        }
    }

    FloatDouble没有实现缓存机制,因为它们是浮点类型,可能的取值范围太大,缓存不切实际。

    // 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
        }
    }
    

    当包装类与基本类型进行==比较时,包装类会自动拆箱为基本类型,然后比较值的大小,这时候使用==是安全的。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐