【String、StringBuilder 和 StringBuffer 的 区别】

news2025/2/3 6:53:25

在这里插入图片描述

✅ String、StringBuilder 和 StringBuffer 的 区别

  • ✅典型解析
  • ✅扩展知识仓
    • ✅String 的不可变性
      • ✅ 为什么JDK 9 中把String 的char[ ] 改成了 byte[ ] ?
    • ✅为什么String设计成不可变的
      • ✅缓存
      • ✅安全性
      • ✅线程安全
      • ✅hashcode缓存
      • ✅ 性能
    • ✅String 的 " + " 是如何实现的
    • ✅StringBuffer 和 StringBuilder
    • ✅不要在for循环中使用+拼接字符串

✅典型解析


String 是不可变的,StringBuilder 和 StringBuffer 是可变的。


而StringBuffer 是线程安全的,而StringBuilder 是非线程安全的。


✅扩展知识仓


✅String 的不可变性


我们都知道 String 是不可变的,但是它是怎么实现的呢?


先来看一段 String 的源码(JDK 1.8):


public final class String implements java.io.Serializable, Comparable<String>,CharSequence {

	/** The value is used for character storage. */

	private final char value[];
	
	/** use serialVersionUID from JDK 1.0.2 for interoperability */

	private static final long serialVersionUID = -6849794470754667710L;

	public String substring(int beginIndex) {
		if (beginIndex < 0) {
			throw new StringIndexOutOfBoundsException(beginIndex);
			
		}
		int subLen = value.length - beginIndex;
		if (subLen < 0) {
			throw new StringIndexOutOfBoundsException(subLen);
		}
		return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
	}
	
	public String concat(String str) {

		int otherLen = str.length();
		if (otherLen == 0) {
			return this;
		}
		int len = value.length ;
		char buf[] = Arrays.copyOf(value, len + otherLen);
		str.getChars(buf, len);
		return new String(buf, true);
	}
}

以上代码,其实就包含了 String 不可变的主要实现了。




1. String类被声明为final,这意味着它不能被继承。那么他里面的方法就是没办法被覆盖的。


2. 用final修饰字符串内容的char[ ] (从JDK 1.9开始,char [ ] 变成了 byte[ ] ),由于该数组被声明为final,一旦数组被初始化,就不能再指向其他数组。


3. String类没有提供用于修改字符串内容的公共方法。例如,没有提供用于追加、删除或修改字符的方法。如果需要对字符串进行修改,会创建一个新的String对象。


再然后,在他的一些方法中,如substring、concat等,在代码中如果有涉及到字符串的修改,也是通过newString()的方式新建了一个字符串。


所以,通过以上方式,使得一个字符串的内容,一旦被创建出来,就是不可以修改的了。


不可变对象是在完全创建后其内部状态保持不变的对象。这意味着,一旦对象被赋值给变量,我们既不能更新引用,也不能通过任何方式改变内部状态。


可是有人会有疑惑,String为什么不可变,我的代码中经常改变String的值啊,如下:


String s = "abcd";
s = s.concat("ef");

这样,操作,不就将原本的"abcd"的字符串改变成"abcdef"了么?


但是,虽然字符串内容看上去从"abcd"变成了“abcdef",但是实际上,我们得到的已经是一个新的字符串了。


在这里插入图片描述

如上图,在堆中重新创建了一个“ abcdef ”字符串,和 “ abcd ” 并不是同一个对象。


So , 一旦一个 String 对象在内存(堆)中被创建出来,他就无法被修改。而且,String 类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。


如果我们想要一个可修改的字符串,可以选择 StringBuffer 或者 StringBuilder 这两个代替 String。


✅ 为什么JDK 9 中把String 的char[ ] 改成了 byte[ ] ?


在Java 9之前,字符串内部是由字符数组char[ ] 来表示的。


/** The walue is used for character storage. */
private final char value[];

由于Java内部使用UTF-16,每个char占据两个字节,即使某些字符可以用一个字节(LATIN-1)表示,但是也仍然会占用两个字节。所以,JDK 9就对他做了优化。


这就是Java 9引入了"Compact String"的概念:


