【String、StringBuilder 和 StringBuffer 的 区别】

在这里插入图片描述

✅ 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 方法进行扩展。而不要使用 + 。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

IDEA2023+JDK17+SpringBoot3+MySQL8后端接口开发实战课笔记

概述 花了很长的时间&#xff0c;终于把心心念念的SpringBoot3的实战课整理出来了。 今天是开心的一天&#xff0c;因为又多了一门课程可以奉献给大家&#xff0c;也是难过的一天&#xff0c;那就是又要开始重新找工作了。 如果我的粉丝里面有关于Golang或者Python的相关工作推…

算法题系列7·获得数组中多数元素

目录 题目描述 实现 提交结果 题目描述 给定一个大小为 n 的数组 nums &#xff0c;返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。 你可以假设数组是非空的&#xff0c;并且给定的数组总是存在多数元素。 示例 1&#xff1a; 输入&#xff1a;…

深度学习 Day21——J1ResNet-50算法实战与解析

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 | 接辅导、项目定制 文章目录 前言一、我的环境二、代码实现与执行结果1.引入库2.设置GPU&#xff08;如果使用的是CPU可以忽略这步&#xff09;3.导入数据4.查…

linux 串口测试指令和测试程序

一、串口设备查看 查看串口 (/dev) ls /dev/tty*查看串口&#xff08;或串口终端&#xff09;属性 ( /proc) cat /proc/tty/driver/serial 或 cat /proc/tt…

《Python Advanced Programming + Design Patterns + Clean Code》

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

什么是误差,什么是重构误差,误差与重构误差有什么区别?

重构误差 1.误差的概念2.重构误差的概念 1.误差的概念 在机器学习中&#xff0c;误差通常是指模型的输出与实际标签或者真实值之间的差异&#xff0c;通常用于评估模型的预测能力或者训练的优化过程。 2.重构误差的概念 重构误差是指通过学习到的模型来重新构建&#xff08;或…

OpenCV | 告别人工目检:深度学习技术引领工业品缺陷检测新时代

文章目录 机器视觉缺陷检测工业上常见缺陷检测方法内容简介作者简介目录读者对象如何阅读本书获取方式 机器视觉 机器视觉是使用各种工业相机&#xff0c;结合传感器跟电气信号实现替代传统人工&#xff0c;完成对象识别、计数、测量、缺陷检测、引导定位与抓取等任务。其中工…

听GPT 讲Rust源代码--src/tools(21)

File: rust/src/tools/miri/src/shims/x86/mod.rs 在Rust的源代码中&#xff0c;rust/src/tools/miri/src/shims/x86/mod.rs文件的作用是为对x86平台的处理提供支持。它包含一些用于模拟硬件操作的shim函数和相关的类型定义。 具体来说&#xff0c;该文件中的函数是通过使用一组…

华为---登录USG6000V防火墙---console、web、telnet、ssh方式登录

目录 一、环境搭建 二、第一次登录USG6000V防火墙&#xff0c;即通过console方式登录 三、用户配置 四、web登录USG6000V防火墙 1. 用web创建的用户通过web方式登录USG6000V防火墙 2. 命令行创建的用户通过web方式登录USG6000V防火墙 五、ssh方式登录USG6000V防火墙 1. 用…

STM32CubeMX配置HAL库输入捕获

STM32CubeMX配置HAL库输入捕获 STM32的输入捕获功能可以用来测量脉冲宽度或者频率。其工作原理是&#xff0c;通过检测TIMx_CHx上的边沿信号&#xff0c;在边沿信号发生跳变&#xff08;比如 上升沿/下降沿&#xff09;的时候&#xff0c;将当前定时器的值&#xff08;TIMx_C…

被有道云笔记成功劝退拥抱Joplin(Joplin使用过程遇到的问题)

