Java中的String、StringBuffer和StringBuilder

简介: Java中的String、StringBuffer和StringBuilder

 作为作为一个已经入了门的java程序猿,肯定对Java中的String、StringBuffer和StringBuilder都略有耳闻了,尤其是String 肯定是经常用的。但肯定你有一点很好奇,为什么java中有三个关于字符串的类?一个不够吗!先回答这个问题,黑格尔曾经说过——存在必合理,单纯一个String确实是不够的,所以要引入StringBuffer。再后来引入StringBuilder是另一个故事了,后面会详细讲到。

  要了解为什么,我们就得先来看下这三者各自都有什么样的特点,有什么样的异同,对其知根知底之后,一切谜团都会被解开。

String

  点开String的源码,可以发现String被定义为final类型,意味着它不能被继承,再仔细看其提供的方法,没有一个能对原始字符串做任何操作的,有几个开启了貌似是操作原字符串的,比如replaceFirst replaceAll,点进去一看,其实是重新生成了一个新的字符串,对原始内容没有做任何修改。

  是的,从实现的角度来看,它是不可变的,所有String的变更其实都会生成一个新的字符串,比String str = "abcdefghijklmnopqrstuvwxy"; str = str + "z"; 之后新生成的a-z并不包含原来的a-y,原来的a-y已经变成垃圾了。简单概括,只要是两个不同的字符,肯定都是两个完全不同不相关的对象,即便其中一个是从另一个subString出来的,两个也没有任何关系。 如果是两个相同的字符串,情况比较复杂,可能是同一份也可能不是。如果在JVM中使用G1gc,而且开启-XX:+UseStringDeduplication,JVM会对字符串的存储做优化,所以如果你的服务中有大量相同字符串,建议开启这个参数。

  Java作为一个非纯面向对象的语言,除了提供分装对象外,也提供了一些原始类型(比如:int long double char),String的使用居然可以像用原始类型一样不需要new,直接String str = "a"这样声明,我觉得String更像是面向对象和非面向对象结合的一个产物。

  String最大的特点就是 __ 不可变__,这是它的优点,因为不可变意味着使用简单,没有线程安全的问题。 但这也是它的缺点,因为每次变更都会生成一个新的字符串,明显太浪费空间了。

StringBuffer

  我觉得StringBuffer是完全因为String的缺点而生的。我们日常使用String的过程中,肯定经常会用到字符串追加的情况,按String的实现,没次追加即便只是一个字符,都是生成一个完全不同的对象,如果这次操作很频繁很多的话会大幅提高内存的消耗,并且增加gc的压力。对于这种问题,StringBuffer是如何解决的呢?我们直接从源码上来看。

  但看StringBuffer里,几乎所有的方法都会调super父类,其实它所有的实现都是在AbstractStringBuilder里的。鉴于我们对其最长用的方法是append,所以我们就从append入手,其实append也是StringBuffer比较核心的功能。

复制

/**
     * The value is used for character storage.
     */
    char[] value;
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }
    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;
    }
    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));
        }
    }
    private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int newCapacity = (value.length << 1) + 2;
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }

  原来是StringBuffer父类AbstractStringBuilder有个char数组value,用来存放字符串所有的字符,StringBuffer默认初始大小是16。StringBuffer在每次append的时候,如果value的容量不够,就会申请一个容量比当前所需大一倍的字符数组,然后把旧的数据拷贝进去。这种一次性扩容一倍的方式,在我们之前HashMap源码浅析中已经看到过了。一次性多申请内存,虽然看起来会有大段的内存空闲,但其实可以减少String append时频繁创建新字符串的问题。

  所以记住,如果你代码中对String频繁操作,千万不用用String而是选择用StringBuffer或者我们下面要讲的StringBuilder。还有一个优化点,如果你能提前知道你字符串最大的长度,建议你在创建StringBuffer时指定其capacity,避免在append时执行ensureCapacityInternal,从而提升性能。

  对于StringBuffer还有一个点没提到,注意看它源码的所有方法,除构造函数外,所有的方法都被synchronized修饰,意味着它是有个线程安全的类,所有操作查询方法都会被加同步,但是如果我们只是单线程呢,想用StringBuffer的优势,但又觉得加同步太多余,太影响性能。这个时候就轮到StringBuilder上场了。

StringBuilder

  StringBuilder从类图上看和StringBuffer完全没有任何区别,再打开它的源码,和StringBuffer一样几乎啥逻辑都没有,全是调调super父类AbstractStringBuilder,它和StringBuffer最大的区别就是所有方法没有用synchronized修复,它不是一个线程安全的类,但也意味着它没有同步,在单线程情况下性能会优于StringBuffer。

总结

  看完上面内容,我觉得你应该知道上面时候用String、什么时候用StringBuffer、什么时候用StringBuilder了。

1. 如果是常量字符串,用String。

2. 多线程环境下经常变动的字符串用StringBuffer。

3. 单线程经常变动的字符串用StringBuilder。

彩蛋

  我们来看个比较底层的东西,是关于jvm对String优化的,现在有如下代码。

复制

public class StringTest {
    public static void main(String[] args) {
        String str = "abc";
        str = str + "d";
        str = str + "e";
    }
}

  我们用javac StringTest.java编译成class文件,然后用javap -c StringTest 生成字节码,内容如下

