ZetCode

Java 自动装箱和拆箱

最后修改时间:2025 年 4 月 13 日

自动装箱和拆箱是 Java 5 中引入的功能,可自动处理原始类型及其对应的包装类之间的转换。 这消除了对显式转换的需要,使代码更简洁易读,同时保持类型安全。

自动装箱是指将原始类型自动转换为其包装器对象等效项,例如 int 转换为 Integerdouble 转换为 Double。 这允许直接使用原始类型,例如在 ArrayList<Integer> 之类的集合中需要对象的地方。

拆箱是相反的过程 - 将包装器对象转换回原始值。 例如,当在算术运算中需要 Integer 对象时,它可以自动转换为 int。 这确保了对象和原始类型之间的无缝交互。

这些功能适用于所有八种原始类型(byteshortintlongfloatdoublecharboolean)及其各自的包装类,从而提高了开发人员的生产力并减少了样板代码。

理解自动装箱

当将原始值分配给包装类变量、作为期望包装器对象的参数传递,或在需要对象的上下文中使用(如集合)时,会发生自动装箱。 Java 编译器会自动处理此转换。

Main.java
package com.zetcode;

public class Main {

    public static void main(String[] args) {
        
        // Autoboxing examples
        Integer intObj = 42;           // int to Integer
        Double doubleObj = 3.14;       // double to Double
        Boolean boolObj = true;        // boolean to Boolean
        Character charObj = 'A';      // char to Character
        
        // Using in collections
        List<Integer> numbers = new ArrayList<>();
        numbers.add(1);  // autoboxing int to Integer
        numbers.add(2);
        numbers.add(3);
        
        System.out.println("Integer: " + intObj);
        System.out.println("Double: " + doubleObj);
        System.out.println("Boolean: " + boolObj);
        System.out.println("Character: " + charObj);
        System.out.println("Numbers list: " + numbers);
    }
}

此示例演示了各种自动装箱场景。 原始值在需要时会自动转换为其对应的包装器对象。 这对于只能存储对象而不能存储原始类型的集合尤其有用。

理解拆箱

拆箱是自动装箱的相反过程 - 将包装器对象转换回其原始值。 当将包装器对象分配给原始变量、在算术运算中使用或传递给需要原始类型的方法时,会发生这种情况。

Main.java
package com.zetcode;

public class Main {

    public static void main(String[] args) {
        
        // Wrapper objects
        Integer intObj = 100;
        Double doubleObj = 2.71828;
        Boolean boolObj = false;
        
        // Unboxing examples
        int i = intObj;            // Integer to int
        double d = doubleObj;       // Double to double
        boolean b = boolObj;        // Boolean to boolean
        
        // Arithmetic operations
        int sum = intObj + 50;      // Integer unboxed to int
        double product = doubleObj * 2;  // Double unboxed to double
        
        // Method calls
        printPrimitive(intObj);     // Integer unboxed to int
        
        System.out.println("int value: " + i);
        System.out.println("double value: " + d);
        System.out.println("boolean value: " + b);
        System.out.println("sum: " + sum);
        System.out.println("product: " + product);
    }
    
    private static void printPrimitive(int num) {
        System.out.println("Primitive value: " + num);
    }
}

此示例重点介绍各种拆箱场景。 在需要进行赋值、算术计算和方法调用时,包装器对象会无缝转换为其对应的原始类型。 编译器通过自动处理这些转换来确保顺利执行,从而使开发人员可以互换地使用原始类型和对象类型,而无需显式转换。

表达式中的自动装箱

自动装箱和拆箱在涉及原始类型和包装器类型的表达式中协同工作。 编译器会根据需要自动在类型之间转换以执行操作,并遵循类型提升和转换的特定规则。

Main.java
package com.zetcode;

public class Main {

