字符串优化不容小觑

String对象是我们日常工作中使用最频繁的对象,它的性能问题也是我们最容易忽略的。String对象作为Java语言中最重要的数据类型,是内存中占据空间最大的对象,高效地使用字符串,可以提升系统的整体性能。

今天这篇文章我们从String对象的实现、特性以及实际使用中的优化三方面,来深入了解String对象。

String对象是如何实现的

在Java更新的版本变化中,对String对象已经做了大量的优化,来节约内存空间,提升String对象在系统中的性能。来看看在Java版本迭代中String的优化过程;

  • 在Java6以及以前的版本中,String对象是对char数组进行了封装实现的对象,主要有四个成员变量:char数组、偏移量offset、字符数量count、哈希值hash。
  • 在Java7和8版本中,Java对String类做了改变,不再有offset和count两个变量,这样可以稍微减少String对象占用的内存。同时,String.substring()不再共享char[],从而解决了使用该方法可能导致的内存泄露问题。
  • 从Java9版本开始,char[]改成了byte[],有维护了一个新的属性coder,它是一个编码格式的标识。

为什么从char[]改变成byte[],我们都知道一个char字符占用16位,2个字节,这种情况在存储单字节编码内的字符就有点浪费。Java9中String类为了更加节约内存空间,选择了占用8位,1字节的byte数组来存放字符串。

新属性coder的作用是在计算字符串长度或者使用indexOf()函数时,我们需要根据这个字段,判断如何计算字符串长度。coder属性默认有0和1两个值,0代表Latin-1(单字节编码),1代表UTF-16。如果String判断字符串只包含了Latin-1,则coder属性值为0,反之则为1.

String对象的不可变性

我们发现在String对象实现中, 不仅实现代码的String类被final关键字修饰,而且遍历charp[]也被final修饰。类被final修饰代表String类不能被继承,而charp[]被private和final修饰,代表了String对象不可被更改。Java实现的这个特性叫做String对象的不可变性,即String对象一旦被创建成功就不能进行修改了。

String对象不可变性有哪些好处?

  1. 保证了String对象的安全性。如果 String 对象是可变的,那么 String 对象将可能被恶意修改。
  2. 保证了hash属性值不会频繁变更,确保了唯一性,使得类似 HashMap 容器才能实现相应的 key-value 缓存功能。
  3. 可以实现字符串常量池。Java中有两种创建字符串对象的方式,一种是通过字符串常量的方式创建,另外一种是字符串变量通过new的形式创建。

当我们使用字符串常量创建字符串对象时,JVM会先检查该对象是在字符串常量池中,如果在就返回该对象的引用,否则新创建一个字符串对象保存到字符串常量池,并使用这个引用。这种方式可以减少同一个值的字符串对象的重复创建,节约了内存空间。

当我们使用new的形式创建,比如String str = new String(“abc”),在编译类文件的时候,“abc”常量字符串将会放入常量结构中,在类加载时,“abc”将会放到常量池中创建,在调用new时,JVM命令将会调用String的构造函数,同时引用常量池中的“abc"字符串,在堆内存中创建一个String对象,最后 str引用String对象。

String对象的优化

上边我们了解了String对象实现原理和特性,下边将结合实际场景,看看String对象在我们实际使用中有哪些需要注意的地方。

1. 字符串拼接

在编程过程中,字符串的拼接很常见。前边我们也说了String对象是不可变的,如果我们使用String对象相加,拼接我们想要的字符串,就会产生多个对象。例如如下代码:

String str = "ab" + "cd" + "ef";

分析可知,首先会生成ab对象,再生成abcd对象,最后生成abcdef对象,从理论上讲,这样做的效率很低。但是实际运行中,我们发现只有一个对象生成,这是为什么?我们再来看看编译后的代码,你会发现上边的代码编译器自动做了优化,如下:

String str = "abcdef";

上面介绍的是字符串常量的累加,再来看看字符串变量的累加:

String str = "abcdef";
for (int i = 0; i<1000;i++) {
    str = str + i;
}

编译后,我们可以看到编译器同样对这段代码进行了优化,Java在进行字符串拼接时,偏向于使用StringBuilder,这样可以提高程序的效率。

String str = "abcdef";
for(int i=0; i<1000; i++) {
            str = (new StringBuilder(String.valueOf(str))).append(i).toString();
}

我们可以看到即使使用‘’+”号作为字符串拼接,也一样被编译器优化成StringBuilder的方式,但是,仔细一看,你会发现编译器优化后的代码,每次循环的时候都会生成一个新的StringBuilder对象,同样会降低系统的性能。

我们平时做字符串拼接的时候,建议显示地使用StringBuilder来提升系统性能,如果是多线程编程中,String对象的拼接涉及到线程安全,可以使用StringBuffer。由于StringBuffer是线程安全的,涉及到锁竞争,所以从性能上说,要比StringBuilder差一些。

2. 使用String.intern节省内存

在每次赋值的时候使用 String 的 intern 方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用,为了更好的理解,我们举一个简单的例子来看一下:

String a =new String("abc").intern();
String b = new String("abc").intern();
        
if(a==b) {
    System.out.print("a==b");
}

输出结果,a==b,分析一下;

创建 a 变量时,调用 new Sting() 会在堆内存中创建一个 String 对象,String 对象中的 char 数组将会引用常量池中字符串。在调用 intern 方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。

创建 b 变量时,调用 new Sting() 会在堆内存中创建一个 String 对象,String 对象中的 char 数组将会引用常量池中字符串。在调用 intern 方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。

而在堆内存中的两个对象,由于没有引用指向它,将会被垃圾回收。所以 a 和 b 引用的是同一个对象。

如果在运行时,创建字符串对象,将会直接在堆内存中创建,不会在常量池中创建。所以动态创建的字符串对象,调用 intern 方法,在 JDK1.6 版本中会去常量池中创建运行时常量以及返回字符串引用,在 JDK1.7 版本之后,会将堆中的字符串常量的引用放入到常量池中,当其它堆中的字符串对象通过 intern 方法获取字符串对象引用时,则会去常量池中判断是否有相同值的字符串的引用,此时有,则返回该常量池中字符串引用,跟之前的字符串指向同一地址的字符串对象。

用一张图来总结String字符串的创建和分配内存地址的情况:

使用 intern 方法需要注意的一点是,一定要结合实际场景。因为常量池的实现是类似于一个 HashTable 的实现方式,HashTable 存储的数据越大,遍历的时间复杂度就会增加。如果数据过大,会增加整个字符串常量池的负担。

3. 如何使用字符串的分割方法

分割字符串我们常用的方法就是Split()方法,Split()方法使用了正则表达式实现了其强大的分割能力,但是正则表达式的性能是很不稳定的,使用不当就会引起回溯问题,很有可能导致CPU居高不下。

在日常使用的时候,可以用String.indexOf()方法代替Split()方法完成对字符串的分割,如果无法满足需要,在使用Split()方法的时候对回溯问题要加以重视。

最后

以一个小问题结束本次分享,下边每组匹配的两个对象是否相等?

String str1= "abc";
String str2= new String("abc");
String str3= str2.intern();
assertSame(str1==str2);
assertSame(str2==str3);
assertSame(str1==str3)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342