复制

public class StringTest {
  public StringTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String abc
       2: astore_1
       3: new           #3                  // class java/lang/StringBuilder
       6: dup
       7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      10: aload_1
      11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      14: ldc           #6                  // String d
      16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      22: astore_1
      23: return
}
➜  java git:(master) ✗ javap -c StringTest
Compiled from "StringTest.java"
public class StringTest {
  public StringTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String abc
       2: astore_1
       3: new           #3                  // class java/lang/StringBuilder
       6: dup
       7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      10: aload_1
      11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      14: ldc           #6                  // String d
      16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      22: astore_1
      23: return
}
➜  java git:(master) ✗ javac StringTest.java
➜  java git:(master) ✗ javap -c StringTest  
Compiled from "StringTest.java"
public class StringTest {
  public StringTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String abc
       2: astore_1
       3: new           #3                  // class java/lang/StringBuilder
       6: dup
       7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      10: aload_1
      11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      14: ldc           #6                  // String d
      16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      22: astore_1
      23: new           #3                  // class java/lang/StringBuilder
      26: dup
      27: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      30: aload_1
      31: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      34: ldc           #8                  // String e
      36: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      39: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      42: astore_1
      43: return
}

  其实可以看出,java底层实现字符串+的时候其实是用StringBuilder的append()来实现的,如果有字符串的连续+,jvm用StringBuilder append也可以实现优化。

备注:源码来自JDK11

目录
相关文章
|
17天前
|
安全 Java API
【Java字符串操作秘籍】StringBuffer与StringBuilder的终极对决!
【8月更文挑战第25天】在Java中处理字符串时,经常需要修改字符串,但由于`String`对象的不可变性,频繁修改会导致内存浪费和性能下降。为此,Java提供了`StringBuffer`和`StringBuilder`两个类来操作可变字符串序列。`StringBuffer`是线程安全的,适用于多线程环境,但性能略低;`StringBuilder`非线程安全,但在单线程环境中性能更优。两者基本用法相似,通过`append`等方法构建和修改字符串。
43 1
|
12天前
|
Kubernetes jenkins 持续交付
从代码到k8s部署应有尽有系列-java源码之String详解
本文详细介绍了一个基于 `gitlab + jenkins + harbor + k8s` 的自动化部署环境搭建流程。其中,`gitlab` 用于代码托管和 CI,`jenkins` 负责 CD 发布,`harbor` 作为镜像仓库,而 `k8s` 则用于运行服务。文章具体介绍了每项工具的部署步骤,并提供了详细的配置信息和示例代码。此外,还特别指出中间件(如 MySQL、Redis 等)应部署在 K8s 之外,以确保服务稳定性和独立性。通过本文,读者可以学习如何在本地环境中搭建一套完整的自动化部署系统。
38 0
|
21天前
|
Java
Java系列之 For input string: ““
这篇文章讨论了Java中因尝试将空字符串转换为其它数据类型(如int)时出现的`For input string: ""`错误,并提供了通过非空检查来避免此错误的解决方法。
|
21天前
|
存储 SQL Java
Java 系类之 Java StringBuffer类详解
这篇文章详细介绍了Java中的StringBuffer类的使用,包括其构造方法、追加字符串、替换字符、反转字符串和删除字符串的方法,并提供了相应的示例代码。
|
21天前
|
安全 Java API
Java系类 之 String、StringBuffer和StringBuilder类的区别
这篇文章讨论了Java中`String`、`StringBuffer`和`StringBuilder`三个类的区别,其中`String`是不可变的,而`StringBuffer`是线程安全的可变字符串类,`StringBuilder`是非线程安全的可变字符串类,通常在单线程环境下性能更优。
Java系类 之 String、StringBuffer和StringBuilder类的区别
|
11天前
|
API C# 开发者
WPF图形绘制大师指南:GDI+与Direct2D完美融合,带你玩转高性能图形处理秘籍!
【8月更文挑战第31天】GDI+与Direct2D的结合为WPF图形绘制提供了强大的工具集。通过合理地使用这两种技术,开发者可以创造出性能优异且视觉效果丰富的WPF应用程序。在实际应用中,开发者应根据项目需求和技术背景,权衡利弊,选择最合适的技术方案。
28 0
|
14天前
|
缓存 安全 Java
Java String类
Java String类
12 0
|
18天前
|
开发工具 数据安全/隐私保护
【Azure Developer】使用MSAL4J 与 ADAL4J 的SDK时候,遇见了类型冲突问题 "java.util.Collections$SingletonList cannot be cast to java.lang.String"
【Azure Developer】使用MSAL4J 与 ADAL4J 的SDK时候,遇见了类型冲突问题 "java.util.Collections$SingletonList cannot be cast to java.lang.String"
|
20天前
|
存储 Java
|
3月前
|
Java UED
Java中String强转int:一种常见的错误和解决方法
在Java中将非数字字符串转换为整数会导致`NumberFormatException`。要解决这个问题,可以使用`try-catch`捕获异常,正则表达式验证数字格式,或利用异常信息提供错误提示。例如,`Integer.parseInt()`会因遇到非数字字符如`&quot;123abc&quot;`而抛出异常,但通过异常处理或正则`\\d+`可确保安全转换。记得在编程时避免直接强转,以防止程序异常中断。