写点什么

Java 为什么需要包装类

用户头像
Rayjun
关注
发布于: 2020 年 05 月 10 日
Java 为什么需要包装类

在 Java 的世界中,对象是一等公民,但 Java 也还是做出了妥协,出于对性能的考虑而保留了 8 种基础数据类型。



本文基于 JDK1.8



但是在某些场景下,无法直接使用基本数据类型,所以还是需要使用对象,Java 的包装类就是这样出现的。

自动装箱和拆箱



看下面的代码:

ArrayList<Integer> list = new ArrayList<Integer>();
int i = 1;
list.add(i); // 装箱;



Java 编译器会自动把基本数据类型转成对象,这个称之为装箱。 到底是怎么做到的呢?看下面的字节码:

// ...
12: invokestatic  #3                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
15: invokevirtual #10                 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
// ...



简单解释一下 invokestatic 和 invokevirtual,这两个都是 JVM 的指令,前者表示调用 Java 的

静态方法,后者表示调用对象方法。



invokestatic 调用了 Integer.valueOf() 方法,所以装箱实际上就是调用了 Integer.valueOf() 方法。



拆箱也很简单,看下面的代码:

Integer i = 1;
int i2 = 1// 拆箱;



拆箱的字节码如下:

  // ...
  7: invokevirtual #2                  // Method java/lang/Integer.intValue:()I
  // ...



同理,拆箱实际调用的是 Integer 对象方法 i.intValue()



从上文可以看出,Java 中基本类型的装箱和拆箱实际上是编译器提供的语法糖,是在编译器层面进行处理的,编译器会将装箱和拆箱编译成调用方法的字节码。在虚拟机层,通过调用方法来实现包装类的装箱和拆箱。



Byte,Short,Long,Float,Double,Boolean,Character 与 Integer 类似。

但是需要注意,还有一个特殊的包装类 Void。 Void 是 void 的包装类,Void 不能被继承,也不能被实例化,仅仅就是一个占位符。



如果一个方法使用 void 修饰,说明方法没有返回值,如果使用 Void 修饰,则该方法只能返回 null。

public Void nullFunc() {
    return null// 返回其他值会编译不通过
}



Void 常用于反射中,判断一个方法的返回值是不是 void。

for (Method method : VoidDemo.class.getMethods()) {
    if (method.getReturnType().equals(Void.TYPE)) {
        // ...
    }
}



包装类的缓存



看下面的代码:

Integer i1 = 200;
Integer i2 = 200;
System.out.println(i2 == i1); // false
Integer i3 = new Integer(100);
Integer i4 = new Integer(100);
System.out.println(i3 == i4); // false
Integer i5 = 100;
Integer i6 = 100;
System.out.println(i5 == i6); // true



上面的代码应该算是一道经典的面试题了。通过上文可知,装箱操作使用的是 Integer.valueOf() 方法,源码如下:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}



关键实现在 IntegerCache 中,在某个范围内的数值可以直接使用已经创建好的对象。IntegerCache 是一个静态内部类,而且不能实例化,仅仅用来缓存 Integer 对象:

private static class IntegerCache {
    static final int low = -128// 缓存对象的最小值,不能配置
    static final int high;
    static final Integer cache[];
    static {
        int h = 127// 缓存对象的最大值可以配置,但是不能超过 Integer的最大值,不能小于 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);
                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() {}
}



缓存对象的默认大小范围是 -128 ~ 127,正数范围可以根据自己的需要进行调整,负数最小就是 -128,不可以调整。如果不在这个范围内,就会创建新的对象。



上面代码的结果就很清晰了,第一个结果为 false 是因为 200 超出了默认的缓存范围,因此会创建新的对象。第二个结果为 false 是因为直接使用 new 来创建对象,而没有使用缓存对象。第三个结果为 true 是因为刚好在缓存的范围内。



所以在使用 Integer 等包装类生成对象时,不要使用 new 去新建对象,而应该尽可能使用缓存的对象,而且比较两个 Integer 对象时不要使用 ==,而应该使用 equals。



其他的包装类的实现基本类似,只是在对象缓存上的实现有些不同:

  • Byte 的范围刚好是 -128~127,所以都可以直接从缓存中获取对象。

  • Short 缓存范围也是 -128 ~ 127,而且不可以调整。

  • Long 的实现与 Short 一致。

  • Character 因为没有负数,所以缓存范围是 0 ~ 127,也不可以调整范围。

  • Boolean 的值只有 true 和 false,在类加载的时候直接创建好。

  • Float,Double 则没有缓存机制,因为是浮点数,可以表示无穷无尽的数,缓存的意义不大。



小心空指针



此外还需要注意的一点就是,使用包装类生成的是对象,是对象就有可能出现空指针异常,在代码中需要进行处理。

Integer integer = null;
int i = integer; // NPE



文 / Rayjun



本文首发于我的公众号 Rayjun,欢迎关注公众号,查看更多内容。

欢迎关注微信公众号



发布于: 2020 年 05 月 10 日阅读数: 112
用户头像

Rayjun

关注

程序员,王小波死忠粉 2017.10.17 加入

非著名程序员,还在学习如何写代码,公众号同名

评论 (2 条评论)

发布
用户头像
严谨起见,这类文章最好说一下所用的JDK版本号,不同版本表现有可能有差别。
2020 年 05 月 27 日 16:58
回复
感谢指正,确实是这样,马上补上😄
2020 年 05 月 29 日 18:13
回复
没有更多了
Java 为什么需要包装类