String 类型是咱们运用最频繁的数据类型,没有之一。那么进步 String 的运转功率,无疑是提高程序功能的最佳手法。

咱们本文将从 String 的源码下手,一步步带你完成字符串优化的小目标。不光教你怎么有用的运用字符串,还为你揭晓这背面的深层次原因

本文涉及的知识点,如下图所示:

4000字!教你如何提升String性能(源码+原理分析)

在看怎么优化 String 之前,咱们先来了解一下 Stl R H F Cring 的特性,毕竟知己知彼,才能6 ] e T # . $ V百战不殆。

字符串的特性

想要了解 String 的特性就必须从它的1 Z + l )源码下手,如下所示:

// 源码基p ; : @ 4 c T *于 JDK 1.8
public fJ a Rinal class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// String 值的实践存储容器
private final char value[];M I ? T G x
public String(U = # x) {
this.value = "".value;
}
public String(String original) {I X D i U # y i Q
this.value = origs J Oinal.value;
this.hash = original.hash;
}
// 忽略其他信息
}

从他的源码咱们能够看出,String 类以及它的 value[]特点都被 final润饰了,其间 value[]是完成字符串存储的终究结构,9 6 xfiO r mnal则表明“最终的、终究的”。

咱们知道,被 f6 2 [ , _inal润饰的类是不能被承继的,也便是说此类将不能具有子类,而被 final润饰的变量即为常量,它的值是不能被C $ L @改动的。这也就说当 String一旦被创立之后,就不能被修正了

String 为什么不d ~ n Z { E + z能被修正?

StO N i A Z oring 的类和特点 value[]都被界说为 final了,这样做Y m @ u = . : c r的好处有以下三点:

  1. 安全性:当你在调用其他办法时,比方调用一些体系级操作指令之n r U h : 2前,或许会有一系列校验,假如是可w x v ^ ~ T J变类的话,或许在你校验往后,它的内部的值又被改动了,这样有或许会引起严峻的体系崩溃问题,所以迫使 String 规划为 final 类的一个重要 h j a ( +原因便是出于安全考虑;
  2. 高功能:String 不可变之后就确保的 hash 值的唯一性,这样它就更加高效,而且更适合做 HashMap 的 key- value 缓存;
  3. 节省内存c ! h & r ( – +:String 的不可变性是它完成字符串常量池的基f u N Z 4础,字符串常量池指的是字符串在创立时,先去“常量池”查找是否有此“字符串”,假如有,则不会开辟新空间创作字符串,而是直接把常量池中的引证回来给此目标,这样就能更加节省空W % h F ? v n 4 F间。例如,通常情况下 String 创立有两种办法,直接赋值的办法,如 String str=”Java”;另一种是 new/ s y N i 5 M W j 形式的创立,如 String str = new String(“Java”)。当代码中9 T N N + _运用榜首种办法创立字符串目标时,JVM 首要会检查该目标是否在字符串常量池中,假如在,就~ z q h N 7 ( {回来该目标引证,不然新的字符串将在常量池中} } u E 4 .被创立。[ i 9 7 j这种办法能够削减同一个值的字符串目标的重复创立,节省内存。String str = new String(“Java”) 这种办法,首要在编译类文件时,“Java”常量字符串将会放入到常量结构中,在类加载时,“w e V n 2 M ]Java”将会在常量池中创立;其次,在调用 new 时,JVM 指令将会调用 String 的结构函数,同时引证常量池中的“Java”字符串,在堆内存中创立一个 String 目标,最终 str 将引证 String 目标。

1.不要直6 & w Z : @ ? )接+=字符串

经过上面的内容,咱们知道l # P S 7了 String 类是不可变的,那么在运用 String 时就不能频繁的 += 字符串了。

优化前代码

public static Strf I y N =ing doAdd() {
String result = "";
for (int i = 0; i < 10000; i++) {
result += (" i:" + i);
}
return resuI / F |lt;
}

有人或许会问,我的业务需求是这样的,那我该怎么完成?

官方为咱们供给了两种字符串拼加的计划: Stril Q nngH j F a . IBufferStringBuilder,其间 StringBuilder为非线程安o s 1 7 6全的,而 StringBuffer则是线程安全} ~ w s | 7 k k的,StringBuffer的拼加办法运用了关键字 synchronized来确保线程的安全,源码如下:

@Override
public synchronk 0 r @ V Qized StringBuffer append(CharSequence s) {
toStringCache = null;
super.append(s);
return this;
}

也因为运用synchronized& V - t d润饰,所以StringBuffer的拼加功能会比1 s EStringBuc . q J 4 N ~ilder低。

那咱们就用StringBuilder来完成字符串的拼加$ ^ Z优化后代码

public static String doAppend() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.b P w & G j `append(" i:" + i);
}
reL _  b Hturn sb.toString();
}

咱们经过代码测验一下,两0 9 y / X : g b个办法之间的功能差别:

public cla= w # / vss StringTest {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
// String
long st1 = System.currentTimeMillis(); // 开端时刻
doAdd();
long et1 = System.currentT4 Y ( !imeMillis(); // 开端时刻E h Q
System.out.println("String 拼加,履行时刻:" + (et1 - st1));
// StringBuilder
long st2 = System.currentTimeMillis(); // 开端时D i p /
doAppend();
long et2 = System.currentTimeMillis(); // 开端时刻
System.out.println("Strin; N 4 ? ~ 4 qgBuilder 拼加,履行时刻:" + (et2 - st2));
System.out.priR f x V 0 m G Sntln();
}
}
pubc ] u 7lic static String doj m . : 1Add() {
String res[ j y k  b ; & ,ult = "";
for (int i = 0; i < 10000; i++) {
result += ("Java中文社群:z K c" + i);
}
return res u x v ] * R  Osult;
}
public staticB _ q C O W h String doApp^ q M [end() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1o ] s ] C -0000; i++) {
sb.append("Java中文社群:" + i);
}
return sb.tw ; 3 i n 8 D - :oString();
}
}

以上程序的履行成果如下:

String 拼加,履# g C a t q # v行时刻:429

StringBuilder 拼加Y y ? # (,履行8 K A ? ] i 2 Z时刻:1

String 拼加,履行时刻:553

StringBuilder 拼加,履行时刻:0

String 拼加,履行时刻:289

StringBuilder 拼加,履行时刻:1

String 拼加,履行时刻:210

StringBuilder 拼加i J D J,履行时刻:2

String 拼加,履行时刻:224

StringBuilder 拼加,履行时刻:1f X P

从成果能够看出,优化前后的功能相差很大。

留意:此功能测验的成果与循环的次数有关,也便是说循环的次数越多,他们功能相除的成果也越大。

接下来,咱们要思考一个问题:为什么 StringBuilder.append() 办法比 += 的功能高? 而且拼接的次数越多功能的差距也越大?

当咱们3 * 9 G打开 StringBuilder 的源码,就能够发现其间的“小秘密”了,StringBuilder 父类Abstg g x M mractStringBuilder 的完成源码如下:

abstract class AbstractStringBuilder implemex Y 5 $ t 8 q / bnts Appendable, CharS=  b Z B _equence {
char[] value;
int count;
@Override
public AbstractStringBuilder append(CharSequence s, int start, int end) {
if (s == null)
s = "null";
if ((sta* 1 x _rt <% Q 3 . * N 0 Y; 0) || (start > end) ||O 6 : Y (end > s.length()))
throw new IndexOutOfBoundsException(
"start " + start + ", end " + end + ", s.le2 w U . ` J U / 1ngth() "
+ s.Q S .  ; H n / =length[ q Q = H N C());
int len = end - stP u nart;
ensureCapacitya 3 H , : + # ^Internal(count + len);
for (int i = start, j = c. a xount; i < end; i++, j++)
valu} U O 5 ^e[j]l p T x = s.charAt(i);
count += len;
return this;
}
// 忽略其他信息...
}

