JDK源码阅读 (一) : 包装类 Integer

本文详细介绍了Java中Integer对象的创建、装箱拆箱机制,包括自动装箱时的缓存策略,以及Integer类中成员变量value的不可变性。此外,还探讨了Java为何需要包装类,整数转换为字符串和字符串转换为整数的方法,以及整数的二进制表示和类型转换规则。同时,简要提及了Long、Short、Boolean、Byte类的相似机制。
摘要由CSDN通过智能技术生成

1. Integer

1.1 自动装箱和自动拆箱

示例代码 ( jdk 8.0 ):

//通过构造器创建一个Integer对象
Integer value1 = new Integer(3);
//通过自动装箱获取一个Integer对象
Integer value2 = 3;

①使用构造器创建Integer对象

//调用Integer类的构造方法时,首先依次调用父类的构造方法
//然后调用本类的构造方法,给成员value赋值
public Integer(int value) {
    this.value = value;
}
//最后在堆中新建了一个Integer实例

自动装箱(auto-boxing)得到Integer对象

自动装箱时调用静态工厂方法valueOf(),其中使用了缓存机制。默认缓存[-128, 127]区间的数。

//首先调用Integer类的静态方法
public static Integer valueOf(int i) {
  	//判断该值是否位于IntegerCache.low和IntegerCache.high之间,默认是-128到127之间
  	//IntegerCache是内部类,此时还没加载,当使用到这个类时将会加载,加载时执行一些初始化操作
    if (i >= IntegerCache.low && i <= IntegerCache.high)
      	//如果在[-128,127]这个区间内,则直接返回缓存池中的对象,无需新建
        return IntegerCache.cache[i + (-IntegerCache.low)];
  	//如果不在该区间,则需要按照第一种方式在堆中新建一个对象
    return new Integer(i);
}

//内部类IntegerCache用于管理Integer对象的缓存池,这样做是为了更快速地获取到常用的Integer对象
private static class IntegerCache {
  		//缓存的最小值为-128
        static final int low = -128;
  		//缓存的最大值
        static final int high;
  		//cache数组用于保存缓存的Integer对象
        static final Integer cache[];
				
  		//静态代码块,在本类被加载的时候初始化阶段被执行	
        static {
          	// high value may be configured by property
            // 缓存的最大值可以通过属性进行配置,这里默认最大是127
            int h = 127;
          	//从属性中获取关于缓存最大值的字符串
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
          	//如果获取到了
            if (integerCacheHighPropValue != null) {
                try {
                    //将该字符串转换成int类型
                    int i = parseInt(integerCacheHighPropValue);
                  	//缓存的最大值应该>=127
                    i = Math.max(i, 127);
                  // 数组的最大容量是Integer.MAX_VALUE,所以要保证high-low+1<= Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                  	//如果该字符串无法解析为一个int类型的值,则忽视
                }
            }
          	//设置缓存的最大值,默认值or配置的值
            high = h;
			//新建一个Integer缓存数组
            cache = new Integer[(high - low) + 1];
            int j = low;
          	//依次给数组中放入对应的Integer对象,作为缓存
            for(int k = 0; k < cache.length; k++)
              	//数组第0号位置存放-128,因此在取对象的时候存在128的偏移量,也就是说0存放在128号位置
                cache[k] = new Integer(j++);

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

        private IntegerCache() {}
    }

自动拆箱(unboxing)时,调用实例方法intValue()。

比如执行下面代码:

Integer value1 = 3;
//拆箱
int value2 = value1;

对象value1执行实例方法intValue()。

public int intValue() {
    return value;
}

装箱拆箱发生时机:发生在前端编译阶段,也就是将源码翻译为字节码的时候,因为它们实际上都是调用了类的静态或非静态方法完成的。

1.2 成员变量value的不可变性

我们都知道,String类中的char型数组被定义为private final,是不可变的。这个特性在Integer类中也有相应的体现,那就是成员变量value被定义为private final int,value中保存了包装类所包装的原始数据类型值。

/**
 * The value of the {@code Integer}.
 *
 * @serial
 */
private final int value;

为什么做这样的定义呢?

主要是为了保证一些安全问题。在Integer类中包含静态方法getInteger(…),可以将某个系统属性的名称作为参数传入,然后以Integer对象的方式返回属性的值。

/**
     * Determines the integer value of the system property with the
     * specified name.
     ...
*/     
public static Integer getInteger(String nm) {
    return getInteger(nm, null);
}

如果我们可以轻易修改该Integer对象内部的值,那就表明我们可以轻易修改某个系统属性。当我们用某属性来设置某个服务的端口,如果我们可以轻松获取到该Integer对象,并改为其他数值,这会严重影响产品的可靠性。

1.3 为什么java需要包装类

与C++不同,java中的泛型实际上是一种伪泛型,仅仅使用了类型擦除。在前端编译期将泛型类型全部转换为对应的特定类型,这个特定的类型可以是Object类型或其子类。当泛型类型仅仅是< T >时,该类型将被全部转换为Object类型。

通过任意一个带泛型的类可以验证,比如ArrayList< T >:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
  /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access
}

