深入了解String,StringBuffer和StringBuilder三个类的异同
Java提供了三个类,用于处理字符串,分别是String、StringBuffer和StringBuilder。其中StringBuilder是jdk1.5才引入的。
这三个类有什么区别呢?他们的使用场景分别是什么呢?
本文的代码是在jdk12上运行的,jdk12和jdk5,jdk8有很大的区别,特别是String、StringBuffer和StringBuilder的实现。
jdk5和jdk8中String类的value类型是char[],到了jdk12,value类型变为byte[]。
jdk5、JDK6中的常量池是放在永久代的,永久代和Java堆是两个完全分开的区域。
到了jdk7及以后的版本,
我们先来看看这三个类的源码。
String类部分源码:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
@Stable
private final byte[] value;
public String(String original) {
this.value = original.value;
this.coder = original.coder;
this.hash = original.hash;
}
public native String intern();
String类由final修饰符修饰,所以String类是不可变的,对象一旦创建,不能改变。
String类中有个value的字节数组成员 变量,这个变量用于存储字符串的内容,也是用final修饰,一旦初始化,不可改变。
java提供了两种主要方式创建字符串:
//方式1
String str = "123";
//方式2
String str = new String("123");
java虚拟机规范中定义字符串都是存储在字符串常量池中,不管是用方式1还是方式2创建字符串,都会从去字符串常量池中查找,如果已经存在,直接返回,否则创建后返回。
java编译器在编译java类时,遇到“abc”,“hello”这样的字符串常量,会将这些常量放入类的常量区,类在加载时,会将字符串常量加入到字符串常量池中。
含有表达式的字符串常量,不会在编译时放入常量区,例如,String str = "abc" + a
常量池的最大作用是共享使用,提高程序执行效率。
看看下面几个案例。
案例1:
1 String str1 = "123";
2 String str2 = "123";
3 System.out.println(str1 == str2);
上面代码运行的结果为true。
运行第1行代码时,现在常量池中创建字符串123对象,然后赋值给str1变量。
运行第2行代码时,发现常量池已经存在123对象,则直接将123对象的地址返回给变量str2。
str1和str2变量指向的地址一样,他们是同一个对象,因此运行的结果为true。

从图中可以看出,str1使用””引号(也是平时所说的字面量)创建字符串,在编译期的时候就对常量池进行判断是否存在该字符串,如果存在则不创建直接返回对象的引用;如果不存在,则先在常量池中创建该字符串实例再返回实例的引用给str1。
案例2:
1 String str1 = new String("123");
2 String str2 = new String("123");
3 String str3 = new String(str2);
4 System.out.println((str1==str2));
5 System.out.println((str1==str3));
6 System.out.println((str3==str2));
上面代码运行的结果是
false
false
false

从上图可以看出,执行第1行代码时,创建了两个对象,一个存放在字符串常量池中,一个存在与堆中,还有一个对象引用str1存放在栈中。
执行第2行代码时,字符串常量池中已经存在“123”对象,所以只在堆中创建了一个字符串对象,并且这个对象的地址指向常量池中“123”对象的地址,同时在栈中创建一个对象引用str2,引用地址指向堆中创建的对象。
执行第3行代码时,在堆中创建一个字符串对象,这个对象的内存地址指向变量str2所执向的内存地址。
通过new方式创建的字符串对象,都会在堆中开辟一个新内存空间,用于存储常量池中的字符串对象。
对于对象而言,==操作是用于比较两个独享的内存地址是否一致,所以上面的代码执行的结果都是false。
案例3:
//这行代码编译后的效果等同于String str1 = "abcd";
String str1 = "ab" + "cd";
String str2 = "abcd";
System.out.println((str1 == str2));
上面代码执行的结果:true。
使用包含常量的字符串连接创建的也是常量,编译期就能确定了,类加载的时候直接进入字符串常量池,当然同样需要判断字符串常量池中是否已经存在该字符串。
案例4:
String str2 = "ab"; //1个对象
String str3 = "cd"; //1个对象
String str4 = str2 + str3 + “1”;
String str5 = "abcd1";
System.out.println((str4==str5));
上面代码执行的结果:false。
当使用“+”连接字符串中含有变量时,由于变量的值是在运行时才能确定。
如果使用的jdk8以前版本的虚拟机,在拼接字符串时,会在jvm堆中生成StringBuilder对象,调用append方法拼接字符串,最后调用StringBuilder的toString方法在jvm堆中生成最终的字符串对象。
通过查看字节码就可以知道jdk8之前版本的"+"拼接字符串时通过StringBuilder实现的。通过查看字节码就可以知道,如下图所示:

