十七、String 和 StringBuilder

文章目录

  • String 和 StringBuilder
    • 3.1 String基本用法
    • 3.2 String底层
    • 3.3 不可变性
    • 3.4 编码转换
    • 3.5 StringBuilder基本用法
    • 3.6 StringBuilder基本实现原理
    • 3.7 String的+和+=运算符

String 和 StringBuilder

本文为书籍《Java编程的逻辑》1和《剑指Java:核心原理与应用实践》2阅读笔记

3.1 String基本用法

可以通过常量定义String变量,也可以通过new创建String变量,还可以直接使用 + + + + = += +=​运算符,如:

    @Testpublic void testNewString() {String newConstant = "constant string";String newConstructor = new String("constructor new string");String addString = newConstant + " " + newConstructor;assertTrue("constant string constructor new string".equals(addString));}

String类包括很多方法,以方便操作字符串,比如:

public boolean isEmpty() // 判断字符串是否为空
public int length() // 获取字符串长度
public String substring(int beginIndex) // 取子字符串
public String substring(int beginIndex, int endIndex) // 取子字符串
public int indexOf(int ch) // 查找字符,返回第一个找到的索引位置,没找到返回 -1
public int indexOf(String str) // 查找子串,返回第一个找到的索引位置,没找到返回 -1
public int lastIndexOf(int ch) // 从后面查找字符
public int lastIndexOf(String str) // 从后面查找子字符串
public boolean contains(CharSequence s) // 判断字符串中是否包含指定的字符序列
public boolean startsWith(String prefix) // 判断字符串是否以给定子字符串开头
public boolean endsWith(String suffix) // 判断字符串是否以给定子字符串结尾
public boolean equals(Object anObject) // 与其他字符串比较,看内容是否相同
public boolean equalsIgnoreCase(String anotherString) // 忽略大小写比较是否相同
public int compareTo(String anotherString) // 比较字符串大小
public int compareToIgnoreCase(String str) // 忽略大小写比较
public String toUpperCase() // 所有字符转换为大写字符,返回新字符串,原字符串不变
public String toLowerCase() // 所有字符转换为小写字符,返回新字符串,原字符串不变
public String concat(String str) // 字符串连接,返回当前字符串和参数字符串合并结果
public String replace(char oldChar, char newChar) // 字符串替换,替换单个字符
public String replace(CharSequence target, CharSequence replacement) // 字符串替换,替换字符序列,返回新字符串,原字符串不变
public String trim() // 删掉开头和结尾的空格,返回新字符串,原字符串不变
public String[] split(String regex) // 分隔字符串,返回分隔后的子字符串数组

3.2 String底层

String类内部用一个字符数组表示字符串,实例变量定义为:

private final char value[];

String中的大部分方法内部也都是操作的这个字符数组。比如:

  1. length()方法返回的是这个数组的长度。
  2. substring()方法是根据参数,调用构造方法String(char value[], int offset, int count)新建了一个字符串。
  3. indexOf()方法查找字符或子字符串时是在这个数组中进行查找。

String中还有一些方法,与这个char数组有关:

public char charAt(int index) // 返回指定索引位置的 char
public char[] toCharArray() // 返回字符串对应的 char 数组, 注意,返回的是一个复制后的数组,而不是原数组
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) // 将 char 数组中指定范围的字符复制入目标数组指定位置

Character类似,String也提供了一些方法,按代码点对字符串进行处理:

public int codePointAt(int index)
public int codePointBefore(int index)
public int codePointCount(int beginIndex, int endIndex)
public int offsetByCodePoints(int index, int codePointOffset)

3.3 不可变性

与包装类类似,String类也是不可变类,即对象一旦创建,就没有办法修改了。String类也声明为了final,不能被继承,内部char数组value也是final的,初始化后就不能再变了。String类中提供了很多看似修改的方法,其实是通过创建新的String对象来实现的,原来的String对象不会被修改。比如,concat()方法的代码:

    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);}

通过Arrays.copyOf方法创建了一块新的字符数组,复制原内容,然后通过new创建了一个新的String,最后一行调用的是String的另一个构造方法。