而StringBuil~ – r / a ~ f tder 运用了父A V O 1类供给的char[]作为自己值的实践存储单元,每次在拼加时会修正 char[]数组,StringBuilder toString()源码如下:

@Override
public S] 6 X 7tring& y y g toString() {
// Create a copy, don't share the array
return new String(value, 0, count)B $  9 - d B;
}

综合以上源码能够看出:StringBuilder 运用了 char[]作为实践存储单元,每次在拼加8 % # 7时只需求修正 char[]数组即可,只是在 toString()时创立了一个字符串;而 String 一旦创立之后就不能被修正,因此在每次拼加时,都需求从头创立新的字符串,所以StringBuilder.? # y o W Aappend() 的功能就会比字符串的 += 功能高许多

2.善用inter% F ~ D _ 8n 办法

善用 String.intern() 办法能够有用的节省内C m y存并提高字符串的运转功率,先来看 intern()办法的界说与源码:

/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately) B ? % @ H * n by the
* class {@code Stw _ e g [ ~ring}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equa+ 8 r &ls(Object)} method, then the strM u qing from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a refeA W x P e * I urence to this {@cy z ; | 4ode String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {R f = d C c K@code t},
* {x % n P s@code s.intern() == t.intern()q r b ` k ^ Y} is {@code true}
* ifR I i 5 $ 7 G s - and only if {@code s.equa9 ] - 3 k als(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interh 8 s , 1 r y yned. SF O h x itring litera] f : f ~ U Zls are defined in section 3.10.5 of the
* <cite>The Java&trade; Language Specification</cx u D $ ( ~ k X &ite>.
*r I ( W u @ y 
* @return  a string that has the same contents as tL z v h Whis string, butk N y y J is
*          guarantee z Z # % Z # Od to be from a pool of unique stri* { } ^ 7 Pngs.
*/
public native String intern()[  0 p ! = c * 3;

能够看出intern()是一个高效的本地办法,它的界说中说的是,当调用 intern办法时,假如字符串常量池中现已包括此字符串,则直接回来此字符串的引证,N U =假如不包括此字符串,先将字符串添加到常量池中,再回来此目标的引证。

那什么情况下适合运用 intern()办法?

Twitter 工程师曾共享过一个 Stri; r S & png.intern() 的运用示例,Twitter 每次发布消息状况的时分,都会产生一个地址信息d 0 i,以其时 Twitter 用户的规模预估,服务器需求 32G 的内存来存储地址信息。

pubM m C C ylic 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 sharedLoj N R ( W l Tcation;
double longitude;
double lal u w 0 ] xtitude;
}

经过优化,数据存储巨细减到了 20G 左右。# q q C j但对于内存存储这个数据来说,仍然很大,怎么办呢?

Twitter 工程师运用 String.intern()使重复性非常高h n t – # E X ; e的地址信息存储巨细从 20G 降到几百兆,然后优化了 String 目标的存储。

完成的核心代码如下:

SharedLocation sharedLocation = new SharedLocation();
sharedLocation.setCi= v + * | $ty(messageInfo.getCity().intern()y q @ | ( M # =);
sharedLocation.setCountryCode(messageInfo.getRegion().intern());
sharedLocation.s4 t retRegion(messageInfo.g4 5 ? 9 . 0 T oetCountryCode().intern());

从 JDK1.7 版别以后,常量池现已兼并到了堆中,所以不会仿制字符串副本,只是会把首次遇到的字符串的引证添加到常量池中。此刻只会判别常量池中是否现! u h Y d已有此字符串,假如有就回来常量池中的字符串引证。

这就相当于以下代码:

String s1 = new StrC 6 -ing("Java中文社群").intern();
String s2 = new String("Java中文社群").intern();
System.out.pr4 B } 1 7 n /intln(s1 == s2);

履行的成果为:trud ) ~e

此处假如有人问为什么不直接赋值(运用String s1 = “Java中文社群”),是因为这段代码是简化了上面TwR 2 D [itter 业L N z e j务代码的语义而创立的,他运用的是目标的办法,而非直接赋值的办法。更多关于 intern() 的内容能够检h S / D – ( g 4查《别再问我new字符串创立了几个目标了!我来证明给你看!》这篇文章。

3.稳重运用Split 办法) B B

之所以要劝各位慎用 Split办法,是因为 Split办法大多数情况下运用的是正则表达式,这种切割办法本身没有什P . + a K d么问题,可是因为正则表达式的功能是非常不稳定的,运用不恰当会引起回溯问题,很或许导致 CPU 居高不下。

例如以下正则表达式:

String badRegex = "^([hH][+ Y u v B @tT]q & 9 f C{2}[pP]://|[hH][tT]{2}[pP][sS]://)(([A-Za-z0-9-~]+).)+([A-Za-z0-9-~\\/])+$";
String bugUrl = "http://www.apigo.com/ X ? } i }dddp-web/pdf/doz 0 8 / d 3 h $ .wnload?request=6e7JGxxxxx4ILd-kExxxxxxxqJ4-CHLmqVnenXC692m74H38sdfdsazxcUmfcOH2fAfY1Vw__%5EDadIfJgiEf";
if (bugUrl.matches(badRegex)) {
System.out.println("match!!");
} else {
System.out.println("no matc= m 0 ~h!!");K 5 C
}

履行效果如下图所示:

4000字!教你如何提升String性能(源码+原理分析)

能够看出此代码导致了 CPU 运用过高。

Java 正则表达式运用的引擎完成是 NFA(Nonu Q / @ + c d c deterministic Finite AutA 7 z 7 s 9omaton,不确) C i _ U g P p K定型有穷自动机)自动机,这种正则表达式引擎在进行字符匹配时会发作回溯(backtracu u j % v d K Rking),而一旦发作回溯,那其耗费y R 2 X A J I的时刻就会变得很长,有或许是几分钟,也有或许是几个小时,时刻长短取决于回$ ` V H + #溯的次数和复杂度。

为了更好地解说什么是回溯,咱们运用以下面比如进行; ? B解说:

text = "abbc";
regex = "ab{1,3}c/ h P"} | X - / p;

上面的这F m B } $ l % :个比如的意图比较简单,匹配以 a 最初,以 c 完毕,中间有 1-3 个 b 字符的字符串。

