String源码阅读笔记
-
为什么设置final
-
为什么安全性不如char[]
-
new string("")的过程
-
怎么比较大小?
-
怎么比较相等?
-
怎么计算hashcode?
-
intern的作用?
-
说说字符串常量池
-
substring方法做了什么
-
包私有构造器
-
大小写转换的原理
1.final类以及字段
被final修饰的类不能被继承,意味着在类似于jdk常量池之类的底层实现的时候,不需要考虑由于有子类带来的其他问题。内部的字符数组也被定义为final,因此string内部的char[]是不能修改的。由于这些特性,java中的string具有不可变的特性,因此jvm可以优化字符串的内存使用:只存储一份字面量字符串在常量池中,这个过程称为interning(翻译为内化?)。
在内存信息敏感的情况下,char[]比string安全,因为string内部的char[]是final类型的,不能用过后立刻擦除,意味着敏感信息一直会在内存中存在直到垃圾回收。
3.字符串构造的过程
假设使用字符串构造一个string对象:
public static void main(string[] args) { new string("hello"); }
将上述代码反编译得到如下结果:
public static void main(java.lang.string[]); code: 0: new #2 // class java/lang/string 3: dup 4: ldc #3 // string hello 6: invokespecial #4 // method java/lang/string."<init>":(ljava/lang/string;)v 9: pop 10: return
0:新建string对象
4:ldc命令,从字符串常量池加载"hello"
6:调用string的构造方法
string构造方法:
/** the value is used for character storage. */ private final char value[]; /** cache the hash code for the string */ private int hash; // default to 0 public string(string original) { this.value = original.value; this.hash = original.hash; }
由此可以推出结论,new string("")方法得到的字符串对象,共享的同一个字符数组char[],达到节约内存的目的,这也是char[]被声明为final的原因,为了在多个string对象之间共享,必须声明为不可变。
如果不想要共享同一个字符数组,则可以使用入参为char[]的构造方法,该方法复制了一份char[]:
public string(char value[]) { this.value = arrays.copyof(value, value.length); }
我们来看看上面两个构造方法有没有满足string的不可变特性。
-
入参是string,由于string的value是不可变的,因此赋值后的value也是不可变的
-
入参是char[],拷贝一份新的字符数组再赋值,赋值后的value是拷贝后的数组的唯一一份引用,没有其他人能改变它,因此也是不可变的
4.包私有构造器
入参是字符数组char[]的构造器,每次都要重新拷贝一份数组,浪费空间,但是为了安全又不得不这么做。
如果是自己人调用,明确不会修改作为入参的字符数组,没有必要每次都重新复制一份字符数组,那能不能提供一个性能更好的构造方法呢,答案是有的:
public string(char value[]) { this.value = arrays.copyof(value, value.length); }
这个构造方法只允许同一个包下面的类调用,也就是自己人调用。新增的参数share只是为了重载用的。
5.intern方法
该方法能够从将字符串缓存到常量池中,并返回常量池中的引用,使得下面语句成立:
assert.asserttrue("abc" == new string("abc").intern())
根据.equals()方法判断字符串是否存在常量池中,如果已经存在,直接返回常量池中的引用,否则缓存入常量池再返回常量池中的引用。
根据实验,intern出来的字符串如果没有被引用,会被垃圾回收。
for (int i = 0; i < integer.max_value; i++) { system.out.println(string.valueof(i).intern()); }
6.substring方法做了什么
返回一份字符数组的拷贝:
public string(char value[], int offset, int count) { this.value = arrays.copyofrange(value, offset, offset+count); }
为什么这里不使用第四点提到的包私有构造方法来节约内存呢?jdk7之前实际上是使用的,jdk7之后才替换成这个,因为使用共享字节数组会造成内存泄漏,如下所示:
string longstring = "...a very long string..."; string part = longstring.substring(20, 40); return part;
假设longstring是一个很长的字符串,但是我们只需要对part进行解析,如果内部数组是从longstring那里共享的,虽然longstring对象可以被回收,但是它的内部数组不能被回收。表面上看part的长度只有20,实际上内部数组的长度却很大,反而浪费了更多的内存。
7.比较大小
compareto()方法
对于每一位char,如果不一致,两者做减法并直接返回相差的值,如果都一样,长度更长的则更大。
规律总结如下:
-
abc>ab
-
abc<d
-
abc>aaaa
8.比较相等
contentequlas与equals方法
-
contentequlas比较的是实现了charsequence方法的类的每个char
-
equlas不仅仅比较char还比较是否是string对象
9.hashcode
hashcode的实现使用了一下数学公式:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
选择31有两个原因,第一是素数,可以减少散列冲突的情况,尽可能散列均匀;第二是2的幂,有利于高性能运算,在cpu层面执行时直接左移5位再减1,避免了更耗时的乘法运算,节约了很多个cpu的时间片。
10.字符串常量池
10.1.原理
10.1.1.常量池中的字符串
当我们创建字符串变量并通过双引号赋值字面量给它的时候,jvm会搜索字符串常量池中的对象,通过equals方法比较是否相等,如果找到,则直接返回该对象的地址的引用,不需要再额外分配内存;如果没找到,则将当前字面量的字符串添加到常量池中并且返回常量池中的引用。例子:
string constantstring1 = "baeldung"; string constantstring2 = "baeldung"; assertthat(constantstring1).issameas(constantstring2);
10.1.2.构造器创建的字符串
如果是通过new操作符创建的字符串,jvm会创建一个新的对象并把它存储到堆空间上。
像这样创建出来的字符串,都会指向一个不同的内存地址。例子:
string constantstring = "baeldung"; string newstring = new string("baeldung"); assertthat(constantstring).isnotsameas(newstring);
10.1.3.字面量字符串vs字符串对象
如果使用双引号创建的字符串,则返回常量池中的字符串;如果是new出来的字符串,则总是会在堆上创建一个新的对象。
证明字符串常量池的例子:
string first = "baeldung"; string second = "baeldung"; system.out.println(first == second); // true
证明创建新对象的例子:
string third = new string("baeldung"); string fourth = new string("baeldung"); system.out.println(third == fourth); // false
两种方式的比较:
string fifth = "baeldung"; string sixth = new string("baeldung"); system.out.println(fifth == sixth); // false
10.2.常量池垃圾回收
根据 的资料显示,java7之前,jvm将字符串常量池放在permgen空间,拥有固定大小,这使得常量池不能够在运行时拓展,也不能被垃圾收集器回收。如果内化了太多的字符串,就会导致oom。
java7之后,字符串常量池存储在堆空间,因此能够被垃圾收集器回收。这个方法的优势是减少了oom的风险,因为不再被引用的字符串会被移出字符串常量池,以释放内存。
如果是字面量的intern,由于会被隐式调用,因此不会被垃圾回收。
10.3.性能和优化
java 6的增大字符串常量池的方法是增大永久代的空间:
-xx:maxpermsize=1g
java 7之后有更多的选项,例如:
-xx:stringtablesize=
`4901
这个值需要在1009 和 2305843009213693951之间
查看常量池大小的方法:
-xx:+printflagsfinal
-xx:+printstringtablestatistics
11.大小写转换
先扫描到第一个大写的字符,拷贝前面的到新的字符串数组,再对之后的每个char作判断,如果大写则转小写,再赋值给数组 。
12.valueof
调用各个包装类型的tostring()方法。
13.重写string类
参考资料
上一篇: 某集团任意文件下载到虚拟主机getshell的方法
下一篇: sql注入正则表达式检测