百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

【Java深度干货】如何高效构造字符串(String)?

wptr33 2025-07-28 00:36 3 浏览

字符串在 Java 中是不可变的,无论构造,还是截取,得到的总是一个新字符串。下面看一下构造一个字符串(String)的源码:

private final char value[];
public String(String original) {
     this.value = original.value;
     this.hash = original.hash;
}

原有的字符串的 value 数组直接通过引用赋值给新的字符串的 value 数组,也就是两个字符串共享一个 char[],因此这种构造方法有着最快的构造速度。

Java 中的 String 对象被设计为不可变,是指一旦程序获得字符串对象引用,则不必担心这个字符串在别的地方被修改,因为修改总意味着获得一个新的字符串,不可变意味着线程安全,不必担心并发修改。

更多时候是通过一个 char[],或者在某些分布式框架反序列化对象时使用 byte[]来构造字符串的,这种情况下性能会非常低。

以下是通过 char[]构造一个新的字符串的源码:

public String(char value[]) {
     this.value = Arrays.copyOf(value, value.length);
}

Arrays.copyOf 会重新复制一份新的数组,方法如下:

//Arrays.copyOf
public static char[] copyOf(char[] original, int newLength) {
    char[] copy = new char[newLength];
    System.arraycopy(original, 0, copy, 0,
                                Math.min(original.length, newLength));
     return copy;
}

可以看到通过 char[]构造字符串,实际上会创建一个新的字符串数组。如果不这样,还是直接引用 char[],那么一旦外部更改 char 数组,则这个新的字符串就被改变了。

char[] cs = new char[]{'a','b'};
String str = new String(cs);
cs[0] ='!'

上面的代码最后一行修改了 cs 数组,但不会影响 str。因为 str 实际上是由新的字符串数组构成的。

通过 char[]构造新的字符串是最常用的方法,后面会看到几乎每个修改的 API,都会调用这个方法构造新的字符串,比如 subString、concat、replace 等。

以下代码验证了通过字符串构造新的字符串,以及使用 char[]构造字符串的性能比较:

String str= "你好,String";
char[] chars = str.toCharArray();

@Benchmark
public String string(){34 │
     return new String(str);
}
@Benchmark
public String stringByCharArray(){
     return new String(chars);
}

结果按照 ns/op 来输出,即每次调用所用的纳秒数。可以看到通过 char[]构造字符串还是相当耗时的,如果数组特别长,那么更加耗时:

Benchmark                                                                               Mode          Score          Units
c.i.c.c.NewStringTest.string                                                      avgt             4.235          ns/op
c.i.c.c.NewStringTest.stringByCharArray                                  avgt             11.704        ns/op

通过 byte[]构造字符串是一种常见的情况,随着分布式和微服务的流行,字符串在客户端序列化成 byte[],并发送给服务器端。服务器端会有一个反序列化操作,通过 byte 构造字符串。

使用 byte[]构造字符串的性能测试:

byte[] bs = "你好,String".getBytes("UTF-8");

@Benchmark
public String stringByByteArray() throws Exception{
    return new String(bs,"UTF-8");
}

通过测试结果可以看到 byte[]构造字符串太耗时了,尤其是要构造的字符串非常长的时候:

Benchmark                                                                                Mode         Score         Units
c.i.c.c.NewStringTest.string                                                       avgt            4.649         ns/op
c.i.c.c.NewStringTest.stringByByteArray                                   avgt            82.166        ns/op
c.i.c.c.NewStringTest.stringByCharArray                                   avgt           12.138         ns/op

通过字节数组构造字符串主要涉及转码过程,内部会调用 StringCoding.decode 进行转码:

this.value = StringCoding.decode(charsetName, bytes, offset, length);

charsetName 表示字符集,bytes 是字节数组,offset 和 length 表示字节的起始位置和长度。

实际负责转码的是 charset 子类,比如 sun.nio.cs.UTF_8 的 decode 方法负责实现字节转码。如果深入了解这个类,会发现你看到的是“冰上一角”,“冰”下面的是一个相当消耗 CPU 计算资源的工作,属于无法优化的部分。

Unicode 是字符集,为每一个字符分配一个编号,UTF-8 是一种将字符转为二进制编码的规则。

UTF-8 一种变长字节编码方式,对于某一个字符的 UTF-8 编码,如果只有一个字节,则其最高二进制位为 0;如果是多字节,则其第一个字节从最高位开始,连续的二进制位值为 1 的个数决定了其编码的位数,其余各字节均以 10 开头。

UTF-8 最多可用到 6 个字节,Unicode 在转为 UTF-8 时需要用到下面的模板:

比如“汉”字的 Unicode 码是 6C49,二进制编码是 0110_1100_0100_1001。如果编码成 UTF-8,则需要用到三字节模板,即表格中的最后一行:1110xxxx 10xxxxxx 10xxxxxx。

将 6C49 按照三字节模板的分段方法分为 0110_110001_001001,依次代替模板中的 x,得到 1110-0110 10-11000110-001001,即 E6 B1 89,这就是其 UTF-8 的编码。

在多次的系统性能优化过程中,会发现通过字节数组构造字符串总是排在消耗 CPU 比较靠前的位置,转码消耗的系统性能相当于上百行的业务代码。

