Java的字符串操作一些简单的思考

时间:2021-04-10 07:06:39

Java的字符串操作

1 .1不可变的String

String对象事不可变的,String类中的每一个看起来会修改String值的方法,实际上都是创建了一个全新的String对象,以包含修改后的字符串内容。而最初修改的String对象则丝毫未动。

public class text {
public static String upcase(String s){
return s.toUpperCase();
}
public static void main(String[] args) {
String s = "Hello";
System.out.println(s);
String ss = upcase(s);
System.out.println(ss);
System.out.println(s);
}
}
/*
运行输出:
Hello
HELLO
Hello
*/

当s传给upcase()方法时,实际传递的是引用的一个拷贝。其实,每当把String对象作为方法的参数时,都会复制一份引用,而该引用的所指的对象其实一直待在单一的物理位置上,从未动过。因此,字符串操作不改变原字符串内容,而是返回新字符串。

早期JDK版本的String总是以char[]存储,它的定义如下:

public final class String {
private final char[] value;
private final int offset;
private final int count;
}

而较新的JDK版本的String则以byte[]存储:如果String仅包含ASCII字符,则每个byte存储一个字符,否则,每两个byte存储一个字符,这样做的目的是为了节省内存,因为大量的长度较短的String通常仅包含ASCII字符:

public final class String {
private final byte[] value;
private final byte coder; // 0 = LATIN1, 1 = UTF16

1.2提升效率的StringBuilder

Java工程师规定了一样好东西,为String对象重载的“+”操作符,使得我们可以直接用“+”拼接字符串,但随之而来也给String带来了一定的效率问题。而为了高效拼接字符串,Java提供了StringBuilder,它是一个可变对象,可以预分配缓冲区,往StringBuilder中新增字符时,不会创建临时对象。

参考以下用操作符“+”连接String:

public class text {
public static void main(String[] args) {
String hello = "Hello";
String s = "abc" + hello + "DEF";
System.out.println(s);
}
}
/*
运行输出:
abcHelloDEF
*/

之后反编译以上代码

public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String Hello
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: ldc #5 // String abc
12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_1
16: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: ldc #7 // String DEF
21: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: bipush 100
26: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
29: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
32: astore_2
33: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
36: aload_2
37: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
40: return
}

基于JDK8版本去反编译(JDK9版本反编译结果非常简单,据说是为了更加统一字符串的优化,提供了StringConcatFactory,作为统一入口,虽然更简单但不利于我理解,争取之后能更加看懂吧!)中可以看出,编译器创建了一个StringBuilder对象,用以构造最终的String,并为每个字符调用一个StringBuilder的append()方法,总计4次,最后调用toString()生成结果,并存为s。虽然我们在源代码中并没用使用StringBuilder类,但是编译器却自作主张地使用了它,因为他更加高效。

虽然有点马后炮,但是不妨想一想编译器要是不为我们自动优化会产生什么结果:String可能有一个append()方法,它会生成一个新的String对象,以包含“abc”与hello连接后的字符串。然后该对象再与“DEF”相连,生成一个新的String对象。假如之后还有需要相连的,便以此类推。而正因String类对象的不可变性,造成字符串连接需要多个中间对象,造成程序执行时的内存浪费并且需要处理垃圾回收,增加工作量。

1.3 线程安全StringBuffer

查阅一些资料,发现最初是先有StringBuffer的,后来的版本增加了StringBuilder,而这两者为了实现修改字符序列的目的,StringBuffer 和 StringBuilder 底层都是利用可修改的(char,JDK 9 以后是 byte)数组,二者都继承了 AbstractStringBuilder,里面包含了基本操作,最大区别在于最终的方法是否加了 synchronized。

  • 线程安全:

    StringBuffer:线程安全,StringBuilder:线程不安全。因为 StringBuffer 的所有公开方法都是 synchronized 修饰的,而 StringBuilder 并没有 StringBuilder 修饰。

  • 缓冲区:

    StringBuffer 每次获取 toString 都会直接使用缓存区的 toStringCache 值来构造一个字符串。而 StringBuilder 则每次都需要复制一次字符数组,再构造一个字符串。

  • 性能:

    StringBuffer 是线程安全的,它的所有公开方法都是同步的,StringBuilder 是没有对方法加锁同步的,StringBuilder 的性能要远大于 StringBuffer。

1.4简单的程序测试检测效率

