分析String、StringBuilder、StringBuffer的区别,内存划分以及性能测试

  • Post author:
  • Post category:其他

思维导图

一、String

1.1 创建String

String两种创建方式

String str = "zbx";
// zbx 存储在常量池中,常量池在方法区中
String str2 = new String("java");
// java 存储在堆中

1.2 多个字符串的内存分析

创建字符串

创建一个字符串

String name1 = "zbx";

引用存放在栈中,值存放在常量池中

new 一个字符串

String name2 = new String("bao");

引用存放在栈中,vaule存放在堆中,值存放在常量池中

分析

看String带参的源码

public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}

original是传入的值,而.value是一个字符数组

private final char value[];

String的引用在栈中,String的值在堆中

但是String的值是一个字符数组,所以堆中存放的是字符数组的引用,而字符数组的值存放在常量池中

如图
在这里插入图片描述

如果此时再使用直接赋值的方式创建一个字符串

String name3 = "bao";

先在常量池中进行查,发现此时常量池中已经有“bao”这个字符串了,所以直接指向已有的内存空间

内存图如下
在这里插入图片描述

如果使用new的方式来创建一个字符串

String name3 = new String("bao");

则先在常量池中查找,发现有“bao”,然后在堆中开辟一个内存空间,存放“bao”的地址引用,栈中存放new出来的引用

内存图如下
在这里插入图片描述

1.3 String常用方法

/**
 * @author 张宝旭
 */
public class StringWays {
    public static void main(String[] args) {
        String string = "this is my love";

        int length = string.length(); // 获取字符串的长度

        char c = string.charAt(0); // 获取单个字符

        int index = string.indexOf("is"); // 查找某个字符串首次出现的下标

        int index1 = string.lastIndexOf("i"); // 查找某个字符串最后一次出现的下标

        String replace = string.replace("my", "your"); // 替换字符串

        String substring = string.substring(0, 7); // 截取字符串

        boolean love = string.contains("love"); // 判断某段字符串是否包含在这整个字符串中

        String trim = string.trim();// 去掉首尾的空格

        char[] chars = string.toCharArray(); // 将字符串转换成字符数组

        String string1 = string.toLowerCase(); // 将字符串中的大写字符转换成小写字符

        String string2 = string.toUpperCase(); // 将字符串中的小写字符转换成大写字符

        boolean love1 = string.endsWith("love"); // 判断字符串是否以某个字符串结尾

        String[] s = string.split(" "); // 以某个字符串做分割,存到字符串数组中

    }
}

1.4 面试题

第一题

String s1 = "abc";
String s2 = "xyz";
String s3 = s1 + s2;
String s4 = "abc" + "xyz";
String s5 = "zbcxyz";
System.out.println(s3 == s4); // false
System.out.println(s4 == s5); // true

s1、s2都在常量池中,而s3=s1+s2,s3不会放在常量池中(只有字面常量才会放在常量池中),所以s3放在堆中,s4是两个字面常量拼接,所以会放在常量池中,内容为“abcxyz”,s5创建时,现在常量池中找到了相同的,所以两者引用相同

所以s3和s4的引用不同,s4和s5的引用相同

第二题

String s1 = "abc";
String s2 = "xyz";
String s3 = s1 + s2;
String s4 = s3.intern();
String s5 = "abcxyz";
System.out.println(s3 == s4); // true
System.out.println(s4 == s5); // true

使用intern方法如果常量池中没有,则把对象复制一份(或对象引用)放入常量池中, 返回常量池中的对象,如果常量池中存在,则直接返回。
JDK1.7之前是复制一份放入常量池,JDK1.7之后则把对象引用到常量池。

s3存放在堆中,而创建s4时发现常量池中没有,则把s3对象复制一份放入常量池中(此时常量池中存放s3在堆中创建的“abcxyz”的地址),然后返回常量池中的对象,所以s3和s4指向同一个地址引用,结果为true

当创建s5时,发现常量池中有“abcxyz”地址,所以s5指向常量池中的地址,然后此地址再指向堆中“abcxyz”,所以s4和s5都指向常量池中的地址,结果为true

二、StringBuilder

2.1 StringBuilder常用方法

String每一次改变都会创建一个新的对象,而StringBuilder的操作都只需创建一个对象即可

StringBuilder的效率远远高于String

StringBuilder的常用操作

/**
 * @author 张宝旭
 */
public class StringBuilderTest {
    public static void main(String[] args) {
        StringBuilder stringBuilder = new StringBuilder("my");
        stringBuilder.append(" love");// 拼接字符串
        stringBuilder.indexOf("l"); // 获取某段字符串首次出现的下标
        stringBuilder.lastIndexOf("y"); // 获取某段字符串最后一次出现的下标
        stringBuilder.insert(2, "is");// 在指定下标的位置插入字符串
        stringBuilder.replace(2, 4, "you"); // 在指定位置替换字符串
        stringBuilder.delete(2, 5); // 删除下标为2到5的字符串
        int length = stringBuilder.length(); // 获取字符串长度
        stringBuilder.reverse(); // 字符串反转
        System.out.println(stringBuilder);
    }
}

