字符串优化:高手必备的优化技能!

186 阅读4分钟

某日深夜,某程序员试图用字符串拼接造火箭:

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万次内存消耗
直接拼接(+)15ms1500ms爆炸
StringBuilder0.5ms5ms稳定✅
StringBuffer0.8ms8ms略高
StringJoiner1ms10ms优雅✨

震惊发现: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); // 瞬间打回原形

第五章 灵魂拷问室

  1. 为什么BigInteger.toString()特别慢?(提示:递归计算+反向填充)
  2. 如何用字符串让CPU缓存命中率暴跌?(试试随机长度的字符串数组)
  3. 为什么说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的坑)

最后送各位一句至理名言: "字符串处理的速度,决定了你下班的时间!"  现在就去检查你的代码,说不定能提前三小时下班撸串呢?