Java的包装类–装箱与拆箱

前言

众所周知,Java是一种纯面向对象的语言(并不是说Java程序员都要看着女朋友编程),不像C++中既可以面向对象也可以面向过程。但是这不意味着Java当中全被的类型都是对象。之前我们也学过Java中的类型包括,对象和8种基本数据类型。之所以不将这八种基本数据类型是因为相对于对象来说,基本数据类型在使用是更加的方便,而且使用的效率上也是远远的高于对象类型。但是,在Java的一些类库中,并不支持对基本数据类型的操作,比如说集合类型。刚开始学Java的新手肯定会发现这样的一个问题。

List<int> list = new List<int>();

上面的这种代码,估计八成就是从C++刚转到Java的新手写出的代码。

第一点List是一个接口,是不能够new的,我们只可以new出他的子类,比如我们经常使用的ArrayList

第二点Java当中的集合类型并不支持基本数据类型,我们应该将基本数据类型转变为相应的包装类。

所以说正确的代码应该是

List<Integer> list = new ArrayList<>();

后面的<>中的类型是可以省略不写的,毕竟前面都已经写过了。

为什么不能使用基本数据类型?

同样是面向对象的语言,Java和C++中的对象是不一样的。Java中对象和python有一点类型,他们都有一个终极的父类Object,所以说所有的对象肯定都是Object的子类。但是C++是不一样的。

现在我们考虑List中为什么不能使用基本数据类型,我们可以先看一下List如果不指定类型会怎么样。

List list2 = new ArrayList();

我们发现这样的代码也可以通过,而且和上面的代码不一样,这个List中可以放任何类型的数据(除了基本数据类型)。为什么呢?因为如果不指定对应的类型的话,List这些容器就会默认指定类型为Object,而Java中的任何的对象都是Object类型的子类,所以说是可以放入的。(但是这样也有一个坏处,我们取出的数据类型也是Object,这时我们可能并不知道将这个数据转成什么样的类型,所以说最好还是要指定好类型)。但是基本数据类型不是对象,他们和Ojbect没有任何的联系,所以说基本数据类型是放不进去的。要想放进去,我们就必须要是基本数据类型和对象之间建立一种一一对应的联系。于是包装类出现了。

就比如说上面的Intger就是int类型的包装类。这个类中封装了一个基本数据类型。这个时候,当集合想要操作基本数据类型的时候,就可以转向基本数据类型的包装类进行操作。

包装类介绍

下面就以int的包装类Integer为例进行包装类的说明。

java.lang.Object 
    java.lang.Number 
        java.lang.Integer 
public final class Integer
extends Number
implements Comparable<Integer>
  • Integer类包装一个对象中的原始类型int的值。类型为Integer的对象包含一个单一字段,其类型为int
  • 此外,该类还提供了一些将int转换为StringString转换为int ,以及在处理int时有用的其他常量和方法。

这个类的方法是非常多的,而且大多为static方法,比如说我们之前经常使用的String转为int的方法就是他的静态方法.Integet.parseInt(String num, int radix)第二个参数是指定进制,默认是十进制。

先看一个这个类的构造函数

Integer(int value) 
// 构造一个新分配的 Integer对象,该对象表示指定的 int值。  
Integer(String s) 
// 构造一个新分配 Integer对象,表示 int由指示值 String参数。  

然后就是他的所有的方法其实是没有必要一一去看的。下面说的是所有的包装类都有的方法。

第一个就是xxxValue()成员方法,返回这个包装类中封装的基本数据类型。如

int intValue() 
// 将 Integer的值作为 int 。  

如果要是double的话就是doubleValue,char就是charValue

第二个就是valueOf(xxx)静态方法,参数是一个基本的数据类型。比如说

static Integer valueOf(int i) 
// 返回一个 Integer指定的 int值的 Integer实例。  

不过也可以是String,指定进制的那个可不是所有的包装类都有的,只是整形才有的方法。

