String、StringBuilder 和 StringBuffer 详解

碎碎念

  这是一道老生常谈的问题了,字符串是不仅是 Java 中非常重要的一个对象,它在其他语言中也存在。比如 C++、Visual Basic、C# 等。字符串使用 String 来表示,字符串一旦被创建出来就不会被修改,当你想修改StringBuffer 或者是 StringBuilder,出于效率的考量,虽然 String 可以通过 + 来创建多个对象达到字符串拼接的效果,但是这种拼接的效率相比 StringBuffer 和 StringBuilder,那就是心有余而力不足了。本篇文章我们一起来深入了解一下这三个对象。

简单认识这三个对象

String

 String 表示的就是 Java 中的字符串,我们日常开发用到的使用 “” 双引号包围的数都是字符串的实例。String 类其实是通过 char 数组来保存字符串的。下面是一个典型的字符串的声明

String s = "abc";

上面你创建了一个名为 abc 的字符串。

字符串是恒定的,一旦创建出来就不会被修改,怎么理解这句话?我们可以看下 String 源码的声明

告诉我你看到了什么?String 对象是由final 修饰的,一旦使用 final 修饰的类不能被继承、方法不能被重写、属性不能被修改。而且 String 不只只有类是 final 的,它其中的方法也是由 final 修饰的,换句话说,Sring 类就是一个典型的 Immutable 类。也由于 String 􏰆的不可变性,类似字符串拼接、字符串截取等操作都会产生新的 String 对象。

所以请你告诉我下面

String s1 = "aaa";
String s2 = "bbb" + "ccc";
String s3 = s1 + "bbb";
String s4 = new String("aaa");

分别创建了几个对象?

 首先第一个问题,s1 创建了几个对象。字符串在创建对象时,会在常量池中看有没有 aaa 这个字符串;如果没有此时还会在常量池中创建一个;如果有则不创建。我们默认是没有的情况,所以会创建一个对象。下同。
 那么 s2 创建了几个对象呢?是两个对象还是一个对象?我们可以使用 javap -c 看一下反汇编代码

public class com.sendmessage.api.StringDemo {
  public com.sendmessage.api.StringDemo();
    Code:
       0: aload_0
       1: invokespecial #1                  // 执行对象的初始化方法
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // 将 String aaa 执行入栈操作
       2: astore_1													# pop出栈引用值,将其(引用)赋值给局部变量表中的变量 s1
       3: ldc           #3                  // String bbbccc
       5: astore_2
       6: return
}

 编译器做了优化 String s2 = "bbb" + "ccc" 会直接被优化为 bbbccc。也就是直接创建了一个 bbbccc 对象。

javap 是 jdk 自带的反汇编工具。它的作用就是根据 class 字节码文件,反汇编出当前类对应的 code 区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。
javap -c 就是对代码进行反汇编操作。
  • 下面来看 s3,s3 创建了几个对象呢?是一个还是两个?还是有其他选项?我们使用 javap -c 来看一下

String、StringBuilder 和 StringBuffer 详解

我们可以看到,s3 执行 + 操作会创建一个 StringBuilder 对象然后执行初始化。执行 + 号相当于是执行 new StringBuilder.append() 操作。所以

String s3 = s1 + "bbb";

==
  
String s3 = new StringBuilder().append(s1).append("bbb").toString();

// Stringbuilder.toString() 方法也会创建一个 String 

public String toString() {
  // Create a copy, don't share the array
  return new String(value, 0, count);
}

所以 s3 执行完成后,相当于创建了 3 个对象。

  • 下面来看 s4 创建了几个对象,在创建这个对象时因为使用了 new 关键字,所以肯定会在堆中创建一个对象。然后会在常量池中看有没有 aaa 这个字符串;如果没有此时还会在常量池中创建一个;如果有则不创建。所以可能是创建一个或者两个对象,但是一定存在两个对象。

 说完了 String 对象,我们再来说一下 StringBuilder 和 StringBuffer 对象。

 上面的 String 对象竟然和 StringBuilder 产生了千丝万缕的联系。不得不说 StringBuilder 是一个牛逼的对象。String 对象底层是使用了 StringBuilder 对象的 append 方法进行字符串拼接的,不由得对 StringBuilder 心生敬意。

 不由得我们想要真正认识一下这个 StringBuilder 大佬,但是在认识大佬前,还有一个大 boss 就是 StringBuffer 对象,这也是你不得不跨越的鸿沟。
String、StringBuilder 和 StringBuffer 详解

StringBuffer

StringBuffer 对象 代表一个可变的字符串序列,当一个 StringBuffer 被创建以后,通过 StringBuffer 的一系列方法可以实现字符串的拼接、截取等操作。一旦通过 StringBuffer 生成了最终想要的字符串后,就可以调用其 toString 方法来生成一个新的字符串。例如

StringBuffer b = new StringBuffer("111");
b.append("222");
System.out.println(b);

 我们上面提到 + 操作符连接两个字符串,会自动执行 toString() 方法。那你猜 StringBuffer.append 方法会自动调用吗?直接看一下反汇编代码不就完了么?
String、StringBuilder 和 StringBuffer 详解

 上图左边是手动调用 toString 方法的代码,右图是没有调用 toString 方法的代码,可以看到,toString() 方法不像 + 一样自动被调用。

StringBuffer 是线程安全的,我们可以通过它的源码可以看出
String、StringBuilder 和 StringBuffer 详解

StringBuffer 在字符串拼接上面直接使用 synchronized 关键字加锁,从而保证了线程安全性。