源码中,ArrayList 容器使用一个Object数组来存放添加进来的元素。

因此,泛型类型必须保证可以转换为Object类型,原始数据类型并不是Object类的子类,所以不能用于泛型。

1.4 如何将整数转换成字符串

该方法传入两个参数,第一个是要转换的int值,第二个是转换后的字符串是以哪种进制表示。

public static String toString(int i, int radix) {
  	//如果传入的进制数小于2或者大于36,一律按10处理
    if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX)
        radix = 10;

    //如果是以十进制表示,则可以以更快的方式求得结果
    if (radix == 10) {
      	//调用重载的toString方法,见下文
        return toString(i);
    }
		//如果不是10进制,新建一个33位的字符数组
    char buf[] = new char[33];
  	//负数标识
    boolean negative = (i < 0);
  	//初始填充位置为数组最后一位,依次往前填充
    int charPos = 32;
		//如果是正数,则改为负数,以统一操作
    if (!negative) {
        i = -i;
    }
		//当该数大于进制时
    while (i <= -radix) {
      	//每次对-i作进制的取模操作,得到最后一位,然后查表得到对应的字符,再填充到数组中
        buf[charPos--] = digits[-(i % radix)];
      	//迭代下去
        i = i / radix;
    }
  	//该数小于进制时,可以直接填充
    buf[charPos] = digits[-i];
		//最后填上符号
    if (negative) {
        buf[--charPos] = '-';
    }
		//将字符数组有值的位置开始到末尾,转换为String返回
    return new String(buf, charPos, (33 - charPos));
}
public static String toString(int i) {
  	//如果该值等于Integer的最小值,则直接返回对应字符串
    if (i == Integer.MIN_VALUE)
        return "-2147483648";
  	//如果是负数,则使用stringSize方法计算该值的非负数值部分的宽度,然后加上符号位1,见下文
    int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
  	//创建满足宽度的字符数组
    char[] buf = new char[size];
  	//将该数以字符形式填充到数组中,见下文
    getChars(i, size, buf);
  	//以String的形式返回
    return new String(buf, true);
}

更快速地计算一个正整数作为字符串时的宽度(以空间换时间)。

final static int [] sizeTable = { 9, 99, 999, 9999, 99999, 999999, 9999999,
                                  99999999, 999999999, Integer.MAX_VALUE };
// x必须是正整数
static int stringSize(int x) {
    for (int i=0; ; i++)
      	//依次比较x和9,99,999,...的大小,当x小于等于其中一个数时,说明宽度与这个数一致
        if (x <= sizeTable[i])
            return i+1;
}

使用getChars方法,将数值以字符形式填充到数组中。

