-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.xml
20 lines (20 loc) · 20.2 KB
/
search.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[Java源码剖析从入门到放弃【类型篇】 — Integer(二)]]></title>
<url>%2F2018%2F01%2F03%2FJava%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E6%94%BE%E5%BC%83%E3%80%90%E7%B1%BB%E5%9E%8B%E7%AF%87%E3%80%91%20%E2%80%94%20Integer(%E4%BA%8C)%2F</url>
<content type="text"></content>
</entry>
<entry>
<title><![CDATA[Java源码剖析从入门到放弃【类型篇】 — Integer(一)]]></title>
<url>%2F2017%2F12%2F17%2FJava%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E6%94%BE%E5%BC%83%E3%80%90%E7%B1%BB%E5%9E%8B%E7%AF%87%E3%80%91%E2%80%94%20Integer(%E4%B8%80)%2F</url>
<content type="text"><![CDATA[文章仅供参考,转载请注明出处: https://cccc0der.github.io/ 前言 Java是一门面向对象的语言,从接触OOP那一刻起,程序员就被告知在OOP的世界中,一切皆是对象。 由此,某书上说程序员是出轨率最高的职业看来也不是没有道理,因为放眼望去,在我们程序员的世界里对象太多了,而我们最怕的也是找不到对象。 在这样的环境下,int,float,double等这样的基本类型单身狗,显然不符合Java特征,那么来看一下Java是如何为这些单身狗设计相应对象的吧。 Number类123456789public abstract class Number implements java.io.Serializable { private static final long serialVersionUID = -8742448824652078965L; public abstract int intValue(); public abstract long longValue(); public abstract float floatValue(); public abstract double doubleValue(); public byte byteValue() { return (byte)intValue(); } public short shortValue() { return (short)intValue(); }} Java的设计者首先为所有的数值类型做了一层抽象,并将所有的基本数值类型方法放到了这个Number类中,这位看官可能要问了:我只要将取值方法的返回值修改为Object类型或者将方法改为泛型方法不就可以了么,为什么要增加这么多方法呢?因为基本类型没有对象啊,单身狗连在程序中都要差一截,这个世界还真是满满的恶意。 在实际使用中,我们也可能想获得一个int值的浮点表示,也可能想取一个小数的整数部分,因此JDK将所有的基本数值类型都放到这个抽象类Number中,而各位机智的看官也一定猜到了Java中所有的数值类都将通过继承Number类进行实现。下面让我们一起来看一下Java是如何为int设计对象的吧。 Integer类定义1public final class Integer extends Number implements Comparable<Integer> 根据类定义可以得出: Integer继承了Number类,并且本身不可被继承。Integer实现了Comparable接口。 上文已经对Number类作了介绍,那么下面让我们结合Integer类成员来了解一下该类的具体实现吧。 Integer类设计Integer类成员变量1234567891011121314/** 序列化标识 */@Native private static final long serialVersionUID = 1360826667806852920L;/** 存放Integer的值 */private final int value;/** Integer的位数 */@Native public static final int SIZE = 32;/** Integer的字节数 */public static final int BYTES = SIZE / Byte.SIZE;/** 32位int可表示的最小值 -2^31 */@Native public static final int MIN_VALUE = 0x80000000;/** 32位int可表示的最大值 2^31-1 */@Native public static final int MAX_VALUE = 0x7fffffff;/** Integer对应的原子类型 */public static final Class<Integer> TYPE = (Class<Integer>) Class.getPrimitiveClass("int"); Integer类中存放的基本成员变量代码如上,其含义都很直观,这里就不再赘述,其中@Native注解表示该字段可能来自本地常量,了解过JVM结构的看官应该对本地方法区都有所了解,这里就不再展开了。 Integer成员方法构造函数12345678/** 接收int类型参数,直接赋值给value */public Integer(int value) { this.value = value;}/** 接收String类型参数,默认转换为10进制值,非10进制有效数字则抛出异常 */public Integer(String s) throws NumberFormatException { this.value = parseInt(s, 10);} Integer类提供了两种形式的构造函数,分别用于接收int类型和十进制的String类型值,Integer使用parseInt方法来进行字符串到数值的转换。 字符串转整型方法parseInt方法 首先我们来考虑一个问题:给定一个十进制数的字符串12345,如何把它转换为一个对应的整形数呢?负数怎么处理呢? 设定int result初值为0,然后从头开始挨个读取字符串string中的字符ch,转换为数字num,并使result = result*10 + num。对于负数,再加上判断首位是否为-即可。那来看一下Integer是怎么处理的吧。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273/** 默认以10进制转换 */public static int parseInt(String s) throws NumberFormatException { return parseInt(s,10);}/** 指定转换进制 */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"); } /** Character.MIN_RADIX = 2 */ if (radix < Character.MIN_RADIX) { throw new NumberFormatException("radix " + radix + " less than Character.MIN_RADIX"); } /** Character.MAX_RADIX = 36 */ if (radix > Character.MAX_RADIX) { throw new NumberFormatException("radix " + radix + " greater than Character.MAX_RADIX"); } int result = 0; boolean negative = false; int i = 0, len = s.length(); int limit = -Integer.MAX_VALUE; int multmin; int digit; if (len > 0) { char firstChar = s.charAt(0); /** 可能有'+'和'-' */ if (firstChar < '0') { if (firstChar == '-') { negative = true; limit = Integer.MIN_VALUE; } else if (firstChar != '+') throw NumberFormatException.forInputString(s); /** 如果字符串中只有一个'+'或'-' */ if (len == 1) throw NumberFormatException.forInputString(s); i++; } multmin = limit / radix; while (i < len) { /** 当前数要在指定进制范围之内,如十进制单个数字在0-9 */ digit = Character.digit(s.charAt(i++),radix); if (digit < 0) { throw NumberFormatException.forInputString(s); } /** 如果result已超过可表示的最大数 */ if (result < multmin) { throw NumberFormatException.forInputString(s); } result *= radix; if (result < limit + digit) { throw NumberFormatException.forInputString(s); } /** 通过进行负数计算来避免正数情况下超过32位int最大值 */ result -= digit; } } else { throw NumberFormatException.forInputString(s); } return negative ? result : -result;} Integer的实现与常规的想法没有太大区别,值得注意的是,Integer采用负数做减法的方式,避免了在转换32位最大负数过程中可能产生的溢出问题。 valueOf方法 在parseInt方法中有这样一段注释 WARNING: This method may be invoked early during VM initializationbefore IntegerCache is initialized. Care must be taken to not usethe valueOf method.警告:该方法会在VM初始化时被调用,这时候IntegerCache还没有初始化。一定注意不要使用valueOf方法 接下来让我们看一下valueOf方法和parseInt方法有什么关系吧。12345678public static Integer valueOf(String s) throws NumberFormatException { return Integer.valueOf(parseInt(s, 10));}public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i);} valueOf方法重载了两种形式,接收String类型参数的方法功能与parseInt基本相同,将一个十进制字符串转换成对应的Integer类型,在其转换的过程中首先调用了parseInt方法将字符串转换位int类型,然后再通过int类型参数的valueOf方法将int转换为Integer对象,这个过程称为装箱。 我们通过javap查看Integer i = 10;编译后的字节码如下:123Code: 0: bipush 10 2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 可以看出这一行代码实际执行过程将会被翻译为Integer i = Integer.valueOf(10),因此通常情况下我们会说,可以使用基本类型操作的情况下不要有意将其转换为对象,在转换过程中会增添装箱操作的消耗,同理可以推及到Long,Float等基本类型的包装类。毕竟找个对象不容易,又得花钱又得花心思。 在装箱函数valueOf(int i)中,首先会对i的值判断,如果在IntegerCache.low和IntegerCache.high之间,则会直接返回预存在IntegerCache中的对象,否则才会新建Integer对象。那么IntegerCache是一个什么样的类,该类又做了什么工作呢?1234567891011121314151617181920212223242526272829303132private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { int h = 127; //读取JVM参数中配置的初始值 String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() {}} IntegerCache是Integer中定义的私有内部类,其构造方法设置为private,该类将在第一次被使用时初始化,调用其中的static部分代码,静态代码块将会为-128~127之间的数创建好相应的对象放到缓存里,使得在对valueOf的调用中,缩短对-128~127的装箱时间。在JAVA6的版本以后,IntegerCache.high的值可以通过在JVM的初始化参数中配置-XX:AutoBoxCacheMax=high来指定缓存的最大值。 让我们再回到parseInt中的那段注释,那段注释并不是面向使用者的,而是面向源码开发者的,其意图是警告开发者在修改Java源码的时候注意不要在其中调用valueOf方法。 parseUnsignedInt方法 Integer类设计的真是十分周到,又为我们提供了字符串转为无符号整数的方法parseUnsignedInt。12345678910111213141516171819202122232425262728293031323334353637public static int parseInt(String s) throws NumberFormatException { return parseInt(s,10);}public static int parseUnsignedInt(String s, int radix) throws NumberFormatException { if (s == null) { throw new NumberFormatException("null"); } int len = s.length(); if (len > 0) { char firstChar = s.charAt(0); if (firstChar == '-') { throw new NumberFormatException(String.format("Illegal leading minus sign " + "on unsigned string %s.", s)); } else { // 括号中的条件可以理解为在这样的情况下整数长度必然在32位无符号数之内 if (len <= 5 || // Integer.MAX_VALUE in Character.MAX_RADIX is 6 digits (radix == 10 && len <= 9) ) { // Integer.MAX_VALUE in base 10 is 10 digits return parseInt(s, radix); } else { // 对可能超出长度的数调用Long中的parse方法,其逻辑与parseInt方法相同。 long ell = Long.parseLong(s, radix); if ((ell & 0xffff_ffff_0000_0000L) == 0) { return (int) ell; } else { throw new NumberFormatException(String.format("String value %s exceeds " + "range of unsigned int.", s)); } } } } else { throw NumberFormatException.forInputString(s); }} parseUnsignedInt同样提供两种形式,在其具体的实现函数中,值得注意的一点是对长整型的判断(ell & 0xffff_ffff_0000_0000L) == 0。我们知道Integer长度是32位,采取64位表示其最大无符号数为0x0000_0000_ffff_ffffL,即前32位一定为0,因此只要在这个范围内的合法整数,与0xffff_ffff_0000_0000做按位与操作,结果一定是0。高富帅都是玩儿位操作的。 Integer类中常用的String -> int|Integer方法也就介绍完了。还有两个方法分别是:123public static Integer decode(String nm);public static Integer getInteger(String nm);public static Integer getInteger(String nm, int val); decode方法接受以(+|-)+(0|0x|#)开头的八、十和十六进制字符串,内部实现主要是对格式判断,转换过程调用的是valueOf方法。getInteger方法并不是用来进行字符串转换的,而是配合System.setProperty()使用,获取设置的环境值。 小结 Integer提供了parseInt,valueOf,parseUnsignedInt方法供我们转换字符串,后两种的实现基本也都是依赖于parseInt进行的。 parseInt和valueOf区别在于前者返回int,后者为Integer,int -> Integer的过程称为装箱,一般情况下尽量使用基本类型操作,减少装箱消耗。整型转字符串 前面提到的方法都是用于计算字符串对应的整型数字,下面来看一下如何通过整型数字获得对应的字符串吧。toString方法12345/** 继承Object */public String toString();/** 静态,默认10进制 */public static String toString(int i);public static String toString(int i, int radix); Integer提供了三种形式的toString,第一种重写继承自Object的toString,其内部通过调用toString(i)得到结果,即调用一个Integer对象的toString方法将获得一个十进制的字符串表示。第三种形式的toString(int i, int radix)在整体实现上没有特别之处,通过逐次取余来获得每一位数字。在对二到三十六进制之外的数的处理中,第三种方法直接调用了第二种方法,即以十进制方式进行处理,并注为更快的模式。1234/* Use the faster version */if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX) radix = 10;if (radix == 10) { return toString(i); } 12345678public static String toString(int i) { if (i == Integer.MIN_VALUE) 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);} 由代码可以看出,更快的模式拢共分三步,第一步,把冰箱门打开!第一步,计算数字长度;第二步,获取相应长度char数组;第三步,新建String对象。12345678final static int [] sizeTable = { 9, 99, 999, 9999, 99999, 999999, 9999999, 99999999, 999999999, Integer.MAX_VALUE }; // Requires positive x static int stringSize(int x) { for (int i=0; ; i++) if (x <= sizeTable[i]) return i+1; } Java在stringSize方法中巧妙的通过最多10次比较来避免使用除法计算位数。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364final static char [] 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', } ;final static char [] 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', } ;final static char[] digits = { '0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , 'a' , 'b' , 'c' , 'd' , 'e' , 'f' , 'g' , 'h' , 'i' , 'j' , 'k' , 'l' , 'm' , 'n' , 'o' , 'p' , 'q' , 'r' , 's' , 't' , 'u' , 'v' , 'w' , 'x' , 'y' , 'z'};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 // q = i/100; r=i%100; i=q; while (i >= 65536) { q = i / 100; // 相当于 r = i - (q*100) r = i - ((q << 6) + (q << 5) + (q << 2)); i = q; buf [--charPos] = DigitOnes[r]; buf [--charPos] = DigitTens[r]; } // Fall thru to fast mode for smaller numbers // assert(i <= 65536, i); for (;;) { // 相当于i/10 q = (i * 52429) >>> (16+3); r = i - ((q << 3) + (q << 1)); // r = i-(q*10) ... buf [--charPos] = digits [r]; i = q; if (i == 0) break; } if (sign != 0) { buf [--charPos] = sign; }} 在getChars方法中使用了很多位操作来避免做除法,在该方法中,首先把一个数以65536为界划分为两部分:在大于65536的时候,通过循环对100除余每次获得末尾的两位数字,并通过查表来直接获取到对应位置的数字,其中q << 6 + q << 5 + q << 2相当于q * 2^6 + q * 2^5 + q * 2^2 = q * 100;在小于等于65536的情况下,通过循环对10除余然后查表来获得末尾的数字,其中(i * 52429) >>> (16 + 3)相当于i * 52429 / 524288 = i * 0.1。那么到这里呢各位看官可能会有几个问题。 1. >>>移位和>>移位有什么区别。 2. 为什么以65536为界划分。 3. digits数组为什么还有a~z。 >>>移位是无符号移位,我们知道对于负数进行移位操作会在高位补1,而>>>会在高位补0,即可以理解为无符号除法。而digits数组因为在对其他进制的转换中也用到,因此该数组列出的是在36进制中所有可以合法表示数字的字符。对于第二个问题,65536是怎么划分的来的呢?1234567891011n = 10, 2^n = 1024, 103/1024 = 0.1005859375n = 11, 2^n = 2048, 205/2048 = 0.10009765625n = 12, 2^n = 4096, 410/4096 = 0.10009765625n = 13, 2^n = 8192, 820/8192 = 0.10009765625n = 14, 2^n = 16384, 1639/16384 = 0.10003662109375n = 15, 2^n = 32768, 3277/32768 = 0.100006103515625n = 16, 2^n = 65536, 6554/65536 = 0.100006103515625n = 17, 2^n = 131072, 13108/131072 = 0.100006103515625n = 18, 2^n = 262144, 26215/262144 = 0.10000228881835938n = 19, 2^n = 524288, 52429/524288 = 0.10000038146972656n = 20, 2^n = 1048576, 104858/1048576 = 0.10000038146972656 首先通过以上数据可以看出,在n = 19时,除数的结果达到小数点后6位即单精度浮点有效小数位数,因此选取了52429 >>> (19)来提升精度,52429的长度在15~16位之间,因此划分界限选取为2^16即65536,这里可能有看官要问了,52429 * 65536也超过了2^31-1了呀,注意到这里使用的是>>>无符号移位,32位整型可表示的最大无符号数位2^33 - 1,因此不必担心溢出造成的损失。 小结 在Java源码中,有很多与数字相关的操作都是通过移位来进行,从这些地方我们可以看到设计者对细节的把控。而在我们自己的开发过程中,是否也有必要这么锱铢必较呢?博主对上述getChars方法中通过移位除10的方式和直接除10的方式进行了比较,在一百万次循环后得到几次不稳定的结果,其平均值也仅在2~3m左右。随着现代JVM优化能力的不断提升,这样操作的性能差距将越来越小。 总结 第一篇文章,由于工作只能陆陆续续在晚上写,那么这一篇主要介绍了Integer类中常用的一些基本方法,下一篇将持续对Integer类解析,着重于其中位操作相关的函数。]]></content>
<categories>
<category>Java源码剖析从入门到放弃【类型篇】</category>
</categories>
<tags>
<tag>Java</tag>
<tag>源码剖析</tag>
</tags>
</entry>
</search>