String 对象作为 Java 语言中重要的数据类型,是内存中占据空间最大的一个对象。高效地
使用字符串,可以提升系统的整体性能。
来一到题来引出这个话题
通过三种不同的方式创建了三个对象,再依次两两匹配,每组被匹配的两个对象是否相等?
代码如下
结尾有东西
String 的不可变性
了解了 String 对象的实现后,你有没有发现在实现代码中 String 类被 final 关键字修饰
了,而且变量 char 数组也被 final 修饰了。
我们知道类被 final 修饰代表该类不可继承,而 char[] 被 final+private 修饰,代表了
String 对象不可被更改。Java 实现的这个特性叫作 String 对象的不可变性,即 String 对
象一旦创建成功,就不能再对它进行改变。
String 对象的优化
1. 如何构建超大字符串
字符串常量的累计
编程过程中,字符串的拼接很常见。前面我讲过 String 对象是不可变的,如果我们使用
String 对象相加,拼接我们想要的字符串,是不是就会产生多个对象呢?例如以下代码
分析代码可知:首先会生成 ab 对象,再生成 abcd 对象,最后生成 abcdef 对象,从理论
上来说,这段代码是低效的。
但实际运行中,我们发现只有一个对象生成,这是为什么呢?难道我们的理论判断错了?我
们再来看编译后的代码,你会发现编译器自动优化了这行代码,如下:
String str= "abcdef";
字符串变量的累计
上面我介绍的是字符串常量的累计,我们再来看看字符串变量的累计又是怎样的呢?
上面的代码编译后,你可以看到编译器同样对这段代码进行了优化。不难发现,Java 在进
行字符串的拼接时,偏向使用 StringBuilder,这样可以提高程序的效率。
综上已知:即使使用 + 号作为字符串的拼接,也一样可以被编译器优化成 StringBuilder
的方式。但再细致些,你会发现在编译器优化的代码中,每次循环都会生成一个新的
StringBuilder 实例,同样也会降低系统的性能。
所以平时做字符串拼接的时候,我建议还是要显示地使用 String Builder 来提升系统性
能。
如果在多线程编程中,String 对象的拼接涉及到线程安全,你可以使用 StringBuffer。但
是要注意,由于 StringBuffer 是线程安全的,涉及到锁竞争,所以从性能上来说,要比
StringBuilder 差一些
2 如何使用String.Intern节省内存
Twitter 每次发布消息状态的时候,都会产生一个地址信息,以当时 Twitter 用户的规模预
估,服务器需要 32G 的内存来存储地址信息。
考虑到其中有很多用户在地址信息上是有重合的,比如,国家、省份、城市等,这时就可以
将这部分信息单独列出一个类,以减少重复,代码如下
通过优化,数据存储大小减到了 20G 左右。但对于内存存储这个数据来说,依然很大,怎
么办呢?
提供公共都有的部分
这个案例来自一位 Twitter 工程师在 QCon 全球软件开发大会上的演讲,他们想到的解决
方法,就是使用 String.intern 来节省内存空间,从而优化 String 对象的存储
具体做法就是,在每次赋值的时候使用 String 的 intern 方法,如果常量池中有相同值,就
会重复使用该对象,返回对象引用,这样一开始的对象就可以被回收掉。这种方式可以使重
复性非常高的地址信息存储大小从 20G 降到几百兆
为了更好地理解,我们再来通过一个简单的例子,回顾下其中的原理
输出结果:
a == b
在字符串常量中,默认会将对象放入常量池;在字符串变量中,对象是会创建在堆内存中,
同时也会在常量池中创建一个字符串对象,复制到堆内存对象中,并返回堆内存对象引用。
如果调用 intern 方法,会去查看字符串常量池中是否有等于该对象的字符串,如果没有,
就在常量池中新增该对象,并返回该对象引用;如果有,就返回常量池中的字符串引用。堆
内存中原有的对象由于没有引用指向它,将会通过垃圾回收器回收.
了解了原理,我们再一起看看上边的例子。
在一开始创建 a 变量时,会在堆内存中创建一个对象,同时会在加载类时,在常量池中创
建一个字符串对象,在调用 intern 方法之后,会去常量池中查找是否有等于该字符串的对
象,有就返回引用。
在创建 b 字符串变量时,也会在堆中创建一个对象,此时常量池中有该字符串对象,就不
再创建。调用 intern 方法则会去常量池中判断是否有等于该字符串的对象,发现有等
于"abc"字符串的对象,就直接返回引用。而在堆内存中的对象,由于没有引用指向它,将
会被垃圾回收。所以 a 和 b 引用的是同一个对象。
下面我用一张图来总结下 String 字符串的创建分配内存地址情况:
使用 intern 方法需要注意的一点是,一定要结合实际场景。因为常量池的实现是类似于一
个 HashTable 的实现方式,HashTable 存储的数据越大,遍历的时间复杂度就会增加。如
果数据过大,会增加整个字符串常量池的负担。
3. 如何使用字符串的分割方法?
最后我想跟你聊聊字符串的分割,这种方法在编码中也很最常见。Split() 方法使用了正则
表达式实现了其强大的分割功能,而正则表达式的性能是非常不稳定的,使用不恰当会引起
回溯问题,很可能导致 CPU 居高不下。
所以我们应该慎重使用 Split() 方法,我们可以用 String.indexOf() 方法代替 Split() 方法完
成字符串的分割。如果实在无法满足需求,你就在使用 Split() 方法时,对回溯问题加以重
视就可以了
结尾更容易懂
题解