本人职业程序员&#xff0c;培训讲师&#xff08;技术类&#xff09;、活动主持人&#xff0c;对多端阅读是有些需求的&#xff0c;平时习惯墨水平板、手机和笔记本电脑登录着有道云笔记。其实本人对内容比较重视&#xff0c;对有道云笔记提供的什么AI服务、PDF转Word等功能是没…

【python】进阶--->网络编程(二)

一、分层模型 OSI/RM(开放系统互联参考模型) 是由国际标准化组织提出来的一种网络互联模型,成为所有的销售商都能实现的开放网络模型.(OSI模型提供我们理解网络协议的内部运作) OSI模型将网络通信工作分为7层,每一层为上一层服务,并为上一层提供一个访问的接口或者界面. 越下…

用Minikube 搭建一个单机k8s玩玩

Minikube 介绍 Minikube是一款单机搭建和管理Kubernetes集群的工具。与Kind 类似&#xff0c;但是个人认为比Kind 好用 Minikube 安装 mac如果安装了 Homebrew&#xff0c;直接执行以下命令安装minikube brew install minikubemac没有安装Homebrew,需要到官网下载选择系统配置…

【Prometheus|报错】Out of bounds

【背景】进入Prometheus地址的9090端口&#xff0c;pushgateway&#xff08;0/1&#xff09;error : out of bounds 【排查分析】 1、out of bounds报错&#xff0c;是由于Prometheus向tsdb存数据出错&#xff0c;与最新存数据的时间序列有问题&#xff0c;有可能当前时间与最…

VGGNet

目录 一、VGGNet介绍 1、VGG块 2、VGG架构 3、LeNet, AlexNet和VGGNet对比 4、总结 二、代码实现 1、定义VGG卷积块 2、VGG网络 3、训练模型 4、总结 一、VGGNet介绍 VGGNet&#xff08;Visual Geometry Group Network&#xff09;是一种深度卷积神经网络&#xff0c;…

Android Studio 安装和使用

前些天&#xff0c;打开了几年前的一个Android Studio app项目&#xff0c;使用安卓虚拟机仿真app崩溃&#xff0c;怀疑是不是中间升级过Android Studio导致异常的&#xff0c;马上脑子一热卸载了&#xff0c;结果上次踩过的坑&#xff0c;一个没少又踩一次&#xff0c;谨以此文…

系列十二(面试)、Java中的GC回收类型有哪些?

一、Java中的GC回收类型 1.1、概述 Java中的GC回收类型主要包含以下几种&#xff0c;即&#xff1a;UseSerialGC、UseParallelGC、UseConcMarkSweepGC、UseParNewGC、UseParallelOldGC、UseG1GC。 1.2、源码

Linux:sudo给予账户特定的权限

我们某些用户权限比较低&#xff0c;如果我们他们的权限提高&#xff0c;或者假如搞权限的组&#xff0c;那么会大大减少安全性&#xff0c;我们可以使用sudo对他们开放指定的命令 我这里有 a1—3 3个用户&#xff0c;现在我切换到a1执行一下重启的命令 发现我们这个用户并无…

【vtkWidgetRepresentation】第十六期 vtkContourRepresentation(三)

很高兴在雪易的CSDN遇见你 VTK技术爱好者 QQ:870202403 前言 本文分享vtkContourLineInterpolator接口的源码剖析和实例应用,希望对各位小伙伴有所帮助! 感谢各位小伙伴的点赞+关注,小易会继续努力分享,一起进步! 你的点赞就是我的动力(^U^)ノ~YO 目录 前言 …

Uniapp 开发 BLE

BLE 低功耗蓝牙&#xff08;Bluetooth Low Energy&#xff0c;或称Bluetooth LE、BLE&#xff0c;旧商标Bluetooth Smart&#xff09;&#xff0c;用于医疗保健、运动健身、安防、工业控制、家庭娱乐等领域。在如今的物联网时代下大放异彩&#xff0c;扮演者重要一环&#xff…