static Integer valueOf(String s) 
// 返回一个 Integer对象,保存指定的值为 String 。  
static Integer valueOf(String s, int radix) 
// 返回一个 Integer对象,保存从指定的String中 String的值,当用第二个参数给出的基数进行解析时。  

这个方法的用法如下所示。

Integer i = Integer.valueOf(3);

还有另一种写法是

Integer i = new Integer(3);

这两种方法其实等价的,但是官方建议使用第一种方法,也就是valueOf()方法,这个方法更加的快一点。这个可能是和包装类的常量池有关,就比如我们使用String的时候使用String a = “hello”;而不推荐使用String a = new String("Hello");因为前者,一是可以加入String的常量池,方便以后再用,而是如果常量池中有的话也可以不用产生新的对象。毕竟Java的中的String是一种不可变类型,基本数据类型的包装类也是如此。好像有点儿扯远了。下面继续回到正题中来。的

上面我们介绍的虽然只是int的包装类,但是其余的包装类的使用其实也是差不多的,方法也都是差不多的。

下面来展示一下基本数据类型和他们的包装类型。

基本数据类型 包装类型 基本数据类型 包装类型
int Integer short Short
char Character double Double
boolean Boolean byte Byte
long Long float Float

可以除了intchar变成了IntegerCharacter的全称之外,其余的包装类都是将首字母大小.(这就是Java的命名规则)

装箱与拆箱

上面说到了Java当中的集合类型只可以放入基本数据类型的包装类,但是这一点确实有点违背我们的常识。比如这样的代码

List<Integer> list = new ArrayList<>();
list.add(1);

可以看到,这个list中我们放入的是1,但是1是一个基本数据类型。我们要放入的本应该是包装类型,却放入了基本数据。那么是真的放入了基本的数据类型吗?其实不然。

在jdk1.5之前这样写代码是错误的。jdk1.5之前,我们需要手动装箱,手懂拆箱。

List<Integer> list = new ArrayList<>();
list.add(Integer.valueOf(1));
// list.add(new Integer(1));

但是jdk1.5之后,Java引入自动装箱,自动拆箱的机制。也就是虽然我们是list.add(1),编译器会将1自动装箱成Integer数据类型,然后放入list中。这一点虽然从原码上看不出来,但是反编译Java的文件,我们甚至可以看出编译器调用的是Integer.valueOf(1)这个方法。

那什么又是拆箱呢?比如如下的代码

List<Integer> list = new ArrayList<>();
list.add(1);
int a = list.get(1);

从上面的知识我们应该知道了,list.get()返回的应该是Object的子类Integer,但是我们确实使用int类型来接受返回值的,这就有点奇怪了。其实这就是自动拆箱。通过反编译Java class文件,我们可以知道调用的intValue方法。也就是Java编译器自动调用intValue方法将Integer中封装的int数据赋值给了左面的int变量。

进一步研究装箱与拆箱

通过上面的研究,我们已经知道了Java中的基本类型和包装类是可以直接相互赋值的。比如一下的代码都是正确的

int a = Integer.valueof(3);
Integer b = 3;

那么除了赋值,加减乘除有如何呢?比如下面代码。

Integer a = 3;
int b = a + 4;
Integer c = a + b;

上面的代码都是正常运行的,可以说明Java的自动装箱拆箱机制还是非常聪明的,基本上和使用原来的基本数据类型一样。为啥是基本上???原因下面再说。

我们先看看第三行代码,a是int,b是Integer。他们两个是不同的类型的。那么Java是装箱呢?还是拆箱呢?这个问题其实不难回答。加入说是装箱的话,变成了两个Integer相加,不过Java中是没有运算符重载的,两个对象加个毛线啊,是加不了的。(我知道String对象是可以加的,不过那个也不算是运算符重载实际上。。。)。所以说我们只能拆箱,变成两个int相加,然后将结果装箱赋值给左面的Integer对象。

