String、StringBuffer、StringBuilder有什么区别

  |  

典型区别

  • String 是 Java 语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的 Immutable 类,被声明成为 final class,所有属性也都是 final 的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。

  • StringBuffer 是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,我们可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,线程是安全的。

  • StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。


String

String的创建机理

由于String在Java世界中使用过于频繁,Java为了避免在一个系统中产生大量的String对象,引入了字符串常量池

  • 其运行机制是:

    1. 创建一个字符串时,首先检查池中是否有值相同的字符串对象,如果有则不需要创建直接从池中刚查找到的对象引用;
    2. 如果没有则新建字符串对象,返回对象引用,并且将新创建的对象放入池中。
    3. 但是,通过new方法创建的String对象是不检查字符串池的,而是直接在堆区或栈区创建一个新的对象,也不会把对象放入池中。上述原则只适用于通过直接量给String对象引用赋值的情况。
    • 举例:

      1
      2
      String str1 = "123"; //通过直接量赋值方式,放入字符串常量池
      String str2 = new String(“123”);//通过new方式赋值方式,不放入字符串常量池

      注意:String提供了 intern() 方法。调用该方法时,如果常量池中包括了一个等于此String对象的字符串(由equals方法确定),则返回池中的字符串。否则,将此String对象添加到池中,并且返回此池中对象的引用。

    • == 的比较

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      @Test
      public void test(){
      String a = "123";
      String b = "123";
      String c = new String("123");
      System.out.println(a==b);//true
      System.out.println(a==c);//false
      String d = c.intern();//把堆内存的值放进常量池返回一个string
      System.out.println(a==d);//true
      }

String的特性

  1. 不可变 (Immutable)。是指String对象一旦生成,则不能再对它进行改变。不可变的主要作用在于当一个对象需要被多线程共享,并且访问频繁时,可以省略同步和锁等待的时间,从而大幅度提高系统性能。不可变模式是一个可以提高多线程程序的性能,降低多线程程序复杂度的设计模式。

  2. 针对常量池的优化。当2个String对象拥有相同的值时,他们只引用常量池中的同一个拷贝。当同一个字符串反复出现时,这个技术可以大幅度节省内存空间。


StringBuffer/StringBuilder

  • 为了实现修改字符序列的目的,StringBuffer 和 StringBuilder 底层都是利用可修改的(char,JDK 9 以后是 byte)数组,二者都继承了 AbstractStringBuilder,里面包含了基本操作,区别仅在于最终的方法是否加了 synchronized。

  • 另外,这个内部数组应该创建成多大的呢?如果太小,拼接的时候可能要重新创建足够大的数组;如果太大,又会浪费空间。目前的实现是,构建时初始字符串长度加 16(这意味着,如果没有构建对象时输入最初的字符串,那么初始值就是 16)。

    • 我们如果确定拼接会发生非常多次,而且大概是可预计的,那么最好就可以指定合适的大小,避免很多次扩容的开销。扩容会产生多重开销,因为要抛弃原有数组,创建新的(可以简单认为是倍数)数组,还要进行 arraycopy。

全部拼接操作是应该都用 StringBuilder 实现吗?

在没有线程安全问题的情况下,全部拼接操作是应该都用 StringBuilder 实现吗?毕竟这样书写的代码,还是要多敲很多字的,可读性也不理想,下面的对比非常明显。

1
2
3
4
String strByBuilder  = new
StringBuilder().append("aa").append("bb").append("cc").append("dd").toString();

String strByConcat = "aa" + "bb" + "cc" + "dd";

其实,在通常情况下,没有必要过于担心,要相信 Java 还是非常智能的。我们来做个实验,把下面一段代码,利用不同版本的 JDK 编译,然后再反编译,例如:

1
2
3
4
5
6
public class StringConcat {
public static void main(String[] args) {
String myStr = "aa" + "bb" + "cc" + "dd";
System.out.println("My String:" + myStr);
}
}

先编译再反编译,比如使用不同版本的 JDK:

1
2
${JAVA_HOME}/bin/javac StringConcat.java
${JAVA_HOME}/bin/javap -v StringConcat.class
  • JDK 8 的输出片段是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    0: ldc           #2                  // String aabbccdd
    2: astore_1
    3: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
    6: new #4 // class java/lang/StringBuilder
    9: dup
    10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
    13: ldc #6 // String My String:
    15: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    18: aload_1
    19: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    22: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  • 而在 JDK 9 中,反编译的结果就非常简单了,片段是:

    1
    2
    3
    4
    5
    0: ldc           #2                  // String aabbccdd
    2: astore_1
    3: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
    6: aload_1
    7: invokedynamic #4, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
  1. 在 JDK 8 中,字符串拼接操作会自动被 javac 转换为 StringBuilder 操作
  2. 而在 JDK 9 里面则是因为 Java 9 为了更加统一字符串操作优化,提供了 StringConcatFactory,作为一个统一的入口。

javac 自动生成的代码,虽然未必是最优化的,但普通场景也足够了,你可以酌情选择。


字符串缓存