StringBuilder

 最后来认识大佬了,StringBuilder 其实是和 StringBuffer 几乎一样,只不过 StringBuilder 是非线程安全的。并且,为什么 + 号操作符使用 StringBuilder 作为拼接条件而不是使用 StringBuffer 呢?我猜测原因是加锁是一个比较耗时的操作,而加锁会影响性能,所以 String 底层使用 StringBuilder 作为字符串拼接。
String、StringBuilder 和 StringBuffer 详解

深入理解 String、StringBuilder、StringBuffer

 我们上面说到,使用 + 连接符时,JVM 会隐式创建 StringBuilder 对象,这种方式在大部分情况下并不会造成效率的损失,不过在进行大量循环拼接字符串时则需要注意。如下这段代码

String s = "aaaa";
for (int i = 0; i < 100000; i++) {
    s += "bbb";
}

这是一段很普通的代码,只不过对字符串 s 进行了 + 操作,我们通过反编译代码来看一下。

// 经过反编译后
String s = "aaa";
for(int i = 0; i < 10000; i++) {
     s = (new StringBuilder()).append(s).append("bbb").toString();    
}

 你能看出来需要注意的地方了吗?在每次进行循环时,都会创建一个 StringBuilder 对象,每次都会把一个新的字符串元素 bbb 拼接到 aaa 的后面,所以,执行几次后的结果如下
String、StringBuilder 和 StringBuffer 详解

 每次都会创建一个 StringBuilder ,并把引用赋给 StringBuilder 对象,因此每个 StringBuilder 对象都是强引用, 这样在创建完毕后,内存中就会多了很多 StringBuilder 的无用对象。

这样由于大量 StringBuilder 创建在堆内存中,肯定会造成效率的损失,所以在这种情况下建议在循环体外创建一个 StringBuilder 对象调用 append()方法手动拼接。

例如:

StringBuilder builder = new StringBuilder("aaa");
for (int i = 0; i < 10000; i++) {
    builder.append("bbb");
}
builder.toString();

这段代码中,只会创建一个 builder 对象,每次循环都会使用这个 builder 对象进行拼接,因此提高了拼接效率。

String

String 还提供了一些其他方法

  • charAt :返回指定位置上字符的值
  • getChars: 复制 String 中的字符到指定的数组
  • equals: 用于判断 String 对象的值是否相等
  • indexOf : 用于检索字符串
  • substring: 对字符串进行截取
  • concat: 用于字符串拼接,效率高于 +
  • replace:用于字符串替换
  • match:正则表达式的字符串匹配
  • contains: 是否包含指定字符序列
  • split: 字符串分割
  • join: 字符串拼接
  • trim: 去掉多余空格
  • toCharArray: 把 String 对象转换为字符数组
  • valueOf: 把对象转换为字符串
StringBuilder

 StringBuilder 类表示一个可变的字符序列,我们知道,StringBuilder 是非线程安全的容器,一般适用于单线程场景中的字符串拼接操作,下面我们就来从源码角度看一下 StringBuilder。

首先我们来看一下 StringBuilder 的定义

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence {...}

StringBuilder 被 final 修饰,表示 StringBuilder 是不可被继承的,StringBuilder 类继承于 AbstractStringBuilder类。实际上,AbstractStringBuilder 类具体实现了可变字符序列的一系列操作,比如:append()、insert()、delete()、replace()、charAt() 方法等。

StringBuilder 实现了 2 个接口

  • Serializable 序列化接口,表示对象可以被序列化。
  • CharSequence 字符序列接口,提供了几个对字符序列进行只读访问的方法,例如 length()、charAt()、subSequence()、toString() 方法等。

StringBuilder 使用 AbstractStringBuilder 类中的两个变量作为元素

char[] value; // 存储字符数组

int count; // 字符串使用的计数
StringBuffer

 StringBuffer 也是继承于 AbstractStringBuilder ,使用 value 和 count 分别表示存储的字符数组和字符串使用的计数,StringBuffer 与 StringBuilder 最大的区别就是 StringBuffer 可以在多线程场景下使用,StringBuffer 内部有大部分方法都加了 synchronized 锁。在单线程场景下效率比较低,因为有锁的开销。

StringBuilder 和 StringBuffer 的扩容问题

 我相信这个问题很多同学都没有注意到吧,其实 StringBuilder 和 StringBuffer 存在扩容问题,先从 StringBuilder 开始看起

首先先注意一下 StringBuilder 的初始容量

public StringBuilder() {
  super(16);
}

StringBuilder 的初始容量是 16,当然也可以指定 StringBuilder 的初始容量。

在调用 append 拼接字符串,会调用 AbstractStringBuilder 中的 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 方法,这个就是扩容方法,我们跟进去看一下

private void ensureCapacityInternal(int minimumCapacity) {
  // overflow-conscious code
  if (minimumCapacity - value.length > 0) {
    value = Arrays.copyOf(value,
                          newCapacity(minimumCapacity));
  }
}

这个方法会进行判断,minimumCapacity 就是字符长度 + 要拼接的字符串长度,如果拼接后的字符串要比当前字符长度大的话,会进行数据的复制,真正扩容的方法是在 newCapacity 中

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;
}

扩容后的字符串长度会是原字符串长度增加一倍 + 2,如果扩容后的长度还比拼接后的字符串长度小的话,那就直接扩容到它需要的长度 newCapacity = minCapacity,然后再进行数组的拷贝。

总结

 本篇文章主要描述了 String 、StringBuilder 和 StringBuffer 的主要特性,String、StringBuilder 和 StringBuffer 的底层构造是怎样的,以及 String 常量池的优化、StringBuilder 和 StringBuffer 的扩容特性等。

我是好文章的搬运工

本文转载自:
作者:程序员cxuan
链接:https://juejin.cn/post/6844904181455831048

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
乘风的头像乘风管理团队
上一篇 2023年12月8日
下一篇 2023年12月8日

相关推荐