in java string StringBuilder StringBuffer intern ~ read.

Java中的字符串操作

本文基于JDK 1.8.0_45


在编程世界中使用最多的数据是数值和字符串,下面就让我们来聊聊Java中的字符串操作。

所谓字符串,就是一串有序的字符。在Java中字符串的数据类型分为了两种,可变字符串和不可变字符串。可变字符串包括StringBuilder和StringBuffer,不可变字符串是String。它们都实现了接口CharSequence。

CharSequence接口

在CharSequence中定义了字符序列操作的一系列方法,如下所示:

int length();// 长度  
char charAt(int index);// index位置的字符  
CharSequence subSequence(int start, int end);// start到end位置之间的子CharSequence  
String toString();// 包含CharSequence中字符的String  
default IntStream chars();// 默认方法,返回一个IntStream。Stream是在JDK1.8中才引入的feature  

StringBuilder和StringBuffer

StringBuilder和StringBuffer都继承自AbstractStringBuilder抽象类,并实现了CharSequence接口和序列化接口Serializable。AbstractStringBuilder实现了CharSequence接口和Appendable接口,因此StringBuilder和StringBuffer均可用来表示一个字符串,同时还可以在声明之后在末尾添加新的字符或字符串从而修改原有的字符串。在AbstractStringBuilder中实现了各种各样的append方法和insert方法,另外还包含了一些其他的工具方法,比如replace、lastIndexOf等。

在StringBuilder和StringBuffer的源码中可以看出,几乎所有的方法均为在它们的父类AbstractStringBuilder中实现的,在二者中仅为在父类的实现基础上添加了一些功能:StringBuilder是将返回类型重写为了自己,StringBuffer是将返回类型重写为了自己并将所有方法加上synchronized修饰符,因此我们可以看出StringBuilder和StringBuffer的区别为:

  1. 二者均继承自AbstractStringBuilder;
  2. 二者都是可变的;
  3. StringBuffer是线程安全的,而StringBuilder不是线程安全的;
  4. 在非多线程场景下,StringBuilder的效率高于StringBuffer;

其中由于StringBuffer的锁是通过synchronized来修饰实例方法的,因此锁是在当前实例上,这就意味着对它的所有操作均共享同一把锁,所有的操作都是线性顺序的。当存在多线程同时修改同一个CharSequence的时候要使用StringBuffer。

public final class StringBuffer... {  
    ...
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }
    ...
}

public final class StringBuilder... {  
    ...
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }
    ...
}

二者在初始化的时候默认是初始化一个长度为16的char的数组。如果构造方法传入的是另一个String或者另一个CharSequence时,长度为另一个的长度+16。类似于ArrayList的实现,当在添加新的char时如果内置数组长度不够会发生扩容,如下所示。

void expandCapacity(int minimumCapacity) {  
    int newCapacity = value.length * 2 + 2;// 默认新的数组长度为原长度的两倍
    if (newCapacity - minimumCapacity < 0)
        newCapacity = minimumCapacity;// 如果两倍老长度无法满足要求的话则为最小能满足的长度
    if (newCapacity < 0) {
        if (minimumCapacity < 0) // overflow
            throw new OutOfMemoryError();
        newCapacity = Integer.MAX_VALUE;// 最大长度为Integer.MAX_VALUE
    }
    value = Arrays.copyOf(value, newCapacity);// 新建长度为newCapacity的数组,并将原有数组的内容拷贝进来
}

String

String是常量,无法被修改,如果需要修改的话请使用StringBuffer或StringBuilder。

String中提供了众多的方法,如检查获取字符串中的某一个char、比较、搜索、生成子字符串、大小写转换等,请注意所有涉及到修改的均为返回一个新的string,不会真的修改到原有的string。以下是常用方法的例子:

    String abc = "abc";
    System.out.println(abc.charAt(1));// b
    System.out.println(abc.codePointAt(1));// 98

    System.out.println(abc.compareTo("abc"));// 0
    System.out.println(abc.compareTo("ab"));// 1
    System.out.println(abc.compareTo("abcd"));// -1

    System.out.println(abc.contains("a"));// true
    System.out.println(abc.contentEquals(new StringBuilder("abc")));// true

    System.out.println(abc.concat("def"));// abcdef

    System.out.println(abc.startsWith("a"));// true
    System.out.println(abc.indexOf("bc"));// 1
    System.out.println(abc.replace('a', 'd'));// dbc
    System.out.println(abc.split("b")[0] + ":" + abc.split("b")[1]);// a:c
    System.out.println(abc.substring(1));// bc
    System.out.println(abc.regionMatches(true, 1, "dbc", 1, 2));// true
    System.out.println(" abc ".trim());// abc

    System.out.println(abc.length());// 3
    System.out.println(abc.isEmpty());// false

    System.out.println(abc.toUpperCase());// ABC

    System.out.println(String.valueOf(123));// 123
    System.out.println(String.join("|", "abc", "123", "001"));// abc|123|001
    System.out.println(String.copyValueOf(new char[]{'a', 'b', 'c'}));// abc
    System.out.println(String.format("%s|%s", abc, 123));// abc|123

    IntStream intStream = abc.chars();
    OptionalInt optionalInt = intStream.findFirst();
    if (optionalInt.isPresent()) {
        char c = (char) optionalInt.getAsInt();
        System.out.println(c);// a
    }

