资讯动态

Java String深度解析:不可变性、常量池与性能优化实战

发布时间:2026/10/9 8:21:45 来源:尧图企业网站定制
1. Java中的String先把它当对象而不是字符串做Java开发这么多年面试别人或者被面试的时候String永远绕不开。很多新手最开始接触String觉得它就是拿来存一段文字的东西跟int、double这种基本类型差不多。这个误解如果不纠正后面理解常量池、字符串不可变、拼接性能这些内容就全乱了。先说清楚String在Java里是一个类一个引用类型。你写String s hello的时候s保存的不是字符本身而是指向堆或者常量池里某个String对象的引用。这一点跟C语言里char*的思路完全不一样Java设计者把它做成对象是为了让字符串拥有方法、可以判等、可以不可变这些特性直接决定了后续所有字符串操作的底层行为。但光知道String是对象还不够。真正要弄明白的是它内部到底存了什么。从JDK 9开始String内部不再是char[]而是byte[]配合一个coder字段来标记当前字符串是LATIN1还是UTF16编码。这么改的原因很实在大部分业务字符串都是纯英文字母、数字和常见符号用单字节就能表示用char[]每个字符固定占2字节太浪费。所以JDK 9之后的内存优化本质上就是把默认按两个字节存改成了能省则省按一个字节存。说到这我插一个我自己的踩坑经历。早期项目里大量使用String s new String(abc)这种写法当时没觉得有问题直到用MATMemory Analyzer Tool分析一个OOM的堆转储发现堆里有几万个内容完全相同的String对象占了几十MB内存。换谁看到那画面都得怀疑人生。从那以后我写代码和review别人代码时都会严格盯着字符串创建方式。String这个类身上还挂着一个非常重要的设计决策final。String类本身被final修饰意味着你不能继承它。同时它内部存的byte[]也是final的一旦赋值就不能再指向别的数组。这俩final叠加起来就是Java字符串不可变immutable的基石。不可变带来了线程安全、可以安全地被多个集合共享、适合做HashMap的key等好处代价就是你每次修改字符串实际都是新造一个对象。对初学者而言理解不可变最好的类比是便利贴和便签本的关系。你在一张便利贴上写字改一个字不是把那个字涂掉重写而是重新拿一张便利贴把整句话写一遍。旧的那张如果没人引用就会被GC回收。所以写循环里拼接字符串本质上是循环里不断撕旧便利贴、写新便利贴性能自然差到离谱。2. 从源码看String的核心设计2.1 字符存储与编码的演进String的字段设计是理解整个类行为的关键。以JDK 8和JDK 17为例差异非常明显JDK 8及以前private final char value[];每个字符固定占2字节不管内容是英文还是中文。英文存进去浪费一半空间但这在当时没有更好的方案属于空间换简单。JDK 9及以后private final byte[] value; private final byte coder;coder有两个值LATIN1 0表示每个字符占1字节UTF16 1表示每个字符占2字节。当你创建字符串时String会根据内容自动选择紧凑还是扩展存储。如果字符串只包含ISO-8859-1LATIN1能表示的字符就用1字节存储一旦出现中文、日语、韩语这类需要更多码位的字符coder自动切换为UTF16。这个改动带来的实际收益是纯英文为主的程序String内存占用直接降一半左右。还有一个跟面试常考相关的字段hash。它被声明为private int hash默认值是0第一次调用hashCode()时计算之后缓存下来。因为String不可变所以这个缓存是安全的这也是为什么String适合当HashMap的key——哈希值只算一次后续取用都是O(1)的。至于serialVersionUID老生常谈但值得强调-6849794470754667710LString实现了Serializable如果你做过把Java对象存Redis或者写本地文件的反序列化场景就知道这个字段的兼容性有多重要。不同JDK版本之间如果这个值变了反序列化直接失败好在官方一直没改过它。注意String实现了ComparableString所以它能直接用来排序。compareTo是按字符的Unicode码位逐位比较的不是按字符串长度也不是按字典序里的先短后长规则这个在写自定义排序时经常被搞混。2.2 不可变设计背后的线程安全价值关于不可变很多人只知道String不能被修改但没深想它到底解决了什么问题。我举一个实际场景。一个Web服务里多个线程并发读取同一个配置项配置项的值是从一个共享的String变量读取的。因为String不可变所有线程看到的都是同一个稳定的对象不会出现一个线程改了内容、另一个线程读到一半的脏数据。如果是可变对象比如StringBuilder你就必须加锁或者用volatile加同步逻辑否则并发读可能看到中间状态。再比如你把它放进HashSet或HashMap当key。如果key可变它的hashCode对应的散列桶位置就会变集合就找不到这个元素了。String的不可变保证了hashCode是一个恒定值HashMap运作的前提就不会被破坏。但不可变也有代价。代价就是所有字符串操作都会生成新对象带来额外的内存分配和GC压力。这引出了Java里另外一个永恒的面试话题字符串拼接的性能对比。这个留到后面实操部分详细说。2.3intern()与字符串常量池常量池的理解水平基本能区分出是背八股还是真懂JVM。正常运行中的JVM字符串字面量就是你在代码里写的abc这种会被放入字符串常量池。这个池子在不同JDK版本里位置不一样JDK 6及以前在永久代PermGenJDK 7开始挪到了堆里。挪到堆的原因很简单——永久代空间太小字符串多了容易OOM就算不OOM频繁Full GC也很痛苦。挪到堆之后常量池可以享受堆的自动扩容和GC回收。intern()方法的作用是如果常量池里已经有相同内容的字符串直接返回池里的那个引用如果没有就把当前字符串内容存入池中并返回引用。它最经典的应用场景是大量重复字符串的省内存。比如从数据库读取100万条记录性别字段就男女两个值如果不做intern处理就有100万个内容相同的String对象躺在堆里。调用了intern()之后内容相同的字符串在池子里只有一份堆里的那100万个对象就可以被GC收走。但intern不是银弹用不好会出大问题。JDK 7之后常量池在堆里如果intern一个超大字符串或者intern的对象太多了直接把堆占满照样OOM。我见过有同事把日志文件每行内容都intern一次结果堆直接炸了。所以interning适用于内容有限且大量重复的场景绝不能用在高基数、内容随机的数据上。这个尺度得拿捏好不然就是拿内存换了个寂寞。3. 日常开发里最常见的String操作陷阱3.1 拼接、concat、StringBuilder怎么选先给结论再讲原理。少量固定字符串拼接直接在代码里用写可读性最好编译器也会帮你优化。循环体里拼接大量字符串必须用StringBuilder。追求线程安全不需要因为字符串操作本身就线程安全StringBuffer在单线程场景纯属浪费性能。很多人奇怪为什么在循环里慢到爆但写着写着好像也没太慢这跟Java编译器的常量折叠优化有关。比如你写String s a b c;编译期就会直接把abc算出运行时不会创建三个中间对象。但如果是循环里String s ; for (int i 0; i 10000; i) { s s i; }每次循环都会新建一个StringBuilderappend当前内容再toString成新String。循环一万次就创建了接近两万个中间对象。就算JIT即时编译器做了逃逸分析也不总是能把所有对象栈上分配掉。所以性能问题的根源是——每轮都在新造一个StringBuilder 新造一个String。具体性能对比数据我用JDK 17做过一个简单测试循环10万次用拼接的数字字符串耗时大约是StringBuilder的15到20倍。差距大到不需要精密仪器肉眼可感知的卡顿。生产环境我见过一个日志输出重灾区在循环里用拼SQL参数、拼JSON、拼日志结果接口响应时间从80ms飙到1.5秒。改成StringBuilder之后回到90ms左右。所以这个不是微优化是必须做到位的。那concat()方法呢它的实现是新建一个长度足够的byte[]拷贝旧内容和新内容再new一个新的String。本质上和的处理方式差不太多但因为少了编译器优化尤其是一连串concat的时候中间对象的数量会上升。日常用或StringBuilder足够concat用的场景其实很少。还有一个特别容易忽略的点StringJoiner。JDK 8引入的StringJoiner适合做加分隔符的拼接比如1,2,3,4这种。它内部其实就是包装了StringBuilder但可以处理首尾分隔符的问题比你自己写一遍第一个元素不加逗号的逻辑干净得多。用String.join()也可以两者的区别在于StringJoiner可以指定前缀后缀String.join更简单直接。3.2equals、和hashCode的纠葛面试八股经典三连比较引用equals比较内容。但如果只有这个认知掉坑只是时间问题。先看代码String s1 hello; String s2 hello; String s3 new String(hello); System.out.println(s1 s2); // true因为字面量指向常量池同一个对象 System.out.println(s1 s3); // false因为new在堆里创建了新对象 System.out.println(s1.equals(s3)); // true这里核心差异在于hello字面量在类加载时进入常量池后续所有相同字面量直接复用而new String(hello)即使内容是同一个hello也会在堆上额外创建一个对象。虽然JDK 7之后常量池在堆里但常量池里的对象和堆里的对象是两码事。再深入一步equals的实现逻辑其实非常短先比较引用是不是同一个对象是就直接返回true。再判断对方是不是String类型不是直接false。然后把内容转成byte数组比较coder和每个字节。JDK 9之后的实现还会利用StringLatin1和StringUTF16两个工具类分开比较LATIN1比较更快因为单字节逐位比较。hashCode的实现用的是31这个质数作为乘数。为什么是31因为它是个奇素数可以减少哈希冲突同时JVM可以用移位和减法来优化乘法31 * i (i 5) - i。这个性能细节在hash计算热路径上还是有意义的。注意重写equals时必须重写hashCode这条规则适用于任何类。String已经帮你做好了。但你自己写的业务类比如一个只有id和name的User类如果只在equals里比较id和name不重写hashCode放进HashMap就是灾难——两个equals相等的对象可能被放进不同的桶导致查不到。3.3 判空isEmpty和isBlank哪个才对这个问题在真实开发里比想象中更常见而且几乎每个团队都有不同写法。JDK 6就有的isEmpty()判断的是length() 0也就是字符串里一个字符都没有。JDK 11新增的isBlank()判断的是字符串是否为空或者只包含空格、制表符、换行符等空白字符。举个例子.isEmpty() // true .isEmpty() // false - 因为有一个空格字符 .isBlank() // true - 因为全是空白字符 \t\n.isBlank() // true如果你想判断用户输入是否空白用isBlank()更贴近真实需求。比如表单里用户只敲了几空格就提交用isEmpty()判不出来直接让脏数据进了业务逻辑。判断null的情况Java依然没有内置语法糖还得str null || str.isBlank()这样写。另外Apache Commons Lang的StringUtils.isBlank()也经常出现在老项目里。这工具类内部逻辑比JDK标准库更宽容把null也当作blank处理所以可以写成StringUtils.isBlank(str)一步到位。在新代码里我更倾向于先用JDK原生判断null再用isBlank()虽然啰嗦一点但不用引第三方依赖。3.4split的坑正则特殊字符与尾随空字符串split也是日常最容易被坑的API。它的参数其实是一个正则表达式不是普通分隔符字符串。所以当你用.、|、*、、[等正则字符直接传参时结果会完全出乎意料。比如你想按IP地址切分192.168.1.1如果直接写str.split(.)得到的数组长度是0。因为正则表达式里.表示匹配任意字符整个字符串都被吞了切出的数组为空。正确写法是str.split(\\.)先转义成字面意义的点号。再比如按竖线切分a|b|c写str.split(|)会得到每个字符都变成单独元素因为|在正则里表示或。同样需要str.split(\\|)。这背后的原理是split方法内部使用了Pattern.compile(regex)来编译正则正则引擎要扫描整个字符串。如果正则复杂性能也会有问题。我见过有人循环里对每行日志调用split正则稍微复杂一点CPU直接被打满。对性能敏感的路径手动用indexOf加substring来切分比split快数倍。还有一个被忽略的细节split默认会丢弃尾部的空字符串。比如a,b,,按逗号split得到的是[a, b]不是[a, b, , ]。如果你需要保留分位符产生的空值比如CSV解析中某些字段确实为空就要用split(,, -1)传入负的limit参数让split保留所有尾随空串。3.5 子串与替换substring、replace、replaceAllsubstring在JDK 8前后也有一个经典变更JDK 6及以前substring会引用原字符串的底层char数组通过偏移量和长度来构造新字符串。这导致一个长期存在的隐患如果你切了一个大字符串的小片段只要这个片段还存在整个大字符串的底层数组就无法被GC回收。一个100MB的日志字符串只取前10个字符就能让100MB内存一直占着不放俗称内存泄漏假象。JDK 7之后substring改为复制内容不再共享底层数组。代价是创建了独立数组内存可能多一点但避免了长期持有大数组的风险。replace和replaceAll的差异也值得一提。replace(CharSequence target, CharSequence replacement)里的target是字面字符串不做正则解析。replaceAll(String regex, String replacement)的第一个参数是正则表达式。所以把字符串里的所有.替换成/应该用str.replace(., /)而不是replaceAll(., /)——后者会匹配所有字符直接得到一串斜杠。replaceFirst则只替换第一个匹配项。实操提示大规模替换时如果替换逻辑简单且是纯字面量replace性能通常优于replaceAll因为它不需要编译正则表达式。如果同一正则要用在大量不同字符串上考虑复用Pattern实例避免每次Pattern.compile的开销。4. String相关的工具类与进阶用法4.1StringBuilder与StringBuffer为什么单线程选前者先明确一个核心差异StringBuffer的所有公开方法都加了synchronized保证多线程下对同一个实例的并发操作是安全的StringBuilder没有任何同步机制。这个差异导致的结果是单线程下StringBuilder明显更快因为不需要获取和释放锁。有多快JDK 17实测10万次追加操作StringBuilder比StringBuffer快约2到3倍。原因不只是锁本身的竞争还包括synchronized导致的偏向锁撤销、内存屏障等额外开销。在高并发场景如果你为每个线程维护独立的StringBuilder实例局部变量本来就没必要同步用StringBuffer纯属白交性能税。唯一的合理用法是一个StringBuffer实例被多个线程共享且需要在不同线程里追加内容。但现实中这种场景极少就算有你也可以考虑用ThreadLocal维护StringBuilder或者干脆用不可变字符串加消息队列在单线程里统一拼接。更细的讨论我就不展开了结论很明确新代码默认StringBuilder看到StringBuffer就当历史遗留处理。还有一个细节StringBuilder默认容量是16。超过容量会触发扩容扩容逻辑是oldCapacity 1再加2。也就是16变34、34变70这样往上翻。如果循环里频繁追加到一个大字符串你可以在创建StringBuilder时预估容量new StringBuilder(capacity)减少扩容次数。所谓扩容就是新申请更大数组把旧内容拷贝过去这个拷贝次数多了就是浪费。预估容量对于拼接超大文本比如生成报表、导出CSV收益非常明显。4.2StringTokenizer、String.format与正则协作老项目里经常见到StringTokenizer这个类是JDK 1.0时代的遗产按分隔符切分字符串。它的行为类似split但它更古老不支持正则而且Enumeration接口用起来也不方便。官方文档已经建议用String.split或者java.util.regex替代。除非你在维护历史代码否则新写的代码就别碰它了。String.format是一个很多人天天用但没注意性能的API。它的底层是Formatter内部会创建大量中间对象。在日志这种低频场景无所谓但如果在一个百万级的循环里用format性能会非常难看。实测format比手工拼接慢5到8倍。如果你只是拼几个固定格式字符串用或StringBuilder足够。真正复杂的国际化格式化需求才需要MessageFormat或NumberFormat这类专门工具。正则和String的合作最典型的场景是数据校验与提取。比如手机号校验String regex ^1[3-9]\\d{9}$; boolean ok phone.matches(regex);这里有个性能细节String.matches()每次都会重新编译正则在高频调用下是浪费。应该在类加载时就编译好private static final Pattern PHONE_PATTERN Pattern.compile(^1[3-9]\\d{9}$); boolean ok PHONE_PATTERN.matcher(phone).matches();这两者差距在千万级循环下能到10倍以上。4.3 从String到其他类型的转换这块内容看着简单坑却不少。String和基本类型之间的转换我列出高频场景和注意点Integer.parseInt(123)解析失败抛NumberFormatException得捕获或校验。注意前导空格会失败 123是解析不了的要先trim。String.valueOf(123)底层调Integer.toString()效率最高。别犯贱去123 虽然写法一样能出结果但会走StringBuilder逻辑多创建无谓对象。Double.parseDouble(3.14)同样注意科学计数法字符串是可以解析的比如1e3解析成1000.0很多人不知道。数字和字符串互转还要注意进制问题Integer.parseInt(ff, 16)可以解析十六进制字符串Integer.toHexString(255)可以输出ff。业务上如果做颜色值、协议字段处理这些API比手写进制转换可靠得多。如果字符串转数字用得多可以试试Integer.valueOf配合缓存。Integer.valueOf(127)和Integer.valueOf(127)返回同一个对象因为IntegerCache缓存了-128到127之间的数值超过这个范围就每次新建对象。所以如果你有大量数字字符串需要频繁转成Integer装箱对象可以考虑自己维护缓存映射避免重复装箱。5. 常见问题排查与性能优化实录5.1 大量重复字符串导致的内存暴涨这是我在生产环境真正处理过的一个案例。上游服务返回了一个超大的JSON数组里面每个对象都有一个userId字段大概有20万个元素。我们在做数据聚合时把每个userId都直接存进了一个List 。本来以为只是存储结果跑了一会儿堆内存直线上升GC频次高到接口超时。排查思路是先用jmap导出堆转储用MAT分析Dominator Tree发现Top贡献者全是String对象内容高度相似。进一步看userId是上游从同一批用户数据里取出来的内容重复度极高。既然是重复值那最佳方案就是用intern()让它们共享同一个对象。但直接对20万个对象挨个intern也会有性能损耗而且如果基数真的大反而更糟。实际操作是我先统计了userId的基数发现只有几百个。于是先建一个HashMap作为归池缓存遍历时先去缓存里取已有值取不到才放入。等List构建完成后缓存就可以清了。内存从1.2GB降到不到300MBFull GC频率从每分钟6次降到接近0。经验总结字符串内存问题一定要先看内容基数再决定要不要intern或缓存。高基数数据用intern就是灾难低基数重复数据不用就是浪费。推荐做法是先抽样统计内容重复率再做决策。5.2 启动失败与Unclosed String Literal错误这类编译错误太典型了任何Java开发者都见过error: unclosed string literal意思是编译器在扫描代码时遇到了一个字符串字面量但一直等不到闭合的双引号。常见原因有中文字符串里用了中文引号“”编译器只认英文半角。字符串内部包含换行但没写成\n直接把代码折行了。字符串里包含了垂直线之类的不可见字符看起来像是闭合了其实没有。在JSON或正则里使用了作为内容但没有转义。举一个翻车案例String s 他说:你好;编译器会把他说:当成一个完整字符串接着在你好处直接语法报错。正确写法是String s 他说:\你好\;还有一种隐藏很深的场景Windows路径。C:\Users\name这种字符串里\U不是合法的转义序列编译器直接报illegal escape character。正确写法是C:\\Users\\name或者用Path类来处理路径不要手拼字符串。5.3 常见的String比较与空值坑问一个老生常谈的问题下面这段代码输出什么String a null; System.out.println(a.equals());答案NullPointerException。遇到null直接崩。反过来写就没问题System.out.println(.equals(a));所以业界有个不成文的规定常量字符串放在equals前面变量放后面。这个习惯能帮你免掉无数个NPE。再一个隐藏很深的坑switch语句里用String。JDK 7开始String可以用于switch。但底层实现其实是先算hashCode再做equals校验所以switch的case值不能是null。一旦switch的String变量是null会抛NPE。代码里如果拿不准是否可能为null加个前置判空更稳。contains也是高频误用。你想判断一个字符串里是否包含某个子串直接str.contains(substr)就行底层是indexOf。但如果你判断的是某个字符是否存在用indexOf(ch)然后判断是否大于等于0就可以不必创建新的Boolean包装。总之判断类操作尽量用基本类型返回减少无谓的包装对象。5.4 字符串性能优化速查表我把实际项目里验证过的优化手段整理成一个表方便直接抄作业场景不推荐推荐理由循环内拼接str str iStringBuilder.append(i)减少中间String对象固定格式拼接多个concat连用编译器编译期可做常量折叠更简洁需要分隔符拼接手写循环加逗号StringJoiner或String.join处理首尾分隔符更优雅大量重复内容去重每人一个对象intern()或自建缓存池显著降低堆占用高频正则匹配str.matches(regex)预编译Pattern并复用避免反复编译正则高频splitstr.split(complexRegex)手工indexOfsubstring正则解析开销大高频formatString.formatStringBuilder或预构建模板format内部对象开销大需要二进制字节str.getBytes()反复调用缓存byte[]结果每次getBytes都有编码开销这里的核心逻辑归根结底就是一个思想字符串是不可变对象一切看起来在改的操作都在造新对象。所以优化的本质就是——减少造对象的次数、缩短对象生命周期、让能复用的内容尽量复用。6. Java面试中关于String的高频考点把String当一道面试题来准备是很值得的因为它的考点几乎覆盖了JVM、并发、集合、编码、正则、反射等一大圈。我整理了几类必考问题附带答题要点和追问方向。第一类基础概念题。String为什么设计成不可变回答要点有几个线程安全不需要额外同步常量池可以安全共享同一个实例hashCode可以缓存适合HashMap key安全机制下不会因为字符串被外部篡改而影响系统。追问方向一般是既然不可变StringBuffer和StringBuilder为什么存在这时候把可变性和拼接性能串起来回答就行。第二类内存与常量池题。new String(abc)和abc有什么区别这是区分度很高的题。大部分人会回答常量池和堆的区别但你要准备后续追问这个过程中到底会发生几次对象创建答案不是固定的。如果常量池里原本没有abc那类加载时会在常量池放一个执行new时又在堆里放一个一共两个。常量池里已经有的话new只会在堆里多造一个。什么时候需要intern大量重复字符串且基数可控时。第三类性能优化题。循环里怎么拼接一万次字符串标准答案是StringBuilder但要能说出为什么。追问时通常考察StringBuilder扩容机制、容量预估、是否线程安全、StringBuffer和它的差别。答得出new StringBuilder(capacity)的容量预估才算真正有实战理解。第四类编码题。String.getBytes(UTF-8)和Charset.forName(UTF-8)区别答案getBytes默认使用平台默认字符集Charset.forName是显式指定。在与外部系统交互时显式指定代码更可移植不会因为部署到不同系统导致编码错乱。另外从JDK 10开始Platform.getCharset()会读取file.encoding不设置的话在不同系统行为不同所以尽量不要依赖默认编码。第五类隐藏考点。String可以做switch的case吗可以JDK 7开始支持。继续追问底层是怎么办到的回答是通过hashCode加equals实现的不是简单的跳转表。String可以直接用比较吗要分情况讨论字面量比较可以用new出来的对象不行建议一律用equals。这些考点的共性其实是同一个对String底层机制有没有真正理解而不只是背结论。准备面试的时候把每个问题往为什么再挖一层效果远比背答案好。7. 我的一些实操习惯与细微心得写到这里把我日常处理String相关代码时的几个习惯分享出来。这些习惯谈不上多高深但有它们在出坑的概率会低很多。第一个习惯凡是外部输入HTTP参数、数据库字段、文件内容要进入业务逻辑一律先判null再判空白。我个人的模板是if (value null || value.isBlank()) { // 按缺省处理或抛异常 }千万别直接value.trim()因为null直接NPE。也别只value.isEmpty()因为空格会漏过。第二个习惯日志里打印字符串的内容时考虑脱敏。用户名、手机号、身份证这些字段直接打进日志排查问题时确实方便但合规风险也大。更好的做法是提供一个脱敏工具类比如保留前3位和后4位中间打码。这个习惯后来在多次安全审计里帮我避免了很多麻烦。第三个习惯做字符串操作前先问自己——这个字符串会很大吗会重复吗会被并发读吗。三个问题对应的就是substring内存共享风险、intern和缓存池的必要性、不可变能否帮助简化并发设计。想完这三个问题再动手写代码能省掉后续一大半的优化和排查工作。第四个习惯能用TextBlockJDK 15的文本块写多行字符串就尽量用可读性比用\n拼接强太多。比如SQL、JSON模板、错误提示用文本块写能直接看到原始排版也不会被转义坑到。不过文本块的缩进规则有讲究上下文中公共的缩进会被去掉保留相对缩进。用过几次之后就会发现写多行SQL时它比引号字符串舒服得多。第五个习惯比较两个字符串是否相等时如果确定两边都不是null但内容可能不同优先考虑Objects.equals(a, b)。这个方法内部会做两个null判断加equals就算有一边是null也不会崩代码还短一截。8. 结尾再补充一个日常的细节最后再分享一个小技巧是关于字符串驻留缓存的自建方案。我之前处理过一个场景系统需要并发生成大量相同前缀的交易号格式类似TX202402271234567890大量线程同时创建相同前缀的字符串。每个线程都拼一遍前缀等于重复创建了N份相同字符串。当时的优化做法是用一个静态StringBuilder先拼好前缀再用new String(prefix)复制出独立字符串。后续如果每个交易号都要拼随机部分用的也是同一个前缀变量。这样在垃圾回收上虽然还是会创建对象但至少前缀本身只有一份。真正极端情况下的优化是维护一个前缀到String的映射用的时候直接取。这种方式适合前缀种类有限但使用频率极高的系统。当然这种优化粒度已经接近性能抠细节级别了一般业务代码真没必要。优化的铁律永远是先量性能再动手没有数据支撑的优化都是耍流氓。String这个类表面上是Java最简单的部分背地里藏了内存模型、编码、正则、并发、JVM调优一大摞知识。把它学透不只是应付面试更是日常写高性能、低内存占用代码的基本功。希望这篇分析能让你对String的认知从会用变成理解后面写代码的时候能少踩几个坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