Java的自动装箱和拆箱

491 阅读4分钟

介绍

Java的自动装箱和拆箱是从Java 1.5开始引入的特性,Java编译器必要的时候在原始类型和对应的包装类之间进行自动转换,减少开发人员输入代码的工作量,使代码更简洁。

自动装箱和拆箱适用的类型

原始类型 包装类
boolean Boolean
byte Byte
char Character
float Float
int Integer
long Long
short Short
double Double

例子

让我们看一个简单的例子:

// BoxingExample.java

public class BoxingExample {

	public static void main(String[] args) {
		boxing1();
		boxing2(2.46f, 'A');

		unboxing1();
		Byte b = Byte.valueOf("B");
		unboxing2(Boolean.TRUE, b);
	}
	
	static void boxing1() {
		Integer i = 1;
		Double d = 3.1415926;
		System.out.println("Integer: " + i);
		System.out.println("Double: " + d);
	}
	
	static void boxing2(Float f, Character c) {
		System.out.println("Float: " + f);
		System.out.println("Character: " + c);
	}
	
	static void unboxing1() {
		long sum = 0;
		Long a = Long.valueOf(100);
		Short b = Short.valueOf((short)9);
		sum += a;
		sum += b;
		System.out.println("sum value: " + sum);
	}
	
	static void unboxing2(boolean isOK, byte b) {
		System.out.println("boolean value: " + isOK);
		System.out.println("byte value: " + b);
	}
}

对于这个例子,Java编译器会采用自动装箱和拆箱操作。我们可以通过启用Java编译器的调试选项-printflat看看Java编译器对上面代码进行自动装箱和拆箱后生成的文件。

-printflat这个选项不是javac的标准选项,不是每个版本的javac都支持。经测试,javac 5 ~ 8支持该选项,javac 9+不支持。

$ javac -version
javac 1.6.0_45

$ mkdir out
$ javac -XD-printflat -d out BoxingExample.java
$ cat out/BoxingExample.java
// out/BoxingExample.java

public class BoxingExample {
    
    public BoxingExample() {
        super();
    }
    
    public static void main(String[] args) {
        boxing1();
        boxing2(Float.valueOf(2.46F), Character.valueOf('A'));
        unboxing1();
        Byte b = Byte.valueOf("B");
        unboxing2(Boolean.TRUE.booleanValue(), b.byteValue());
    }
    
    static void boxing1() {
        Integer i = Integer.valueOf(1);
        Double d = Double.valueOf(3.1415926);
        System.out.println("Integer: " + i);
        System.out.println("Double: " + d);
    }
    
    static void boxing2(Float f, Character c) {
        System.out.println("Float: " + f);
        System.out.println("Character: " + c);
    }
    
    static void unboxing1() {
        long sum = 0;
        Long a = Long.valueOf(100);
        Short b = Short.valueOf((short)9);
        sum += a.longValue();
        sum += b.shortValue();
        System.out.println("sum value: " + sum);
    }
    
    static void unboxing2(boolean isOK, byte b) {
        System.out.println("boolean value: " + isOK);
        System.out.println("byte value: " + b);
    }
}

通过这个例子可以看到,Java编译器对原始Java文件的语句进行了自动装箱和拆箱:

  • boxing1()

    Integer i = 1;          ==> Integer i = Integer.valueOf(1);
    Double d = 3.1415926;   ==> Double d = Double.valueOf(3.1415926);
    
  • boxing2()

    boxing2(2.46f, 'A'); ==> boxing2(Float.valueOf(2.46F), Character.valueOf('A'));
    
  • unboxing1()

    sum += a;  ==> sum += a.longValue();
    sum += b;  ==> sum += b.shortValue();
    
  • unboxing2()

    unboxing2(Boolean.TRUE, b); ==> unboxing2(Boolean.TRUE.booleanValue(), b.byteValue());
    

自动装箱和拆箱的条件

在什么情况下Java编译器会应用装箱和拆箱呢?

自动装箱

  • 通过赋值操作把原始类型的值赋值给对应包装类的变量。如本文例子中的boxing1()
  • 当原始类型的值作为参数传递给一个方法,而该方法的形式参数是该原始类型对应的包装类型。如本文例子中的boxing2()

自动拆箱

  • 通过赋值操作把包装类型的值赋值给对应原始类型的变量。如本文例子中的unboxing1()
  • 把包装类型的值作为参数传递给一个方法,而该方法的形式参数是该包装类型对应的原始类型。如本文例子中的unboxing2()

valueOf()

从本文的例子可以看到,Java编译器应用自动装箱生成的代码采用的是对应包装类的静态工厂方法:valueOf(v XXX),而不是调用包装类的构造函数new XXX()。调用valueOf()方法在大部分情况下相对于new XXX()构造函数创建包装类的对象,有更好的性能。因为JDK的包装类实现中,对于常用的原始类型值创建了缓存,比如Integer类的实现,默认缓存了-128到127之间的值对应的Integer对象。因此valueOf()方法会优先查找已经缓存的包装类对象,如果原始类型的值对应的包装类对象已经在缓存中,则直接返回缓存中的对象,只有在缓存中无法找到对应的对象时,才会去创建新的对象。当我们自己在代码中需要显式地把原始类型转换成包装类型的时候,应该采用valueOf()方法。

    // Integer.java

    /**
     * Returns an {@code Integer} instance representing the specified
     * {@code int} value.  If a new {@code Integer} instance is not
     * required, this method should generally be used in preference to
     * the constructor {@link #Integer(int)}, as this method is likely
     * to yield significantly better space and time performance by
     * caching frequently requested values.
     *
     * This method will always cache values in the range -128 to 127,
     * inclusive, and may cache other values outside of this range.
     *
     * @param  i an {@code int} value.
     * @return an {@code Integer} instance representing {@code i}.
     * @since  1.5
     */
    @HotSpotIntrinsicCandidate
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

    static final Integer cache[];

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);

        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

小结

本文对于Java自动装箱和拆箱的适用场景和实现作了简要介绍,具体的规范和实现细节,可以查询Java语言规范和JDK的实现代码。

参考资料

  1. The Java Tutorials - Autoboxing and unboxing
  2. The Java Language Specification - Boxing Conversion