一、简介

String 类型可能是 Java 中应用最频繁的引用类型,但它的性能问题却常常被忽略。高效的使用字符串,可以提升系统的整体性能。当然,要做到高效使用字符串,需要深入了解其特性。

互联网基本上只干一件事:处理字符串,可以处理好字符串是 Web 服务器的基本要求。他的重要性不言而喻。值得注意的是:String 创建的字符串存储在公共池中,而 new 创建的字符串对象在堆上。

二、String 的不可变性

首先我们来看 String 的开始部分源码,即定义:

1
2
3
4
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

String 类被 final 字段修饰,说明它是不可继承的类

String 类的实体数据存储在 char[] 数组中, 且被 final 修饰,说明 String 对象不可更改

为什么要进行这样的设计?

  1. 保证 String 对象的安全性。 防止 String 被篡改,所以具有天生的线程安全性。
  2. 保证 hash 值不会频繁变更。 整个 java 生命周期中同意个对象的多次计算哈希值都是相同的。
  3. 可以实现字符串常量池。 通常有两种创建字符串对象的方式,一种是通过字符串常量的方式创建,String str = "abc"; 另一种是字符串变量通过 new 形式的创建,如 String str = new String("abc");

使用第一种方式创建字符串对象时,JVM 首先会检查该对象是否在字符串常量池中,如果在,就返回该对象引用,否则新的字符串将在常量池中被创建。这种方式可以减少同一个值的字符串对象的重复创建,节约内存。

而使用第二种方式创建时,首先在变异文件时,abc 常量字符串将会放到常量结构中,在类加载的时候,abc 将会在常量池(方法区中的运行时常量池)中创建;其次,在调用 new 时,JVM 命令将会调用 String 的构造函数,同时引用常量池中的 abc 字符串,在堆中创建一个 String 对象;最后 str 将会引用 String 对象。

永远注意一点:String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象

具体如下图所示:

三、String 的性能考量

3.1 字符串拼接

字符串常量的拼接,编译器会将其优化为一个常量字符串。 【示例】字符串常量拼接

1
2
3
4
5
6
public static void main(String[] args) {
    // 本行代码在 class 文件中,会被编译器直接优化为:
    // String str = "abc";
    String str = "a" + "b" + "c";
    System.out.println("str = " + str);
}

字符串变量的拼接,编译器会优化成 StringBuilder 方式 【示例】 字符串变量的拼接

1
2
3
4
5
6
7
8
public static void main(String[] args) {
    String str = "";
    for(int i = 0; i < 1000; i++) {
        //本行代码会被编译器优化为:
        // str = (new StringBuilder(String.valueOf(str)).append(i).toString();
        str = str + i;
    }
}

但是,每次循环都会生成一个新的 StringBuilder 实例,同样也会降低系统的性能。

所以,字符串拼接的正确方案:

  • 如果需要用字符串拼接,应该优先考虑 StringBuilderappend 方法替代 + 号
  • 如果在并发编程中,String 对象的拼接涉及到线程安全,可以使用 StringBuffer 。 但是要注意,由于 StringBuffer 是线程安全的,涉及到锁竞争,所以从性能上来说,要比 StringBuilder 差一些。

3.2 字符串分割

Stringsplit() 方法使用正则表达式实现其强大的分割功能。 而正则表达式的性能非常不稳定,使用不恰当会引起回溯问题,很可能导致 CPU 居高不下。

所以,慎重使用 split() 方法,可以考虑用 String.indexOf() 代替 split() 方法完成字符串的分割。如果实在无法满足需求,就在使用 split() 方法时,对回溯问题加以重视即可。

回溯:涉及到正则表达式引擎:DFA 自动机-确定有限状态自动机 和 NFA 自动机-非确定有限状态自动机;而后者在匹配过程中存在大量的分支回溯,算法复杂度相对前者高, java 的正则表达式则是采用 NFA 引擎,它是一种基于贪婪模式尽可能多匹配。避免回溯的方法就是:使用懒惰模式和独占模式。 具体说明参考如下

3.3 String.intern()

