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,则该数不等于自身。