每当我们创建一个字符串时,如果它的所有字符都可以用单个字节(Latin-1)表示,那么将会在内部使用字节数组来保存一半所需的空间,但是如果有一个字符需要超过8位来表示,Java将继续使用UTF-16与字符数组。


Latin1(又称ISO 8859-1)是一种字符编码格式,用于表示西欧语言,包括英语、法语、德语、西班牙语、葡萄牙语、意大利语等。它由国际标准化组织(ISO)定义,并涵盖了包括ASCII在内的128个字符。


Latin1编码使用单字节编码方案,也就是说每个字符只占用一个字节,其中第一位固定为0,后面的七位可以表示128个字符。这样,Latin1编码可以很方便地与ASCII兼容。


那么,问题来了,所有字符串操作时,它如何区分到底用Latin-1还是UTF-16表示呢?


为了解决这个问题,对String的内部实现进行了另一个更改。引入了一个名为coder的字段,用于保存这些信息。


/**
 *  The value is used for character storage.
 *
 * @implNote This field is trusted by the VM, and is a subject to
 * constant folding if String instance is constant. Overwriting this
 * field after construction will cause problems.
 * 
 *  Additionally, it is marked with (@link Stable] to trust the contents
 *  of the array. No other facility in JDK provides this functionality (yet).
 *  (@link Stablel is safe here, because value is never null.
 */

@Stable
private final byte[] value;


/**
 * The identifier of the encoding used to encode the bytes in
 * (@code value]. The supported values in this implementation are
 * 
 * LATIN1
 * UTF16
 * 
 *  @implNote This field is trusted by the VM, and is a subject to
 * constant folding if String instance is constant. Overwriting this
 *  field after construction will cause problems.
 */
 private final byte coder;

coder字段的取值可以是以下两种:


static final byte LATIN1 = 0;
static final byte UTF16 = 1;

在很多字符串的相关操作中都需要做一下判断,如:


public int indexOf(int ch, int fromIndex) {
	return islatin1()
	    ? StringLatin1.indexOf(value, ch, fromIndex)
	    :StringUTF16.index0f(value, ch, fromIndex);
}

private boolean isLatin1()  {
	return COMPACT_STRINGS && coder == LATIN1;
}

✅为什么String设计成不可变的


为什么要把String设计成不可变的呢? 有什么好处呢?


这个问题,困扰过很多人,甚至有人直接问过Java的创始人James Gosling。

在一次采访中James Gosling被问到什么时候应该使用不可变变量,他给出的回答是:

I would use an immutable whenever I can.

那么,他给出这个答案背后的原因是什么呢? 是基于哪些思考的呢?

其实,主要是从缓存、安全性、线程安全和性能等角度出发的。

✅缓存


字符串是使用最广泛的数据结构。大量的字符串的创建是非常耗费资源的,所以,Java提供了对字符串的缓存功能,可以大大的节省堆空间。


JVM中专门开辟了一部分空间来存储Java字符串,那就是字符串池。


通过字符串池,两个内容相同的字符串变量,可以从池中指向同一个字符串对象,从而节省了关键的内存资源。

String s ="abcd";
String s2 = s;

对于这个例子,s和s2都表示”abcd”,所以他们会指向字符串池中的同一个字符串对象:


在这里插入图片描述

但是,之所以可以这么做,主要是因为字符串的不变性。试想一下,如果字符串是可变的,我们一旦修改了s的内容,那必然导致s2的内容也被动的改变了,这显然不是我们想看到的。


✅安全性


字符串在Java应用程序中广泛用于存储敏感信息,如用户名、密码、连接url、网络连接等。JVM类加载器在加载类的时也广泛地使用它。


因此,保护String类对于提升整个应用程序的安全性至关重要。


当我们在程序中传递一个字符串的时候,如果这个字符串的内容是不可变的,那么我们就可以相信这个字符串中的内容。


但是,如果是可变的,那么这个字符串内容就可能随时都被修改。那么这个字符串内容就完全不可信了。这样整个系统就没有安全性可言了。


✅线程安全


不可变会自动使字符串成为线程安全的,因为当从多个线程访问它们时,它们不会被更改。


因此,一般来说,不可变对象可以在同时运行的多个线程之间共享。它们也是线程安全的,因为如果线程更改了值,那么将在字符串池中创建一个新的字符串,而不是修改相同的值。因此,字符串对于多线程来说是安全的。