static void getChars(int i, int index, char[] buf) {
    int q, r;
  	//当前填充位置,初始时刻指向数组的末尾的后一位
    int charPos = index;
  	//定义符号位
    char sign = 0;
		//如果是负数,则符号位更新为'-'
    if (i < 0) {
        sign = '-';
      	//将负数改为正数,方便后续计算
        i = -i;
    }

    // Generate two digits per iteration
  	//对于超过65536的数,每次循环中对最后两位形成字符并填充进数组
    while (i >= 65536) {
      	//i先除100,去掉最后两位
        q = i / 100;
		// 下式等价于:r = i - (q * 100),左移和加减运算的效率高于乘法,源码中大量使用移位运算来替代乘除运算
        r = i - ((q << 6) + (q << 5) + (q << 2));
      	//将i更新为q
        i = q;
    //DigitOnes数组中的保存着字符0,1,2,3,4,5,6,7,8,9...总共重复该序列10次,以此取得两位数的个位
        buf [--charPos] = DigitOnes[r];
      //DigitTens数组中依次保存字符10个0,10个1,...,10个9,以此取得两位数的十位
        buf [--charPos] = DigitTens[r];
    }

    //当i小于65536时
    for (;;) {
      	//等价于i除以10
        q = (i * 52429) >>> (16+3);
      	//等价于r = i-(q*10),取得个位数
        r = i - ((q << 3) + (q << 1));
      	//填充
        buf [--charPos] = digits [r];
      	//更新i
        i = q;
      	//当i为0时,填充完毕
        if (i == 0) break;
    }
  	//如果是负数,再加上符号位
    if (sign != 0) {
        buf [--charPos] = sign;
    }
}

1.5 如何将字符串转换成整数

//第二个参数radix表示该字符串是以哪种进制表示的,返回原始数据类型int
public static int parseInt(String s, int radix)
            throws NumberFormatException
{
    /*
     * WARNING: This method may be invoked early during VM initialization
     * before IntegerCache is initialized. Care must be taken to not use
     * the valueOf method.
     */
		//空值判断
    if (s == null) {
        throw new NumberFormatException("null");
    }
		//进制非法
    if (radix < Character.MIN_RADIX) {
        throw new NumberFormatException("radix " + radix +
                                        " less than Character.MIN_RADIX");
    }
		//进制非法
    if (radix > Character.MAX_RADIX) {
        throw new NumberFormatException("radix " + radix +
                                        " greater than Character.MAX_RADIX");
    }
		//result用于保存结果
    int result = 0;
  	//符号初始化为非负
    boolean negative = false;
    int i = 0, len = s.length();
  	//转换后的int值限制为-(2^31-1),而int取值范围是[-2^31,2^31-1]
    int limit = -Integer.MAX_VALUE;
  	// multmin也是作为一个限制条件,其作用下面代码可知
    int multmin;
  	//digit将作为字符串中每一位的数值表示
    int digit;
		
    if (len > 0) {
      	//取得字符串的第一个字符
        char firstChar = s.charAt(0);
      	//如果第一个字符不是数字
        if (firstChar < '0') {
          	//如果是负号
            if (firstChar == '-') {
              	//设置符号为负
                negative = true;
              	//负数时,限制最小值为-2^31
                limit = Integer.MIN_VALUE;
            } else if (firstChar != '+')//如果不是正号也不是负号,则抛出异常
                throw NumberFormatException.forInputString(s);
          	//不可以只有一个正负号
            if (len == 1) 
                throw NumberFormatException.forInputString(s);
            i++;
        }
      	// multmin作为限制,只比limit少一位,也就是limit右移一个进制位后的值
        multmin = limit / radix;
        while (i < len) {
            // Accumulating negatively avoids surprises near MAX_VALUE
          	//result中保存的是负数,为什么是负数呢?是因为负数比正数多一个,用负数可以统一操作
          	//将第i个位置的字符,转换成对应进制数的单个数值
            digit = Character.digit(s.charAt(i++),radix);	
          	//返回的digit<0表示获取失败,说明该字符不符合进制数的条件,比如字符‘9’不符合八进制
            if (digit < 0) {
                throw NumberFormatException.forInputString(s);
            }
//首先对result进行溢出判断,如果result比multmin还小,那么result乘上一个进制数再减去digit后,必然会溢出
          //比如parseInt("21474836471",10),当已经解析到result=-2147483647,最后还剩一位是1,
          //而multmin只是-214748364,此时result已经小于multmin,那么再减去digit,肯定溢出
            if (result < multmin) {
                throw NumberFormatException.forInputString(s);
            }
          	//上一轮循环的结果result乘上一个进制数
            result *= radix;	
						//digit即将累积到result上去,但是要提前判断result-digit是否小于所限制的最小值,
          	//如果是,则已经脱离了int取值范围
     //比如parseInt("2147483648",10),此时result=-214748364,digit=8,如果让result-digit,必然溢出
            if (result < limit + digit) {
                throw NumberFormatException.forInputString(s);
            }
          	//将digit累积到result上去
            result -= digit;
        }
    } else {
        throw NumberFormatException.forInputString(s);
    }
  	//最后加上符号
    return negative ? result : -result;
}