而如果使用的是jdk9以后版本的虚拟机,则是调用虚拟机自带的InvokeDynamic拼接字符串,并且保存在堆中。字节码如下所示:

str4的对象在字符串常量池中,str5的对象在堆中,所以他们的不是同一个对象,所以返回的结果是false。
案例5:
String s5 = new String(“2”) + new String(“3”);
和案例4一样,因为new String("2")创建字符串,也是在运行时才能确定对象内存地址,和案例4一样。
案例6:
final String str1 = "b";
String str2 = "a" + str1;
String str3 = "ab";
System.out.println((str2 == str3));
上面代码执行的结果为true。
str1是常量变量,在编译期就确定,直接放入到字符串常量池中,上面的代码效果等同于:
String str2 = "a" + "b";
String str3 = "ab";
System.out.println((str2 == str3));
调用String类的intern()方法,会将堆中的字符串实例放入到字符串常量池中。
案例7:
String str2 = "ab";
String str3 = "cd";
String str4 = str2 + str3 + "1";
str4.intern();
String str5 = "abcd1";
System.out.println((str4==str5));
上面代码执行的结果:true。调用了str4.intern()方法后,将str4放入到字符串常量池中,和str5是同一个实例。
StringBuffer部分源码:
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, Comparable<StringBuffer>, CharSequence
{
StringBuilder部分源码:
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, Comparable<StringBuilder>, CharSequence
{
可见StringBuffer和StringBuilder都继承了AbstractStringBuilder类。
AbstractStringBuilder类源码:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
byte[] value;
AbstractStringBuilder也有一个字节数组的成员变量value,这个变量用于存储字符串的值,这个变量不是用final修饰,所以是可以改变的,这个是和String的最大区别。
在调用append方法的时候,会动态增加字节数组变量value的大小。
StringBuffer和StringBuilder功能是一样的,都是为了提高java中字符串连接的效率,因为直接使用+进行字符串连接的话,jvm会创建多个String对象,因此造成一定的开销。AbstractStringBuilder中采用一个byte数组来保存需要append的字符串,byte数组有一个初始大小,当append的字符串长度超过当前char数组容量时,则对byte数组进行动态扩展,也即重新申请一段更大的内存空间,然后将当前bute数组拷贝到新的位置,因为重新分配内存并拷贝的开销比较大,所以每次重新申请内存空间都是采用申请大于当前需要的内存空间的方式,这里是2倍。
StringBuffer和StringBuilder最大的区别是StringBuffer是线程安全,而StringBuilder是非线程安全的,从它们两个类的源码就可以知道,StringBuffer类的方法前面都是synchronized修饰符。
String一旦赋值或实例化后就不可更改,如果赋予新值将会重新开辟内存地址进行存储。
而StringBuffer和StringBuilder类使用append和insert等方法改变字符串值时只是在原有对象存储的内存地址上进行连续操作,减少了资源的开销。
总结:
1、频繁使用“+”操作拼接字符时,换成StringBuffer和StringBuilder类的append方法实现。
2、多线程环境下进行大量的拼接字符串操作使用StringBuffer,StringBuffer是线程安全的;
3、单线程环境下进行大量的拼接字符串操作使用StringBuilder,StringBuilder是线程不安全的。
深入了解String,StringBuffer和StringBuilder三个类的异同的更多相关文章
- Java String, StringBuffer和StringBuilder实例
1- 分层继承2- 可变和不可变的概念3- String3.1- 字符串是一个非常特殊的类3.2- String 字面值 vs. String对象3.3- String的方法3.3.1- length ...
- String,StringBuffer和StringBuilder源码解析[基于JDK6]
最近指导几位新人,学习了一下String,StringBuffer和StringBuilder类,从反馈的结果来看,总体感觉学习的深度不够,没有读出东西.其实,JDK的源码是越读越有味的.下面总结一下 ...
- String,StringBuffer和StringBuilder的异同
String,StringBuffer和StringBuilder的异同 ...
- String,StringBuffer与StringBuilder
1. String,StringBuffer与StringBuilder的区别 String:存储在常量池中:是不可变的字符序列,任何对String值的改变都会引发新的String对象的生成,因此执行 ...
- String, StringBuffer and StringBuilder
一 String 概述: String 被声明为 final,因此它不可被继承. 在 Java 8 中,String 内部使用 char 数组存储数据. public final class Stri ...
- String,StringBuffer和StringBuilder
String,StringBuffer和StringBuilder分别应该在什么情况下使用? String 是Java的字符串类,其实质上也是用Char类型存储的,但是除了hash属性,其他的属性都声 ...
- 面试长谈的String,StringBuffer,StringBuilder三兄弟有啥区别
1.String: /** Strings are constant; their values cannot be changed after they * are created. String ...
- String StringBuffer和StringBuilder区别及性能
结论: (1)如果要操作少量的数据用 String: (2)多线程操作字符串缓冲区下操作大量数据 StringBuffer: (3)单线程操作字符串缓冲区下操作大量数据 StringBuilder(推 ...
- String,StringBuffer和StringBuilder的区别
(1)String类的API概述是这样的:String类代表字符串,Java程序中的所有字符串字面值都作为此类的实例体现.字符串是常量,它们的值在创建之后不能更改.可见,String是对象且为不可变对 ...
随机推荐
- Hadoop3.1.2 + Hbase2.2.0 设置lzo压缩算法
Hadoop3.1.2 + Hbase2.2.0 设置lzo压缩算法: 写在前面,在配置hbase使用lzo算法时,在网上搜了很多文章,一般都是比较老的文章,一是版本低,二是一般都是使用hadoop- ...
- APPARENT DEADLOCK!!!c3p0数据库连接池死锁问题
项目进行压力测试的时候,运行大概1小时候,后台抛出以下异常: Nov 9, 2012 1:41:59 AM com.mchange.v2.async.ThreadPoolAsynchronousRun ...
- ThreadPoolExecutor执行任务,异常日志缺失问题
之前在使用自定义线程池异步执行耗时任务时,一直记着如果业务方法抛出异常没有捕获,那么是看不到日志框架输出的异常日志的,所以总是在业务方法中包裹一层try-catch捕获可能发生的异常.也未去深入为什么 ...
- POJ 2155 Matrix (2维树状数组)
POJ-Matrix 题意:给你一个n*n矩阵的灯泡,灯泡的初始状态都为0,T次操作,分别是翻转操作:将x1,y1 --- x2, y2的灯泡状态反转 和 查询操作 找出x1, y1位置灯泡的状态. ...
- Nginx简介及配置文件详解
http://blog.csdn.net/hzsunshine/article/details/63687054 一 Nginx简介 Nginx是一款开源代码的高性能HTTP服务器和反向代理服务 ...
- Linux 安装二进制MySQL 及 破解MySQL密码
1.确保系统中有依赖的libaio 软件,如果没有: yum -y install libaio 2.解压二进制MySQL软件包 tar xf mysql-5.7.24-linux-glibc2.12 ...
- 持续集成高级篇之Jenkins windows/linux混合集群搭建(二)
系列目录 前面我们说过,要使用ssh方式来配置windows从节点,如果采用ssh方式,则windows和linux配置从节点几乎没有区别,目前发现的惟一的区别在于windows从节点上目录要设置在c ...
- 使用ECMAScript 6 模块封装代码
JavaScript 用"共享一切"的方法加载代码,这是该语言中最容易出错且最容易让人感到困惑的地方.其他语言使用诸如包这样的概念来定义代码作用域,但在 ECMAScript 6 ...
- android 端缓存清理的实现
首先关于缓存清理,网上已经有太多的工具类,但是遗憾的是,基本上都不完善,或者说根本就不能用,而项目中又要求实现这个烂东西(其实这玩意真没一点屁用,毕竟第三方清理/杀毒软件都带这么一个功能),但是只好硬 ...
- jquery ajax到servlet出现中文乱码(utf-8编码下)
个人遇到的该问题有两大类: 第一类很普遍,就是jsp页面编码没有规定,servlet中接收参数没有转码,response没有使用setContentType()和setCharacterEncodin ...