在每次赋值的时候使用 String 的 intern 方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用,这样一开始的对象就可以被回收掉。

在字符串常量中,默认会将对象放入常量池;在字符串变量中,对象是会创建在堆内存中,同时也会在常量池中创建一个字符串对象,复制到堆内存对象中,并返回堆内存对象引用。

如果调用 intern 方法,会去查看字符串常量池中是否有等于该对象的字符串,如果没有,就在常量池中新增该对象,并返回该对象引用;如果有,就返回常量池中的字符串引用。堆内存中原有的对象由于没有引用指向它,将会通过垃圾回收器回收。

【示例】

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//该行为 intern() 方法的源码
public native String intern();

public class SharedLocation{
    private String city;
    private String region;
    private String countryCode;
}

SharedLoaction sharedLocation = new SharedLocation();
sharedLocation.setCity(messageInfo.getCity().intern());
sharedLocation.setRegion(messageInfo.getRegion().intern());
sharedLocation.setCountryCode(messageInfo.getCountryCode().intern());

使用 intern() 方法要注意:一定要结合实际场景。因为常量池的实现是类似于一个 HashTable 的实现方式,HashTable 存储的数据越多,遍历的时间就会越长。如果数据过大,会增加整个字符串常量池的负担。

三、 String、StringBuffer、StringBuilder 的区别

String 是 Java 语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的 Immutable 类,被声明成 final class,所有属性,方法也都是 final 的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。

StringBuffer 是为了解决上面提到拼接产生太多中间对象的问题而提供的一个类,我们可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者制定位置。StringBuffer 是一个线程安全的可修改字符序列。StringBuffer 的线程安全是通过修改数据的方法上使用 Synchronized 关键字修饰实现的。

StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。

StringBufferStringBuilder 底层都是利用可修改的(char,JDK 9 以后是 byte)数组,二者都继承了 AbstractStringBuilder,里面包含了基本操作,区别仅在于最终的方法是否加了 synchronized。构建时初始字符串长度加 16(这意味着,如果没有构建对象时输入最初的字符串,那么初始值就是 16)。我们如果确定拼接会发生非常多次,而且大概是可预计的,那么就可以指定合适的大小,避免很多次扩容的开销。扩容会产生多重开销,因为要抛弃原有数组,创建新的(可以简单认为是倍数)数组,还要进行 arraycopy

除非有线程安全的需要,不然一般都使用 StringBuilder。

四、String 的一些常用方法

4.1 构造器

方法名 方法说明
public String() 无参构造函数,创建 ”“ 的字符串
public String(String original) 用已知的字符串value创建一个String对象
public String(char value[]) 用字符数组value创建一个String对象。
public String(byte value[]) 用比特数组values创建一个String对象。
public String(char value[], int offset, int count) 用字符数组 char 的 offset 开始的 count 个字符创建一个 String 对象。

4.2 常用方法

  1. 求字符串某一位置字符(charAt)

public char charAt(int index): 获取下标为 index 的字符。下标超出范围则抛出异常。

  1. 求字符串长度(length)

