String、StringBuffer、StringBuilder有什么区别?

news2024/11/24 18:12:13

第5讲 | String、StringBuffer、StringBuilder有什么区别?

在这里插入图片描述

今天我会聊聊日常使用的字符串,别看它似乎很简单,但其实字符串几乎在所有编程语言里都是个特殊的存在,因为不管是数量还是体积,字符串都是大多数应用中的重要组成。

今天我要问你的问题是,理解 Java 的字符串,String、StringBuffer、StringBuilder 有什么区别?

典型回答

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

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

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

考点分析

几乎所有的应用开发都离不开操作字符串,理解字符串的设计和实现以及相关工具如拼接类的使用,对写出高质量代码是非常有帮助的。关于这个问题,我前面的回答是一个通常的概要性回答,至少你要知道 String 是 Immutable 的,字符串操作不当可能会产生大量临时字符串,以及线程安全方面的区别。

如果继续深入,面试官可以从各种不同的角度考察,比如可以:

通过 String 和相关类,考察基本的线程安全设计与实现,各种基础编程实践。

考察 JVM 对象缓存机制的理解以及如何良好地使用。

考察 JVM 优化 Java 代码的一些技巧。

String 相关类的演进,比如 Java 9 中实现的巨大变化。

针对上面这几方面,我会在知识扩展部分与你详细聊聊。