因此在设计分布式系统时,需要仔细设计传输的字段,尽量避免用 String,比如时间可以用 long 类型来表示,业务状态可以用 int类型来表示。需要序列化的对象如下:

public class OrderResponse{
    //订单日期,格式为 yyyy-MM-dd
    private String createDate;
    //订单状态,0 表示正常
    private String status;
    private String payStatus;
}

可以改进成更好的定义,以减小序列化和反序列化的负担:

public class OrderResponse{
     //订单日期
     private long createDate;
     //订单状态,0 表示正常
     private int status;
     private int payStatus;
}

有的系统为了高效地存储和传输 OrderResponse 对象,甚至会把 status 和 payStatus 合成一个 int 类型的值,高位表示 status,低位表示 payStatus。


内容摘自《高性能Java系统权威指南》第二章

本书特点:

内容上,总结作者从事Java开发20年来在头部IT企业的高并发系统经历的真实案例,极具参考意义和可读性。

对于程序员和架构师而言,Java 系统的性能优化是一个超常规的挑战。这是因为 Java 语言和 Java 运行平台,以及 Java 生态的复杂性决定了 Java 系统的性能优化不再是简单的升级配置或者简单的 “空间换时间”的技术实现,这涉及 Java 的各种知识点。

本书从高性能、易维护、代码增强以及在微服务系统中编写Java代码的角度来描述如何实现高性能Java系统,结合真实案例,让读者能够快速上手实战。

风格上本书的风格偏实战,读者可以下载书中的示例代码并运行测试。读者可以从任意一章开始阅读,掌握性能优化知识为公司的系统所用。

本书适合:

中高级程序员和架构师;

以及有志从事基础技术研发、开源工具研发的极客阅读;

也可以作为 Java 笔试和面试的参考书。

相关推荐

HIVE 窗口函数详解(hive常用开窗函数)

什么是窗口函数窗口函数是SQL中一类特别的函数。和聚合函数相似,窗口函数的输入也是多行记录。不同的是,聚合函数的作用于由GROUPBY子句聚合的组,而窗口函数则作用于一个窗口,这里,窗口...

SQL高效使用20招:数据分析师必备技巧

基础优化技巧善用EXPLAIN分析执行计划EXPLAINSELECT*FROMordersWHEREorder_date>'2024-01-01';...

答记者问之 - Redis 的高效架构与应用模式解析

问:极客程序员你好,请帮我讲一讲redis答:redis主要涉及以下核心,我来一一揭幕Redis的高效架构与应用模式解析...

MySQL通过累计求新增(mysql新增表字段语句)

前两天的那篇内容《MySQL递归实现单列分列成多行》...

一文讲懂SQL窗口函数 大厂必考知识点

大家好,我是宁一。今天是我们的第24课:窗口函数。...

圣诞快乐:用GaussDB T 绘制一颗圣诞树,兼论高斯数据库语法兼容

转眼就是圣诞的节日,祝大家节日快乐。用GaussDBT(也就是GaussDB100)绘制一棵圣诞树,纯国产,更喜庆。话不多说,上图:SQL如下:SELECTCASEWHENENMOTE...

Minitab:功能强大的质量管理、统计分析及统计图形软件

一、Minitab简介Minitab软件是为质量改善、教育和研究应用领域提供统计软件和服务的先导,是全球领先的质量管理和六西格玛实施软件工具及持续质量改进的良好工具软件,她具有强大的功能和简易的可视化...

如何熟练使用SQL查询(如何熟练使用sql查询内容)

要熟练使用SQL查询(StructuredQueryLanguage),你需要系统地从语法入门,到实战练习,再到性能优化与多表查询的掌握。下面是一条循序渐进、实战驱动的学习路径:第一阶段:S...

SAP SE38如何在多个系统间同步代码

上一篇文章写了如何在多个系统之间同步开发对象:多套SAPERP之间一键同步ABAP开发内容,有兄弟问有没有简单办法同步SE38程序代码的,因为使用请求的方式同步代码有点小题大做了。...

Python | 垂直模态分解(phython垂直输出)

...

技术栈:刷了百道SQL题,还是不会用?你应该这样补短板

这是来自用户的提问,也是很多人遇到的困惑:...

mysql窗口函数为了解决更加复杂的问题

为了解决复杂问题的窗口函数我们先讲一下窗口函数是什么窗口和普通的函数作用相同在不同列上进行查询和返回比如我们有如下的表...

MariaDB开窗函数(开窗函数 mysql)

在使用GROUPBY子句时,总是需要将筛选的所有数据进行分组操作,它的分组作用域是整张表。分组以后,为每个组只返回一行。而使用基于窗口的操作,类似于分组,但却可以对这些"组"(即窗口...

一文掌握 DuckDB 时间序列分析:窗口函数实战详解

...

一篇文章搞定MySQL中的窗口函数(mysql常用的窗口函数)

我是孙斌,北理数学系毕业,分享数据分析相关知识,点击右上角“关注”,学习更多数据分析知识。在MySQL中,分组groupby一般和聚合函数连用,如groupby+sum,这样能够得到每个组的总和,...