某日深夜,某程序员试图用字符串拼接造火箭:
String rocket = "";
for(int i=0; i<100000; i++){
rocket += "燃料罐"+i;
}
结果...电脑炸了?不,是老板炸了——这代码跑得比蜗牛还慢!今天我们就来聊聊字符串优化的骚操作,保你代码快过闪电侠!
第一章 字符串の千层套路(底层原理)
你以为String是纯情少女?不!她是妥妥的"海王":
- 不可变の心:每次操作都造新对象,旧爱全进GC垃圾场
- 内存双面人:堆里本体+常量池分身(JDK7后分身搬到了堆里)
- 字节码の秘密:用jad反编译能看到new StringBuilder的暗箱操作
举个:
String s = "" + "你" + "太美";
字节码真相:
new StringBuilder().append("").append("你").append("太美").toString();
原来编译器早把+号变成了StringBuilder!但为什么循环里还是翻车?
且听我娓娓道来
第二章 六大车祸现场(性能陷阱)
场景1:循环の诅咒
// 青铜写法:每秒创建n个对象
String result = "";
for(int i=0; i<10000; i++){
result += i;
}
// 王者写法:
StringBuilder sb = new StringBuilder();
for(int i=0; i<10000; i++){
sb.append(i);
}
原理揭秘:每次循环都new StringBuilder,像极了渣男不停换新女友!
场景2:splitの毒苹果
// 错误示范:拆东墙补西墙
String[] arr = "a,b,c".split(",");
// 高端操作:
StringTokenizer st = new StringTokenizer("a,b,c", ",");
性能对比:split耗时是StringTokenizer的3倍!因为它...偷偷编译了正则表达式。
场景3:hashCodeの预谋
// 青铜选手:每次调用都重新计算
public int hashCode(){
return name.hashCode() ^ age;
// 钻石大佬:
private int hash;
public int hashCode(){
if(hash == 0){
hash = name.hashCode() ^ age;
}
return hash;
}
冷知识:String自己就缓存了hashCode,这就是为什么它这么"快"!
第三章 性能核爆试验场
用JMH测试(单位:纳秒/操作):
| 操作方式 | 1万次 | 10万次 | 内存消耗 |
|---|---|---|---|
| 直接拼接(+) | 15ms | 1500ms | 爆炸 |
| StringBuilder | 0.5ms | 5ms | 稳定✅ |
| StringBuffer | 0.8ms | 8ms | 略高 |
| StringJoiner | 1ms | 10ms | 优雅✨ |
震惊发现:StringBuilder比直接拼接快300倍!同步锁让StringBuffer躺枪。
第四章 黑魔法の进阶手册
1.intern()の禁忌游戏
String s1 = new String("abc").intern(); // 从常量池找对象
String s2 = "abc";
// s1 == s2 成立!
警告⚠️:用得好省内存,用不好PermGen分分钟OOM(JDK8后是元空间了)
2.char[]の秘密交易
char[] buffer = new char[1024];
str.getChars(0, len, buffer, 0); // 比substring快3倍
原理:避开String的防御性拷贝,直接操作底层数组
3.Compact Stringsの魔法
JDK9后String改用byte[]存储,拉丁字符省一半内存!但别高兴太早:
new String(bytes, StandardCharsets.UTF_16); // 瞬间打回原形
第五章 灵魂拷问室
- 为什么BigInteger.toString()特别慢?(提示:递归计算+反向填充)
- 如何用字符串让CPU缓存命中率暴跌?(试试随机长度的字符串数组)
- 为什么说StringBuilder初始容量是渣男承诺?(默认16,用完就扩容翻倍!)
第六章 优化实战
Twitter 每次发布消息状态的时候,都会产生一个地址信息,以当时 Twitter 用户的规模预估,服务器需要 32G 的内存来存储地址信息。
public class Location {
private String city;
private String region;
private String countryCode;
private double longitude;
private double latitude;
}
考虑到其中有很多用户在地址信息上是有重合的,比如,国家、省份、城市等,这时就可以将这部分信息单独列出一个类,以减少重复,代码如下:
public class SharedLocation {
private String city;
private String region;
private String countryCode;
}
public class Location {
private SharedLocation sharedLocation;
double longitude;
double latitude;
}
通过优化,数据存储大小减到了 20G 左右。但对于内存存储这个数据来说,依然很大,怎么办呢?这个案例来自一位 Twitter 工程师在 QCon 全球软件开发大会上的演讲,他们想到的解决方法,就是使用 String.intern 来节省内存空间,从而优化 String 对象的存储。具体做法就是,在每次赋值的时候使用 String 的 intern 方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用,这样一开始的对象就可以被回收掉。这种方式可以使重复性非常高的地址信息存储大小从 20G 降到几百兆。
SharedLocation sharedLocation = new SharedLocation();
sharedLocation.setCity(messageInfo.getCity().intern()); sharedLocation.setCountryCode(messageInfo.getRegion().intern());
sharedLocation.setRegion(messageInfo.getCountryCode().intern());
Location location = new Location();
location.set(sharedLocation);
location.set(messageInfo.getLongitude());
location.set(messageInfo.getLatitude());
我在公司也复刻了这一优化点,因此获得了优秀员工!!!
终极大招:字符串优化自查表
✅ 循环拼接必用StringBuilder
✅ 大量重复值考虑intern()
✅ 解析文本优先用indexOf
❌ 不要用split切蛋糕式分割
❌ 慎用replaceAll正则
❌ 警惕substring内存泄漏(JDK6的坑)
最后送各位一句至理名言: "字符串处理的速度,决定了你下班的时间!" 现在就去检查你的代码,说不定能提前三小时下班撸串呢?