✅hashcode缓存


由于字符串对象被广泛地用作数据结构,它们也被广泛地用于哈希实现,如HashMap、HashTable、HashSet等。在对这些散列实现进行提作时,经常调用hashCode() 方法。


不可变性保证了字符串的值不会改变。因此,hashCode0方法在String类中被重写,以方便缓存,这样在第一次hashCode调用期间计算和缓存散列,并从那时起返回相同的值。


在String类中,有以下代码:

private int hash;//this is used to cache hash code.

✅ 性能


前面提到了的字符串池、hashcode缓存等,都是提升性能的体现。

因为字符串不可变,所以可以用字符串池缓存,可以大大节省堆内存。而且还可以提前对hashcode进行缓存,更加高效。

由于字符串是应用最广泛的数据结构,提高字符串的性能对提高整个应用程序的总体性能有相当大的影响。


✅String 的 " + " 是如何实现的

使用 + 拼接字符串,其实只是Java提供的一个语法糖,那么,我们就来解一解这个语法糖,看看他的内部原理到底是如何实现的。

还是这样一段代码。我们把他生成的字节码进行反编译,看看结果。

String wechat = "Hollis";
String introduce = "Chuang";
String hollis = wechat + "," + introduce;

我们把这些个代码反编译以下,会得到如下代码:

String wechat ="Hollis";
String introduce = "Chuang";
String hollis = (new Stringbuilder()).append(wechat).append(",").append(introduce).tostring();

通过查看反编译以后的代码,我们可以发现,原来字符串常量在拼接过程中,是将String转成了StringBuilder后,使用其append方法进行处理的。

那么也就是说,Java中的 + 对字符串的拼接,其实现原理是使用StringBuilderappend。


✅StringBuffer 和 StringBuilder


接下来我们看看 StringBuffer 和 StringBuilder的实现原理。


和String 类相似,StringBuilder 类也封装了一个字符数组,定义如下:

char [] value;

与String不同的是,它并不是final的,所依它也是可以修改的,另外,与String 不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数,定义如下:

int count;

其append源码如下:

public StringBuilder append(String str);

	super.append(str);
	return this; 
	                                                           

该类继承了 ‘AbstractStringBuilder ’ 类,看其下 ‘ append ’ 方法:


 public AbstractStringBuilder append(String str) {
 	if (str == nul1)
 		return appendNul1();
 	int len = str.length();
 	ensureCapacityInternal(count + len);
 	str.getChars(0,en, value, count);
 	count += len;
	return this;
 }

append会直接拷贝字符到内部的字符数组中,如果字符数组长度不够,会进行扩展。


StringBuffer 和 StringBuilder 类似,,最大的区别就是 StringBuffer 是线程安的,看一下 StringBuffer 的 ‘append’ 方法。


public synchronized StringBuffer append(String str) {
	toStringCache = null;
	super.append(str);
	return this;
}

该方法使用 synchronized 进行声明,说明是一个线程安全的方法。而 StringBuilder 则不是线程安全的.。


✅不要在for循环中使用+拼接字符串


前面我们分析过,其实使用+拼接字符串的实现原理也是使用的StringBuilder,那为什么不建议大家在for循环中使用呢?


//我们把以下代码反编译下:

long t1 = System.currentTimeMillis();
String str = "hollis";

for (int i = 0; i < 50000; i++) {
	String s = String.valueOf(i);
	str += s;
}

long t2 = System.currentTimeMillis();
System.out.println("+ cost:" + (t2 - t1));

反编译后代码如下:


long t1 = System.currentTimeMillis();
String str = "hollis";
for(int i = 0; i < 50000; i++) {
	String s = String.value0f(i);
	str = (new StringBuilder()).append(str).append(s).toString();
long t2 = System.currentTimeMillis();
System.out.println((new StringBuilder()).append("+ cost:").append(t2 - t1).toString());
}

我们可以看到,反编译后的代码,在 for 循环中,每次都是 new 了一 StringBuilder ,然后再把 String 转成 StringBuilder ,再进行 append。


而频繁的新建对象当然要耗费很多时间了,不仅仅会耗费时间,频繁的创建对象,还会造成内存资源的浪费


