跳至主要內容

理解String#intern

Zenghr大约 8 分钟Java

理解 String

提示

本文主要分析 String 包,以及StringBuffer、StringBuilder的操作,运行环境:jdk 1.8.0_212

参考资料

深入理解String#internopen in new window

String 概念

String 类是代表字符串,Java中所有的字符字面量都是此类的实现;

内部使用 char 类型的数组存储数据,该数组被 final 修饰,代表该数组赋值初始化后就不能被修改,并且内部没有实行可以修改该数组的方法,因此 String是不可变的

/** The value is used for character storage. */
private final char value[];

不可变的好处

1. String Pool 字符缓存池

如果一个 String 对象已经被创建过,那么就会从 String Pool 中引用,只有 String 是不可变的,才可能使用 String Pool,内存图如下👇

String str1 = "abc";
String str2 = "abc";
System.out.println(str1 == str2); // true
String str3 = new String("abc");
System.out.println(str3 == str1); // false
mark
mark

2. 线程安全

因为 String 不可变特性,所以 String 天生具备线程安全,可以在多个线程中使用

String,StringBuffer,StringBuilder

1. 可变性

  • String 不可变
  • StringBuffer 和 StringBuilder 可变

2. 线程安全

  • String 不可变,所以线程安全
  • StringBuffer 线程安全,内部使用 synchronized 实现
  • StringBuilder 线程不安全

StringBuffer

提示

StringBuffer(字符缓冲区) 可以当成是一个包装类,内部封装了一个字符数组,并提供相应的 增、删、改、查等操作方法,也可以称为 字符串容器,由于 StringBuffer 是线程安全,所以效率比StringBuilder低,所以我们平时优先使用 StringBuilder

StringBuffer 构造方法

  • StringBuffer() :初始容量是 16 的可变字符串
  • StringBuffer(int capacity) :构造一个容量是 capacity 的可变字符串

StringBuffer和StringBuilder 常用方法

StringBuffer 主要操作是 appendinsert 方法

  • append: 在原有的字符串后面追加数据
  • insert(int offset, String str): 在指定位置插入字符串
  • delete(int start, int end): 删除指定区间的字符串,含头不含尾
  • setCharAt(int index, char ch): 替换指定位置的字符
  • replace(int start, int end, String str): 替换指定区间的字符串
  • capacity(): 返回容量大小

StringBuilder Api 和 StringBuffer Api一致,唯一不同就是线程安全与否

自动扩容源码分析

StringBuffer 和 StringBuilder 调用 append 方法,都会调用 父类的 append 方法,父类的 append 方法源码如下👇 - (源码版本:JKD 1.8)

append

public AbstractStringBuilder append(String str) {
    if (str == null)
        return appendNull();
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}

ensureCapacityInternal

可以看出核心的代码为 ensureCapacityInternal 方法,传入参数为,原有长度(count) + 新字符串长度(len),点击查看 ensureCapacityInternal 源码👇

private void ensureCapacityInternal(int minimumCapacity) {
    // overflow-conscious code
    // 如果 新字符串长度 - 数组容量 value.length 大于 0 即超出最大容量
    // 则执行 数组拷贝,生成新的数组
    if (minimumCapacity - value.length > 0) {
        value = Arrays.copyOf(value, newCapacity(minimumCapacity));
    }
}

ensureCapacityInternal 方法中,判断 如果 新字符串长度(minimumCapacity) - 数组容量 value.length 大于 0 即超出最大容量,则执行 newCapacity 方法计算新的扩容长度,然后执行 Arrays.copyOf 生成新的字符数组, newCapacity 源码如下👇

newCapacity

private int newCapacity(int minCapacity) {
    // overflow-conscious code
    // 先将 原有容量长度 << 1,相当于 乘于 2,翻倍,再加 +2
    int newCapacity = (value.length << 1) + 2;
    // 如果 新计算的 容量长度比新字符串长度小,那么直接将 新字符串长度 赋值成新的数组容量
    if (newCapacity - minCapacity < 0) {
        newCapacity = minCapacity;
    }
    // 如果 新的容量小于等于0 或者 新容量大小比数组最大长度大,则重新计算容量
    // 否则 直接返回上面计算的新的容量大小
    return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
        ? hugeCapacity(minCapacity)
        : newCapacity;
}

查看 newCapacity 源码则知道,先将原有容量大小 左移 1,也就是容量翻倍,再 +2,得出新的容量大小 newCapacity

再将新的容量 newCapacity 跟新字符串长度进行比较,如果比新字符串长度小,那么直接将 新字符串长度 赋值成新的数组容量 newCapacity = minCapacity;

最后再 校验一下,如果 新的容量小于等于0 或者 新容量大小比数组最大长度大,就要执行 hugeCapacity 方法重新计算容量大小,否则 直接返回 新容量大小 newCapacity

查看 hugeCapacity 方法源码👇

