String源码阅读笔记

  • 为什么设置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.常量池垃圾回收

根据 https://www.baeldung.com/java-string-pool 的资料显示,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类

https://stackoverflow.com/questions/22094111/how-to-print-the-whole-string-pool

参考资料

https://www.baeldung.com/java-string-pool

https://stackoverflow.com/questions/18406703/when-will-a-string-be-garbage-collected-in-java

http://xmlandmore.blogspot.com/2013/05/understanding-string-table-size-in.html

https://www.hollischuang.com/archives/99

posted @ 2019-06-01 02:10  Datartvinci  阅读(249)  评论(0编辑  收藏  举报