1.6 整数表示方式

jdk源码中大规模使用移位运算来代替乘除运算,还会使用到与/或运算等计算方法。

要搞懂这些运算,首先应该了解java中的整数是如何用二进制表示的。

java中的整数都是有符号的整数,分为负整数、0、正整数,取值范围是[-2 ^ 31, 2 ^ 31 - 1]。

有符号整数以二进制补码的方式表示。负数的最高位是1,非负数最高位是0。

该整数的十进制的值 等于 二进制形式下每一位的值对应的阶数然后累加的和,二进制形式下的高位全部填充为最高有效位的值。

比如:

① int i = 12,其二进制补码为1100,因为有12 = 2 ^ 3 + 2 ^ 2,由于是正数,最高有效位为0,通过符号扩展,高位全部填充为0,这里已略去。

代码验证:

//toBinaryString方法将一个int类型的数转换成对应的二进制数的字符串
String s = Integer.toBinaryString(12);
System.out.println(s);// 1100

②int i = -12,其二进制补码为11111111111111111111111111110100,因为有-12 = -2 ^ 4 + 2 ^ 2,由于是负数,高位全部填充为最高有效位的值1。

代码验证:

String s = Integer.toBinaryString(-12);
System.out.println(s);// 11111111111111111111111111110100

值得注意的移位运算

java语言中的移位运算分为三种:左移运算算术右移逻辑右移

  • 左移k位 i << k :将二进制表示下的 i 的右端补上 k 个0;
  • 算术右移k位 i >> k :在二进制表示下的 i 的左端补上 k 个最高有效位的值;
  • 逻辑右移k位 i >>> k :在二进制表示下的 i 的左端补上 k 个0。

更详细的介绍:https://blog.csdn.net/Longstar_L/article/details/109078464

1.7 类型转换

Integer类中提供进行类型转换的方法,如:

public short shortValue() {
    return (short)value;
}

public byte byteValue() {
    return (byte)value;
}

public long longValue() {
    return (long)value;
}

①当把位数更多的int类型转换为位数较少的byte或者short类型时,会发生位截断。把32位截断为8位或者16位,也就是把高24位或者高16位全部去掉,然后继续以补码方式解释转换后的byte或者short类型。

下列代码对截断作出了验证:

int i = 53191;
byte bx = (byte) i; // bx为-57
short sx = (short) i; // sx为-12345

i 的二进制表示为00000000 00000000 11001111 11000111。

  • 当把前24位截断后,剩余的位是11000111,以补码方式解释后(第一位是符号位)的十进制数值为 -27 + 26+ 22 + 2 + 1 = 57。因此,bx的值为 -57。
  • 当把前16位截断后,剩余的位是11001111 11000111,以补码方式解释后(第一位是符号位)的十进制数值为-12345。因此,sx的值为 -12345。

