1 年经验面试说说:String、StringBuffer、StringBuilder

  • J3 - 西行
  • 面试题(面试 # 基础 # String)

JavaSE 基础题目了,可以说字符串所要了解的内容还是非常多的,其中涉及字符串可变、字符串拼接、字符串安全、字符串内存位置等等。

下面,咱们就具体来分析这一问题。

1、String

String 是 Java 定义的一个字符串类型类,源码(JDK11,本篇所有源码环境都是 11 )如下:

在这里插入图片描述

这里说明一点,Java 在不同版本对 String 源码做了点修改,具体改动如下图。

在这里插入图片描述

改动最大的莫过于将存储字符串的 char 类型数组改成了 byte 类型。那这是为什么呢!

J3:节省 String 占用的内存。

Java 程序语言是按照 Unicode 编码标准存储字符串的,而我们都知道 UTF - 8 编码占用两个及以上的字节个数、ISO-8859-1 编码则是单字节编码只占一个字节。在大部分的时候计算机任然使用的是 ISO-8859-1 编码,所以在存储像字母时,则会白白浪费一个字节的空间,也正是这个原因,Java 才会将 char 改成 byte。

那多了的一个属性 code 是干啥?

J3:标识字符串编码方式

源码:

在这里插入图片描述

coder 属性默认有 0 和 1 两个值。如果 String 判断字符串只包含了 Latin-1,则 coder 属性值为 0 ,反之则为 1

  • 0 代表Latin-1(单字节编码)。
  • 1 代表 UTF-16 编码。

另外 String 类是被 final 修饰的,表示最终类即不可被继承。而且内部存储字符串值的数组属性也是被 final 修饰表明 String 类型变量一旦被定义赋值,则值不可修改(下面会解释不可修改这个点)。

在这里插入图片描述

以上介绍了 String 的基本情况,那再来说说它在 JVM 中的内存布局。

JVM 内部划分为两个组件和两个系统(《Java 虚拟机运行时数据区》):

两个子系统为

  • Class Loader(类装载子系统)
  • Execution Engine(执行引擎)

两个组件为

  • Runtime Data Area(运行时数据区)
  • Native Interface(本地接口)

String 所涉及的区为 Runtime Data Area(运行时数据区) ,在该区中 String 类型的字符串常量存放区域倒是因为 JDK 版本的不一样而略有不同。

  • JDK1.6 及以前字符串常量都存放在方法区的字符串常量池中。
  • JDK1.7 及以后字符串常量池被移到了堆中,所以字符串常量自然就存放在堆中了。

那下面来看看几行代码:

public class StringTest {

    public static void main(String[] args) {
        // 直接赋值一个字符串常量值
        String name = "J3";
        String name1 = "J3";
        // 创建一个 String 对象赋值
        String name2 = new String("J3");
        System.out.println("name 重新赋值前:" + name);
        // name 和 name1 是否相等
        System.out.println("name 和 name1 是否相等:" + (name == name1));
        // 给 name 重新赋值
        name = "刘亦菲";
        System.out.println("name 重新赋值后:" + name);
        System.out.println("name2 赋值:" + name2);
        // name 和 name1 是否相等
        System.out.println("name 和 name1 是否相等:" + (name == name1));
    }
}

上面代码的 5,6,8 行代码都是给变量赋值,体现在 JVM 中的效果如图:

在这里插入图片描述

紧接着 13 行代码体现图如下:

在这里插入图片描述

结合上图,当字符串常量池中出现相同的字符串时,JVM 不会再生成对应的字符串而时将已经存在的字符串地址赋给变量,从而在字符串常量池中相同的字符串只会存在一份。当栈中变量重新赋值字符串时,则会将变量引用指向新创建的常量池中字符串地址,而常量池原先的值是不会改变的,所以 String 类型变量重新赋值只是变量指向的地址变化,不是值变化。

2、StringBuffer 与 StringBuilder

类继承图:

在这里插入图片描述

StringBuffer 是一个字符串可变的序列,通过其提供的方法可以改变这个字符串对象的字符串序列。

StringBuilder 是从 JDK1.5 开始出现的,功能和 StringBuffer 类似,不同点是 StringBuffer 线程安全,StringBuilder 线程不安全。

这里有两个点,字符串可变和线程安全。

1、字符串可变

String 类型字符串不可变是因为内部存储值的属性是被 final 修饰,所以其值不可变。如果在进行字符串拼接的时候,字符串常量值不会在原来的字符串后面添加字符串,而是重新生成一个拼接后的字符串放到字符串常量池中。

看如下代码:

String name3 = "J3" + "-西行";

继续结合上图,效果如下:

在这里插入图片描述

由图可发现,原来的字符串其实是不会改变,而是重新在字符串常量池中生一个新字符串,这就是 String 字符串不可变的真正地方。

而 StringBuffer 字符串可变是体现在什么地方,咱上源码。

StringBuffer # append

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    // 调用父类(AbstractStringBuilder)拼接字符串方法
    super.append(str);
    return this;
}

既然调用了父类方法,那点进去瞧瞧。

AbstractStringBuilder # append

public AbstractStringBuilder append(String str) {
    // 拼接字符串为空,那就直接拼接一个空字符串
    if (str == null)
        return appendNull();
    // 获取拼接的字符串长度
    int len = str.length();
    // (重点代码)扩容!!!将原始 char 数组扩容到可以容纳拼接后字符串的长度
    ensureCapacityInternal(count + len);
    // 真正开始字符串拼接
    str.getChars(0, len, value, count);
    // 重新计算字符串长度
    count += len;
    // 返回字符串对象
    return this;
}