知识扩展

  1. 字符串设计和实现考量

    我在前面介绍过,String 是 Immutable 类的典型实现,原生的保证了基础线程安全,因为你无法对它内部数据进行任何修改,这种便利甚至体现在拷贝构造函数中,由于不可变,Immutable 对象在拷贝时不需要额外复制数据。

    我们再来看看 StringBuffer 实现的一些细节,它的线程安全是通过把各种修改数据的方法都加上

    synchronized 关键字实现的,非常直白。其实,这种简单粗暴的实现方式,非常适合我们常见的线程安全类实现,不必纠结于 synchronized 性能之类的,有人说“过早优化是万恶之源”,考虑可靠性、正确性和代码可读性才是大多数应用开发最重要的因素。

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

    另外,这个内部数组应该创建成多大的呢?如果太小,拼接的时候可能要重新创建足够大的数组;如果太大,又会浪费空间。目前的实现是,构建时初始字符串长度加 16(这意味着,如果没有构建对象时输入最初的字符串,那么初始值就是 16)。我们如果确定拼接会发生非常多次,而且大概是可预计的,那么就可以指定合适的大小,避免很多次扩容的开销。扩容会产生多重开销,因为要抛弃原有数组,创建新的(可以简单认为是倍数)数组,还要进行 arraycopy。

    前面我讲的这些内容,在具体的代码书写中,应该如何选择呢?

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

    
    String strByBuilder  = new
    StringBuilder().append("aa").append("bb").append("cc").append
                ("dd").toString();
                 
    String strByConcat = "aa" + "bb" + "cc" + "dd";
    

    其实,在通常情况下,没有必要过于担心,要相信 Java 还是非常智能的。

    我们来做个实验,把下面一段代码,利用不同版本的 JDK 编译,然后再反编译,例如:

    
    public class StringConcat {
         public static String concat(String str) {
           return str + “aa” + “bb”;
         }
    }
    

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

    
    ${JAVA_HOME}/bin/javac StringConcat.java
    ${JAVA_HOME}/bin/javap -v StringConcat.class
    

    JDK 8 的输出片段是:

    
             0: new           #2                  // class java/lang/StringBuilder
             3: dup
             4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
             7: aload_0
             8: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
            11: ldc           #5                  // String aa
            13: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
            16: ldc           #6                  // String bb
            18: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
            21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    

    而在 JDK 9 中,反编译的结果就会有点特别了,片段是:

    
             // concat method
             1: invokedynamic #2,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
             
             // ...
             // 实际是利用了MethodHandle,统一了入口
             0: #15 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
    

    你可以看到,非静态的拼接逻辑在 JDK 8 中会自动被 javac 转换为 StringBuilder 操作;而在 JDK 9 里面,则是体现了思路的变化。Java 9 利用 InvokeDynamic,将字符串拼接的优化与 javac 生成的字节码解耦,假设未来 JVM 增强相关运行时实现,将不需要依赖 javac 的任何修改。

    在日常编程中,保证程序的可读性、可维护性,往往比所谓的最优性能更重要,你可以根据实际需求酌情选择具体的编码方式。

  2. 字符串缓存

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

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

    看起来很不错是吧?但实际情况估计会让你大跌眼镜。一般使用 Java 6 这种历史版本,并不推荐大量使用 intern,为什么呢?魔鬼存在于细节中,被缓存的字符串是存在所谓 PermGen 里的,也就是臭名昭著的“永久代”,这个空间是很有限的,也基本不会被 FullGC 之外的垃圾收集照顾到。所以,如果使用不当,OOM 就会光顾。

    在后续版本中,这个缓存被放置在堆中,这样就极大避免了永久代占满的问题,甚至永久代在 JDK 8 中被 MetaSpace(元数据区)替代了。而且,默认缓存大小也在不断地扩大中,从最初的 1009,到 7u40 以后被修改为 60013。你可以使用下面的参数直接打印具体数字,可以拿自己的 JDK 立刻试验一下。

    -XX:+PrintStringTableStatistics
    

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

    -XX:StringTableSize=N
    

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

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

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

    -XX:+UseStringDeduplication
    

    前面说到的几个方面,只是 Java 底层对字符串各种优化的一角,在运行时,字符串的一些基础操作会直接利用 JVM 内部的 Intrinsic 机制,往往运行的就是特殊优化的本地代码,而根本就不是 Java 代码生成的字节码。Intrinsic 可以简单理解为,是一种利用 native 方式 hard-coded 的逻辑,算是一种特别的内联,很多优化还是需要直接使用特定的 CPU 指令,具体可以看相关源码(http://hg.openjdk.java.net/jdk/jdk/file/44b64fc0baa3/src/hotspot/share/classfile/vmSymbols.hpp,搜索“string”以查找相关 Intrinsic 定义。当然,你也可以在启动实验应用时,使用下面参数,了解 intrinsic 发生的状态。

    
    -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 平台工程师和科学家付出如此大且默默无闻的努力,我们得到的很多便利都是来源于此。

    我会在专栏后面的 JVM 和性能等主题,详细介绍 JVM 内部优化的一些方法,如果你有兴趣可以再深入学习。即使你不做 JVM 开发或者暂时还没有使用到特别的性能优化,这些知识也能帮助你增加技术深度。

  3. String 自身的演化

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

    其实在 Java 6 的时候,Oracle JDK 就提供了压缩字符串的特性,但是这个特性的实现并不是开源的,而且在实践中也暴露出了一些问题,所以在最新的 JDK 版本中已经将它移除了。

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

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

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

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

    今天我从 String、StringBuffer 和 StringBuilder 的主要设计和实现特点开始,分析了字符串缓存的 intern 机制、非代码侵入性的虚拟机层面排重、Java 9 中紧凑字符的改进,并且初步接触了 JVM 的底层优化机制 intrinsic。从实践的角度,不管是 Compact Strings 还是底层 intrinsic 优化,都说明了使用 Java 基础类库的优势,它们往往能够得到最大程度、最高质量的优化,而且只要升级 JDK 版本,就能零成本地享受这些益处。

    一课一练

    关于今天我们讨论的题目你做到心中有数了吗?限于篇幅有限,还有很多字符相关的问题没有来得及讨论,比如编码相关的问题。可以思考一下,很多字符串操作,比如 getBytes()/String(byte[] bytes) (https://docs.oracle.com/javase/9/docs/api/java/lang/String.html#String-byte:A-)等都是隐含着使用平台默认编码,这是一种好的实践吗?是否有利于避免乱码?

请你在留言区写写你对这个问题的思考,或者分享一下你在操作字符串时掉过的坑,我会选出经过认真思考的留言,送给你一份学习鼓励金,欢迎你与我一起讨论。

etBytes()/String(byte[] bytes) (https://docs.oracle.com/javase/9/docs/api/java/lang/String.html#String-byte:A-)等都是隐含着使用平台默认编码,这是一种好的实践吗?是否有利于避免乱码?

请你在留言区写写你对这个问题的思考,或者分享一下你在操作字符串时掉过的坑,我会选出经过认真思考的留言,送给你一份学习鼓励金,欢迎你与我一起讨论。

你的朋友是不是也在准备面试呢?你可以“请朋友读”,把今天的题目分享给好友,或许你能帮到他。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/333363.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【PR】源窗口

【PR】源窗口源窗口粗剪源窗口按钮功能标记按钮出入点相关插入与覆盖插入覆盖导出帧使用软件&#xff1a;Premiere2020 源窗口粗剪 我们手上可能有一些很长的视频&#xff0c;但是我就想要其中的几段&#xff0c;这个时候粗剪就很方便&#xff0c;把想要的视频段落剪出来先凑一…

私募证券基金动态-23年1月报

成交量&#xff1a;1月日均7,901.31亿元2023年1月A股两市日均成交7,901.31亿元&#xff0c;环比上升0.33%、同比下降25.18%。1月恰逢春节仅16个交易日&#xff0c;节后2个交易日交易量明显回暖。管理人&#xff1a;新提交备案51家&#xff0c;备案通过21家1月新提交备案申请的5…

分析了 200 个 DeFi 项目,我发现了这些规律

作者&#xff1a;Ren & Heinrich翻译&#xff1a;dongdong在这篇文章中&#xff0c;我分享了我通过分析当前排名前 200 的 DeFi 加密项目的见解。这不是一项学术研究。尽管如此&#xff0c;这些发现对加密货币投资者来说具有附加值。我使用 https://defillama.com/ 的公共数…

财报解读:业务复苏迹象明显,中国中免能否重写增长神话?

2月3日&#xff0c;中国中免披露2022年度业绩快报&#xff0c;2022年总营收为544.63亿元&#xff0c;同比下降19.52%&#xff1b;实现归属于上市公司股东的净利润50.25亿元&#xff0c;同比下降47.95%。来源&#xff1a;中国中免2022年度业绩快报业绩近乎腰斩&#xff0c;但从长…

库存管理系统软件哪个好用 盘点前十名!

库存管理怎么做&#xff1f;库存管理系统有什么用处&#xff1f;市面上那么多库存管理系统&#xff0c;我们又如何挑选呢&#xff1f; 本文将为您介绍库存管理系统挑选的方法以及库存管理系统的选型。 库存管理系统前十名为&#xff1a;1、简道云库存管理软件&#xff1b;2、…

网络通讯的理解

tcp/ip 协议族ip在真实环境中&#xff0c;会把主机号再分成一个子网号和一个主机号。这样的主机号才是最终容纳的主机数量。所以需要使用子网掩码&#xff08;32位&#xff09;来分子网号和主机号。其中值为1的比特是网络号和子网号&#xff0c;值为0的是比特是主机号。可以在w…

真的麻了,别再为难软件测试员了......

前言 有不少技术友在测试群里讨论&#xff0c;近期的面试越来越难了&#xff0c;要背的八股文越来越多了,考察得越来越细&#xff0c;越来越底层&#xff0c;明摆着就是想让我们徒手造航母嘛&#xff01;实在是太为难我们这些测试工程师了。 这不&#xff0c;为了帮大家节约时…

Nacos【一】Nacos集群部署配置

系列文章目录 暂无 文章目录系列文章目录前言一、Nacos集群架构1.ip直连2. SLB3. 域名-SLB二、集群部署准备2.1 机器准备2.2 Nginx安装配置1.安装2.负载均衡配置2.3 nacos安装配置1.nacos节点2. MySQL准备1.Docker安装MySQL2. nacos对应数据库初始化三、 集群启动1.失败原因汇…

Python深度学习实战PyQt5信号与槽的连接

本文讲解信号与槽的连接机制&#xff0c;详细示范各种类型的信号/槽连接的实现方法&#xff0c;这是图形用户界面的核心内容。还将介绍面向对象的程序设计&#xff0c;这是图形用户界面的基本思想目录1. 信号与槽&#xff08;Signals and slots&#xff09;信号与槽机制是 PyQt…

源码分析Spring解决循环依赖的过程

循环依赖是之前很爱问的一个面试题&#xff0c;最近不咋问了&#xff0c;但是梳理Spring解决循环依赖的源码&#xff0c;会让我们对Spring创建bean的流程有一个清晰的认识&#xff0c;有必要搞一搞。开始搞之前&#xff0c;先参考了这个老哥写的文章&#xff0c;对Spring处理循…

4. Spring 之 AOP

文章目录1. AOP 简介2. AOP 入门案例3. AOP 工作流程&#xff08;略&#xff09;4. AOP 切入点表达式4.1 语法格式4.2 通配符4.3 书写技巧5. AOP 通知类型5.1 前置通知、后置通知5.2 环绕通知(重点)5.3 返回后通知(了解)5.4 抛出异常后通知(了解)6. 案例&#xff1a;业务层接口…

Elasticsearch为什么快?

1、搜索引擎为什么快&#xff1f;倒排索引核心原理概括的说&#xff0c;一个优秀的搜索引擎的设计&#xff0c;至少应该具备以下几点要求&#xff1a;高效的压缩算法快速的编码和解码算法合理的数据结构通用最小化算法结合以上几点&#xff0c;后面我将通过一个案例来讲解&…

MQ消息中间件常见题及解决办法

目录儿常见MQRocketMQ2、RocketMQ测试可用MQ常见问题1、幂等性问题2、如何保证消息不丢失3、消息积压问题4、事务消息设计分析常见MQ RocketMQ RocketMQ又四部分组成 NameServer 同步Broker服务信息&#xff0c;给消费者和生产者提供可用Broker的服务信息。Broker 消息存储业…

Python-第二天 Python基础语法

Python-第二天 Python基础语法一、 字面量1.1 常用的值类型1.1.1 字符串&#xff08;string&#xff09;二、注释2.1 注释的作用2.2 注释的分类三、变量3.1 什么是变量3.2 变量的特征四、数据类型4.1 数据类型4.2 type()语句4.3 type()语句的使用方式4.4 变量有类型吗&#xff…

【电商】后台订单生成

结合商品流转的电商系列介绍了一些了&#xff0c;商品已经采购入库、价格税率设置好了、活动及相关模板也已经准备完毕&#xff0c;下面就应该上架销售了&#xff0c;现在接着聊下订单的生成。 订单从产生到最终的关闭需要经历很多的环节&#xff0c;订单也是电商系统的核心数据…

Springcloud Alibaba 及各微服务组件整合梳理

Spring Cloud Alibaba 微服务各组件整合项目: 版本说明&#xff1a; <spring-boot.version>2.6.3</spring-boot.version><spring-cloud.version>2021.0.1</spring-cloud.version><spring-cloud-alibaba.version>2021.0.1.0</spring-cloud-a…

抖音的外卖行业入局,为中小外卖企业创业者的机会给了哪些机会?

一则关于抖音进入外卖市场的消息&#xff0c;让美团“非常受伤”。 2月8日&#xff0c;美团(03690.HK&#xff09;盘中跌幅超9%。截至收盘&#xff0c;美团报收153.1港元&#xff0c;跌幅6.48%。美团大幅下跌的根源就是前一天关于抖音外卖进展的消息传闻。 2月7日&#xff0c…

一站式 Elasticsearch 集群指标监控与运维管控平台

上篇文章写了一下消息运维管理平台&#xff0c;今天带来的是ES的监控和运维平台。目前初创企业&#xff0c;不像大型互联网公司&#xff0c;可以重复的造轮子。前期还是快速迭代试错阶段&#xff0c;方便拿到市场反馈&#xff0c;及时调整自己的战略和产品方向。让自己活下去&a…

大数据入门怎么学习

大数据学习不能停留在理论的层面上&#xff0c;大数据方向切入应是全方位的&#xff0c;基础语言的学习只是很小的一个方面&#xff0c;编程落实到最后到编程思想。学习前一定要对大数据有一个整体的认识。 大数据是数据量多吗&#xff1f;其实并不是&#xff0c;通过Hadoop其…

chatGPT 官网使用详细教程 (亲测可行)

文章目录1. chatGPT 介绍2. 进入官网3. 开始使用1. chatGPT 介绍 chatGPT 是一款由 OpenAI 开发的聊天机器人模型&#xff0c;它能够模拟人类的语言行为&#xff0c;与用户进行自然的交互。它的名称来源于它所使用的技术—— GPT-3架构&#xff0c;即生成式语言模型的第3代…