    public static void main(String[] args) {

        Integer a = 10;
        Integer b = 20;
        int c = 30;
        
        // Mixed operations
        Integer result1 = a + b;      // a and b unboxed, result autoboxed
        int result2 = a * c;          // a unboxed, result remains primitive
        Double result3 = b / 2.0;     // b unboxed, result autoboxed to Double
        
        // Comparison operations
        boolean test1 = a < c;       // a unboxed, primitive comparison
        boolean test2 = a.equals(b);  // object comparison
        
        // Ternary operator
        Number num = (a > 5) ? a : 3.14f;  // a remains Integer, 3.14f autoboxed to Float
        
        System.out.println("result1: " + result1);
        System.out.println("result2: " + result2);
        System.out.println("result3: " + result3);
        System.out.println("test1: " + test1);
        System.out.println("test2: " + test2);
        System.out.println("num: " + num);
    }
}

此示例演示了自动装箱和拆箱如何在表达式中自然集成。 包装器对象被拆箱以进行算术运算,在需要进行对象赋值时重新装箱,并在比较和三元运算中无缝转换。 编译器通过动态应用类型提升规则来确保高效执行。

性能注意事项

虽然自动装箱提供了便利,但它会由于对象创建和垃圾回收而引入性能开销。 与原始类型不同,原始类型可以有效地存储在内存中,而每个自动装箱的值都会生成一个新对象(缓存值除外),从而增加了堆使用率并影响运行时效率。

在对性能要求苛刻的应用程序中,循环或计算中过多的自动装箱可能会由于不必要的内存分配而降低执行速度。 在处理大型数据集或密集计算时,使用原始类型而不是包装类可以显着提高性能。

Main.java
package com.zetcode;

public class Main {

    public static void main(String[] args) {

        long startTime, endTime;
        final int COUNT = 1_000_000;

        // Using primitives for efficient computation
        startTime = System.nanoTime();
        primitiveTest(COUNT);
        endTime = System.nanoTime();
        System.out.println("Primitive execution time: " + (endTime - startTime) + " ns");

        // Using wrappers with autoboxing (less efficient)
        startTime = System.nanoTime();
        wrapperTest(COUNT);
        endTime = System.nanoTime();
        System.out.println("Wrapper execution time: " + (endTime - startTime) + " ns");
    }

    private static void primitiveTest(int count) {
        int sum = 0;
        for (int i = 0; i < count; i++) {
            sum += i;
        }
    }

    private static void wrapperTest(int count) {
        Integer sum = 0;
        for (int i = 0; i < count; i++) {
            sum += i;  // Autoboxing and unboxing occur in each iteration
        }
    }
}

此基准测试突出了原始类型和包装类之间的性能差距。 原始方法运行速度更快并且消耗更少的内存,而基于包装器的方法则会受到重复的自动装箱和垃圾回收开销的影响。 在优化关键循环时,最好使用原始类型以确保高效执行。

缓存包装器对象

Java 缓存特定范围的包装器对象,以优化性能和内存使用率。 对于 IntegerByteShortLongCharacter,缓存 -128127 之间的值。 Boolean 缓存 truefalse。 由于其浮点性质,FloatDouble 不使用缓存。

Main.java
package com.example;

public class Main {

    public static void main(String[] args) {
        // Cached Integer values
        Integer a = 100;
        Integer b = 100;
        System.out.println("a == b (100): " + (a == b));  // true (cached object)
        System.out.println("a.equals(b): " + a.equals(b)); // true (value equality)

        // Non-cached Integer values
        Integer c = 200;
        Integer d = 200;
        System.out.println("c == d (200): " + (c == d));   // false (distinct objects)
        System.out.println("c.equals(d): " + c.equals(d)); // true (value equality)

        // Boolean caching
        Boolean bool1 = true;
        Boolean bool2 = true;
        System.out.println("bool1 == bool2: " + (bool1 == bool2));   // true (cached object)
        System.out.println("bool1.equals(bool2): " + bool1.equals(bool2)); // true

        // Using valueOf for explicit creation
        Integer e = Integer.valueOf(50);
        Integer f = Integer.valueOf(50);
        System.out.println("e == f (50): " + (e == f));   // true (cached object)
        System.out.println("e.equals(f): " + e.equals(f)); // true
    }
}

