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

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


1、String


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


image.png


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


image.png


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


J3:节省 String 占用的内存。


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


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


J3:标识字符串编码方式


源码:


image.png


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


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

1 代表 UTF-16 编码。

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


image.png


以上介绍了 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));
    }
}


image.png

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


image.png


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


image.png


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


2、StringBuffer 与 StringBuilder


类继承图:


image.png


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


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


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


1、字符串可变


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


看如下代码:

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



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


image.png


由图可发现,原来的字符串其实是不会改变,而是重新在字符串常量池中生一个新字符串,这就是 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 提供的可变字符串内部原理,总结一下可变原因。


内部存放字符串值的 char 类型数组没有被修饰成 final。

实现了一套可扩容的数组机制。


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-1、UTF-8 等),如果 char 类型在 ISO-8859-1 字符编码中字母类型只占 1 个字节,而 UTF-8 则会占用 2 个字节,这就造成了空间浪费。


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


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


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


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


目录
相关文章
|
7天前
|
SQL 分布式计算 监控
Sqoop数据迁移工具使用与优化技巧:面试经验与必备知识点解析
【4月更文挑战第9天】本文深入解析Sqoop的使用、优化及面试策略。内容涵盖Sqoop基础,包括安装配置、命令行操作、与Hadoop生态集成和连接器配置。讨论数据迁移优化技巧,如数据切分、压缩编码、转换过滤及性能监控。此外,还涉及面试中对Sqoop与其他ETL工具的对比、实际项目挑战及未来发展趋势的讨论。通过代码示例展示了从MySQL到HDFS的数据迁移。本文旨在帮助读者在面试中展现Sqoop技术实力。
22 2
|
7天前
|
监控 负载均衡 Cloud Native
ZooKeeper分布式协调服务详解:面试经验与必备知识点解析
【4月更文挑战第9天】本文深入剖析ZooKeeper分布式协调服务原理,涵盖核心概念如Server、Client、ZNode、ACL、Watcher,以及ZAB协议在一致性、会话管理、Leader选举中的作用。讨论ZooKeeper数据模型、操作、会话管理、集群部署与管理、性能调优和监控。同时,文章探讨了ZooKeeper在分布式锁、队列、服务注册与发现等场景的应用,并在面试方面分析了与其它服务的区别、实战挑战及解决方案。附带Java客户端实现分布式锁的代码示例,助力提升面试表现。
29 2
|
7天前
|
数据采集 消息中间件 监控
Flume数据采集系统设计与配置实战:面试经验与必备知识点解析
【4月更文挑战第9天】本文深入探讨Apache Flume的数据采集系统设计,涵盖Flume Agent、Source、Channel、Sink的核心概念及其配置实战。通过实例展示了文件日志收集、网络数据接收、命令行实时数据捕获等场景。此外,还讨论了Flume与同类工具的对比、实际项目挑战及解决方案,以及未来发展趋势。提供配置示例帮助理解Flume在数据集成、日志收集中的应用,为面试准备提供扎实的理论与实践支持。
23 1
|
1月前
|
安全 Java
Java StringBuffer 和 StringBuilder 类
Java StringBuffer 和 StringBuilder 类
15 0
|
8天前
|
分布式计算 资源调度 监控
Hadoop生态系统深度剖析:面试经验与必备知识点解析
本文深入探讨了Hadoop生态系统的面试重点,涵盖Hadoop架构、HDFS、YARN和MapReduce。了解Hadoop的主从架构、HDFS的读写流程及高级特性,YARN的资源管理与调度,以及MapReduce编程模型。通过代码示例,如HDFS文件操作和WordCount程序,帮助读者巩固理解。此外,文章强调在面试中应结合个人经验、行业动态和技术进展展示技术实力。
|
10天前
|
移动开发 安全 Java
String、StringBuffer 、StringBuilder、StringJoiner
String、StringBuffer 、StringBuilder、StringJoiner
|
1月前
|
存储 算法 安全
【数据结构与算法初学者指南】【冲击蓝桥篇】String与StringBuilder的区别和用法
【数据结构与算法初学者指南】【冲击蓝桥篇】String与StringBuilder的区别和用法
|
1月前
|
存储 安全 Java
String、StringBuilder、StringBuffer的区别
String、StringBuilder、StringBuffer的区别
13 0
|
1月前
|
安全 Java
针对String、StringBuffer、Stringbuilder区别及使用场景
针对String、StringBuffer、Stringbuilder区别及使用场景
|
21天前
|
Java 程序员
java线程池讲解面试
java线程池讲解面试
38 1