Java基础2.1 — 关于字符串String、StringBuilder、StringBuffer那些事

一、String的不可变性

咱们先来看一下String类的声明

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    
    /** Cache the hash code for the string */
    private int hash; // Default to 0

首先知道的是String实际上内部维护的是char数组,并且这个数组和String一样,都用final修饰,证明String是不可变的和不可被继承的,进一步解释就是一旦String对象被创建,那么内部的成员变量的值包括基本数据类型都不能被改变,不能指向其他对象,指向的对象的状态也不能被改变,那么这样设计的好处有什么呢?原因有以下

  1. 只有当String是不可变时,String常量池才有可能实现并且为heap节省了空间
  2. 网络安全,否则黑客可以改变String指向的对象的值而造成安全漏洞问题
  3. 线程安全,可以被多个线程共享
  4. 性能,因为String不可变,所以String创建时的hashcode也具有唯一性,作为Map的键时比其他键对象快

二、String Constant Pool(String常量池)

JVM为了提升性能和减少内存开销,避免字符串的重复创建,维护了一块特殊的内存空间,即String Pool(字符串池)

常量池底层方法是 String#intern() 使用StringTable数据结构保存字符串引用,StringTable是一个固定大小的Hashtable,默认大小是1009。基本逻辑与Java中HashMap相同,也使用拉链法解决碰撞问题。既然是拉链法,那么如果放进的String非常多,就会加剧碰撞,导致链表非常长。最坏情况下, String#intern() 的性能由O(1)退化到O(n)。

深入解析String#intern
https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html

JDK6的版本中,String Pool使用固定容量的HashMap实现并存储在永久代中的,后面变为可配置,因为永久带内存有限,所以在JDK7开始就移动到heap(堆内存)中,这就意味着你可以通过调整堆大小来调整应用程序,通过JVM参数-XX:StringTableSize可以调整String常量池的大小(质数),同样的Size,处理的量越大就越慢,不同的Size,越大性能越好

创建字符串对象的方式有两种

  1. 通过字面常量赋值
  2. 通过new关键字新建字符串对象

这两种方式在性能和内存占用上存在差别,下面来看一下这两种方式还有其他的一些情况下JVM中发生了什么

1.字面常量赋值
String s1 = "abc";
String s2 = "ab"+"c";
System.out.println(s1 == s2); //true

常量折叠:这里穿插一个概念,由于编译期的优化,对于用"+"连接的字面常量会在编译器直接并起来.比如上例的
String s2 =“ab”+“c”;会在编译器被优化成 String s2 = “abc”;

采用字面常量去创建一个字符串时,JVM会在运行时常量池寻找有没有该字符串,有则直接返回常量池中的引用,没有就直接在常量池中创建该字符串,然后返回引用。所以上例的s1和s2指向的都是同一个对象,用 == 比较就会返回true,我们也可以通过字节码来进一步确认

0 ldc #2 <abc> 
2 astore_1
3 ldc #2 <abc>
5 astore_2
6 return

当调用ldc #2,如果 #2 的symbol还没解析,则会调用C++底层的 StringTable::intern 方法生成char数组,并将引用保存在 StringTable和常量池中,当下次调用 ldc #2,通过将常量池中 #2对应的字符串推送到栈顶获取到 “abc”,避免再次到StringTable中查找。astore_1“abc” 保存到 局部变量

2.使用new关键字新建
String s3 = new String("abc");
String s4 = "abc";
System.out.println(s3 == s4); //false

我们来分析一下发生了什么
①因为"abc"是用字面常量定义了,所以JVM会在运行时常量池中寻找,有则进入②,没有则创建然后进入②
②由于使用了new,所以JVM会在 heap(堆) 中创建一个内容相同的String对象,然后返回堆中Sring对象的引用

下面是字节码

 0 new #2 <java/lang/String>
 3 dup
 4 ldc #3 <abc>  
 6 invokespecial #4 <java/lang/String.<init>>
 9 astore_1
10 return

所以,分别在常量池和堆中生成了两个内容相同的String对象

3.使用变量连接的情况
String s5 = "ab";
String s6 = s5 + "c";

重点在s6,因为s5是一个变量,即使我们知道这个值,但是Jvm仍然认为这是一个变量,所以在编译期,这个值是未知的。在运行期,JVM就在 heap(堆) 中创建了一个内容为"abc"的对象并返回给s6,而"ab"和"c"是以字面常量的形式定义的,所以会在常量池中出现.

下面是字节码

 0 ldc #2 <ab>
 2 astore_1
 3 new #3 <java/lang/StringBuilder>
 6 dup
 7 invokespecial #4 <java/lang/StringBuilder.<init>>
10 aload_1
11 invokevirtual #5 <java/lang/StringBuilder.append>
14 ldc #6 <c>
16 invokevirtual #5 <java/lang/StringBuilder.append>
19 invokevirtual #7 <java/lang/StringBuilder.toString>
22 astore_2
23 return

字符串变量的连接动作,在编译期会被转化成StringBuilder的append操作

4.使用final关键字修饰String
final String s7 = "ab";
String s8 = s7 + "c";

在这种情况下,final修饰的s7被视为一个常量,所以常量池里会有"ab",s7在编译期已经是确定了,所以s7+“c"连接后的字符串s8会在常量池中出现,也就是"abc”

下面是字节码

0 ldc #2 <ab>
2 astore_1
3 ldc #3 <abc>
5 astore_2
6 return

三、String、StringBuilder和StringBuffer的区别

  1. String是字符串 常量,而 tringBuilder和StringBuffer都是字符串 变量
  2. StringBuilder是 线程不安全 的,而StringBuffer是 线程安全 的,这样就以为者后者会带来额外的系统开销,所以StringBuilder的效率比StringBuffer高
  3. String每次修改操作都要在堆内存中new一个对象,而StringBuffer和StringBuilder不用,并且提供了一定的缓存功能,默认16个字节数组的大小。扩容就原来的大小 x 2 + 2,可以考虑初始化StringBuilder的大小来提高代码的效率。

四、一些题目

1.下面程序运行的结果是什么

String s1 = "abc";
StringBuffer s2 = new StringBuffer(s1);
System.out.println(s1.equals(s2));  //false String的equals有对参数进行instance of String判断

StringBuffer s3 = new StringBuffer("abc");
System.out.println(s3.equals("abc"));  //StringBuffer没有重写equals方法,实际上是 == 比较对象,StringBuilder也是
System.out.println(s3.toString().equals("abc")); //true 比较的是值


String s4 = "abc";
System.out.println("abc"==s4.subString(0));   //true,如果subString的index是0,直接返回对象
System.out.println("abc"==s4.subString(1));   //false,不为0就new一个sub之后的对象返回

除此之外,toLowerCasetoUpperCase都是new一个对象返回

2.下面语句一共创建了多少个对象

String str = new String("xyz"); 

这是一道有歧义的题,因为没有说明时机,实际上可以问涉及到几个,答案是两个,一个是在类加载过程中在常量池里面创建的"abc"对象,另外一个是运行期间创建在堆内存的"abc"对象。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值