2.2 JVM优化

String的拼接操作,会自动被JVM优化为StringBuilder的操作

public class MyString {
    public static void main(String[] args) {
        String string = "java";
        string += " golang";
        string += " C++";
        System.out.println("I Love " +string);
    }
}

反编译查看JVM的优化

import java.io.PrintStream;

public class MyString
{

	public MyString()
	{
	}

	public static void main(String args[])
	{
		String string = "java";
		string = (new StringBuilder()).append(string).append(" golang").toString();
		string = (new StringBuilder()).append(string).append(" C++").toString();
		System.out.println((new StringBuilder()).append("I Love ").append(string).toString());
	}
}

以上看到String的拼接操作都被JVM优化了,为了提高性能

三、StringBuffer

3.1 StringBuffer常用方法

/**
 * @author 张宝旭
 */
public class StringBufferTest {
    public static void main(String[] args) {
        StringBuffer stringBuffer = new StringBuffer("name");
        stringBuffer.append(" is"); // 拼接字符串
        stringBuffer.charAt(0); // 获得某个字符
        stringBuffer.delete(4, 7); // 删除指定区间的字符串
        stringBuffer.deleteCharAt(1); // 删除指定下标的字符
        stringBuffer.indexOf("m"); // 获得指定元素的下标
        stringBuffer.insert(1, "a"); // 在指定下标插入元素
        stringBuffer.length(); // 获取字符串的长度
        stringBuffer.replace(1, 2, "VV"); // 替换指定区间的字符串
        String substring = stringBuffer.substring(1, 2);// 截取指定区间字符串,并返回到一个新的字符串中
        stringBuffer.reverse(); // 字符串反转
        System.out.println(stringBuffer);
        System.out.println(substring);
    }
}

3.2 线程安全的StringBuffer

StringBuffer和StringBuilder的方法都类似,只是在StringBuffer内部的方法中都添加了synchronized关键字

所以StringBuffer是线程安全的,StringBuilder不是线程安全的

但是StringBuffer的效率比StringBuilder的效率要低

比如如下对比

StringBuilder的append()方法

@Override
public StringBuilder append(String str) {
    super.append(str);
    return this;
}

StringBuffer的append()方法

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

两者在大致上时相同的,只是StringBuffer添加了synchronized关键字,更加安全,但是效率也更低

四、String、StringBuilder、StringBuffer性能测试

为了让结果更准确,不受常量池中已存在数据的影响,对三者分别做测试(每次运行只测试一种)

/**
 * @author 张宝旭
 */
public class StringPerformanceTest {
    public static final int COUNT = 10000;

    public static void main(String[] args) {
        stringTest();
//        stringBufferTest();
//        stringBuilderTest();
    }

    /**
     * 测试String拼接时间。
     */
    public static void stringTest() {
        long startTime = System.currentTimeMillis();
        String str = "name";
        for (int i = 0; i < COUNT; i++) {
            str += i;
        }
        long endTime = System.currentTimeMillis();
        System.out.println("String拼接时间: " + (endTime - startTime));
    }

    /**
     * 测试StringBuilder拼接时间。
     */
    public static void stringBuilderTest() {
        long startTime = System.currentTimeMillis();
        StringBuilder stringBuilder = new StringBuilder("name");
        for (int i = 0; i < COUNT; i++) {
            stringBuilder.append(i);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("StringBuilder拼接时间: " + (endTime - startTime));
    }

    /**
     * 测试StringBuffer拼接时间。
     */
    public static void stringBufferTest() {
        long startTime = System.currentTimeMillis();
        StringBuffer stringBUffer = new StringBuffer("name");
        for (int i = 0; i < COUNT; i++) {
            stringBUffer.append(i);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("StringBuffer拼接时间: " + (endTime - startTime));
    }
}

当测试次数为10000次时,结果

String拼接时间: 368

StringBuilder拼接时间: 1

StringBuffer拼接时间: 1

当测试次数为一百万次时,结果

String拼接时间: 很久,没有执行完

StringBuilder拼接时间: 46

StringBuffer拼接时间: 57

结论

由以上结果看到,String的拼接效率很低,而StringBuilder和StringBuffer的拼接效率却很快很快,但是由于StringBuffer是线程安全的,加了synchronized关键字,所以性能比StringBuilder略低

建议在不使用多线程的情况下,尽量使用StringBuilder来操作字符串


版权声明:本文为weixin_42193813原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。