浅谈Java中字符串的初始化及字符串操作类(下)二

简介: 大家好,我是本周的值班编辑 江南一点雨 ,本周将由我为大家排版并送出技术干货,大家可以在公众号后台回复“springboot”,获取最新版 Spring Boot2.1.6 视频教程试看。 在深入学习字符串类之前, 我们先搞懂JVM是怎样处理新生字符串的. 当你知道字符串的初始化细节后, 再去写 Strings="hello"或 Strings=newString("hello")等代码时, 就能做到心中有数。

详解字符串操作类

明白了字符串常量池, 我相信关于字符串的创建你已经有十足的把握了. 但是这还不够, 作为一名合格的Java工程师, 我们还必须对字符串的操作做到了如指掌. 注意! 不是说你不用查api能熟练操作字符串就了如指掌了, 而是说对String, StringBuilder, StringBuffer三大字符串操作类背后的实现了然于胸, 这样才能在开发的过程中做出正确, 高效的选择。

String, StringBuilder, StringBuffer的底层实现

点进String的源码, 我们可以看见String类是通过char类型数组实现的。

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

接着查看StringBuilder和StringBuffer的源码, 我们发现这两者都继承自AbstractStringBuilder类, 通过查看该类的源码, 得知StringBuilder和StringBuffer两个类也是通过char类型数组实现的。

abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
...
}

而且通过StringBuilder和StringBuffer继承自同一个父类这点, 我们可以推断出它俩的方法都是差不多的. 通过查看源码也发现确实如此, 只不过StringBuffer在方法上添加了 synchronized关键字, 证明它的方法绝大多数方法都是线程同步方法. 也就是说在多线程的环境下我们应该使用StringBuffer以保证线程安全, 在单线程环境下我们应使用StringBuilder以获得更高的效率。

既然如此, 我们的比较也就落到了StringBuilder和String身上了。

关于StringBuilder和String之间的讨论

通过查看StringBuilder和String的源码我们会发现两者之间一个关键的区别: 对于String, 凡是涉及到返回参数类型为String类型的方法, 在返回的时候都会通过new关键字创建一个新的字符串对象; 而对于StringBuilder, 大多数方法都会返回StringBuilder对象自身。

/**
* 下面截取几个String类的方法
*/
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
/**
* 下面截取几个StringBuilder类的方法
*/
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
@Override
public StringBuilder replace(int start, int end, String str) {
super.replace(start, end, str);
return this;
}

就因为这点区别, 使得两者在操作字符串时在不同的场景下会体现出不同的效率。

下面还是以拼接字符串为例比较一下两者的性能:

public class Main {
    public static int time = 50000;
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        String s = "";
        for(int i = 0; i < time; i++){
            s += "test";
        }
        long end = System.currentTimeMillis();
        System.out.println("String类使用时间: " + (end - start) + "毫秒");
    }
}
//String类使用时间: 4781毫秒
public class Main {
    public static int time = 50000;
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for(int i = 0; i < time; i++){
            sb.append("test");
        }
        long end = System.currentTimeMillis();
        System.out.println("StringBuilder类使用时间: " + (end - start) + "毫秒");
    }
}
//StringBuilder类使用时间: 5毫秒

就拼接5万次字符串而言, StringBuilder的效率是String类的956倍。

我们再次通过反编译代码看看造成两者性能差距的原因, 先看String类. (为了方便阅读代码, 我删除了计时部分的代码, 并重新编译, 得到的main方法反编译代码如下)

public static void main(java.lang.String[]);
Code:
0: ldc #2 // String, 将""空字符串加载到栈顶
2: astore_1 //存放到s变量中
3: iconst_0 //把int型数0压栈
4: istore_2 //存到变量i中
5: iload_2 //把i的值压到栈顶(0)
6: getstatic #3 // Field time:I 拿到静态变量time的值, 压到栈顶
9: if_icmpge 38 // 比较栈顶两个int值, for循环中的判定, 如果i比time小就继续执行, 否则跳转
//从这里开始, 就是for循环部分
12: new #4 // class java/lang/StringBuilder
15: dup
16: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
19: aload_1
20: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: ldc #7 // String test
25: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
31: astore_1 //每拼接完一次, 就把新的字符串对象引用保存在第二个本地变量中
//到这里一次for循环结束
32: iinc 2, 1 //变量i加1
35: goto 5 //继续循环
38: return

