底层对string,stringbuffer和stringbuilder的理解

15 篇文章 0 订阅

目录

1 简单介绍

2 底层原理

2.1 String原理

2.2 字符串相加

3 对比总结


1 简单介绍

java中用于处理字符串常用的有三个类:

1、java.lang.String

2、java.lang.StringBuffer

3、java.lang.StrungBuilder

三者共同之处:都是final类,不允许被继承,主要是从性能和安全性上考虑的,因为这几个类都是经常被使用着,且考虑到防止其中的参数被参数修改影响到其他的应用。

StringBuffer是线程安全,可以不需要额外的同步用于多线程中;

StringBuilder是非同步,运行于多线程中就需要使用着单独同步处理,但是速度就比StringBuffer快多了;

StringBuffer与StringBuilder两者共同之处:可以通过append、indert进行字符串的操作。

在java中有3个类来负责字符的操作。

1.Character 是进行单个字符操作的,

2.String 对一串字符进行操作。不可变类。

3.StringBuffer 也是对一串字符进行操作,但是可变类。

String:
是对象不是原始类型.
为不可变对象,一旦被创建,就不能修改它的值.
对于已经存在的String对象的修改都是重新创建一个新的对象,然后把新的值保存进去.
String 是final类,即不能被继承.

StringBuffer:
是一个可变对象,当对他进行修改的时候不会像String那样重新建立对象
它只能通过构造函数来建立,
StringBuffer sb = new StringBuffer();
note:不能通过付值符号对他进行付值.
sb = "welcome to here!";//error
对象被建立以后,在内存中就会分配内存空间,并初始保存一个null.向StringBuffer
中付值的时候可以通过它的append方法.
sb.append("hello");

StringBuilder:

是一个可变对象,和StringBuffer相比,不是线程安全的,一般用在单个线程操作的时候(这种情况很普遍,所以一般优先选用StringBuilder),速度比StringBuffe快很多。

字符串连接操作中StringBuffer的效率要比String高:

String str = new String("welcome to ");
str += "here";
的处理步骤实际上是通过建立一个StringBuffer,让侯调用append(),最后
再将StringBuffer toSting();
这样的话String的连接操作就比StringBuffer多出了一些附加操作,当然效率上要打折扣.

并且由于String 对象是不可变对象,每次操作Sting 都会重新建立新的对象来保存新的值.
这样原来的对象就没用了,就要被垃圾回收.这也是要影响性能的.

2 底层原理

2.1 String原理

在深入学习字符串类之前, 我们先搞懂JVM是怎样处理新生字符串的. 当你知道字符串的初始化细节后, 再去写 Strings="hello"或 Strings=newString("hello")等代码时, 就能做到心中有数。

首先得搞懂字符串常量池的概念。

常量池是Java的一项技术, 八种基础数据类型除了float和double都实现了常量池技术. 这项技术从字面上是很好理解的: 把经常用到的数据存放在某块内存中, 避免频繁的数据创建与销毁, 实现数据共享, 提高系统性能。

字符串常量池是Java常量池技术的一种实现, 在近代的JDK版本中(1.7后), 字符串常量池被实现在Java堆内存中。

下面通过三行代码让大家对字符串常量池建立初步认识:

我们先来看看第一行代码 Strings1="hello";干了什么.

从底层彻底搞懂String,StringBuilder,StringBuffer的实现

对于这种直接通过双引号""声明字符串的方式, 虚拟机首先会到字符串常量池中查找该字符串是否已经存在. 如果存在会直接返回该引用, 如果不存在则会在堆内存中创建该字符串对象, 然后到字符串常量池中注册该字符串。

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

温馨提示: 图中的字符串常量池中的数据是虚构的, 由于字符串常量池底层是用HashTable实现的, 存储的是键值对, 为了方便大家理解, 示意图简化了字符串常量池对照表, 并采用了一些虚拟的数值。

下面看 Strings2=newString("hello");的示意图:

当我们使用new关键字创建字符串对象的时候, JVM将不会查询字符串常量池, 它将会直接在堆内存中创建一个字符串对象, 并返回给所属变量。

所以s1和s2指向的是两个完全不同的对象, 判断s1 == s2的时候会返回false。

第一行代码 Strings1=newString("hello ")+newString("world");的执行过程是这样子的:

1.依次在堆内存中创建"hello "和"world"两个字符串对象

2.然后把它们拼接起来 (底层使用StringBuilder实现, 后面会带大家读反编译代码)

3.在拼接完成后会产生新的"hello world"对象, 这时变量s1指向新对象"hello world"

执行完第一行代码后, 内存是这样子的:

第二行代码 s1.intern();