由此我们可以知道,当包装类在进行加减乘除的运算的时候,会拆箱成基本的数据类型进行运算。

上面说了使用包装类其实并不能和使用基础数据类型完全一模一样,下面来举了例子。

short a = 1;
short b = 2;
a += b; // if it is 'a = a + b', it will be error!

Short a = 1;
Short b = 2;
a += b; // Error!!

上面的使用基本数据类型是对的,下面的是错误的。可能很多人都看不出来对与错,大概率是因为不懂+=这个符号的含义。其实上面的a += b就是相当于a = (short)(a + b);两个short值相加是一个int,这个知识点确实是很多人的盲区。int虽然不可以直接赋值给short,但是+=自带了一个强转,而int是可以强转为short的。再看看下面的代码,a += b是相当于a = (Short)(a + b);这里我们是要将一个int的值转为一个Short,这明显是不可能的。所以说这行代码将会报错了。综上所述,可以使用基本数据类型的时候,我们非要使用他的包装类,这不是闲得蛋疼嘛。别没事找事干就不会有什么问题。

不过还有一个特殊的云算符==,这个符号似乎是既可以应用于对象之间也可以应用于基本数据之间。对象还有一个equals()方法与==的作用相似。之前我写过东西谈过这个问题了。这里不再赘述这个问题了。举例说明。

Integer a = 3;
Integer b = 3;
int c = 3;
System.out.println(a == b); // true
System.out.println(a == c); // true
System.out.println(a.equals(b)); // true
System.out.println(a.equals(c)); // true

可以看到上面的三个结果都是true。看一下Integer中的equals()方法的源码。

public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}

可以看到是做了重写的(重写了Object的equals()方法)。而且是比较包装类中的值。

那么现在来看一下第五行的代码,一个int值和Integer类对象进行==比较。究竟是将int装箱呢?还是将Integer拆箱呢?这两种方法好像是都可以的,毕竟无论是对象还是基本数据类型都是可以使用==操作符的。不过Java采用的是将Integer拆箱。为什么呢?知道==equals()的区别的人应该都不会意外。对象的==比较的是地址(引用),如果把int装箱,这个地址都不是之前我们可以决定的,又有什么比较的意义呢?

不过现在你可能有所发现,==比较的是地址,为何第四行输出的也是true?? 这个就是另外的一个问题了。且听下面慢慢道来。

包装类的缓存

之前我们或许都遇到过关于String缓存的例子。(上面我也谈到了缓存这一块的知识了).

String a = "Hello World";
String b = new String("Hello World");
String c = "Hello World";
String d = new String("Hello World");
System.out.println(a == b); // false
System.out.println(a == c); // true
System.out.println(b == d); // false

上面的第四行和第五行的输出结果相异,主要就是因为String常量池。我们使用String xx = “xxx;”的方式声明一个String变量的时候,系统会将这个“xxx”放入常量池。(毕竟String是一个不可变的对象),然后将地址赋给xx。下面如果还有String变量的值和这个常量池中相等的话,就不会新建String对象,会直接返回常量池中的String对象。所以说上面的ac的地址相同的。使用new String("xxx");的方式声明的String对象虽然也是不可变的对象,不过他位于堆中而不是常量池中。所以说他们的地址是不相等的。

那么上面说提及的包装类是不是也是这么个情况呢?写个代码测试一下.

public class Test {
    public static void main(String[] args) {
        Integer a = -200;
        Integer b = -200;
        Integer c = -100;
        Integer d = -100;
        Integer e = 100;
        Integer f = 100;
        Integer g = 200;
        Integer h = 200;
        System.out.println(a == b); // false
        System.out.println(c == d); // true
        System.out.println(e == f); // true
        System.out.println(g == h); // false
    }
}

在我写这个代码的时候,idea就一直提示我,不要使用==用于包装类的比较,建议使用equals()。这个和String的比较倒是非常类似的,都是建议使用equals()方法。不过这个结果就和String类非常的不类似了,竟然不是都输入true,还有的输入的是false