JVM针对字符串的编译期优化

我们有以下代码:

1    public class StringExample {  
2        public static void main(String[] args) {  
3            String str1 = "a" + "b" + "c";  
4            String str2 = "c";  
5            String str3 = "ab" + str5;  
6            System.out.println(str1);// 一定要有,负责JVM可能会判断第3行为无效代码从而在字节码中剔除  
7            System.out.println(str2);  
8            System.out.println(str3);  
9        }  
10    }  

通过以下命令可以查看字节码,不同版本的JVM可能存在细微的不同:

➜  javap -verbose StringExample
        ...
         0: ldc           #16                 // String abc
         2: astore_1
         3: ldc           #18                 // String c
         5: astore_2
         6: new           #20                 // class java/lang/StringBuilder
         9: dup
        10: ldc           #22                 // String ab
        12: invokespecial #24                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
        15: aload_2
        16: invokevirtual #27                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: invokevirtual #31                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        22: astore_3
        ...

可见JVM针对代码做了以下优化,从而提升了程序运行速度:

  1. 针对第3行代码,JVM在编译期将三个直接声明的字符串连接在一起作为一个字符串,见字节码0-2;
  2. 针对第5行代码,JVM在编译期见字符串的+操作替换为了StringBuilder的append操作,最后通过toString方法获取连接后的string;

但是要小心的是在一些情况下JVM的优化不但不会提升程序运行速度,反而会带来更大的开销,比如以下代码:

1    public class StringExample {  
2        public static void main(String[] args) {  
3            String result = "";  
4            for (int i = 0; i < 10; i++) {  
5                result += "abc";  
6            }  
7            System.out.println(result);  
8        }  
9    }  

查看字节码:

➜  javap -verbose StringExample
    ...
         0: ldc           #16                 // String
         2: astore_1
         3: iconst_0
         4: istore_2
         5: goto          31
         8: new           #18                 // class java/lang/StringBuilder
        11: dup
        12: aload_1
        13: invokestatic  #20                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
        16: invokespecial #26                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
        19: ldc           #29                 // String abc
        21: invokevirtual #31                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        24: invokevirtual #35                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        27: astore_1
        28: iinc          2, 1
        31: iload_2
        32: bipush        10
        34: if_icmplt     8
        37: getstatic     #39                 // Field java/lang/System.out:Ljava/io/PrintStream;
        40: aload_1
        41: invokevirtual #45                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        44: return
    ...

其中3、4、5、28、31、32、34是for循环体生成的字节码,8-27是循环体内部的代码生成的字节码。从中可见在每一次循环的时候都会生成一个StringBuilder来处理第五行的字符串连接代码,这会带来很大的性能开销。使用以下代码可以进行优化:

public class StringExample {  
    public static void main(String[] args) {
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < 10; i++) {
            result.append("abc");
        }
        System.out.println(result);
    }
}

intern方法

String类管理这一个string的池,初始化状态是空的。当intern方法被调用时,如果池里已经包含了跟这个string相同内容的对象则返回该对象的引用,否则将这个string加入到池里并返回该对象的引用。这是因为在程序运行过程中大量的string对象会被创建和使用,该方式可以帮助我们更好的节省内存。可以通过JVM参数-XX:StringTableSize=N来调整该池的大小。

以下是intern方法的一个例子:

String str1 = new String("abc");  
String str2 = new String("abc");  
String str3 = "abc";  
String str4 = "a" + "b" + "c";  
String str5 = "c";  
String str6 = "ab" + str5;  
String str7 = "a".concat("b").concat("c");

System.out.println(str1.intern() == str2);// false  
System.out.println(str1.intern() == str3);// true  
System.out.println(str1.intern() == str4);// true  
System.out.println(str1.intern() == str6);// false  
System.out.println(str1.intern() == str2.intern());// true  
System.out.println(str1.intern() == str7);// false  

让我们分析以下结果:

  1. 由于str2是new出来的string,不会进入字符串池,因此二者不相等;
  2. 由于str3是直接声明的字符串,而根据上节可知直接声明的字符串在编译期会被优化为“abc”,所以会默认进入字符串池,因此二者相等;
  3. 直接声明的字符串相加会默认进入字符串池,因此二者相等;
  4. 如果相加的字符串中有变量的话编译期不会被优化,所以不会进入字符串池,因此二者不等;
  5. 由于二者均调用了intern方法,因此二者相等;
  6. 由于concat永远都是返回一个new String(...),因此根据1可知二者不相等;

但是要注意的是调用intern方法会一方面由于进行了更多的操作而耗费更多的时间,另一方面在代码中大量使用这个方法的话会带来代码管理的开销。因此建议在处理大量使用的有限字符串的时候使用该方法。

分享按钮