private int hugeCapacity(int minCapacity) {
    // 如果 新字符串长度 比 Integer 最大值还要大,则抛出异常
    if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
        throw new OutOfMemoryError();
    }
    // 如果 新字符串长度 小于 最大数组长度,返回 新字符串长度,否则返回 最大数组长度
    return (minCapacity > MAX_ARRAY_SIZE)
        ? minCapacity : MAX_ARRAY_SIZE;
}

该方法主要判断新字符串是否超出 Integer 最大值,超出则抛出异常

如果 新字符串长度 小于 最大数组长度,则返回 新字符串长度,否则返回 最大数组长度

总结

注:

Integer.MAX_VALUE(Integer 最大值):2147483647

MAX_ARRAY_SIZE(数组最大长度):Integer.MAX_VALUE - 8

提示

在原有容量大小的基础上 翻倍再+2 得出 newCapacity,如果还是小于新字符串长度,直接将新字符串长度设置成新的容量,否则使用翻倍后的容量

校验 newCapacity 后发现新计算的容量不合法 小于0 或者 大于数组最大长度,则需要根据新的字符串重新计算容量,合法就使用 newCapacity 作为新的容量,重新计算规则为:

  1. 前提条件:新的字符串长度不能超出 Integer 的最大值,则直接抛出异常 OutOfMemoryError
  2. 在前提下,字符串长度比最大数组长度大,直接使用字符串长度作为新的容量
  3. 否则使用 数组最大长度 作为新的容量

String.intern()

使用 String.intern() 可以保证相同内容的字符串变量引用同一的内存对象

String 常量池

String 常量池 也称为 缓存池,八大基本数据类型都有自己的缓存池,使用方法:

  • 直接使用双引号声明出来 String 对象会直接存储在 常量池中
  • 使用 new String() 创建的 String 对象,会在堆区生成一个String 对象,还会在常量池中生成 相应的字符串对象(如果常量池不存在该对象的话)

提示

String s = new String("abc") 这个语句生成了几个对象,根据上面总结的可知,一共生成两个对象,常量池中的 abc 对象,堆区的 String 对象

intern 分析

判断下面代码会输出什么结果👇

public static void main(String[] args) {
    String s = new String("1");
    s.intern();
    String s2 = "1";
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    s3.intern();
    String s4 = "11";
    System.out.println(s3 == s4);
}

打印结果是:

  • jdk6: false,false
  • jdk7: false,true

如果我将 intern() 方法都往下移一句,会发生什么结果呢👇

public static void main(String[] args) {
    String s = new String("1");
    String s2 = "1";
    s.intern();
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    String s4 = "11";
    s3.intern();
    System.out.println(s3 == s4);
}

打印结果:

  • jdk6: false,false
  • jdk8: false,false

JDK 1.6 分析

mark
mark

因为 jdk6中的常量池是放在 Perm 区中的,Perm 区和正常的 JAVA Heap 区域是完全分开的

而我们 new 出来的 String 对象是在 堆区(Heap) 中的,所以拿一个 JAVA Heap 区域的对象地址和字符串常量池的对象地址进行比较肯定是不相同的,即使调用String.intern方法也是没有任何关系的

JDK 1.7 分析

提示

Jdk6 以及以前的版本中,字符串的常量池是放在堆的 Perm 区的,Perm 区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容

jdk7 的版本中,字符串常量池已经从 Perm 区移到正常的 Java Heap(堆) 区域了

mark
mark
  • 我们先看 s和s2 两个对象,通过 new 创建的 String 对象,会在堆中生成String对象,以及常量池中的 "1" 对象,我们执行 s.intern() 方法时,就是 s对象 去常量池中寻找后发现 "1" 已经在常量池中了
  • 接着执行 s2 = "1" ,生成一个引用指向常量池中的 "1" 对象,所以 s和s2 的引用地址不同,从图中我们可以清晰的看出
  • 再看 s3和s4 字符串,String s3 = new String("1") + new String("1") 代码最终生成了两个对象,一个是常量池中的 "1",以及堆中的 s3 引用对象 String,该对象的内容是 "11",此时常量池中是没有 "11" 对象的
  • 接着 s3.intern() 代码执行,将 s3 中引用的字符存储进常量池中,因为此时常量池中是没有 "11" 对象的,所以会在 常量池中生成一个 "11" 的对象,该过程跟 JDK 1.6 是一致的,但是在 JDK 1.7 中,常量池中可以存储堆中的地址引用
  • 接着 String s4 = "11" ,会去常量池中创建,创建的时候会发现已经存在,因为常量池中 "11" 对象保存的是 s3 的地址引用,所以 s4 就指向 s3 所在的内存地址,因此最后 s3 == s4 的结果就是 true
mark
mark
  • 看第二段代码,代码唯一的改变就是 intern 的位置顺序改变了,intern 方法位置变到 String s4 = "11" 后面
  • 因此执行 String s4 = "11" 语句时,常量池中没有 "11" 的对象,所以 s4 会创建一个新的对象,而 s3 也是在堆中创建的对象,两个对象地址都不一致,所以 s3 == s4 的结果是 false

总结

jdk7 版本对 intern 操作和常量池都做了一定的修改

  • String 常量池 从 Perm 移到 Java Heap 区
  • String#intern 方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象