String类的源码中有对 intern()方法的详细介绍, 翻译过来的意思是: 当调用 intern()方法时, 首先会去常量池中查找是否有该字符串对应的引用, 如果有就直接返回该字符串; 如果没有, 就会在常量池中注册该字符串的引用, 然后返回该字符串。

由于第一行代码采用的是new的方式创建字符串, 所以在字符串常量池中没有保存"hello world"对应的引用, 虚拟机会在常量池中进行注册, 注册完后的内存示意图如下:

第三行代码 Strings2="hello world";

这种直接通过双引号""声明字符串背后的运行机制我们在第一个案例提到过, 这里正好复习一下。

首先虚拟机会去检查字符串常量池, 发现有指向"hello world"的引用. 然后把该引用所指向的字符串直接返回给所属变量。

执行完第三行代码后, 内存示意图如下:

如图所示, s1和s2指向的是相同的对象, 所以当判断s1 == s2时返回true。

最后我们对字符串常量池进行总结:

当用new关键字创建字符串对象时, 不会查询字符串常量池; 当用双引号直接声明字符串对象时, 虚拟机将会查询字符串常量池. 说白了就是: 字符串常量池提供了字符串的复用功能, 除非我们要显式创建新的字符串对象, 否则对同一个字符串虚拟机只会维护一份拷贝。

我们来看一下这张对String操作时内存变化的图:

      我们可以看到,初始String值为“hello”,然后在这个字符串后面加上新的字符串“world”,这个过程是需要重新在栈堆内存中开辟内存空间的,最终得到了“hello world”字符串也相应的需要开辟内存空间,这样短短的两个字符串,却需要开辟三次内存空间,不得不说这是对内存空间的极大浪费。为了应对经常性的字符串相关的操作,就需要使用Java提供的其他两个操作字符串的类——StringBuffer类和StringBuild类来对此种变化字符串进行处理。

2.2 字符串相加

《Java编程思想》提到,String 对象的操作符“+”其实被赋予了特殊的含义,该操作符是 Java 中仅有的两个重载过的操作符。而通过反编译可以看到,String 对象在进行“+”操作的时候,其实是调用了 StringBuilder 对象的 append() 方法来加以构造

按照惯例,我们先来看两个字符串相加的例子

public void stringTest4(){
    String a1 = "helloworld";
    String a = "hello" + "world";

    String b = "hello";
    String c = "world";
    String d = b + c;

    System.out.println(a);
    System.out.println(d);
    System.out.println(a == d);
    System.out.println(a1 == a);
    System.out.println(a1 == d);
}

输出结果如下

helloworld
helloworld
false
true
false

先来看 String a,“hello” + “world” 在 String 编译期间进行优化,优化结果为 “helloworld”,而该值在常量池中已经存有一份,因此 a 也指向了该常量池中的字符串,因此 a1 和 a 相等,输出 true

在对 b 和 c 进行相加的过程中,其实是分两个步骤来进行。先是调用 StringBuilder 对象的 append() 方法,加上 “hello” 字符;然后在调用一次 append() 方法,加上 “world”,最后默认调用 StringBuilder 的 toString() 方法输出“helloworld”。

很明显 d 对象指向的是堆中的 String 对象,而 a1 则指向的是常量池中的字符串,两者引用明显不同,因此输出 false。a 和 d 的比较与之类似,这里不加以赘述

这道题解了, s3是通过new关键字获得字符串对象的,也就是说字符串常量表中没有存储"hello world"的引用, 当s4以引号的形式声明字符串时, 由于在字符串常量池中查不到相应的引用, 所以会在堆内存中新创建一个字符串对象. 所以s3和s4指向的不是同一个字符串对象, 结果为false。

我用反编译来看一下直接字符串相加使用引用相加的区别
这里写图片描述
这里我就抽取了一部分反编译的结果。

1).可以看到,在第0行的时候,字符串就已经变成 helloworld 了,说明 “hello” + “world” 在编译的期间就已经进行了优化

2). 第13行开始,也就是在执行 b+c 这个语句的时候,就开始调用 append() 的方法了。当执行到 b 的时候,创建一个 StringBuilder 对象,调用 append() 方法加入,当执行到 c 的时候,再次调用 append() 加入。此时完成相加,以 String 类型返回。b + c 返回的结果相当于存入了一个新的 String 对象,最后在用变量 d 指向这个新的对象

3). 关于相加的效率,那么字符串引用相加(a + b)的效率,会比直接相加低,因为编译器不会对引用变量加以优化

3 对比总结

执行效率

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

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

StringBuilder > StringBuffer > String

当然这个是相对的,不一定在所有情况下都是这样。

比如String str = “hello” + "world"的效率就比 StringBuilder st = new StringBuilder().append(“hello”).append(“world”)要高。