所以,阿里巴巴Java开发手册建议: 循环体内,字符审的连接方式,使用 StringBuilder 的 append 方法进行扩展。而不要使用 + 。

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

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

相关文章

Ubuntu 常用命令之 du 命令用法介绍

&#x1f4d1;Linux/Ubuntu 常用命令归类整理 Ubuntu系统下的du命令是一个用来估计和显示文件和目录所占用的磁盘空间的命令。du是“disk usage”的缩写&#xff0c;这个命令可以帮助用户了解磁盘被哪些文件和目录使用。 du命令的常见参数有 -a&#xff1a;列出所有文件和目…

STM32的以太网外设+PHY(LAN8720)使用详解(3):PHY寄存器详解

0 工具准备 1.野火 stm32f407霸天虎开发板 2.LAN8720数据手册 3.STM32F4xx中文参考手册1 PHY寄存器 前面介绍到&#xff0c;站管理接口&#xff08;SMI&#xff09;允许应用程序通过2线时钟和数据线访问任意PHY寄存器&#xff0c;同时该接口支持访问最多32个PHY&#xff0c;也…

一个基于多接口的业务自动化测试框架!

这是一个成熟的框架&#xff0c;不是要让别人当小白鼠&#xff0c;它已经先后在两家公司的5条业务线进行了推广应用&#xff0c;用例条数到了几千条以上&#xff0c;并且从2018年开始每天都在CI/CD流程中被调用执行。 已有那么多接口测试框架&#xff0c;为什么重复造轮子&…

在芯片设计端,从事DFT岗位是什么体验?

从1975年PHILIPS公司实验中心首次提出“可测性设计”概念至今&#xff0c;DFT已经伴随着芯片走过了半个世纪。 但DFT在数字IC设计岗位中还是显得很神秘很低调。 你说他重要吧&#xff0c;并不是所有芯片设计公司都有这个岗位&#xff0c;你说他不重要吧&#xff0c;但凡芯片产…

数据结构 | 北京大学期末试卷查漏补缺

目录 顺序存储 优点 缺点 适用于&#xff1a; 链式存储 优点 缺点 适用于&#xff1a; 折半查找为什么要使用顺序存储结构 树的存储结构​编辑 对于一个数据结构&#xff0c;一般包括 DFS&BFS 什么是递归程序 C语言不带头结点的单链表逆置 检测字符…

【CMake保姆级教程】制作动静态链接库、指定动静态库输出路径

文章目录 前言一、动静态链接库的介绍1.1 动态链接库 (DLL)1.2 静态链接库 (LIB) 二、制作静态库三、制作动态库四、指定动静态库输出路径4.1 方式1 - 适用于动态库4.2 方式2 - 都适用 总结 前言 在软件开发中&#xff0c;我们经常听到动态链接库&#xff08;Dynamic Link Lib…

c语言易错题之数据类型变换

1.题目 #include<stdio.h> int main() {int arr[]{1,2,3,4,5};short*p (short*)arr;int i 0;for(i0;i<4;i){*(pi)0;}for(i0;i<5;i){printf("%d ",arr[i];}return 0; }2.解析 这道题主要容易错在&#xff0c;大家会以为通过指针赋值的时候&#xff0c;…

基于springboot的日记本系统源码+数据库+安装使用说明

之前写的SpringBoot日记本系统备受好评&#xff0c;考虑到还是有很多小伙伴不会部署&#xff0c;所以这一篇文章就单独来讲一下部署步骤吧。 需要资源 idea&#xff08;破不破解都行&#xff09; MySQL&#xff08;最好5.7以上版本&#xff0c;最好8.0&#xff09; Navicat…

SpringBoot Elasticsearch全文搜索

文章目录 概念全文搜索相关技术Elasticsearch概念近实时索引类型文档分片(Shard)和副本(Replica) 下载启用SpringBoot整合引入依赖创建文档类创建资源库测试文件初始化数据创建控制器 问题参考 概念 全文搜索&#xff08;检索&#xff09;&#xff0c;工作原理&#xff1a;计算…

Node.js-模块化(二)