在 StringBuffer 和 StringBuilder 中,存储字符串的数组还是 char 类型,并且没有被 final 修饰,所以其指向的 char 类型数组引用可以重新赋值。

那现在来关注一下重点代码,扩容。

AbstractStringBuilder # ensureCapacityInternal

private void ensureCapacityInternal(int minimumCapacity) {
    // overflow-conscious code
    // 如果合并后的字符串长度,大于原始字符串长度,才开始扩容
    if (minimumCapacity - value.length > 0) {
        // 数组扩容,底层调用 System.arraycopy 方法,原理是生成一个新的数组,将原始数组中的内容移动到新数组中,最终赋值给 value
        value = Arrays.copyOf(value,
                newCapacity(minimumCapacity));
    }
}

扩容代码仅仅只是一个开始,保证最后字符串拼接的时候不会导致 char 类型数组溢出,那最后只剩下字符串拼接了,上代码。

AbstractStringBuilder # getChars

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
    // 各种字符串长度校验
    if (srcBegin < 0) {
        throw new StringIndexOutOfBoundsException(srcBegin);
    }
    if (srcEnd > value.length) {
        throw new StringIndexOutOfBoundsException(srcEnd);
    }
    if (srcBegin > srcEnd) {
        throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
    }
    // 最终走到这里,将 待拼接字符串:value,赋值到 目标数组:dst 中,完成拼接。
    System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}

以上就是 Java 提供的可变字符串内部原理,总结一下可变原因。

  1. 内部存放字符串值的 char 类型数组没有被修饰成 final。
  2. 实现了一套可扩容的数组机制。

2、线程安全问题

在字符串可变问题上,我已经贴出了可变字符串类型中的一个重要字符串拼接方法(StringBuffer # append)源代码,其中方法上被 synchronized 修饰了,这就是其是一个线程安全的字符串拼接类。

大家可以仔细留意一下,只要是涉及改变字符串内容的方法,都被 synchronized 修饰了,以此来保证线程安全。

why?为什么,这是为什么?

说一下我的理解:保证字符串拼接出的结果和我们预期的一样,系统中可变字符串对象引用可以被多个方法所执行,而他们都想进行字符串拼接,那同一时刻多个方法调用同一个可变字符串对象进行字符串拼接,我们能得到预期的结果嘛,显然是不能的,所以 Java 就在方法的开头加了一把锁(synchronized)谁能第一个锁住这个对象,那就谁先来执行字符串拼接。

而对资源进行加锁与解锁毕竟是要有点开销的,所以 StringBuffer 在字符串拼接的时候效率就会有点损耗 StringBuilder 则不会,因为它内部拼接方法没有加锁,但这也是它线程不安全的原因。

3、我的面试答案

面试官你好,对于这个问题我说一下我的理解:

Java 中常用的字符串类型就莫过于 String 类型了,它是一个被 final 修饰的类,表示类不可被继承。其中存储字符串的数组属性同样也被 final 修饰这也是其字符串不可变的原因,并在不同的 JDK 版本中存储字符串的数组类型也是不一样。

在 JDK1.8 及以前存储字符串的数组类型为 char 之后则是改成了 byte 类型,究其原因则是为了节省字符串占用空间。我们都知道 Java 的字符串编码规则是按 Unicode 编码,Unicode 只是一个规范(其实现有 ISO-8859-1UTF-8 等),如果 char 类型在 ISO-8859-1 字符编码中字母类型只占 1 个字节,而 UTF-8 则会占用 2 个字节,这就造成了空间浪费。

而 StringBuffer 和 StringBuilder 是属于字符串可变类,内部存储字符的是一个 char 类型数组并没有被 final 修饰,且其内部实现了一套可变的数组代码,这就使得其可以在 char 数组中进行扩容添加字符。

对于可变字符串类 StringBuffer 和 StringBuilder 两者功能基本一样,只不过两者在拼接字符串的时候考虑的使用环境不同。StringBuffer 类在线程安全和不安全环境都可以使用,因为其内部拼接方法都被 synchronized 修饰了,使其变成了一个线程安全方法,但效率有点损耗;StringBuilder 类内部拼接方法则没有保证线程安全未被 synchronized 修饰,所以其只能在线程安全环境下使用,也正是其未被 synchronized 修饰,所以在字符串拼接的时候效率比 StringBuffer 高一点。

到这里,内心窃喜,没有被难倒

今天的内容到这里就结束了,关注我,我们下期见

联系方式:

QQ:1491989462

微信:13207920596

做个好友,来个点赞之交。


  • 由于博主才疏学浅,难免会有纰漏,假如你发现了错误或偏见的地方,还望留言给我指出来,我会对其加以修正。

  • 如果你觉得文章还不错,你的转发、分享、点赞、留言就是对我最大的鼓励。

  • 感谢您的阅读,十分欢迎并感谢您的关注。

^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

个人站点:J3

CSDN:J3

掘金:J3

知乎:J3

这是一个技术一般,但热衷于分享;经验尚浅,但脸皮够厚;明明年轻有颜值,但非要靠才华吃饭的程序员。

^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

J3code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值