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

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JWT是什么?它有什么用?

1. 什么是 JWT&#xff1f; JWT是 JSON Web Token 的缩写&#xff0c;通过数字签名的方式&#xff0c;以 JSON 对象为载体&#xff0c;在不同的服务器终端之间安全传输的信息。 2. JWT 有什么用&#xff1f; JWT 最常见的场景就是授权认证&#xff0c;一旦用户登录&#xff…

[已解决] Ubuntu远程桌面闪退+登录显示“远程桌面由于数据加密错误 , 这个会话将结束“

两个月前&#xff0c;由于跑代码在Ubuntu配置环境&#xff0c;乱七八糟的下载了很多东西&#xff0c;导致了一系列问题..... 问题1 Ubuntu远程桌面闪退 实验室有两台服务器&#xff0c;IP后三位分别为141和142&#xff0c;其中141在输入密码后立即闪退&#xff0c;142可以正常…

【Vue新手必看】ElementUI表单实战教程,轻松掌握!

ElementUI表单 el的表单官网内容很多&#xff0c;看了一眼觉得心累了。但实际上它使用起来非常的方便,el为我们封装了各种组件&#xff0c;样式也大众。 一个简单的案例 代码如下&#xff08;使用时确保引入了相关的组件&#xff09; <template> <el-form ref"…

麒麟信安日志轮询分割操作说明

1、背景介绍 由于模块上面硬盘容量有限&#xff0c;需要定时清理系统日志。为了方便用户使用&#xff0c;在系统中设定自动日志轮询操作&#xff0c;让日志占用容量由操作系统自动管理&#xff0c;用户无需担心日志太多把硬盘容量占满。 2、操作说明 新建需要分割的日志logr…

2024应届大学生,为云计算高薪岗位做好准备了吗?

云计算正处于快速发展阶段&#xff0c;对于企业和个人来说&#xff0c;云计算提供了方便、灵活和智能的解决方案&#xff0c;对各行各业都有着重要的影响和推动作用。 随着云计算新市场、新业务、新应用的不断出现&#xff0c;人力需求迅猛。国家相继出台一系列政策大力扶持云…

UG通过曲线组

通过曲线组&#xff08;放样&#xff09; 通过至少两个截面之间放样生产实体或曲面 截面可以是开放或封闭的曲线或体的边 放样规则&#xff1a; 1、截面顺序不能颠倒 2、截面方向必须一致&#xff0c;注意鼠标选择的位置 3、截面节点必须对应&#xff0c;必要时打断图形 …

HackTheBox - Medium - Linux - Jupiter

Jupiter Jupiter 是一台中等难度的 Linux 机器&#xff0c;它有一个使用 PostgreSQL 数据库的 Grafana 实例&#xff0c;该数据库在权限上过度扩展&#xff0c;容易受到 SQL 注入的影响&#xff0c;因此容易受到远程代码执行的影响。一旦站稳脚跟&#xff0c;就会注意到一个名…

虚拟机的下载、安装(模拟出服务器)

下载 vmware workstation&#xff08;收费的虚拟机&#xff09; 下载vbox 网址&#xff1a;Oracle VM VirtualBox&#xff08;免费的虚拟机&#xff09; 以下选择一个下载即可&#xff0c;建议下载vbox&#xff0c;因为是免费的。安装的时候默认下一步即可&#xff08;路径最好…

【分布式技术专题】「授权认证体系」深度解析OAuth2.0协议的原理和流程框架实现指南(授权流程和模式)

深度解析OAuth2.0协议的原理和流程框架实现指南 背景介绍OAuth1.0协议访问令牌案例分析 OAuth2.0OAuth2.0与OAuth1.0 OAuth2.0协议体系的Roles角色OAuth定义了四个角色资源所有者资源服务器客户端授权服务器 传统的客户机-服务器身份验证模型的问题协议流程 认证授权类型授权码…

14 Vue3中使用v-model绑定输入框

概述 v-model用于实现双向数据绑定&#xff0c;使用v-model绑定输入框是Vue3中最常见的用法之一。 比如&#xff0c;在制作登录界面的时候&#xff0c;我们会使用v-model绑定用户名和密码&#xff0c;这里的用户名和密码都是输入框。 基本用法 我们创建src/components/Demo…

80x86汇编—指令系统

文章目录 MOV非法传送 XCHGXLAT堆栈指令 push 和 pop标志寄存器指令运算指令控制转移类指令&#xff08;重点&#xff09;条件转移指令 顺序是按照我们老师教的顺序&#xff0c;仅仅作为复习笔记。 汇编入门真的简单&#xff0c;深入难&#xff0c;毕竟学过计组CPU都只寄组的难…

华为安防监控摄像头

华为政企42 华为政企 目录 上一篇华为政企城市一张网研究报告下一篇华为全屋wifi6蜂鸟套装标准