String vs StringBuilder vs StringBuffer(底层实现)

String vs StringBuilder vs StringBuffer(底层实现)

一.搞懂字符串常量池细节

常量池是java的一项技术,8种基础数据类型除了float和double都实现了常量池技术。即,把经常用到的数据存放在某块内存中,避免频繁的数据创建与销毁。

字符串常量池是Java常量池技术的一种实现,在较新的JDK版本中,字符串常量池被实现在Java堆内存中。

对字符串常量池建立初步认识:

public static void main(String[] args){
	String s1 = "hello";
	String s2 = new String("hello");
	System.out.println(S1==s2); //false
}

第一行代码:
在这里插入图片描述
JVM首先会到字符串常量池中查找该字符串是否已经存在,如果存在会直接返回该引用,如果不存在则会在堆内存中创建该字符串对象,然后到字符串常量池中去注册该字符串。

在本案例中虚拟机首先会到字符串常量池中查找是否有存在"hello"字符串对应的引用. 发现没有后会在堆内存创建"hello"字符串对象(内存地址0x0001), 然后到字符串常量池中注册地址为0x0001的"hello"对象, 也就是添加指向0x0001的引用. 最后把字符串对象返回给s1。

第二行代码:
在这里插入图片描述
当我们使用new关键字创建字符串对象时,JVM不会查询字符串常量池,会直接在堆内存中创建一个字符串对象,并返回给所属变量。

难一点的:

public static void mian(String []args){
	String s1 = new String("hello") +new String("world");
	s1.intern();
	String s2 = "hello world";
	System.out.println(s1 == s2);//true
	}

第一行代码:

  1. 依次在堆内存中创建“hello”和“world”两个字符串对象
  2. 拼接起来(底层使用StringBuilder,后面会讲)
  3. 拼接完成产生新对象“hello world”,变量s1指向新对象

内存情况:在这里插入图片描述
第二行代码:
String类的源码中有intern()方法的介绍:当调用intern()方法时,首先会去常量池中查找是否有该字符串对应的引用,如果有就直接返回该字符串,如果没有,就会在常量池中注册该字符串的引用,然后返回该字符串。

由于第一行用的是new方法创建,所以常量池中肯定没有对应的引用,因此,会在常量池中进行注册。
在这里插入图片描述
第三行代码:
先找常量池,发现刚好有指向“hello world”的引用,直接将引用指向的字符串返回给所属变量。

内存示意图如下:
在这里插入图片描述
总结一下就是,new创建的时候,不会查询常量池;直接创建时,会先查询常量池。

最后再做到压轴题:

public class Main{
	public static void main(String []args){
		String s1 ="hello";
		String s2 = "world";
		String s3 = s1+s2;
		String s4 = "hello world";
		System.out.println(s3==s4);
	}
}

其实判断这题的结果为true还是false的关键在于第三行代码String s3 = s1+s2;我们不了解。它到底是new创建的,还是直接创建的。这种时候,我们就可以去读一下这段代码的反编译代码

在命令行中输入javap-c对应class文件的绝对路径,按回车后即可看到反编译文件的代码段。

在这里插入图片描述
解释:

  • 首先调用构造器完成Main类的初始化
  • 0:ldc#2 从常量池中获取“hello”字符串并推送至栈顶,此时拿到了“hello”的引用
  • astore_1 将栈顶的字符串引用存入第二个本地变量s1,也就是s1指向了“hello”;
  • 3:/5: 重复开始的步骤,此时将变量s2指向“world”
  • 6: new#4 这时创建了一个StringBuilder,并把其引用值压到栈顶
  • 9:dup 复制栈顶的值,并继续压入栈顶,意味着从上到下有两份StringBuilder的引用,要操作两次StringBuilder
  • 10: 调用StringBuilder的一些初始化方法,静态方法或父类方法,完成初始化
  • 13:aload_1 把第二个本地变量s1压入栈顶,现在栈顶从上往下数两个数据依次是:s1变量和StringBuilder的引用
  • 14:调用StringBuilder的append方法,接下来s2的时候再调用一次append方法(这就是两次StringBuilder的引用拷贝目的)
  • 完成后,StringBuilder已经拼接好“hello world”了。
  • 21:/24: 拼接完成后,虚拟机调用StringBuilder的toString()方法获得字符串hello world,并存放至s3
  • 接下来就看StringBuilder中的toString()方法源码。

在这里插入图片描述

从这里可以看到s3是通过new关键字获得字符串对象的。所以上面题目的答案是false。

二.详解字符串操作类

String, StringBuilder, StringBuffer的底层实现。

进入String的源码,可以看到String类是通过char类型数组实现的。

在这里插入图片描述
接着查看StringBuilder和StringBuffer的源码,发现这两者都继承自AbstractStringBuilder类。通过该类的源码,得知这两类也是通过char类型数组实现的。

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

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

三.StringBuilder vs String

通过对两者的源码分析,有一个关键的区别: 对于String,凡是涉及到返回参数类型为String类型的方法,在返回时都会通过new关键字创建一个新的字符串对象;而对于StringBuilder,大多数方法都会返回StringBuilder对象自身。
在这里插入图片描述
因为这一区别,使得两者在操作字符串时,在不同场景下会体现出不同的效率。
以拼接字符串为例,比较两者的性能:
在这里插入图片描述

明显地,StringBuilder类的效率更高。
通过反编译代码看造成两者性能差距的原因:

在这里插入图片描述
当用String拼接字符串时,每次都会生成一个StringBuilder对象,然后调用两次append()方法把字符串拼接好,最后通过StringBuilder的toString()方法new出一个新的字符串对象。

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

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

在这里插入图片描述直接把要拼接的字符串放到栈顶进行append,除了开始时创建了StringBuilder对象,运行时期没有创建过其他任何对象,每次循环只调用一次append方法。所以从效率上看,拼接大量字符串时,StringBuilder要比String类快很多。

String类也不是没有优势,它操作字符串的API更多,而且,如果是简单的拼接,如:String =“hello”+“world”,它的效率会更高一点。 这里可以思考一下,为什么效率会更高一点。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值