/*
实现的东西很简单,就是使用三种不同的方式来拼接字符串,以此来观察效率和内存消耗
*/
public class WitherStringBuilder {
public String implicit(String[] fields){//编译器隐式创建StringBuilder
String result = "";
for (int i = 0; i < fields.length; i++){
result += fields[i];
}
return result;
}

public String explicitStringBuilder(String[] fields){
//直接用StringBuilder的append()方法拼接
StringBuilder result = new StringBuilder();
for (int i = 0; i < fields.length; i++) {
result.append(fields[i]);
}
return result.toString();
}

public String explicitStringBuffer(String[] fields){
//直接用StringBuffer的append()方法拼接
StringBuffer result = new StringBuffer();
for (int i = 0; i < fields.length; i++) {
result.append(fields[i]);
}
return result.toString();
}
}

我们采用这么一段代码,其实也就是对字符串的拼接分别采用3种不同的方式,与此同时查看各自反编译后运行的逻辑比对与实际运行时的效率比对。程序入口采用了Java内置的一些查看内存消耗和程序运行时间的方法,并在控制台输出方便我们查看。关键代码如下:

public class Main {
public static void main(String[] args) {
String[] fields = new String[10000];//这里设置需要监测的字符串数组大小
String result;
for (int i = 0; i < fields.length; i++) {
fields[i] = "abc";
}

Examination.start();//监测程序运行的时间和内存消耗,具体代码不再这累赘
//减少工作量,每次只运行以下其中一条,分别对应
//1.用String拼接字符串
String s = new WitherStringBuilder().implicit(fields);
//2.用StringBuilder拼接字符串
String s = new WitherStringBuilder().explicitStringBuilder(fields);
//3.用StringBuffer拼接字符串
String s = new WitherStringBuilder().explicitStringBuffer(fields);
Examination.end();
}

在10000个单元数量级的字符串数组进行拼接的情况下,出现以下结果:

  • 采用String拼接:

    ---------------您的代码执行时间为:300.10 ms, 消耗内存:138.11 M

  • 采用StringBuilder拼接:

    ---------------您的代码执行时间为:1.02 ms, 消耗内存:0 M

  • 采用StringBuffer拼接:

    ---------------您的代码执行时间为:2.07 ms, 消耗内存:0.64 M

在100000个单元数量级的字符串数组拼接的情况下,出现以下结果:

  • 采用String拼接:

    ---------------您的代码执行时间为:16943.61 ms, 消耗内存:250.40 M

  • 采用StringBuilder拼接:

    ---------------您的代码执行时间为:7.42 ms, 消耗内存:2.68 M

  • 采用StringBuffer拼接:

    ---------------您的代码执行时间为:7.40 ms, 消耗内存:2.68 M

结果分析:以上简单的两组结果,用到的数量级从一万到十万,不难看出String用于拼接字符串简直既消耗时间,又消耗内存!(属实慢!!)StringBuilder和StringBuffer相差无几,但是当数量级上去,涉及线程安全的StringBuffer还是会略显弟弟。

接下来我尝试反编译去查看它的处理逻辑,分析为何他们差别为何会如此之大。

  • implicit方法:

    public java.lang.String implicit(java.lang.String[]);
    Code:
    0: ldc #2 // String
    2: astore_2
    3: iconst_0
    4: istore_3
    5: iload_3
    6: aload_1
    7: arraylength
    8: if_icmpge 38
    11: new #3 // class java/lang/StringBuilder
    14: dup
    15: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
    18: aload_2
    19: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    22: aload_1
    23: iload_3
    24: aaload
    25: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    28: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    31: astore_2
    32: iinc 3, 1
    35: goto 5
    38: aload_2
    39: areturn

    从第8行到第35行构成了一个循环体,重点是在这个循环体里,每循环一次就要创建一个StringBuilder对象。这应该可以解释,采用String直接用“+”拼接字符串,会产生过多冗余的中间对象,浪费很多内存,导致执行相同结果的代码却有天差地别的效率体现!

  • explicitStringBuilder方法:

    public java.lang.String explicitStringBuilder(java.lang.String[]);
    Code:
    0: new #3 // class java/lang/StringBuilder
    3: dup
    4: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
    7: astore_2
    8: iconst_0
    9: istore_3
    10: iload_3
    11: aload_1
    12: arraylength
    13: if_icmpge 30
    16: aload_2
    17: aload_1
    18: iload_3
    19: aaload
    20: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    23: pop
    24: iinc 3, 1
    27: goto 10
    30: aload_2
    31: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    34: areturn

    显式地创建StringBuilder意味着源代码部分我就告诉编译器用StringBuilder,循环过程中可以看出来只生成了一个StringBuilder对象,操作效率高,内存浪费也少!

  • explicitStringBuffer方法:其实同上面StringBuffer,因为还没涉及到线程安全问题暂且不累赘分析