此示例说明了 Java 的包装器对象缓存机制。 在缓存范围内(例如,Integer-128127),Java 重复使用同一个对象实例,使 == 比较返回 true。 在此范围之外,将创建新对象,因此即使对于相等的值,== 也会返回 false。 为了可靠地比较包装器对象值,请使用 equals,它检查值是否相等,而与缓存无关。

该代码避免使用像 Integer 构造函数这样的已弃用方法,而是使用 Integer.valueOf。 这确保了与现代 Java 实践的兼容性,并有效地利用了缓存。 理解缓存对于编写健壮的代码至关重要,尤其是在比较包装器对象时。

空值处理和潜在陷阱

当拆箱空包装器对象时,自动装箱可能会导致 NullPointerException。 在处理可能为空的包装器对象时必须小心,尤其是在集合或方法返回中。

Main.java
package com.zetcode;

import java.util.ArrayList;
import java.util.List;

public class Main {

    public static void main(String[] args) {

        List<Integer> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(null);  // Valid, but dangerous
        numbers.add(3);
        
        try {
            for (Integer num : numbers) {
                int value = num;  // NullPointerException when num is null
                System.out.println(value);
            }
        } catch (NullPointerException e) {
            System.out.println("Caught NullPointerException: " + e.getMessage());
        }
        
        // Safe unboxing
        for (Integer num : numbers) {
            if (num != null) {
                int value = num;
                System.out.println("Safe value: " + value);
            } else {
                System.out.println("Found null value");
            }
        }
    }
}

此示例显示了拆箱空值的危险以及如何安全地处理它。 集合可以包含空包装器对象,并且尝试拆箱它们将抛出 NullPointerException。 当可能存在空值时,始终在拆箱之前检查空值。

使用自动装箱的方法重载

Java 中的自动装箱通过影响编译器选择最合适的方法的方式来影响方法重载解析。 编译器优先考虑完全类型匹配,然后是自动装箱/拆箱转换、加宽原始类型转换,最后是 varargs。 理解这些规则对于避免歧义并确保可预测的方法解析至关重要。

Main.java
package com.example;

public class Main {

    public static void main(String[] args) {

        int primitiveInt = 10;
        Integer wrapperInt = 20;
        long primitiveLong = 30L;

        // Method overloading with int and Integer
        process(primitiveInt);    // Calls int version
        process(wrapperInt);      // Calls Integer version
        process(40);             // Calls int version (exact match)

        // Method overloading with int and long
        processNumber(50);       // Calls int version
        processNumber(60L);      // Calls long version
    }

    private static void process(int num) {
        System.out.println("Processing int: " + num);
    }

    private static void process(Integer num) {
        System.out.println("Processing Integer: " + num);
    }

    private static void processNumber(int num) {
        System.out.println("Processing number as int: " + num);
    }

    private static void processNumber(long num) {
        System.out.println("Processing number as long: " + num);
    }
}

此示例演示了方法重载如何与自动装箱和原始类型交互。 编译器根据参数类型选择方法,优先选择完全匹配。 例如,将 40int 字面量)传递给 process 会调用 int 版本,从而避免自动装箱到 Integer。 同样,processNumber(50) 调用 int 版本,而 processNumber(60L) 调用 long 版本,因为显式的 long 字面量。

该示例通过使用不同的方法名称和清晰的参数类型来避免歧义。 这确保了编译器的方法解析是可预测的,优先考虑完全匹配而不是转换,从而提高了代码的清晰度和性能。

来源

Java 语言规范 - 装箱转换

在本文中,我们深入探讨了 Java 的自动装箱和拆箱功能。 这些功能提供了原始类型及其包装类之间的便捷自动转换,但理解它们的行为、性能影响和潜在陷阱对于编写健壮的 Java 代码至关重要。

作者

我叫 Jan Bodnar,是一位经验丰富的程序员。 我于 2007 年开始撰写编程文章,至今已撰写了 1400 多篇文章和八本电子书。 凭借超过八年的教学经验,我致力于分享我的知识并帮助其他人掌握编程概念。

列出所有Java教程