我们粗略统计过,把常见应用进行 堆转储(Dump Heap),然后分析对象组成,会发现平均 25% 的对象是字符串,并且其中约半数是重复的。如果能避免创建重复字符串,可以有效降低内存消耗和对象创建开销。

  • 提供 intern() 方法把字符串放进常量池缓存

    • String 在 Java 6 以后提供了 intern() 方法,目的是提示 JVM 把相应字符串缓存起来,以备重复使用。在我们创建字符串对象并调用 intern() 方法的时候,如果已经有缓存的字符串,就会返回缓存里的实例,否则将其缓存起来。一般来说,JVM 会将所有的类似“abc”这样的文本字符串,或者字符串常量之类缓存起来。

一般使用 Java 6 这种历史版本,并不推荐大量使用 intern,因为被缓存的字符串是存在所谓 PermGen 里的,也就是 臭名昭著的“永久代”,这个空间是很有限的,也基本不会被 FullGC 之外的垃圾收集照顾到。所以,如果使用不当,OOM 就会光顾

针对intern的勉强解决和问题

  • 在后续版本中,这个缓存被放置在堆中,这样就极大避免了永久代占满的问题,甚至永久代在 JDK 8 中被 MetaSpace(元数据区)替代了

    • 而且,默认缓存大小也在不断地扩大中,从最初的 1009,到 7u40 以后被修改为 60013。你可以使用下面的参数直接打印具体数字,可以拿自己的 JDK 立刻试验一下。

      1
      -XX:+PrintStringTableStatistics
    • 你也可以使用下面的 JVM 参数手动调整大小,但是绝大部分情况下并不需要调整,除非你确定它的大小已经影响了操作效率。

      1
      -XX:StringTableSize=N
  • 问题:

    • Intern 是一种显式地排重机制,但是它也有一定的副作用,因为需要开发者写代码时明确调用,一是不方便,每一个都显式调用是非常麻烦的;另外就是我们很难保证效率,应用开发阶段很难清楚地预计字符串的重复情况,有人认为这是一种污染代码的实践。

更好的解决使用G1 GC 下的字符串排重

在 Oracle JDK 8u20 之后,推出了一个新的特性,也就是 G1 GC 下的字符串排重。它是通过将相同数据的字符串指向同一份数据来做到的,是 JVM 底层的改变,并不需要 Java 类库做什么修改。

注意这个功能目前是默认关闭的,你需要使用下面参数开启,并且记得指定使用 G1 GC:

1
-XX:+UseStringDeduplication
  • 前面说到的几个方面,只是 Java 底层对字符串各种优化的一角,在运行时,字符串的一些基础操作会直接利用 JVM 内部的 Intrinsic 机制,往往运行的就是特殊优化的本地代码,而根本就不是 Java 代码生成的字节码。

    • Intrinsic 可以简单理解为,是一种利用 native 方式 hard-coded 的逻辑,算是一种特别的内联,很多优化还是需要直接使用特定的 CPU 指令,具体可以看相关源码,搜索“string”以查找相关 Intrinsic 定义。当然,你也可以在启动实验应用时,使用下面参数,了解 intrinsic 发生的状态。

      1
      2
      3
      4
      5
      6
      -XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
      // 样例输出片段
      180 3 3 java.lang.String::charAt (25 bytes)
      @ 1 java.lang.String::isLatin1 (19 bytes)
      ...
      @ 7 java.lang.StringUTF16::getChar (60 bytes) intrinsic

可以看出,仅仅是字符串一个实现,就需要 Java 平台工程师和科学家付出如此大且默默无闻的努力,我们得到的很多便利都是来源于此。


String自身的演化

对于 Java 的字符串,在历史版本中,它是使用 char 数组来存数据的,这样非常直接。但是 Java 中的 char 是两个 bytes 大小,拉丁语系语言的字符,根本就不需要太宽的 char,这样无区别的实现就造成了一定的浪费。密度是编程语言平台永恒的话题,因为归根结底绝大部分任务是要来操作数据的。

在 Java 9 中,我们引入了 Compact Strings 的设计,对字符串进行了大刀阔斧的改进。将数据存储方式从 char 数组,改变为一个 byte 数组加上一个标识编码的所谓 coder,并且将相关字符串操作类都进行了修改。另外,所有相关的 Intrinsic 之类也都进行了重写,以保证没有任何性能损失。

虽然底层实现发生了这么大的改变,但是 Java 字符串的行为并没有任何大的变化,所以这个特性对于绝大部分应用来说是透明的,绝大部分情况不需要修改已有代码。

  • 理论上的问题:

    • 在极端情况下,字符串也出现了一些能力退化,比如最大字符串的大小。你可以思考下,原来 char 数组的实现,字符串的最大长度就是数组本身的长度限制,但是替换成 byte 数组,同样数组长度下,存储能力是退化了一倍的!还好这是存在于理论中的极限,还没有发现现实应用受此影响。

      • byte是字节数据类型、有符号型的、占1个字节、大小范围为-128——127

      • char是字符数据类型、无符号型的、占2个字节(unicode码)、大小范围为0-65535

在通用的性能测试和产品实验中,我们能非常明显地看到紧凑字符串带来的优势,即更小的内存占用、更快的操作速度

Copyright © 2018 - 2020 Kuanger All Rights Reserved.

访客数 : | 访问量 :