String 类型是我们使用最频繁的数据类型,没有之一。那么提高 String 的运行效率,无疑是提升程序性能的最佳手段。
我们本文将从 String 的源码入手,一步步带你实现字符串优化的小目标。不但教你如何有效的使用字符串,还为你揭晓这背后的深层次原因。
本文涉及的知识点,如下图所示:
在看如何优化 String 之前,我们先来了解一下 String 的特性,毕竟知己知彼,才能百战不殆。
字符串的特性
想要了解 String 的特性就必须从它的源码入手,如下所示:
// 源码基于 JDK 1.8 public final class String implements java.io.Serializable, Comparable<String>, CharSequence { // String 值的实际存储容器 private final char value[]; public String() { this.value = "".value; } public String(String original) { this.value = original.value; this.hash = original.hash; } // 忽略其他信息 }
从他的源码我们可以看出,String 类以及它的 value[]
属性都被 final
修饰了,其中 value[]
是实现字符串存储的最终结构,而 final
则表示“最后的、最终的”。
我们知道,被 final
修饰的类是不能被继承的,也就是说此类将不能拥有子类,而被 final
修饰的变量即为常量,它的值是不能被改变的。这也就说当 String 一旦被创建之后,就不能被修改了。
String 为什么不能被修改?
String 的类和属性 value[]
都被定义为 final
了,这样做的好处有以下三点:
- 安全性:当你在调用其他方法时,比如调用一些系统级操作指令之前,可能会有一系列校验,如果是可变类的话,可能在你校验过后,它的内部的值又被改变了,这样有可能会引起严重的系统崩溃问题,所以迫使 String 设计为 final 类的一个重要原因就是出于安全考虑;
- 高性能:String 不可变之后就保证的 hash 值的唯一性,这样它就更加高效,并且更适合做 HashMap 的 key- value 缓存;
- 节约内存:String 的不可变性是它实现字符串常量池的基础,字符串常量池指的是字符串在创建时,先去“常量池”查找是否有此“字符串”,如果有,则不会开辟新空间创作字符串,而是直接把常量池中的引用返回给此对象,这样就能更加节省空间。例如,通常情况下 String 创建有两种方式,直接赋值的方式,如 String str="Java";另一种是 new 形式的创建,如 String str = new String("Java")。当代码中使用第一种方式创建字符串对象时,JVM 首先会检查该对象是否在字符串常量池中,如果在,就返回该对象引用,否则新的字符串将在常量池中被创建。这种方式可以减少同一个值的字符串对象的重复创建,节约内存。String str = new String("Java") 这种方式,首先在编译类文件时,“Java”常量字符串将会放入到常量结构中,在类加载时,“Java”将会在常量池中创建;其次,在调用 new 时,JVM 命令将会调用 String 的构造函数,同时引用常量池中的“Java”字符串,在堆内存中创建一个 String 对象,最后 str 将引用 String 对象。
1.不要直接+=字符串
通过上面的内容,我们知道了 String 类是不可变的,那么在使用 String 时就不能频繁的 += 字符串了。
优化前代码:
public static String doAdd() { String result = ""; for (int i = 0; i < 10000; i++) { result += (" i:" + i); } return result; }
有人可能会问,我的业务需求是这样的,那我该如何实现?
官方为我们提供了两种字符串拼加的方案:StringBuffer
和 StringBuilder
,其中 StringBuilder
为非线程安全的,而 StringBuffer
则是线程安全的,StringBuffer
的拼加方法使用了关键字 synchronized
来保证线程的安全,源码如下:
@Override public synchronized StringBuffer append(CharSequence s) { toStringCache = null; super.append(s); return this; }
也因为使用 synchronized
修饰,所以 StringBuffer
的拼加性能会比 StringBuilder
低。
那我们就用 StringBuilder
来实现字符串的拼加,优化后代码:
public static String doAppend() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10000; i++) { sb.append(" i:" + i); } return sb.toString(); }
我们通过代码测试一下,两个方法之间的性能差别:
public class StringTest { public static void main(String[] args) { for (int i = 0; i < 5; i++) { // String long st1 = System.currentTimeMillis(); // 开始时间 doAdd(); long et1 = System.currentTimeMillis(); // 开始时间 System.out.println("String 拼加,执行时间:" + (et1 - st1)); // StringBuilder long st2 = System.currentTimeMillis(); // 开始时间 doAppend(); long et2 = System.currentTimeMillis(); // 开始时间 System.out.println("StringBuilder 拼加,执行时间:" + (et2 - st2)); System.out.println(); } } public static String doAdd() { String result = ""; for (int i = 0; i < 10000; i++) { result += ("Java中文社群:" + i); } return result; } public static String doAppend() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10000; i++) { sb.append("Java中文社群:" + i); } return sb.toString(); } }
以上程序的执行结果如下:
String 拼加,执行时间:429
StringBuilder 拼加,执行时间:1
String 拼加,执行时间:325
StringBuilder 拼加,执行时间:1
String 拼加,执行时间:287
StringBuilder 拼加,执行时间:1
String 拼加,执行时间:265
StringBuilder 拼加,执行时间:1
String 拼加,执行时间:249
StringBuilder 拼加,执行时间:1
从结果可以看出,优化前后的性能相差很大。
注意:此性能测试的结果与循环的次数有关,也就是说循环的次数越多,他们性能相除的结果也越大。
接下来,我们要思考一个问题:为什么 StringBuilder.append() 方法比 += 的性能高?而且拼接的次数越多性能的差距也越大?
当我们打开 StringBuilder 的源码,就可以发现其中的“小秘密”了,StringBuilder 父类 AbstractStringBuilder 的实现源码如下:
abstract class AbstractStringBuilder implements Appendable, CharSequence { char[] value; int count; @Override public AbstractStringBuilder append(CharSequence s, int start, int end) { if (s == null) s = "null"; if ((start < 0) || (start > end) || (end > s.length())) throw new IndexOutOfBoundsException( "start " + start + ", end " + end + ", s.length() " + s.length()); int len = end - start; ensureCapacityInternal(count + len); for (int i = start, j = count; i < end; i++, j++) value[j] = s.charAt(i); count += len; return this; } // 忽略其他信息... }
而 StringBuilder 使用了父类提供的 char[]
作为自己值的实际存储单元,每次在拼加时会修改 char[]
数组,StringBuilder toString()
源码如下:
@Override public String toString() { // Create a copy, don't share the array return new String(value, 0, count); }
综合以上源码可以看出:StringBuilder 使用了 char[]
作为实际存储单元,每次在拼加时只需要修改 char[]
数组即可,只是在 toString()
时创建了一个字符串;而 String 一旦创建之后就不能被修改,因此在每次拼加时,都需要重新创建新的字符串,所以 StringBuilder.append() 的性能就会比字符串的 += 性能高很多。