1. 模块化的基本概念 1.1 什么是模块化 模块化是指解决一个复杂问题时&#xff0c;自顶向下逐层将系统拆分成若干模块的过程。对于整个系统来说&#xff0c;模块是可组合、分解和更换的单元。 1.2 编程领域中的模块化 编程领域中的模块化&#xff0c;就是遵守固定的规则&…

Spring源码分析 @Autowired 是怎样完成注入的?究竟是byType还是byName亦两者皆有

1. 五种不同场景下 Autowired 的使用 第一种情况 上下文中只有一个同类型的bean 配置类 package org.example.bean;import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;Configuration public class FruitCo…

git首次使用--去公司第一次拉取

文章目录 一&#xff0c; 在企业中首次拉取项目二&#xff0c;提交项目1. 提交----新添加的文件2. 将分支上的代码同步到master3. 提交----更改后的文件 三&#xff0c;常见问题1. Git Pull Failed 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一&am…

【组合数学】Pólya 计数理论

目录 1. 引言2. 置换群3. Burnside 引理共轭类k 不动置换类Burnside 引理 4. Plya 计数定理4.1 对点着色问题4.2 对面着色问题4.3 重复球放盒子 1. 引言 Plya 计数理论是数学中的一个分支&#xff0c;主要研究的是对称性在组合计数问题中的应用。该理论以匈牙利数学家乔治波利…

《Python Advanced Programming + Design Patterns + Clean Code》

清洁代码 — 学习如何编写可读、可理解且可维护的代码 高级Python编程知识 Python之常用设计模式 Advanced Programming装饰器 decorators生成器 & 迭代器with 上下文管理器面向对象Mixin 模式反射机制并发编程 Design Patterns设计模式分类简单工厂模式工厂模式 √抽象工厂…

指标体系构建-02-从0开始,梳理数据指标体系

指标体系构建-02-从0开始&#xff0c;梳理数据指标体系 一个例子&#xff0c;看懂并列式指标梳理 并列式指标体系&#xff0c;一般用于&#xff1a;描述个体情况 当我们想从几个不同角度&#xff0c;描述问题的时候&#xff0c;就需要并列关系 举个栗子&#x1f330;&#xf…

安全基础~实战应用

文章目录 HTTP请求头应用X-Forwarded-ForHTTP动作练习(修改请求方式)浏览器信息伪造(修改User-Agent)来源请求伪造(referer应用) 密码的应用SQL注入漏洞测试(前部分)PHP_encrypt_1(ISCCCTF) XShell连接Linxu连接Windows连接 HTTP请求头应用 X-Forwarded-For 原理作用 一般的…

浅谈在线监测系统与配电能效平台在供水水厂的应用

贾丽丽 安科瑞电气股份有限公司 上海嘉定 201800 【摘要】针对自来水厂工艺老化资金有限的问题&#xff0c;设计水厂在线监测系统&#xff0c;采用安科瑞&#xff0c;对原水滤后水、出厂水进行采样分析&#xff0c;并通过基于组态的上位机系统实现水质数据的实时监测。该系统…

深入了解UI标签栏设计细节:你不能错过的要点

UI 标签栏的作用有哪些&#xff1f; 导航是移动 UI 中最常见的组成部分&#xff0c;通常放置在 UI 标签栏上&#xff0c;以帮助我们在不同的页面之间切换。UI 标签栏可以保持界面的可控性&#xff0c;并提高可用性。简而言之&#xff0c;UI 标签栏可以加强交互&#xff0c;让用…

渲染图和效果图的一样吗?渲染图与效果图区别?

在建筑、设计及电影制作等一系列领域&#xff0c;你可能经常听说渲染图和效果图这两个词汇。它们虽然在视觉表现上有许多相似之处&#xff0c;但在实质上却有着极其不同的特性和用途。此文主要探讨提供优质效果图云渲染服务&#xff0c;以及渲染图与效果图之间的区别。 一、 效…

快递收发线上管理教程

前台快递收发几乎是每家公司行政前台的“必修课”&#xff0c;所以网络上制度模板满天飞&#xff0c;但现实中能彻底解决快递收发管理难题的几乎为零&#xff0c;那前台快递收发管理&#xff0c;究竟要如何才能摆脱制度的桎梏&#xff1f; 纵观各种前台快递收发制度范本&#…