从反汇编代码中可以看到, 当用String类拼接字符串时, 每次都会生成一个StringBuilder对象, 然后调用两次append()方法把字符串拼接好, 最后通过StringBuilder的toString()方法new出一个新的字符串对象。

也就是说每次拼接都会new出两个对象, 并进行两次方法调用, 如果拼接的次数过多, 创建对象所带来的时延会降低系统效率, 同时会造成巨大的内存浪费. 而且当内存不够用时, 虚拟机会进行垃圾回收, 这也是一项相当耗时的操作, 会大大降低系统性能。

下面是使用StringBuilder拼接字符串得到的反编译代码:

public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: astore_1
8: iconst_0
9: istore_2
10: iload_2
11: getstatic #4 // Field time:I
14: if_icmpge 30
//从这里开始执行for循环内的代码
17: aload_1
18: ldc #5 // String test
20: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: pop
//到这里一次for循环结束 
24: iinc 2, 1
27: goto 10
30: return

可以看到StringBuilder拼接字符串就简单多了, 直接把要拼接的字符串放到栈顶进行append就完事了, 除了开始时创建了StringBuilder对象, 运行时期没有创建过其他任何对象, 每次循环只调用一次append方法. 所以从效率上看, 拼接大量字符串时, StringBuilder要比String类给力得多。

当然String类也不是没有优势的, 从操作字符串api的丰富度上来讲, String是要多于StringBuilder的, 在日常操作中很多业务都需要用到String类的api。

在拼接字符串时, 如果是简单的拼接, 比如说 Strings="hello "+"world";, String类的效率会更高一点。

但如果需要拼接大量字符串, StringBuilder无疑是更合适的选择。

讲到这里, Java中的字符串背后的原理就讲得差不多, 相信在了解虚拟机操作字符串的细节后, 你在使用字符串时会更加得心应手. 字符串是编程中一个重要的话题, 本文围绕Java体系讲解的字符串知识只是字符串知识的冰山一角. 字符串操作的背后是数据结构和算法的应用, 如何能够以尽可能低的时间复杂度去操作字符串, 又是一门大学问。

相关文章
|
1天前
|
Java
类信息的“隐形守护者”:JAVA反射技术全揭秘
【7月更文挑战第1天】Java反射技术是动态获取类信息并操作对象的强大工具。它基于Class对象,允许在运行时创建对象、调用方法和改变字段。例如,通过`Class.forName()`动态实例化对象,`getMethod()`调用方法。然而,反射可能破坏封装,影响性能,并需处理异常,故使用时需谨慎。它是Java灵活性的关键,常见于框架设计中。
6 0
|
1天前
|
安全 Java
解决Java中集合类的内存占用问题
解决Java中集合类的内存占用问题
|
1天前
|
Java 容器
Java中使用Optional类的建议
Java中使用Optional类的建议
|
1天前
|
存储 安全 Java
Java详解 : 单列集合 | 双列集合 | Collections类
Java详解 : 单列集合 | 双列集合 | Collections类
|
1天前
|
Java
Java中的Object类 ( 详解toString方法 | equals方法 )
Java中的Object类 ( 详解toString方法 | equals方法 )
|
1天前
|
安全 Java 索引
带你快速掌握Java中的String类和StringBuffer类(详解常用方法 | 区别 )
带你快速掌握Java中的String类和StringBuffer类(详解常用方法 | 区别 )
|
1天前
|
Java 索引
Java中的Arrays类
Java中的Arrays类
|
1天前
|
Java
Java面向对象 ( 类与对象 | 构造方法 | 方法的重载 )
Java面向对象 ( 类与对象 | 构造方法 | 方法的重载 )
|
1天前
|
存储 Java 容器
Java数组的初始化方法
Java数组的初始化方法
|
1天前
|
Java
Calendar类在Java中的高级应用与使用技巧
Calendar类在Java中的高级应用与使用技巧