这到底是什么原因呢?我开始查阅资料,和观察Integer的源码。发现了这样的一个类。

    /**
     * Cache to support the object identity semantics of autoboxing for values between
     * -128 and 127 (inclusive) as required by JLS.
     *
     * The cache is initialized on first usage.  The size of the cache
     * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
     * During VM initialization, java.lang.Integer.IntegerCache.high property
     * may be set and saved in the private system properties in the
     * jdk.internal.misc.VM class.
     *
     * WARNING: The cache is archived with CDS and reloaded from the shared
     * archive at runtime. The archived cache (Integer[]) and Integer objects
     * reside in the closed archive heap regions. Care should be taken when
     * changing the implementation and the cache array should not be assigned
     * with new Integer object(s) after initialization.
     */

    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer[] cache;
        static Integer[] archivedCache;

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

            // Load IntegerCache.archivedCache from archive, if possible
            VM.initializeFromArchive(IntegerCache.class);
            int size = (high - low) + 1;

            // Use the archived cache if it exists and is large enough
            if (archivedCache == null || size > archivedCache.length) {
                Integer[] c = new Integer[size];
                int j = low;
                for(int i = 0; i < c.length; i++) {
                    c[i] = new Integer(j++);
                }
                archivedCache = c;
            }
            cache = archivedCache;
            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

其实这个代码一点都看不懂,不过看看上面的文档就可以的。文档中说

  • 缓存以支持JLS要求的自动装箱的对象标识语义,其值介于-128和127(含)之间。
  • 首次使用时会初始化缓存。 缓存的大小可以由{@code -XX:AutoBoxCacheMax = <size>}选项控制。
  • 在VM初始化期间,可以设置java.lang.Integer.IntegerCache.high属性并将其保存在jdk.internal.misc.VM类的私有系统属性中。
  • 警告:缓存是使用CDS存档的,并在运行时从共享存档中重新加载。 归档的缓存(Integer [])和Integer对象位于封闭的归档堆区域中。 更改实现时应注意,初始化后不应为缓存数组分配新的Integer对象。

第一点就是告诉我们,如果我们不指定的话,系统将会默认缓存[-128, 127]之间的包装类。后面就是我们修改缓存大小的方式。

这样我们就明白了为什么上面的有一些是是输出的true,而有一些是输出的false。原因是在于-200和200已经不在Integer的缓存的范围中了,所以需要另外的构建对象,因而返回false。而-100和100在缓存的范围当中,所以直接获取了缓存的值。因而返回true

我们也可以来看看Integer.valueOf()方法,这个方法中也有缓存的体现。

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

上面的if当中的范围就是缓存的范围。如果不在缓存中的范围的时候才去new一个Integer对象。

那么除了Integer类,其余的包装类的缓存范围又是多少呢?

包装类型 缓存范围 包装类型 缓存范围
Boolean true, false(全部) Byte [-128, 127](全部)
Short [-128, 127] Character [0, 127]
Integer [-128, 127] Long [-128, 127]
Float Double

其实他们的缓存值还是非常有规律的。浮点值类型没有缓存值,布尔类型只有两个值当然都要缓存。至于其他的都是[-128, 127]。不过因为Character是没有负值的,所以去掉了负数的部分。

其实缓存值中只有Integer是最特殊的。我们是可以修改Integer缓存的上限的。(127是最低的上限)。Integer的下限-128是固定的不能修改的。至于如何修改上限,这里就不折腾了。可以干,但是没有必要。

总结

Java当中的包装类可谓是一个非常重要而且非常常用的一个类。关键是用到的时候你或许还没意识到自己已经用到了这个类了,这就是所谓的自动装箱和自动拆箱。既然这个类这么常用,我们就势必应该缓存这个类的常用的部分,以便后面多次使用节省时间和内存,这就是所谓的包装类的缓存。


一枚小菜鸡