拆箱装箱都不知道?(从源码带你理解包装类)

众所周知,java 是一门面向对象的高级语言。但是 java 中的基本类型不能作为对象使用,为了解决对象的调用问题,为每个基本类型创造了对应的包装类型。
先来看一道包装类的题目吧

int a = 10;
Integer b = 10;
System.out.println(a == b);        ①
System.out.println(b.equals(a));   ②
Integer c = 100;
Integer d = 100;
System.out.println(c == d);        ③
System.out.println(c.equals(d));   ④
Integer e = new Integer(100);
Integer f = new Integer(100);
System.out.println(e == f);        ⑤
System.out.println(e.equals(f));   ⑥
Integer g = 1000;
Integer h = 1000;
System.out.println(g == h);        ⑦
System.out.println(g.equals(h));

在看知识点前先看一下习题,看看是否存在不了解的点,以便对症下药
这些题考查的都是 java 中很基础的一类概念
包括包装类与基本类型的转换,包括自动拆箱装箱
以及 == 和 equals 的区别

给上答案
①true ②true ③true ④true ⑤false ⑥true ⑦false ⑧true
以下示范都以Integer为例,其他包装类型与之无明显差异,大家看会了 Integer 其他包装类也就都能理解了

为什么需要包装类

  • 基本类的值不属于对象,因此基本类型只存在一个值,没有各种属性,也没有各种方法可以调用。
  • 很多场景需要使用对象,比如 java 中的集合,所以为了能使基本类型的值也能存入集合中,或者用于其他类中,则需要把它们包装成对象。
  • 每个非基本类都继承自基类 Object ,所以天生神力,自带一身方法。但是基本类型没有方法可以调用,用包装类可以继承 Object 方法,也可以添加自身方法。
  • 非基本类型变量不赋值默认为 null,但基本类型没有 null 值,比如 int 默认为 0,Boolean 默认为 false。
  • 在某些场合中,我们可能会希望使用到 null 值,表示某些意义,那基本类型则就做不到了。

包装类与基本类型的转换 (包括自动装拆箱)

JDK自从1.5之后就引入了自动装拆箱的功能,这大大简化了开发人员的代码复杂度,使得对基本类型的代码书写效率大大提高。
但是这也带来了一个问题,很多小白搞不明白基本类型和包装类型,使很多开发人员对此形成误解。

首先,最基本的,用 new 来构造一个 Integer 对象
再用 Integer 的 intValue() 方法返回它的int值

Integer a = new Integer(1);
int b = a.intValue();

但在 java 中一般不提倡 new 一个 Integer 包装类对象
用 Integer.valueOf() 静态方法来获取一个 Integer 对象

a = Integer.valueOf(5);
Integer a = 5; // 也可以写成

而这个方法即是 int --> Integer 的自动装箱方法 (即上面两行代码是等同的)
同理自动拆箱

Integer a = 5;
// int b = a.intValue();
int b = a; // 这行代码与上面一行代码等同

比如之前题目中的①和② 就有许多装拆箱的知识点

int a = 10;
Integer b = 10; // 自动装箱
// 将b自动拆箱与a比较
System.out.println(a == b);
// 将a自动装箱成为对象才能作为equals中的参数
System.out.println(b.equals(a));

但是在自动装箱需要注意一个细节,也就是之前题目中的③④⑦⑧。
在 -128 ~ 127 的数中,也就是 byte 的取值范围,自动装箱时,会从常量池中去取Integer对象,因而包装出的 Integer 对象是同一个对象,不在这个范围内的数字,包装出的 Integer 对象则都是 new 出来的新对象。

Integer c = 100;  // 从常量池中取出Integer对象
Integer h = 1000; // 相当于用new创建出一个新对象

因此在之前题目中值为 100 的 Integer 对象 == 为 true,而值为 1000 的Integer对象则为 false。

基本类型和包装类型与String的转换

由于存在自动装拆箱的功能,所以对于 String 与数字转换时,可以将其转换为基本类型 int,也可以转换为包装类 Integer,通过自动装拆箱即可以正确赋值给变量
String 和 Integer 之间的转换

// String --> Integer
Integer a = new Integer("12345");
// Integer --> String
String s = a.toString();

String 和 int 之间的转换