public int length(): 得到字符串长度。

  1. 提取子串(subString)
  • public String substring(int beginIndex): 从beginIndex位置起,从当前字符串中取出剩余的字符作为一个新的字符串返回。

  • public String subString(int beginIndex, int endIndex): 取 [beginIndex, endIndex) 范围的字串

  1. 字符串比较(compareTo)
  • public int compareTo(String anotherString): 该方法是对字符串内容按字典顺序进行大小比较,通过返回的整数值指明当前字符串与参数字符串的大小关系。若当前对象比参数大则返回正整数,反之返回负整数,相等返回 0。

  • public int compareToIgnoreCase(String str): 与 compareTo 方法类似,但忽略大小写。

  • public boolean equals(Object anotherObject): 比较当前字符串和参数字符串内容,在两个字符串相等的时候返回true,否则返回false。

  • public boolean contentEquals(CharSequence cs): 通常参数为 StringBuilder 与 StringBuffer,比较此 String 与 其内容是否相同。

  1. 字符串连接(concat)
  • public String concat(String str): 将参数中的字符串str连接到当前字符串的后面,效果等价于"+"。
  1. 字符串中单个字符查找(indexOf)
  • public int indexOf(int ch / String str): 用于查找当前字符串中字符或子串,返回字符或子串在当前字符串中从左边起首次出现的位置,若没有出现则返回-1。

  • public int indexOf(int ch / String str, int fromIndex): 方法与第一种类似,区别在于该方法从 fromIndex 位置(包括)向后查找。

  • public int lastIndexOf(int ch/ String str): 该方法与第一种类似,区别在于该方法从字符串的末尾位置向前查找。等同于lastIndexOf(ch, value.length - 1);

  • public int lastIndexOf(int ch, int fromIndex): 该方法与第二种方法类似,区别于该方法从 fromIndex 位置(包括)向前查找。

  1. 字符串中字符的大小写转换(toLowerCase)
  • public String toLowerCase(): 返回将当前字符串中所有字符转换成小写后的新串

  • public String toUpperCase(): 返回将当前字符串中所有字符转换成大写后的新串

  1. 字符串中字符的替换(replace)
  • public String replace(char oldChar, char newChar): 用字符 newChar 替换当前字符串中所有的 oldChar 字符,并返回一个新的字符串。

  • public String replace(CharSequence target, CharSequence replacement): 等同于 Pattern.compile(target.toString(), Pattern.LITERAL).matcher(this).replaceAll(Matcher.quoteReplacement(replacement.toString())); 该方法将第一个匹配到的 target 内容替换为 replacement 内容。

  • public String replaceFirst(String regex, String replacement): 等同于 Pattern.compile(regex).matcher(this).replaceFirst(replacement) 该方法用字符 replacement 的内容替换当前字符串中遇到的第一个和字符串 regex 相匹配的子串,应将新的字符串返回。

  • public String replaceAll(String regex, String replacement): 等同于 Pattern.compile(regex).matcher(this).replaceAll(replacement); 该方法用字符replacement的内容替换当前字符串中遇到的所有和字符串regex相匹配的子串,应将新的字符串返回。

  1. 其他类方法
  • String trim(): 截去字符串两端的空格,但对于中间的空格不处理。

  • boolean startsWith(String prefix)或boolean endWith(String suffix): 用于判断起始或终止字符串是否与当前字符串相同。

  • public boolean startsWith(String prefix, int toffset): 上面方法的实现。从第 toffset 开始的字串是否与源字符串匹配。

  • regionMatches(boolean b, int firstStart, String other, int otherStart, int length): 从当前字符串的firstStart位置开始比较,取长度为length的一个子字符串,other字符串从otherStart位置开始,指定另外一个长度为length的字符串,两字符串比较,当b为true时字符串不区分大小写。

  • public String[] split(String regex, int limit): 以正则表达式 regex 为分隔符分割字符串,limit 参数控制匹配的次数,当 limit > 0,表示分割得到的字符串数组最多含有 limit - 1 个元素;当 limit == 0,说明匹配最多次数且结尾的空字符串会舍去;当 limit < 0,表示尽可能多地匹配字符串分割。

4.3 与基本类型的转换

  1. 字符串转换为基本类型

java.lang 包中有 Byte、Short、Integer、Float、Double类的调用方法:

  • public static byte parseByte(String s)

  • public static short parseShort(String s)

  • public static short parseInt(String s)

  • public static long parseLong(String s)

  • public static float parseFloat(String s)

  • public static double parseDouble(String s)

  1. 基本类型转换为字符串类型

String类中提供了String valueOf()放法,用作基本类型转换为字符串类型。

  • static String valueOf(char data[])

  • static String valueOf(char data[])

  • static String valueOf(boolean/char/int/long/double b)

  1. 进制转换

使用Long类中的方法得到整数之间的各种进制转换的方法:

  • Long.toBinaryString(long l)

  • Long.toOctalString(long l)

  • Long.toHexString(long l)

  • Long.toString(long l, int p)//p作为任意进制