Jdk源码解读-String/Integer
String
public final class String implements java.io.Serializable, Comparable<String>, CharSequence
从上面的String定义可以看出,String类由final修饰,无法被继承,并且实现Serializable接口,可以序列化和反序列化。它有两个私有变量:
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
从注释就可以看出他们分别是用于存储字符串的char型数组和缓存字符串的hashcod值。从构造方法里也能看出,是由这两个属性决定了String的值,value数组由final修饰,也就使String有了不可变的特性。
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
我们日常使用的‘+’用于连接字符串其实是为java 提供的一种特殊的连接操作符,并且内部是由StringBuilder或StringBuffer的append实现的
/*The Java language provides special support for the string
* concatenation operator ( + ), and for conversion of
* other objects to strings. String concatenation is implemented
* through the {@code StringBuilder}(or {@code StringBuffer})
* class and its {@code append} method.
*/
常用方法:
//去除首尾的一个或n个空格
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
while ((st < len) && (val[st] <= ' ')) {//计算前面的空格数量
st++;
}
while ((st < len) && (val[len - 1] <= ' ')) {//长度减去尾部空格的数量
len--;
}
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;//通过截取的方式获取新的
}
//根据字符串的每个字符的unicode码按照从左到右的顺序比较,如果大于就返回正数,如果小于就返回负数
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
jdk1.6以前String里面的substring截取字符串出现过内存泄漏的问题:
private static char[] value;
private static int offset;
private static int count;
public String(int offset, int count, char value[]) {
this.value = value;
this.offset = offset;
this.count = count;
}
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > count) {
throw new StringIndexOutOfBoundsException(endIndex);
}
if (beginIndex > endIndex) {
throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
}
return ((beginIndex == 0) && (endIndex == count)) ?
this : new String(offset + beginIndex, endIndex - beginIndex, value);//value是String的静态全局变量
}
public static void main(String[] args){
String str = "abcdefghijklmnopqrst";//当str为1G的字符串时
String sub = str.substring(1, 3);
str = null;
}
执行main方法,当str够大或执行substring次数过多时,就会发生out of memory 异常,原因就是当substring时调用的new String()构造方法里面的参数value用的是String的静态全局变量,str和sub共同指向"abcdefghijklmnopqrst"所在的常量池空间,当str置空时,并不会回收这部分的内存,因为sub仍然还指向那段空间,这部分内存会在jvm运行周期内一直存在,当str够大时或substring执行次数过多时,则会导致内存溢出。jdk1.7之后sun就修改了substring的实现方式,即:
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
//Arrays.copyOfRange
public static char[] copyOfRange(char[] original, int from, int to) {
int newLength = to - from;
if (newLength < 0)
throw new IllegalArgumentException(from + " > " + to);
char[] copy = new char[newLength];//新建了一个字符数组用于保存薪的字符串
System.arraycopy(original, from, copy, 0,
Math.min(original.length - from, newLength));
return copy;
}
replace方法替换字符,通过while循环找到第一个需要替换的字符的下标i,然后复制从0到i的字符数组a,再循环i和i之后的字符数组,将oldchar替换为newchar,并将值都插入到a中。
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
String针对常量池的优化;
针对常量池的优化指:当2个String对象拥有相同的值时,他们只引用常量池中的同一个拷贝。当同一个字符串反复出现时,这个技术可以大幅度节省内存空间。
String str1="abc";//jvm开辟了2个空间(1个栈空间和1个常量池)
String str2="abc";//jvm开辟了1个空间(1个栈空间),共用了str1的abc常量池
String str3=new String("abc");//jvm开辟了2个空间(1个栈空间和一个堆空间),共用了str1的abc常量池
System.out.println(str1==str3.intern()); //返回true,intern方法返回string在常量池中的引用
总结:
1.不可变性:String内部通过final、private修饰的字符数组实现,且String是一个用final修饰的类,不能被继承,所以使它在赋值之后不能改变它的值。(重新赋值之后只是改变了它指向的新的内存空间)
2.对常量池的特殊优化:当重复使用同一个字符串时,String会返回常量池中同一个字符串的引用,大幅节省内存消耗,通过intern()方法可以查看指向的常量池地址。
3.jdk1.6及之前版本存在内存泄漏问题,原因是subString()实现的方法中调用的构造方法new String(offset, count, value)中的参数value为String中的全局变量,多次调用将不会被gc回收,最后出现内存泄漏的情况。
Integer
以下将部分代码段加注释便于理解
public final class Integer extends Number implements Comparable<Integer> {
@Native public static final int MIN_VALUE = 0x80000000;//Integer存储最小值-2^31
@Native public static final int MAX_VALUE = 0x7fffffff;//Integer存储最大值2^31-1
public static final Class<Integer> TYPE = (Class<Integer>) Class.getPrimitiveClass("int");//表示是int的包装类型
private final int value;//表示初始化之后不可修改,即常量
//Integer对象创建的时候会初始化-128到127区间内的整数到内存缓存起来
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];//存储的数组
static {
// high value may be configured by property
int h = 127;
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定义的一个静态内部类,当Integer被加载的时候将会初始化一段缓存用于存储-128-127,总共256个数字。这也是出现下面这种情况的原因
Integer integer1 = 127;
Integer integer2 = 127;
Integer integer3 = 128;
Integer integer4 = 128;
System.out.println(integer1 == integer2);//true
System.out.println(integer3 == integer4);//false
//当直接值在-128-127区间内时,则会直接返回常量池中该值引用,当不在区间时,则会重新new一个Integer存放该值,也就出现了128 != 128的情况
十进制转换为2~32进制:
public static String toString(int i, int radix) {
...
while (i <= -radix) {
buf[charPos--] = digits[-(i % radix)];//和进制取余,放入数组尾部
i = i / radix;//得到除数继续下次循环
}
...
}
再来看看toString(int i )
public static String toString(int i) {
if (i == Integer.MIN_VALUE)
return "-2147483648";
int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);//得到i的位数,当为负数时+1位
char[] buf = new char[size];//创建存储字符的空间
getChars(i, size, buf);//存入字符数组
return new String(buf, true);
}
static int stringSize(int x) {
for (int i=0; ; i++)
if (x <= sizeTable[i])
return i+1;
}
final static int [] sizeTable = { 9, 99, 999, 9999, 99999, 999999, 9999999,
99999999, 999999999, Integer.MAX_VALUE };
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
while (i >= 65536) {
q = i / 100;
// really: r = i - (q * 100);
r = i - ((q << 6) + (q << 5) + (q << 2));//得到大于等于65536这部分的数据的字符,应该是两位数
i = q;//继续往前取剩下的数字
buf [--charPos] = DigitOnes[r];//得到的两位数r%10得到个位数
buf [--charPos] = DigitTens[r];//得到的两位数r/10得到十位数
}
// Fall thru to fast mode for smaller numbers
// assert(i <= 65536, i);
for (;;) {
q = (i * 52429) >>> (16+3);//得到小于65536这部分的个位数
// r = i-(q*10) ...
r = i - ((q << 3) + (q << 1)); //得到除了q之外的其他数字
buf [--charPos] = digits [r];
i = q;
if (i == 0) break;
}
if (sign != 0) {
buf [--charPos] = sign;//符号位
}
}
之前我没看懂上面的代码,左移右移什么鬼(不就是int转string至于这么麻烦么,直接1111+""不就行了么…),然后看了一大神的博文,顿时豁然开朗,原来代码还能这么写:
r = i - ((q << 6) + (q << 5) + (q << 2))
可以转换为
r = i - (q * 2^6 + q * 2^5+q * 2^2) =>r = i - (q * 100)
q = (i * 52429) >>> (16+3)
可以转换为
r = i * 52429/2^19 =>r = i - (q * 100) =>q = (i * 52429) / 524288 => q= i * 0.1 = > q=i/10
用左右移的意义是可以更快的得到想得到的位数
即:
移位的效率比直接乘除的效率要高
乘法的效率比除法的效率要高
由此可见sun的工程师是真的厉害,致敬一波
总结:
- Integer也是常量,无法改变(重新赋值之后只是改变了它指向的新的内存空间)
- 有缓冲池,缓存-128~127之间的值,当取的数不在此区间时,相同的值指向的地址会不同