千家信息网

Java字符串的构造和拼接

发表于:2025-02-13 作者:千家信息网编辑
千家信息网最后更新 2025年02月13日,这篇文章主要讲解了"Java字符串的构造和拼接",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"Java字符串的构造和拼接"吧!2.1 构造字符串字符串在
千家信息网最后更新 2025年02月13日Java字符串的构造和拼接

这篇文章主要讲解了"Java字符串的构造和拼接",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"Java字符串的构造和拼接"吧!

2.1 构造字符串

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

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

原有的字符串的value数组直接通过引用赋值给新的字符串value,也就是俩个字符串共享一个char数组,因此这种构造方法有着最快的构造。Java里的String对象被设计为不可变。意思是指一旦程序获得了字符串对象引用,不必担心这个字符串在别的地方被修改,不可变意味着线程安全,在第三章对不可变对象线程安全性又说明。

构造字符串更多的情况构造字符串是通过一个字符串数组,或者在某些框架的反序列化,使用byte[] 来构造字符串,这种情况下性能会非常低。 如下是通过char[]数组构造一个新的字符串源码

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

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[] cs = new char[]{'a','b'};String str = new String(cs);cs[0] ='!'

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

通过char数组构造新的字符串是最长用的方法,我们后面看到几乎每个字符串API,都会调用这个方法构造新的字符串,比如subString,concat等方法。如下代码验证了通过字符串构造新的字符串,以及使用char数组构造字符串性能比较

