欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

String源码阅读笔记

程序员文章站 2022-06-28 20:02:27
为什么设置final 为什么安全性不如char[] new String("")的过程 怎么比较大小? 怎么比较相等? 怎么计算hashcode? intern的作用? 说说字符串常量池 substring方法做了什么 包私有构造器 大小写转换的原理 为什么设置final 为什么安全性不如char[ ......
  • 为什么设置final

  • 为什么安全性不如char[]

  • new string("")的过程

  • 怎么比较大小?

  • 怎么比较相等?

  • 怎么计算hashcode?

  • intern的作用?

  • 说说字符串常量池

  • substring方法做了什么

  • 包私有构造器

  • 大小写转换的原理

1.final类以及字段

被final修饰的类不能被继承,意味着在类似于jdk常量池之类的底层实现的时候,不需要考虑由于有子类带来的其他问题。内部的字符数组也被定义为final,因此string内部的char[]是不能修改的。由于这些特性,java中的string具有不可变的特性,因此jvm可以优化字符串的内存使用:只存储一份字面量字符串在常量池中,这个过程称为interning(翻译为内化?)。

2.安全问题

在内存信息敏感的情况下,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类

参考资料