揭秘:Java字符串对象的内存分布原理(二)

接上篇揭秘:Java字符串对象的内存分布原理

再看看下面几道关于String的真实面试题,看看你废不废?

在这里插入图片描述

String str1 = "Hello";
String str2 = "He" + "llo";
String str3 = "He";
String str4 = "llo";
String str5 = str3 + str4;
String str6 = str3 + "llo";


System.out.println(str1 == str2); // true or false?
System.out.println(str1.equals(str5)); // true or false?
System.out.println(str1 == str5); // true or false?
System.out.println(str1.equals(str5)); // true or false?
System.out.println(str1 == str6); // true or false?

public class StringExercise07 {
    public static void main(String[] args) {
        Test1 ex = new Test1();
        ex.change(ex.str, ex.ch);
        // 输出结果是?
        System.out.println(ex.str + " and ");
        System.out.println(ex.ch);
    }
}

class Test1 {
    String str = new String("ly");
    final char[] ch = {'j', 'a', 'v', 'a'};

    public void change(String str, char ch[]) {
        str = "java";
        ch[0] = 'h';
    }
}

④比较下面两种字符串拼接的差异

// 第一种拼接:使用+操作符
String strPlus = "";
for (int i = 0; i < 1000; i++) {
    strPlus += "Hello, World!";
}

// 第二种拼接:使用StringBuilder
StringBuilder strBuilder = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    strBuilder.append("Hello, World!");
}
String result = strBuilder.toString();

如果要正确回答上面几个面试题,不深入理解String类型的内存原理是不行的,本篇的唯一目的就是言简意赅、深入浅出的梳理String类型的底层原理。

一,字符串拼接

下面这道面试题考察的是字符串拼接的底层原理,字符串拼接可以分为两种:

  • ①字面量拼接,如String str2 = "He" + "llo";,拼接的所有部分都是字面量,对于这种拼接,Java在编译时就会优化,将其合并为一个字符串:
// 源码:
String str2 = "He" + "llo";

// 编译优化后:
String str2 = "Hello";
  • ②变量拼接,String str5 = str3 + str4;String str6 = str3 + "llo";,只要有一个变量参与,就是变量拼接。对于这种拼接,JVM底层会调用StringBuilderappend方法进行拼接,拼接完成后调用toString方法,toString方法中使用构造函数new String()创建新的字符串对象

小结:

  • 纯字面量拼接相当于用字面量声明一个字符串对象,字符串对象会保存到字符串常量池
  • 变量拼接的相当于通过构造函数声明一个字符串对象,不会存入字符串常量池

所以,对于如下面试题:

String str1 = "Hello";
String str2 = "He" + "llo";
String str3 = "He";
String str4 = "llo";
String str5 = str3 + str4;
String str6 = str3 + "llo";


System.out.println(str1 == str2); // true or false?
System.out.println(str1.equals(str5)); // true or false?
System.out.println(str1 == str5); // true or false?
System.out.println(str1.equals(str5)); // true or false?
System.out.println(str1 == str6); // true or false?
字面量拼接:
String str1 = "Hello";
String str2 = "He" + "llo";
System.out.println(str1 == str2); // true or false?
System.out.println(str1.equals(str5)); // true or false?

因为编译器的优化,代码String str2 = "He" + "llo";相当于String str2 = "Hello";,类似下图,在字符串常量池的作用下,变量str1str2会指向同一个字符串对象:

在这里插入图片描述
所以其输出结果是:

在这里插入图片描述

变量拼接:
String str1 = "Hello";
String str2 = "He" + "llo";
String str3 = "He";
String str4 = "llo";
String str5 = str3 + str4;
String str6 = str3 + "llo";


System.out.println(str1 == str5); // true or false?
System.out.println(str1.equals(str5)); // true or false?
System.out.println(str1 == str6); // true or false?

变量str5str6是通过字符串变量拼接得到,底层是通过字符串构造函数创建的,所以不会通过常量池复用已经创建的对象,所以str1str5两个变量指向不同地址的对象,用运算符==比较的结果是false;但两个对象存储的字符串内容是相同的,用equals比较的结果是true

在这里插入图片描述

二,为什么要使用StringBuilder进行字符串拼接

我们运行下面的代码,统计一下两个循环的耗时:

④比较下面两种字符串拼接的差异

long s = System.currentTimeMillis();
        String strPlus = "";
        for (int i = 0; i < 100000; i++) {
            strPlus += "Hello, World!";
        }
        System.out.println("字符串变量拼接耗时:" + (System.currentTimeMillis() - s));

        s = System.currentTimeMillis();
        // 第二种拼接:使用StringBuilder
        StringBuilder strBuilder = new StringBuilder();
        for (int i = 0; i < 100000; i++) {
            strBuilder.append("Hello, World!");
        }
        String result = strBuilder.toString();
        System.out.println("StringBuilder拼接耗时:" + (System.currentTimeMillis() - s));

发现第一种拼接方式耗时超过7秒,但是第二种拼接方式耗时不到3毫秒,差距惊人。
在这里插入图片描述
原因在于:

  • ①使用+号进行字符串变量循环拼接时,每次拼接都会创建一个StringBulider对象,并调用字符串构造函数创建一个新的字符串对象,循环次数很大时,创建大量对象耗时很长
  • ②使用StringBuilder拼接对象时,是把字符串通过append方法插入到StringBuilder类的一个字符数组中,数组的插入是非常快的,尽管循环次数多,但是效率非常高,耗时极短

最佳实践

  • ①如果存在大量的字符串拼接,使用StringBuilder是更高效的方式
  • ②如果拼接次数很少,比如少于10次,使用+进行拼接会使代码更简洁

所以,不必所有的字符串都用StringBuilder进行拼接。

三,扩展:就近原则

public class StringExercise07 {
    public static void main(String[] args) {
        Test1 ex = new Test1();
        ex.change(ex.str, ex.ch);
        // 输出结果是?
        System.out.println(ex.str + " and ");
        System.out.println(ex.ch);
    }
}

class Test1 {
    String str = new String("ly");
    final char[] ch = {'j', 'a', 'v', 'a'};

    public void change(String str, char ch[]) {
        str = "java";
        ch[0] = 'h';
    }
}

先看下运行结果:

在这里插入图片描述
对于初学者,很难理解这个输出,调用了Test1change方法为什么成员变量str的值没有改变,但是变量ch的值却改变了?

如果仔细看change方法的代码:

public void change(String str, char ch[]) {
        str = "java";
        ch[0] = 'h';
}

change方法的第一个参数和Test1的成员变量str同名,JVM在执行这个方法时,会优先从change方法栈帧的局部变量表中查找str变量,如果有,就不会使用Test1类的成员变量,所以Test1的成员变量str的值并没有被修改。

如果像下面这样修改change的代码:

```cpp
public void change(String str, char ch[]) {
        this.str = "java";
        ch[0] = 'h';
    }

因为有了this关键字的作用,JVM会查找成员变量str,然后修改其值,打印结果如下:

![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/028b9778c2d7422ebf5092f25a4b4f3b.png)



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小手追梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值