因此,如果是字符串相加操作或者改动较少的情况下,使用 String str = "hello" + "world" + ... 这种形式;土改时字符串相加比较多的话,建议使用 StringBuilder

线程安全

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

 @Override
public synchronized StringBuffer append(Object obj) {
    toStringCache = null;
    super.append(String.valueOf(obj));
    return this;
}
public StringBuilder append(StringBuffer sb) {
    super.append(sb);
    return this;
}

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

1.输出以下结果

String a = "hello";
String b = "hello";

String a1 = new String("hello");
String a2 = new String("hello");

StringBuilder c = new StringBuilder("hello");
StringBuilder d = new StringBuilder("hello");

a == b
a1 == a2
c == d
a1.equals(a2)
a1.equals(c)
a.equals(c)
c.equals(d)

答案为 true、false、false、true、false、false。
a 和 b 指向同一个常量池,由此相等;a1 和 a2,c 和 d 都是因为对象不同所以不等;a1 和 a2 同属于 String 类型且字符相等;a1 和 c ,a 和 c 因为类型不同所以直接返回 false;
c 和 d 因为对象不同返回 false。

2.输出以下结果

String a = "hellonihao";
String b = "hello" + "nihao";

a == b

输出结果为 true。原因上面也提到了,“hello” + “nihao” 在编译的过程中就已经优化成了 “hellonihao”,此时常量池中已经有一个 “hellonihao”,这个时候不要再去新建一个对象,因此 a 和 b 指向的是同一个对象

3.输出以下结果

String a = "nihaoshijie";

String b = "nihao";
String c = "shijie";
String d = b + c;

a == d

输出结果为 false。因为"+"在Java中是重载运算符,String 对象直接相加的过程不会在编译期间优化,相加的结果会保存在一个新的对象中,因此 a 和 d 指向的根本不是一个对象

4.输出以下结果

String a = "helloworld";
final String b = "hello";
final String c = "world";
String d = b + c;

a == d;

输出结果居然是 true。对于被 final 修饰符修饰的变量,一经修饰便不能改变此时这两个 String 对象 b 和 c 在相加的过程中都能指向其他对象了。因此 String d = b + c 在编译期间就会被优化成:String d = "hello" + "world" = "helloworld",而这个常量在常量池已经有了,因此返回 true。下面是反编译的验证
这里写图片描述
5.输出以下结果

String a = "hello";
String b = new String("hello");
String c = a.intern();

a == b;
a == c;

结果为false、true。第一个很好理解;第二个的话,由于常量池中已经存在了 “hello” 这个字符串,所以直接返回该字符串的引用

intern():

当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。它遵循对于任何两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。

对一个字符串调用intern()方法后,会先检查池内是否有该字符串,若有则返回;若没有没有则先创建再返回,确保返回的字符串已经以字面量的形式存在于池中。

4 常用方法

StringBuffer 方法

以下是 StringBuffer 类支持的主要方法:

序号方法描述
1public StringBuffer append(String s)
将指定的字符串追加到此字符序列。
2public StringBuffer reverse()
 将此字符序列用其反转形式取代。
3public delete(int start, int end)
移除此序列的子字符串中的字符。
4public insert(int offset, int i)
将 int 参数的字符串表示形式插入此序列中。
5replace(int start, int end, String str)
使用给定 String 中的字符替换此序列的子字符串中的字符。

下面的列表里的方法和 String 类的方法类似:

序号方法描述
1int capacity()
返回当前容量。
2char charAt(int index)
返回此序列中指定索引处的 char 值。
3void ensureCapacity(int minimumCapacity)
确保容量至少等于指定的最小值。
4void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
将字符从此序列复制到目标字符数组 dst
5int indexOf(String str)
返回第一次出现的指定子字符串在该字符串中的索引。
6int indexOf(String str, int fromIndex)
从指定的索引处开始,返回第一次出现的指定子字符串在该字符串中的索引。
7int lastIndexOf(String str)
返回最右边出现的指定子字符串在此字符串中的索引。
8int lastIndexOf(String str, int fromIndex)
返回 String 对象中子字符串最后出现的位置。
9int length()
 返回长度(字符数)。
10void setCharAt(int index, char ch)
将给定索引处的字符设置为 ch
11void setLength(int newLength)
设置字符序列的长度。
12CharSequence subSequence(int start, int end)
返回一个新的字符序列,该字符序列是此序列的子序列。
13String substring(int start)
返回一个新的 String,它包含此字符序列当前所包含的字符子序列。
14String substring(int start, int end)
返回一个新的 String,它包含此序列当前所包含的字符子序列。
15String toString()
返回此序列中数据的字符串表示形式。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值