因此,位数较多的类型缩短为位数较少的类型,值发生变化。

接下来,我们再把short类型的sx转换回int类型,看看会发生什么。

int i = 53191;
short sx = (short) i;
byte bx = (byte) i;// sx为-12345
//short转换会int
int j = sx; // j为 -12345

在把位数较少的short转换成位数较多的int类型时,发生符号扩展,即把short的最高有效位,填充到全部的高16位。这里,sx 的最高有效位为1,则在int的高16位全部填1。最后,得到int类型的 j 等于 -12345。

因此,位数较少的类型拉长为位数较多的类型,值不变。

2. Long

Long类中也存在缓存池,缓存的区间是[-128,127]。

private static class LongCache {
    private LongCache(){}
		//区间[-128,127]
    static final Long cache[] = new Long[-(-128) + 127 + 1];

    static {
        for(int i = 0; i < cache.length; i++)
            cache[i] = new Long(i - 128);
    }
}

当调用静态方法Long.valueOf()时,也会首先尝试从缓存池中获取对象。

public static Long valueOf(long l) {
    final int offset = 128;
    if (l >= -128 && l <= 127) { // will cache
        return LongCache.cache[(int)l + offset];
    }
    return new Long(l);
}

3. Short

Short类同样存在缓存机制,缓存范围[-128, 127]。

public static Short valueOf(short s) {
    final int offset = 128;
    int sAsInt = s;
    if (sAsInt >= -128 && sAsInt <= 127) { // must cache
        return ShortCache.cache[sAsInt + offset];
    }
    return new Short(s);
}

4. Boolean

boolean只有true和false两种值,所以Boolean类中仅使用了两个成员对象作为缓存。

public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);

当自动装箱时,直接判断传入的布尔值与哪个包装对象相等,然后返回对应的对象。

public static Boolean valueOf(boolean b) {
  	//判断b == TRUE时,TRUE调用booleanValue()方法自动拆箱为true
    return (b ? TRUE : FALSE);
}

5. Byte

Byte也存在缓存机制,缓存范围[-128, 127]。

private static class ByteCache {
    private ByteCache(){}

    static final Byte cache[] = new Byte[-(-128) + 127 + 1];

    static {
        for(int i = 0; i < cache.length; i++)
            cache[i] = new Byte((byte)(i - 128));
    }
}

6. Double

6.1 获取Double对象的方式

而Double类中没有缓存机制,当使用自动装箱机制时,调用valueOf方法。

public static Double valueOf(double d) {
  	//仍然使用构造器方式新建对象
    return new Double(d);
}

6.2 了解NaN

NaN全称Not a Number(非数),表示未定义或不可表示的值,用于浮点运算中处理的错误情况。

在Double类中,有一个成员变量就是NaN。

/**
 * A constant holding a Not-a-Number (NaN) value of type
 * {@code double}. It is equivalent to the value returned by
 * {@code Double.longBitsToDouble(0x7ff8000000000000L)}.
 */
public static final double NaN = 0.0d / 0.0;

这里用0.0除以0.0表示一个NaN。

实际上,有三种方式可以返回NaN:

  • 至少有一个参数是NaN的运算

  • 不定式

    • 下列除法运算:0/0、∞/∞、∞/−∞、−∞/∞、−∞/−∞
    • 下列乘法运算:0 * ∞、0 * −∞
    • 下列加法运算:∞ + (−∞)、(−∞) + ∞
    • 下列减法运算:∞ - ∞、(−∞) - (−∞)
  • 产生复数结果的实数运算。例如:

    • 对负数进行开偶次方的运算
    • 对负数进行对数运算
    • 对正弦或余弦到达域以外的数进行反正弦或反余弦运算

Double类中有一个方法用于判断一个double类型的值是否是NaN。

public static boolean isNaN(double v) {
    return (v != v);
}

由上述代码可知,如果一个数是NaN,则该数不等于自身。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值