String str= "你好,String";char[] chars = str.toCharArray();[@Benchmark](https://my.oschina.net/u/3268003)public String string(){  return new String(str);}[@Benchmark](https://my.oschina.net/u/3268003)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[] bs = "你好,String".getBytes("UTF-8");[@Benchmark](https://my.oschina.net/u/3268003)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计算转码的工作,属于无法优化的部分.

在我多次的系统性能优化过程中,都会发现通过字节数据组构造字符串总是排在消耗CPU比较靠前的位置,转码消耗的系统性能抵得上百行的业务代码。 因此我们系统在设计到分布式的,需要仔细设计需要传输的字段,尽量避免用String。比如时间可以用long类型来表示,业务状态也可以用int来表示。如下需要序列化的对象

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

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

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

关于在微服务中,序列化和反序列化传输对象,会在第四章和五章再次介绍对象的序列化

2.2 字符串拼接

JDK会自动将使用+号做的字符串拼接自动转化为StringBuilder,如下代码:

String a="hello";String b ="world "String str=a+b;

虚拟机会编译成如下代码

String str = new StringBuilder().append(a).append(b).toString();

如果你运行JMH测试这俩段代码,性能其实一样的,因为使用+连接字符串是一个常见操作,虚拟机对如上俩个代码片段都会做一些优化,虚拟使用-XX:+OptimizeStringConcat 打开字符串拼接优化,(默认情况下是打开的)。 如果采用以下代码,虽然看是跟上面的代码片段差不多,但虚拟机无法识别这种字符串拼接模式,性能会下降很多

StringBuilder sb = new StringBuilder();sb.append(a);sb.append(b);

运行StringConcatTest类,代码如下

String a = "select u.id,u.name from user  u";String b="  where u.id=? "   ;[@Benchmark](https://my.oschina.net/u/3268003)public String concat(){  String c = a+b;  return c ;}[@Benchmark](https://my.oschina.net/u/3268003)public String concatbyOptimizeBuilder(){  String c = new StringBuilder().append(a).append(b).toString();  return c;}@Benchmarkpublic String concatbyBuilder(){  //不会优化  StringBuilder sb = new StringBuilder();  sb.append(a);  sb.append(b);  return sb.toString();}

有如下结果说明了虚拟机优化起了作用

Benchmark                                           Mode    Score    Units         c.i.c.c.StringConcatTest.concat                     avgt   25.747    ns/op         c.i.c.c.StringConcatTest.concatbyBuilder            avgt   90.548    ns/op         c.i.c.c.StringConcatTest.concatbyOptimizeBuilder    avgt   21.904    ns/op

可以看到concatbyBuilder是最慢的,因为没有被JVM优化

这里说的JVM优化,指的是虚拟机JIT优化,我们会在第8章JIT优化说明

读者可以自己验证一下a+b+c这种字符串拼接性能,看一下是否被优化了

同StringBuilder类似的还有StringBuffer,主要功能都继承AbstractStringBuilder, 提供了线程安全方法,比如append方法,使用了synchronized关键字

@Overridepublic synchronized StringBuffer append(String str) {  //忽略其他代码  super.append(str);  return this;}

几乎所有场景字符串拼接都不涉及到线程同步,因此StringBuffer已经很少使用了,如上的字符串拼接例子使用StringBuffer,

  @Benchmark  public String concatbyBuffer(){    StringBuffer sb = new StringBuffer();    sb.append(a);    sb.append(b);    return sb.toString();  }

输出如下

Benchmark                                           Mode      Score   Unitsc.i.c.c.StringConcatTest.concatbyBuffer             avgt    111.417   ns/opc.i.c.c.StringConcatTest.concatbyBuilder            avgt     94.758   ns/op

可以看到,StringBuffer拼接性能跟StringBuilder相比性能并不差,这得益于虚拟机的"逃逸分析",也就是JIT在打开逃逸分析情况以及锁消除的情况下,有可能消除该对象上的使用synchronzied限定的锁。

逃逸分析 -XX:+DoEscapeAnalysis和 锁消除-XX:+EliminateLocks,详情参考本书第8章JIT优化

如下是一个锁消除的例子,对象obj只在方法内部使用,因此可以消除synchronized

void foo() {  //创建一个对象  Object obj = new Object();   synchronized (obj) {    doSomething();  }}

程序不应该依赖JIT的优化,尽管打开了逃逸分析和锁消除,但不能保证所有代码都会被优化,因为锁消除是在JIT的C2阶段优化的,作为程序员,应该在无关线程安全情况下,使用StringBuilder。

使用StringBuilder 拼接其他类型,尤其是数字类型,则性能会明显下降,这是因为数字类型转字符在JDK内部,需要做很多工作,一个简单的Int类型转为字符串,需要至少50行代码完成。我们在第一章已经看到过了,这里不再详细说明。当你用StringBuilder来拼接字符串,拼接数字的时候,你需要思考,是否需要一个这样的字符串。

2.10 BigDecimal

我们都知道浮点型变量在进行计算的时候会出现丢失精度的问题。如下一段代码

System.out.println(0.05 + 0.01);System.out.println(1.0 - 0.42);

输出: 0.060000000000000005 0.5800000000000001

可以看到在Java中进行浮点数运算的时候,会出现丢失精度的问题。那么我们如果在进行商品价格计算的时候,就会出现问题。很有可能造成我们手中有0.06元,却无法购买一个0.05元和一个0.01元的商品。因为如上所示,他们两个的总和为0.060000000000000005。这无疑是一个很严重的问题,尤其是当电商网站的并发量上去的时候,出现的问题将是巨大的。可能会导致无法下单,或者对账出现问题。

通常有俩个方法来解决这种问题,如果能用long来表示账户余额以分为单位,这是效率最高的。如果不能,则只能使用BigDecimal类来解决这类问题。

BigDecimal a = new BigDecimal("0.05");BigDecimal b = new BigDecimal("0.01");BigDecimal ret = a.add(b);System.out.println(ret.toString());

通过字符串来构造BigDecimal,才能保证精度不丢失,如果使用new BigDecimal(0.05),则因为0.05本身精度丢失,使得构造出来的BigDecimal也丢失精度。

BigDecimal能保证精度,但计算会有一定性能影响,如下是测试余额计算,用long表示分,用BigDecimal表示元的性能对比

BigDecimal a = new BigDecimal("0.05");BigDecimal b = new BigDecimal("0.01");long c = 5;long d = 1;@Benchmark@CompilerControl(CompilerControl.Mode.DONT_INLINE)public long addByLong() {  return (c + d);}@Benchmark@CompilerControl(CompilerControl.Mode.DONT_INLINE)public BigDecimal addByBigDecimal() {  return a.add(b);}

在我的机器行,上面代码都能进行精确计算,通过JMH,测试结果如下

Benchmark                                 Mode   Score    Units    c.i.c.c.BigDecimalTest.addByBigDecimal    avgt   8.373    ns/op    c.i.c.c.BigDecimalTest.addByLong          avgt   2.984    ns/op

感谢各位的阅读,以上就是"Java字符串的构造和拼接"的内容了,经过本文的学习后,相信大家对Java字符串的构造和拼接这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是,小编将为大家推送更多相关知识点的文章,欢迎关注!

0