因为字符串的不可变性,JVM专门为字符串提供了一个常量池,凡是放在常量池中的字符串对象都可以共享,查看下面的代码:

    @Testpublic void testConstantPool() {String name1 = "测试字符串常量池";String name2 = "测试字符串常量池";assertTrue(name1 == name2);}@Testpublic void testNotConstantPool() {String name1 = new String("测试字符串常量池");String name2 = new String("测试字符串常量池");assertFalse(name1 == name2);}

看上面代码,通过常量定义name1 == name2得到的是true,但是通过new得到的字符串得到的结果是false。那么,现在有一个问题,哪些方式是使用常量池中的数据呢,哪些是新建放在堆中的呢?

  1. 直接使用"..."得到的字符串对象放在常量池。
  2. 直接"..."+"..."拼接的字符串对象放在常量池。
  3. 两个指向"..."的final常量拼接结果放在常量池。
  4. 所有字符串对象.intern()方法得到的结果放在常量池。
  5. 除以上四种方式,其他方式得到的字符串结果都在堆中。
    @Testpublic void testCreateConstantPool() {String s1 = "helloworld"; // 1、常量池中String s2 = "hello" + "world"; // 2、常量池中final String s3 = "hello";final String s4 = "world";String s5 = s3 + s4; // 3、指向"..."的 final 常量 + 指向"..."的 final 常量在常量池String s6 = new String("hello");String s7 = new String("world");String s8 = s6 + s7;String s9 = s8.intern();// 4、字符串对象.intern() 的结果都在常量池assertTrue(s1 == s2 && s1 == s5 && s1 == s9);String s10 = "hello";String s11 = "world";String s12 = s10 + s11;assertFalse(s1 == s12);String s13 = s10 + "world";assertFalse(s1 == s13);String s14 = s6 + "world";assertFalse(s1 == s14);String s15 = String.valueOf(new char[] { 'h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd' });assertFalse(s1 == s15);String s16 = "hello".concat("world");assertFalse(s1 == s16);}

3.4 编码转换

String内部是按UTF-16BE处理字符的,对BMP字符,使用一个char,两个字节,对于增补字符,使用两个char,四个字节。我们知道有各种编码,不同编码可能用于不同的字符集,使用不同的字节数目,以及不同的二进制表示。如何处理这些不同的编码呢?这些编码与Java内部表示之间如何相互转换呢?Java使用Charset类表示各种编码,它有两个常用静态方法:

public static Charset defaultCharset()
public static Charset forName(String charsetName)

第一个方法返回系统的默认编码,第二个方法返回给定编码名称的Charset对象,其charset名称可以是US-ASCIIISO-8859-1windows-1252GB2312GBKGB18030Big5UTF-8等,比如:

Charset charset = Charset.forName("GB18030");

String类提供了如下方法,返回字符串按给定编码的字节表示:

public byte[] getBytes()
public byte[] getBytes(String charsetName)
public byte[] getBytes(Charset charset)

第一个方法没有编码参数,使用系统默认编码;第二个方法参数为编码名称;第三个方法参数为Charset

String类有如下构造方法,可以根据字节和编码创建字符串,也就是说,根据给定编码的字节表示,创建Java的内部表示。

public String(byte bytes[], int offset, int length, String charsetName)
public String(byte bytes[], Charset charset)

3.5 StringBuilder基本用法

如果字符串修改操作比较频繁,应该采用StringBuilderStringBuffer类,这两个类的方法基本是完全一样的,它们的实现代码也几乎一样,唯一的不同就在于StringBuffer类是线程安全的,而StringBuilder类不是。

StringBuilder的基本用法很简单。使用new创建StringBuilder对象,通过append方法添加字符串,然后通过toString方法获取构建后的字符串:

    @Testpublic void testStringBuilder() {StringBuilder stringBuilder = new StringBuilder();stringBuilder.append("hello");stringBuilder.append("world");assertFalse("helloworld" == stringBuilder.toString());assertTrue("helloworld".equals(stringBuilder.toString()));}

3.6 StringBuilder基本实现原理

StringBuilder类是怎么实现的呢?我们来看下它的内部组成,以及一些主要方法的实现,代码基于Java 7。与String类似,StringBuilder类也封装了一个字符数组,定义如下:

char[] value;

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

/*** The count is the number of characters used.*/
int count;

StringBuilder继承自AbstractStringBuilder,它的默认构造方法是:

    public StringBuilder() {super(16);}

调用父类的构造方法,父类对应的构造方法是:

AbstractStringBuilder(int capacity) {value = new char[capacity];
}

也就是说,new StringBuilder()代码内部会创建一个长度为 16 16 16的字符数组,count的默认值为 0 0 0。来看append方法的代码:

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

append会直接复制字符到内部的字符数组中,如果字符数组长度不够,会进行扩展,实际使用的长度用count体现。具体来说,ensureCapacityInternal(count+len)会确保数组的长度足以容纳新添加的字符,str.getChars会复制新添加的字符到字符数组中,count+=len会增加实际使用的长度。

ensureCapacityInternal的代码如下:

private void ensureCapacityInternal(int minimumCapacity) {//overflow-conscious codeif(minimumCapacity - value.length > 0)expandCapacity(minimumCapacity);
}

如果字符数组的长度小于需要的长度,则调用expandCapacity进行扩展,其代码为:

void expandCapacity(int minimumCapacity) {int newCapacity = value.length * 2 + 2;if(newCapacity - minimumCapacity < 0)newCapacity = minimumCapacity;if(newCapacity < 0) {if (minimumCapacity < 0) //overflowthrow new OutOfMemoryError();newCapacity = Integer.MAX_VALUE;}value = Arrays.copyOf(value, newCapacity);
}

扩展的逻辑是:分配一个足够长度的新数组,然后将原内容复制到这个新数组中,最后让内部的字符数组指向这个新数组。这里主要看下newCapacity是怎么算出来的。参数minimumCapacity表示需要的最小长度,需要多少分配多少不就行了吗?不行,因为那就跟String一样了,每append一次,都会进行一次内存分配,效率低下。这里的扩展策略是跟当前长度相关的,当前长度乘以 2 2 2,再加上 2 2 2,如果这个长度不够最小需要的长度,才用minimumCapacity。比如,默认长度为 16 16 16,长度不够时,会先扩展到 16 ∗ 2 + 2 16*2+2 162+2 34 34 34,然后扩展到 34 ∗ 2 + 2 34*2+2 342+2 70 70 70,然后是 70 ∗ 2 + 2 70*2+2 702+2 142 142 142,这是一种指数扩展策略。为什么要加 2 2 2?这样,在原长度为 0 0 0​时也可以一样工作。

除了appendtoString方法, StringBuilder还有很多其他方法,包括更多构造方法、更多append方法、插入、删除、替换、翻转、长度有关的方法。

3.7 String的+和+=运算符

Java中,String可以直接使用 + + + + = += +=运算符,这是Java编译器提供的支持,背后,Java编译器一般会生成StringBuilder++=操作会转换为append。比如,如下代码:

String hello = "hello";
for(int i=0; i<3; i++){hello+=", world";
}
System.out.println(hello);

背后,Java编译器一般会转换为:

String hello = "hello";
for(int i=0; i<3; i++){StringBuilder sb = new StringBuilder(hello);sb.append(", world");hello = sb.toString();
}
System.out.println(hello);

既然直接使用 + + + + = += +=就相当于使用StringBuilderappend,那还有什么必要直接使用StringBuilder呢?在简单的情况下,确实没必要。不过,在稍微复杂的情况下,Java编译器可能没有那么智能,它可能会生成过多的StringBuilder,尤其是在有循环的情况下,在循环内部,每一次 + = += +=操作,都会生成一个StringBuilder。所以,对于简单的情况,可以直接使用String + + + + = += +=​,对于复杂的情况,尤其是有循环的时候,应该直接使用StringBuilder


  1. 马俊昌.Java编程的逻辑[M].北京:机械工业出版社,2018. ↩︎

  2. 尚硅谷教育.剑指Java:核心原理与应用实践[M].北京:电子工业出版社,2023. ↩︎

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

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

相关文章

pytorch-textsummary,中文文本摘要实践

pytorch-textsummary&#xff0c;中文文本摘要实践 pytorch-textsummary是一个以pytorch和transformers为基础&#xff0c;专注于中文文本摘要的轻量级自然语言处理工具&#xff0c;支持抽取式摘要等。 目录 数据使用方式paper参考 项目地址 pytorch-textsummary: https://g…

docker (十)-docker compose容器编排

在实际工作中&#xff0c;部署一个应用可能需要部署多个容器&#xff0c;一个一个部署非常不方便。docker compose可以一键部署和启动多个容器&#xff0c;它使用yaml文件来编排服务。github和docker hub很多项目都提供了docker-compose.yaml文件&#xff0c;我们可以一键部署项…

浅谈木材加工企业的电气火灾隐患及电气火灾监控系统的应用

摘要&#xff1a;本文分析了木材加工企业的特点、现状及常见电气火灾隐患&#xff0c;提出了消灭电气火灾隐患的措施。结尾介绍了木材加工企业常用电气设备的选用及电气火灾监控系统在其低压配电系统的应用方案及产品选型。 关键词&#xff1a;木材加工企业&#xff1b;电气火…

redis 异步队列

//produceMessage.ts 模拟生产者 import Redis from ioredis; const redis new Redis(); // 生产者&#xff1a;将消息推送到队列 async function produceMessage(queueName:string, message:string) {try {await redis.rpush(queueName, message);console.log(Produced messa…

Unity摄像机跟随

Unity摄像机跟随 方法一&#xff1a;摄像机子物体 将摄像机直接拖拽到被跟随的目标下面即可&#xff0c;这样摄像机永远在目标的后面 缺点&#xff1a; 屏幕旋转太平滑了目标物体在屏幕上的位置永远不变目标物体被销毁时总不能把摄像机也销毁了吧 方法二&#xff1a;子物体…

19个Web前端交互式3D JavaScript框架和库

JavaScript &#xff08;JS&#xff09; 是一种轻量级的解释&#xff08;或即时编译&#xff09;编程语言&#xff0c;是世界上最流行的编程语言。JavaScript 是一种基于原型的多范式、单线程的动态语言&#xff0c;支持面向对象、命令式和声明式&#xff08;例如函数式编程&am…

从零开始手写mmo游戏从框架到爆炸(十六)— 客户端指定回调路由与登录

导航&#xff1a;从零开始手写mmo游戏从框架到爆炸&#xff08;零&#xff09;—— 导航-CSDN博客 我们这次来把注册、登录、选择英雄&#xff0c;进入主页-选择地图的功能完善。 在这之前&#xff0c;我们还要解决一个问题&#xff0c;就是服务端往客户端发消息的路由问题…

phpcms v9敏感词内容替换

后台先在"扩展"——>"敏感词管理"中添加敏感词&#xff0c;然后修改phpcms\modules\content\content.php文件来实现添加或者编辑内容时敏感词的替换。&#xff08;如果涉及会员投稿和留言等&#xff0c;也需要在对应模块中做类似处理&#xff09; 在ad…

ADO.NET实现读写分离

在 ADO.NET 中&#xff0c;分片和垂直分表的支持并不是直接提供的&#xff0c;而是需要你在应用程序设计和数据库架构中手动实现。下面是如何在 ADO.NET 中支持分片和垂直分表的一些步骤和策略&#xff1a; 分片 (Sharding) 1. 设计分片策略 首先&#xff0c;你需要设计一个…

《基于CEEMDAN-小波包分析的隧道爆破信号去噪方法》论文思路

相比于小波降噪&#xff0c;小波包分析具有更高的频率分辨率&#xff0c;可以进一步消除高频部分存在的噪声余量&#xff0c;提高去噪精度 依据EEMD 分解的取值范围&#xff0c;利用“试错法”得到本次试验中CEEMDAN分解的特征参数为&#xff1a;正负高斯白噪声标准差为0.2&a…

Linux--shell编程中有关while循环的详细内容

文章关于while循环的内容目录 一、while循环 ​​​​​​​​​​​​​​二、无限循环 ​​​​​​​​​​​​​​三、case语句 ​​​​​​​四、跳出循环 ​​​​​​​​​​​​​​五、break ​​​​​​​六、continue​​​​​​​ ​​​​​​​一、w…

Java Web(六)--XML

介绍 官网&#xff1a;XML 教程 为什么需要&#xff1a; 需求 1 : 两个程序间进行数据通信&#xff1f;需求 2 : 给一台服务器&#xff0c;做一个配置文件&#xff0c;当服务器程序启动时&#xff0c;去读取它应当监听的端口号、还有连接数据库的用户名和密码。spring 中的…

二叉树及其练习题

文章目录 树概念及结构树的概念树的相关概念树的表示形式树的应用 二叉树概念及结构概念两种特殊的二叉树二叉树的性质二叉树的存储二叉树的基本操作二叉树的遍历前中后序遍历递归实现二叉树的基本操作 二叉树相关oj题 树概念及结构 树的概念 树是一种非线性的数据结构&#…

c++:蓝桥杯中的基础算法1(枚举,双指针)

枚举 基础概念&#xff1a; 枚举&#xff08;Enum&#xff09;是一种用户定义的数据类型&#xff0c;用于定义一个有限集合的命名常量。在C中&#xff0c;枚举类型可以通过关键字enum来定义。 下面是一个简单的枚举类型的定义示例&#xff1a; #include <iostream>enum…

【面试题】谈谈MySQL的索引

索引是啥 可以把Mysql的索引看做是一本书的目录&#xff0c;当你需要快速查找某个章节在哪的时候&#xff0c;就可以利用目录&#xff0c;快速的得到某个章节的具体的页码。Mysql的索引就是为了提高查询的速度&#xff0c;但是降低了增删改的操作效率&#xff0c;也提高了空间…

数字经济概念辨析

一些常见的数字经济&#xff0c;数字金融概念辨析 博士学位点-应用经济学、统计学、工商管理、管理科学与工程 可以不懂&#xff0c;但不能装懂&#xff1b;可以不会&#xff0c;但不能不学&#xff1b;可以偷懒&#xff0c;但不能停滞。 增程式和混合动力的电动车都是混合动…

医疗在线问诊小程序:开启数字化医疗新篇章

随着科技的飞速发展&#xff0c;医疗行业正逐步向数字化转型。其中&#xff0c;医疗在线问诊小程序作为一种新型的医疗健康服务模式&#xff0c;为人们提供了更为便捷、高效的医疗咨询服务。本文将探讨医疗在线问诊小程序的发展背景、优势及应用场景&#xff0c;以期为医疗行业…

【JavaScript】模块的导入和导出

文章目录 1. 导出模块1.1 基本导出1.2 默认导出 2. 导入模块2.1 基本导入2.2 导入全部2.3 默认导入 3. 在实际项目中的应用3.1 模块化开发3.2 组织项目结构 4. 模块的导入导出语法比较4.1 命名导出4.2 默认导出 5. 总结 在现代 JavaScript 开发中&#xff0c;模块化编程是一项关…

elementUI 动态校验表单数据的方法

elementUI 动态校验表单数据的方法 直接设置如下 list 为动态获取的数据值列表数据 这里主要设置两块内容 prop为动态数据 rules设置需要校验的值 :prop“list.${index}.title” :rules“rules.title” //title 名称可自己定义 //这里主要设置两块内容 prop为动态数据 rules…

axios介绍和使用

1. Axios是什么 Axios框架全称&#xff08;ajax – I/O – system&#xff09; Axios是一个基于Promise的JavaScript HTTP客户端&#xff0c;用于浏览器和Node.js环境。它可以发送HTTP请求并支持诸如请求和响应拦截、转换数据、取消请求以及自动转换JSON数据等功能。 Axios提…