StringTable

151 阅读5分钟

StringTable


String基本特性

  • String 类中使用 final 关键字修饰字符数组来保存字符串, 表示不可继承。

🐛 修正 : 我们知道被 final 关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,final 关键字修饰的数组保存字符串并不是 String 不可变的根本原因,因为这个数组保存的字符串是可变的(final 修饰引用类型变量的情况)。

  • JDK 8中用char[ ]保存字符串,JDK 9改为 byte[ ]

image-20220325111511043

  • String实现了Serializable接口表示字符串支持序列化,String实现了Comparable接口表示String可比较大小
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[];
    //...
}
  • 字符串常量池不会存储相同内容的字符串

String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009。如果放进string Pool的string非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降。

使用-XX:StringTablesize可设置StringTable的长度

在jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。StringTableSize设置没有要求在jdk7中,StringTable的长度默认值是60013,1009是可设置的最小值。

String 真正不可变原因

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[];
    //...
}
  1. 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
  2. String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

字符串拼接操作

  • 常量与常量的拼接结果在常量池中,原理是编译期优化

常量指的是"ab" 或者使用final String s1 = "ab"

//示例一: 字符串常量
@Test
public void testString() {
    String a = "hello";
    String b = "world";
    String c = "hello" + "world";
    String d = a + b;
    System.out.println(c == d);
}
输出:false  //c的拼接是在字符串常量池中进行;d的拼接是变量的拼接,在堆中(JDK 1.7以前可以说是在堆中,1.7及其之后字符串常量池被移到堆中后,可以表述为在堆中非字符串常量池的位置,见下图比较)

image-20220323203150001

image-20220323203212611

//示例二: 常量引用:用final修饰的字符串就是在编译期可知的,编译期就会将以上代码优化
@Test
public void testString() {
    final String a = "hello";
    final String b = "world";
    String c = "helloworld";
    String d = a + b;
    System.out.println(c == d);
}
输出:true //final修饰认为是常量,在字符串常量池中拼接
  • 常量池中不会存在相同内容
  • 只要其中有一个是变量,结果就在堆中。变量拼接原理是StringBuilder.append( )

image-20220325121528350

  • 如果拼接的结果调用intern( )方法,则主动将常量池中没有的字符串放入池中,并返回此对象地址。

使用 + 拼接 和 StringBuilder.append()

  1. 使用“+”进行字符串的拼接

    编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象,内存占用大。

    如果进行GC,也需要花费时间。

  2. StringBuilder.append()

自始至终只创建一个StringBuilder对象

上边的例子也告诉我们,字符串拼接底层不一定使用的是StringBuilder.append(),如果拼接符号左右两边都是字符串常量或者常量引用,则使用编译期优化。

String.intern()方法

String.intern()是一个Native方法,底层调用C++的 StringTable::intern 方法,源码注释:当调用 intern 方法时,如果常量池中已经该字符串,则返回池中的字符串;否则将此字符串添加到常量池中,并返回字符串的引用。

字符串常量池比较特殊,它的主要使用方法有两种:

  1. 直接使用双引号声明出来的 String 对象会直接存储在字符串常量池中。
  2. 如果不是用双引号声明的 String 对象,使用 String 提供的 intern() 方法也有同样的效果。String.intern() 是一个 Native 方法,它的作用是:如果字符串常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,JDK1.7 之前(不包含 1.7)的处理方式是在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用,JDK1.7 以及之后,字符串常量池被从方法区拿到了堆中,jvm 不会在常量池中创建该对象,而是将堆中这个对象的引用直接放到常量池中,减少不必要的内存开销。

String s1 = new String("abc");这句话创建了几个字符串对象?

会创建 1 或 2 个字符串:

  • 如果字符串常量池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”。
  • 如果字符串常量池中没有字符串常量“abc”,那么它将首先在字符串常量池中创建(在编译期 new 方式创建的字符串就会被放入到编译期的字符串常量池中),然后在堆空间中创建,栈中的引用指向堆中创建的这个对象,因此将创建总共 2 个字符串对象。

JDK6及以前的内存结构:

image-20220326094134990

JDK7(JDK 8之后 无永久代):

image-20220326094247695

验证:

String s1 = new String("abc");// 堆内存的地址值
String s2 = "abc";
System.out.println(s1 == s2);// 输出 false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。
System.out.println(s1.equals(s2));// 输出 true

结果:

false
true

那么问题来了,以下这段代码的执行结果为 true 还是 false?

String s1 = new String("javaer-wang");
String s2 = new String("javaer-wang");
System.out.println(s1 == s2);
输出:false