NFA 引擎对其解析的过程是这姿态的:

  • 首要,读取正则表达式榜首个匹配符 a和 字符串6 b / ! [ c 6 榜首个字符 a比较,匹配上了j V o H t , Y @,所以读取正则表达式第二个字符;
  • 读取正则表达式第二个匹配符 b{1,3}和字符串的第二个字符 b 比较,匹配上了。但因为 b{1,3}表明 1-3 个 b字符串,以及 NFA 自动机的贪婪特性(也便是说要尽或许多地匹配),所以此刻并不会再去读取下一个正则表达式的匹配符,而是o ) ( X N 7 e . 依旧运用 b{1,3}和字符串的第三个字符 b比较,发现v ` s S 4 z B !还是匹配上了,所以继续运用 b{1,3}和字符串的第四个字符 c比较,发现不匹配了,此刻就会发作回溯;
  • 发作回溯后,咱们现已读取的字符串第四个字符T s H c将被吐出去,指针回到第三个字符串的位置,之后程序& q A } # g % 1 1读取正则表达式的下一个操作符 c,然后再读取当前指针的下一个字符 c进行比照,发现匹配S % s c上了,所以读取下一个操作符,然后发现现已完毕了。

这便是正则匹配履行的流程和简单的h r c回溯履行流程,而上面的示例在匹配到“com/dzfp-web/pdf/download?request=6e7JGm38jf…..”时因为贪婪匹配的原因,所以程序会一直读后面的字符串进行匹配,最终发现没有点号,所以就一个个字符回溯回去了,所以就会导致了 CPU 运转过高。

所以咱q & P f们应该稳重运用 Split() 办法,咱们= R R A k N = 能够用 Str{ I e + ?ing.indexOf() 办法代替 Split() 办法完成字符串的切割。假如实在无法满意需求^ X b,你就在运用 Split() 办法时,对回溯问题加以注重就能够了。

总结

本文经过 String 源码分析,发现了 String 的不可变特性,以及不可变特性的 3 大优点解说;然后讲[ = : 9 b Z l @ n了字符串l ! m t ~优化的三个手法:不要直接 += 字符串、善用 intev ; prn() 办法和稳重运用 Split() 办法。而且经过 Stc 0 | ; k e @ TringBuilder 的源码分析,了解了 appeo z $nd() 功能高的主要原因,以及正则表达式不稳定性导致回溯问题,进入导致 CP+ { Z iU 运用过高的案例分析,希望能够切实的协助到你。

最终的话

原创不易,假如觉得本文对你有用,请随手点击一个「赞y % _ g ~ J,这是对作者最大的支撑与鼓励,谢谢你r [ = O % { T

参阅 & 鸣谢

gk.link/a/10hUM

blog.cO H R B 7 . qsdn.M ) 0 ? G 6 `net/ityouknow/a…

更多精彩内容,请重视微信大众号「Java中文社群」

4000字!教你如何提升String性能(源码+原理分析)