// String --> int
int a = Integer.parseInt("12345");
// String --> Integer --> int
a = new Integer("12345");
// int --> Integer --> String
String s = a + ""; //自动装箱调用toString()
s = (Integer.valueOf(a)).toString();

包装类基础

基本类 包装类 继承 基本类类信息 包装类对应类信息
byte Byte Number byte.class Byte.TYPE
short Short Number short.class Short.TYPE
int Integer Number int.class Integer.TYPE
long Long Number long.class Long.TYPE
float Float Number float.class Float.TYPE
double Double Number double.class Double.TYPE
char Character Object char.class Character.TYPE
boolean Boolean Object boolean.class Boolean.TYPE

一些关键源码(解释写在注释之中)

首先是 Integer 的范围,与 int 相同,都为 -2的31次方~2的31次方-1。
其中0x表示16进制,但是仔细一看,会发现最小值比最大值大一,这是因为在二进制中负数是用补码表示的,最高位是符号位,0是正,1位负,所以正数最大值没有问题,而负数最小值则根据首位1推出为负数。
对于补码不细讲,只需记住
正数的补码 = 原码
负数的补码 = 原码符号位不变 + 数值位按位取反后+1
或者 = 原码符号位不变 + 数值位从右边数第一个1及其右边的0保持不变,左边安位取反

@Native public static final int   MIN_VALUE = 0x80000000;
@Native public static final int   MAX_VALUE = 0x7fffffff;

构造方法很简单

public Integer(int value) {
    this.value = value;
} // 给指定值创建对象
public Integer(String s) throws NumberFormatException {
    this.value = parseInt(s, 10);
} // 通过字符串解析出值创建对象

装箱方法

public static Integer valueOf(int i) {
    // 如果值在low和high之间 low为-128 high一般默认为127
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        // 则直接返回常量池中的对象
        return IntegerCache.cache[i + (-IntegerCache.low)];
    // 否则用new新建一个Integer对象返回
    return new Integer(i);
}

在源码中我们发现了一个静态嵌套类IntegerCache,它拥有两个 final 值 low 和 high,代表缓存的 Integer 的对象的范围,这样在装箱时可以直接获取,而不用再创建对象。

private static class IntegerCache {
    static final int low = -128;  // 缓存最小值
    static final int high;        // 缓存最大值
    // low到high的Integer对象都被保存在这里 装箱时取
    static final Integer cache[];
    // 根据类加载机制 以下方法块在类初始化时执行
    // 会率先将low到high的Integer保存在cache数组中
    static {
        int h = 127;
        /* 此处省略一些无关代码  */
        high = h;
        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);
    }
    // 私有构造方法 保证类无法创建对象
    // 因为该类仅作为事先缓存Integer对象
    private IntegerCache() {}
}

再看toString()方法

public static String toString(int i) {
    if (i == Integer.MIN_VALUE) //为最小值则直接return
        return "-2147483648";
    // 计算数字的长度
    int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
    // 根据长度创建字符数组
    char[] buf = new char[size];
    // 不断获取字符放入数组中
    getChars(i, size, buf);
    // 根据字符数组返回新字符串
    return new String(buf, true);
}

里面的 stringSize 方法很有趣,很多情况一般会用循环除10来计算长度,不众所周知,JDK源码是非常讲究效率的,它直接保存了在 MAX 范围内的所有每个长度最大值,直接依次比较,十分高效。

final static int [] sizeTable = { 9, 99, 999, 9999, 
    99999, 999999, 9999999, 
    99999999, 999999999, Integer.MAX_VALUE };
static int stringSize(int x) {
    for (int i=0; ; i++)
        if (x <= sizeTable[i])
            return i+1;
}

总之对于我们来说,java 中的每一个基础类我们都应把它学习透彻,理解它的执行原理,尽量对源码中的关键方法都能掌握,这样在我们的编程生涯中,才能做到游刃有余,不犯一些低级错误。
同时,jdk 的源码都是十分优秀,高效的,在 jdk 版本不断迭代的过程中,很多关键类的代码都会进行优化和调整,确保程序的高效执行。

文章到这里就结束啦,我还是大学生哦,喜欢学习的小伙伴可以评论交流,或者加关注,一起学习更轻松。
素质三连。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章