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

createh51个月前 (02-01)技术教程8

字符串在 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 笔试和面试的参考书。

相关文章

汉字转拼音Chinese to Pinyin(汉字转拼音大写在线翻译)

从网上找的资料,记得以前在C#中曾经用过这类资料,保存下来以后再进一步测试和应用。一、引入maven依赖 com.belerweb pinyin4j 2.5.0 二、工具类Pinyi...

前端 JavaScript 字符串中提取数字

var str ="4500元"; var num = parseInt(str); alert(num);//4500 如果字符串前面有非数字字符,上面这种方法就不行了:var...

MySql字符串拆分实现split功能(字段分割转列、转行)

字符串转多行字符串拆分: SUBSTRING_INDEX(str, delim, count)替换函数:replace( str, from_str, to_str)获取字符串长度:LENGTH( s...

Java往oracle存clob类型的值时,字符长度过长怎么办?

业务场景将照片转为数字长串后,由于字符过长,java往数据库中直接存为clob字段时,oracle会报ORA-01704问题:字符串文字过长。这是因为一般对含有CLOB字段的数据操作。如果CLOB字段...

Java代码审计之SpEL表达式注入(spring的setter注入)

SpEL 表达式注入Spring Expression Language(简称 SpEL)是一种功能强大的表达式语言、用于在运行时查询和操作对象图;语法上类似于 Unified EL,但提供了更多的特...

二、Java字符串/时间处理(java字符串时间格式转换)

二、Java字符串/时间处理1、 文章背景工作已有五年之久,回望过去,没有在一线城市快节奏下学习成长,只能自己不断在工作中学习进步,最近一直想写写属于自己的文章,记录学习的内容和知识点,当做一次成长。...