最近开始对Java进行系统性的学习,学习过程中,对很多之前很基础的知识有了新的理解和认识,随即决定以阅读源码的方式进行深入的学习探讨。在阅读源码的过程中我发现,源码不仅包含了众多大牛们的编程思想,更包含了许多我们平时并不在意的在语言层面的细节和原理,在这里把我个人的学习成果和经验分享给诸位。
数据类型可以说是一门编程语言中的最基础的部分之一。
在Java语言中,基本数据类型包括:“byte”、“short”、“int”、“long”、“float”、“double”、“char”、“boolean”。
而为了在某些实际应用中,更加方便地使用这些数据类型,在Java语言中还封装了上述八种基本数据类型的对象包装器(wrapper)类。
也就是我们熟悉的大写字母开头的这些数据类型:“Byte”、“Bhort”、“Integer”、“Long”、“Float”、“Double”、“Character”、“Boolean”。
在第一章,我们先来深入了解一下“Integer”类。
首先,我们最常用的就是 toString() 这个方法,主要功能是将一个整型数转换成十进制字符串形式输出。相信很多人也曾经自己实现过类似的功能,或者做过类似的算法题,但如何做到机器层面的较优算法,源码给出了答案。
这个方法有多个重载,可静态调用也可对象调用。对象调用最终使用静态方法 Integer.toString(int i)实现。
首先看代码:
public static String toString(int i) {
int size = stringSize(i);
if (COMPACT_STRINGS) {
byte[] buf = new byte[size];
getChars(i, size, buf);
return new String(buf, LATIN1);
} else {
byte[] buf = new byte[size * 2];
StringUTF16.getChars(i, size, buf);
return new String(buf, UTF16);
}
}
首先通过整型数求出转化后的字符串长度,这个一会再说,可以看到下面两个条件体里代码基本类似。
它们又是什么意思呢?
"COMPACT_STRINGS" 是JDK9加入的特性,目的是在某些场景下压缩字符串以获得程序性能上的提升。在绝大多数情况下,字符串中只包含了数字、字母等这些简单的字符,用两个字节编码存储这样的字符时,大部分的空间都是被浪费掉的。所以在默认情况下,Java使用单字节字串来进行存储。
更详细的关于压缩字符串的解释在下面对 Aleksey Shipilev(Oracle) 的 采访记录 中。
除了在字符串存储上的优化,现在让我们回到刚刚跳过的部分:求字符串长度。
首先贴上代码:
static int stringSize(int x) {
int d = 1;
if (x >= 0) {
d = 0;
x = -x;
}
int p = -10;
for (int i = 1; i < 10; i++) {
if (x > p)
return i + d;
p = 10 * p;
}
return 10 + d;
}
如何从一个整型数求得其字符串形式的长度?上述代码给出了一眼看下来并看不懂的答案。
现在让我们认真地阅读分析一下。
首先定义了 d 这个变量,根据上下文,这个表示的是符号位。如果 x 为负数,则负号要占据最终字符串的一位长度;如果 x 为正数,则符号位不占用字符串位置(默认正号不显示)。
而在判断 x 是否为正数的条件体内,还有一句语句。
“x = -x”;
意思是,如果 x 为正数的时候,把 x 变号,暂且把这个放一边,继续往下看。
可以看到,代码中用了一个临时变量 p 来作为比较位数的标记。
按惯常的思路,我们可能会想到检查 x / 10 是否大于零,然后循环计数,知道 x / 10 等于零时,计数值即为 x 的位数。
但曾经在学习C语言的时候,我们就听过,在CPU上,除法的效率是低于乘法,这也就是为什么这里采用的是将 p 循环乘以 10 来与 x 作比较。看,算法优化的细节无处不在。
再回头来看,为什么是当 x 为正数的时候,把 x 统一变为负数来比较,而不是把 x 统一变成正数来比较呢?
是因为作者心情选择?还是因为CPU效率上的原因?就留给读者们自己去思考了。
最后一组代码,把整型数转成字节数组:
static int getChars(int i, int index, byte[] buf) {
int q, r;
int charPos = index;
boolean negative = i < 0;
if (!negative) {
i = -i;
}
// Generate two digits per iteration
while (i <= -100) {
q = i / 100;
r = (q * 100) - i;
i = q;
buf[--charPos] = DigitOnes[r];
buf[--charPos] = DigitTens[r];
}
// We know there are at most two digits left at this point.
q = i / 10;
r = (q * 10) - i;
buf[--charPos] = (byte)('0' + r);
// Whatever left is the remaining digit.
if (q < 0) {
buf[--charPos] = (byte)('0' - q);
}
if (negative) {
buf[--charPos] = (byte)'-';
}
return charPos;
}
首先,这个方法是建立在单字节字串的基础上的,其中比较重要的常量如下:
static final byte[] DigitOnes = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
} ;
static final byte[] DigitTens = {
'0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
'1', '1', '1', '1', '1', '1', '1', '1', '1', '1',
'2', '2', '2', '2', '2', '2', '2', '2', '2', '2',
'3', '3', '3', '3', '3', '3', '3', '3', '3', '3',
'4', '4', '4', '4', '4', '4', '4', '4', '4', '4',
'5', '5', '5', '5', '5', '5', '5', '5', '5', '5',
'6', '6', '6', '6', '6', '6', '6', '6', '6', '6',
'7', '7', '7', '7', '7', '7', '7', '7', '7', '7',
'8', '8', '8', '8', '8', '8', '8', '8', '8', '8',
'9', '9', '9', '9', '9', '9', '9', '9', '9', '9',
} ;
这里又是一个很巧妙的空间换时间的技巧。不得不说,大牛们满脑子都是骚操作。
先说这两个常量,结合代码我们可以看到,将一个两位整数同时带入 DigitOnes 以及 DigitTens 中时,在前者,我们恰好得到这个整数的个位,后者恰好得到这个整数的十位。
结合代码,这样做的好处是一次循环我们就可以得到连续的两位数字转成的字节,直接减少了一半的循环次数。
当然了,如果同时处理三个或三个以上,代码就没法看了。
最后再处理剩下至多两位数字以及符号位,就大功告成了。
通过以上对 Integer 中很小的一个方法 toString()的阅读分析,我们发现,源码里真的有很多我们应该学习的细节和原理,再接下去的文章中,我们还将对 Integer 其他部分的源码进行深入的赏析,下次再会。