关于 String 的一切

core-java
标签: #<Tag:0x00007f1d26b84c38>

#1

首先, 从最基本的说起

String s1 = new String("Hello world");
String s2 = "Hello world";
String s3 = new String("Hello world").intern();
String s4 = "Hello world".intern();

s1 == s2 : false
s2 == s3 : true
s2 == s4 : true

我们一个一个来说:

String s1 = new String(“Hello world”); 的执行流程是(在没有指令重排的情况下).

  1. 在 heap 堆内存中申请一片存储空间
  2. 初始化 String. JVM先将字符串”Hello world”写入内存中, 然后检查 String pool 字符串常量池中是否有相同的字符 "Hello world”, 如果没有, 则把这个字符串加入池中.
  3. 将内存空间指向 “s1”;

String s2 = “Hello world”; 的流程是

  1. 在 heap 堆内存中申请一片存储空间 (1.7版本之后String Pool才被移到 heap里)
  2. 在 String Pool 中检查是否有这个字符串,
    a. 如果有, 则将刚才申请的内存地址指向已存在的字符串对象.
    b. 如果没有, 则先开辟一块新的内存, 把字符写入内存, 然后将这个字符串对象加入 String pool, 再将刚才申请的内存地址指向字符串对象

String s3 = new String(“Hello world”).intern();

先介绍一下 intern() 方法 (它在1.7之后被修改过, 行为与之前版本略有不同, 但基本功能没变.)
首先它是一个 native 方法, 也就是说它的实现不在 java 中, 通过调用 JNI (Java Native Interface) 调用其他语言实现对系统更底层的访问
intern() 会返回一个字符串,内容与此字符串相同,但它保证来自字符串池中。

然后讲流程, 其实具体流程我也不知道, 有懂得同学请举手纠正我.
我说说我理解. 我觉得new String(“Hello world”) 这个部分和之前的相同, 但调用 intern() 方法后,

  1. 先检查String Pool 中有没有相同的字符串, 如果没有, 则把当前对象加入到String Pool 中, 然后返回当前对象.
  2. 如果String Pool 中有相同字符串. 则返回 String Pool 中的对象.

看完以上流程, 大家应该就明白了为什么s1 != s2了吧, 因为 new String 永远都会创建一个新的字符串, 而不是使用已存在于 String pool 中的.
而 s2, s3, s4 都是指向 String pool 中的同一个对象.

未完待续…


#2

String 这个类是final 的

public final class String

String 对象中保存字符串的属性也是 final 的

private final char value[];

为什么要这么设计呢?
我觉得这样设计都是为了一个原因, 就是安全.
final 关键字说明 String 是不能被继承的, 这样其他人就无法通过继承修改 String 中的一些行为. String 这个类在大部分工程中都会大量的用到, 所以性能问题就尤为重要. 为了提高性能, SUN 的工程师对 String 做了很多优化比如 String Pool 的使用. 如果其他人通过继承修改了 String 的行为. 可能会破坏一些精心设计的优化行为. 甚至导致发成异常.
顺便一提, String Pool 是一个比较典型的Flyweight design pattern 羽量级设计模式.

另外 String 的 value[]被设计成immutable. 所以, 每次对于String对象的修改都将产生一个新的String对象,而原来的对象保持不变. String 的trim, uppercase, substring等方法,它们返回的都是新的String对象,并不是直接修改原来的对象。如果结合 String Pool 的设计思路, 这点就很容易理解了. 而且字符串数组设计为 final 也可以保证多线程的情况下线程安全.

总之, String 类是 final, value也是 final 这些设计都是为了让我们能更安全, 高效的使用 String, 因为 String 这个类在我们的工程中实在太重要了.

再问大家一个问题. String 是线程安全的吗?
理论上说, 因为 String 是 immutable 的, 所以一定是线程安全的.


#3

接下来说说 String, StringBuilder, StringBuffer 的关系

这三个类都是 final 的, 这里可能有些同学有疑问, 有时面试也会被问到, 既然StringBuilder, StringBuffer是 final 的, 那为什么它们可以被修改? 其实 StringBuilder, StringBuffer 只有类是 final 的, 这里的 final 仅仅表明这两个类不能被继承. 但是它们内部用来存放字符串的属性不是 final 的. String 和它们不同 String 内部存放字符串数组的属性也是 final 的. 所以 String 是 immutable 的, 而StringBuilder, StringBuffer不是.

StringBuilder, StringBuffer被引入的主要原因之一就是提高拼接字符串的性能.

先说最简单的加号拼接方法:

String s = "Hello” + “world”;

这行代码其实会被编译器优化为

String s = "Hello world”;

不过我们一般也不会把一个字符串拆开写, 所以这种情况比较少见 还有一种情况是

String s1 = "world"
String s = "Hello" + s1;

其实在JDK1.5 引入了 StringBuilder 之后, 编译器对以上这行代码做了优化. 以上代码会被编译器转为

String s = (new StringBuilder()).append("Hello").append(s1).toString;

所以我们可以理解为用加号拼接字符串和用 StringBuilder的效率是一样的吗?
不一定! 坑还真是多呀. 以上情况确实是一样的, 但这种情况就不同了

String s1 = "world"
String s = “”;
for (int i = 0; i < 100; i++) {
    s += “Hello” + s1;
}

以上代码会被优化为

String s = “”;
for (int i = 0; i < 1000000000; i++) {
    s += (new StringBuilder()).append("Hello").append(s1).toString;
}

这就坑爹了, 每次循环都要new 一个 StringBuilder. 性能及其低下.

还有一种方法是用 concat() 方法连接.

String s = "Hello";
s.concat("world.");

因为 String 是 immutable 对象, 所以每次拼接都会先新建一个 char 数组然后把两个字符串复制进数组里. 性能可想而知.

所以我们就需要StringBuilder, StringBuffer了

StringBuilder, StringBuffer都继承了AbstractStringBuilder, 它们的不同点在这里

StringBuffer:
public synchronized StringBuffer append(String str) { 
    super.append(str); return this; 
}
StringBuilder:
public StringBuilder append(String str) { 
    super.append(str); return this; 
}

一幕了然, StringBuffer是线程安全, StringBuilder不是. 所以理论上说 StingBuilder 性能更高, 但是缺点是线程不安全

我一直想找一个需要线程安全的拼接字符串的例子, 但是实在想不出来, 因为一般来说, 我们在拼接字符串的时候, 顺序是很重要的. 但是我们又没法控制多个线程执行的先后顺序. 所以本来你想拼个 “Hello world” 但结果是”world Hello”. 所以如果要实现多线程拼接字符串其实还是比较复杂的, 并不是单靠一个 StringBuffer 就可以实现的, 那么如果我们自己实现了多线程拼接. 那么就不需要用StringBuffer了.
所以我目前还不能理解 StringBuffer 存在的意义

最后盗一张性能对比图.
来自这篇文章 String concatenation with Java 8


#4

调用 intern() 方法后,
1> 先检查String Pool 中有没有相同的字符串, 如果没有, 把当前对象加入到String Pool 中, 返回当前对象.

加上这一步应该就完整了


#5

恩, 少了这一点