前言
字符串就是因为它太常用,用起来也简单,所以总是会被忽视,所以本文就是带你了解这个你“很懂”的String类
思维导图
String类的组成
String类在java的lang包下,通过源码(java8)可以知道String类存对象的本质使用的是char数组
不可变性
不可变性是Java中String类的一个很重要的特性,这里有两个问题:
- 那么为什么说它是不可变的,Java是如何实现的?
- 为什么Java要把String类设计成不可变的?
如何实现不可变
Java的String类由final关键字修饰,意味着String类不可继承
同时String类的本质是char数组,这个私有数组且是被final关键字修饰,意味着此数组无法被修改引用
并且String类中没有提供任何方法如setValue这类方法修改value的值
除了上面几点以外,还有一点就是,String类内部使用一些操作String的方法实质上都是会创建一个新的String对象的,然后修改指针让用户看起来是在修改原String
如String的subString
方法,最后还是调用了new String重新生成一个新的String对象
还比如concat
方法,也是生成一个新的String对象返回
所以Java的String是不可变的
为什么要设计成不可变
设计成如此主要有几个好处:
- 只有String设计成不可变的,才能实现
字符串常量池
,如果String可变,那么多个引用常量池中同一常量的变量在修改时就会互相影响! - String在Java中大量使用,不可变性可以保证数据不被篡改
- 不可变的String,hash code也是固定的,所以String类中有hash的成员变量,涉及到hash运算的时候就不用重新计算,提高了效率
- 不可变的String是线程安全的,因为任意线程都无法修改String对象,它是固定的不可变的
有没有办法强行修改
那必须的,因为Java有反射
的存在,反射太imba了!
字符串常量池
为什么要设计字符串常量池
String在Java中使用非常频繁,如果设计上它和其他对象一样,那么频繁的创建对象势必会造成严重的性能问题
所以Java设计实现了字符串常量池,相当于一个字符串的缓存池。在创建字符串对象前会先去字符串常量池查找是否有此字符串,如果有,就直接返回引用,否则就创建一个字符串常量丢到池中再返回引用
这样就大大提高了字符串的利用率,减少了频繁的对象创建
下面来看看Java中String的创建过程
字符串对象创建过程
Java中字符串创建的最基本的流程就是:先查常量池,有则直接返回引用,无则创建再返回引用
这里存在一个重要的设计模式享元模式
但这不是全部,实际的过程会不同情况会有些许的变化,这是一个基础,知道了这个流程我们再去看看一些实际的例子,加深一些印象
String a = “Hello World”
流程:
- 先在字符串常量池中查找,有没有"Hello World"字符串缓存
- 有则直接返回"Hello World"对象的引用
- 没有则在字符串常量池中创建"Hello World"对象,再将引用返回
String a = new String(“Hello World”)
new String这个过程就和上面有点不同,它调用了构造方法,会去堆空间创建一个String对象出来
创建了几个对象呢?
- 当常量池存在"Hello World",创建一个String对象,在堆中,value直接复制常量池缓存
- 当常量池不存在"Hello World",先在堆中创建String对象value是"Hello World",然后缓存"Hello World"到常量池
理解了上面两个最基础的再往下看
String ab = “a” + “b”
(一下前提都是常量池不存在任何常量缓存)
这个过程中会先在常量池创建"a"和"b"
由于是两个常量池中常量的连接,Java在处理此类情况时会采取"COW"机制,分别copy出"a"和"b"的副本在常量池,而后实际的连接是由两个副本进行,被优化成"ab",完成后副本不再存在
所以整个过程最后常量池会有三个对象"a",“b”,“ab”
String ab = a + b
String a = "a";
String b = "b";
String ab = a + b;
这样一段代码看起来和上面的String ab = "a" + "b"
一样,但是参与运算的是两个变量(a和b),Java会编译期间会将连接运算自动优化成StringBuilder的append
查看这段代码的字节码文件
神奇的发现,此过程Java会自动转化成StringBuilder
来字符串的运算
所以过程变成了:
先创建"a","b"两个常量,然后返回引用
再在堆中创建StringBuilder对象,调用StringBuilder的append方法进行拼接操作,最后会调用toString,生成新对象
这里有一个非常重要的知识点,就是StringBuidler的toString方法
看看它的源代码
它调用的是new String(value, 0 ,count)
而这一段的本质只是创建一个新的String对象,新String对象内部的值是由原数组进行copy得来的
这个过程没有在常量池产生新的常量缓存!!!!!!!!!!!!!!
所以最后常量池只有"a"和"b"缓存,而堆中有value为"ab"的String对象
这一点非常重要,尤其是在后续intern函数的学习中,这个点很重要
String ab = new String(“a”) + b
这个过程和上面的String ab = a + b
基本相同,也会在编译期转为StringBuilder进行优化
区别就是上面的a是先创建到常量池的,而这里的a是new出来的一个String对象,同时也缓存到了常量池
相同的变式还有就是:String ab = new String(“a”) + new String(“b”)
final关键字的修饰
相信上面的应该都能理解,这里有一个特殊的例子就是经过final关键字修饰的String
final String a = "a";
final String bc = "bc";
String abc = a + bc;
当有final关键字修饰时,a和bc在参与运算时会自动转成"a"和"bc"
所以此时就等于 String abc = “a” + “bc”;
小结
核心:先查常量池,有则直接返回引用,无则创建再返回引用
当String参与运算时,会自动优化成StringBuilder,最后会执行StringBuilder的toString重新创建新的String对象,但此时不会缓存连接后的字串到常量池
但诸如"a" + "b"
这种运算则会自动优化直接创建字符串到常量池中(“COW”),而不会创建StringBuilder
奇怪的intern()函数
只有看完了上面这个,了解了字符串对象的创建过程,看到他们就能反应出来是怎么创建的,我们才能学习intern函数
首先解释下它是做什么的?相信大家用的很少,没有专门看过一定不熟悉这个函数
这个函数在java6及其以前和以后是不同的
如这样一段代码
String ab = new String("a")+new String("b");
相信看完了上面完整的字符串对象创建过程的介绍后,可以知道此行代码执行完成后:堆中会有3个对象,字符串常量池只缓存了"a"和"b"
如果再执行intern:
String ab = new String("a")+new String("b");
ab.intern();
注意下面是我个人的理解,翻译,是一个我能够接受的版本,因为网上太多解释很复杂,理解起来困难!!!!!!!
在6及其以前作用是:判断常量池有没有"ab",如果有直接返回常量池中"ab"的引用,否则就缓存一个到"ab"到常量池再返回常量池引用
相信这个还是容易理解的
但在7及其以后,变成了:判断常量池有没有"ab",如果有直接返回常量池中"ab"的引用,否则在常量池中放入一个引用,指向堆中的String对象
ok到这里,估计大概率还是看不懂这句话“否则在常量池中放入一个引用,指向堆中的String对象”,来几个例子,解释下,应该能豁然开朗
在学习此部分内容时,我在网络上查阅了大量的博客,其中有一个很经典,是美团的博客,里面有一个很经典的例子,我们把他摘出来一行一行分析下
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
}
这段代码在java6和java7的结果是不同的:
- java6:false false
- java7:false true
为什么会这样呢?我们将代码分为上下两部分,逐行分析
上部分:
下部分:
再回头看看前面的概念:
在6及其以前作用是:
判断常量池有没有"ab",如果有直接返回常量池中"ab"的引用,否则就缓存一个到"ab"到常量池再返回常量池引用
在7及其以后,变成了:
判断常量池有没有"ab",如果有直接返回常量池中"ab"的引用,否则在常量池中放入一个引用,指向堆中的String对象
相信你能理解了?如果你还是不能理解,那我就要祭出我找到的宝藏博文了,此处顶礼膜拜大佬,大佬666,写的我这种菜鸡看完就懂了
ok到这里intern就介绍完了:)
StringBuilder和StringBuffer
前文写了很多介绍Java中的String类,这是Java中使用最最频繁的类,它已经可以完成各种各样的字符串操作了,但Java还是提供了StringBuilder和StringBuffer这两个类,用于操作字符串,同样这里就有几个疑问了:
- 为什么Java已经有了String类还是要提供StringBuilder和StringBuffer
- StringBuilder和StringBuffer是如何解决String不可变性带来的性能问题的
- StringBuilder和StringBuffer之间有什么异同
下面来慢慢探索…
为什么要有StringBuilder和StringBuffer
Java让String类不可变,同时配合字符串常量池,大大提高了字符串在Java使用中的效率
但是这同时也带来了一些问题,我们知道字符串是Java中使用且修改频率都很高的,如在这一段代码中做一个频繁修改字符串的模拟:
public class TestString {
public static void main(String[] args) {
String a = "";
for (int i = 0; i < 100; i++) {
a += "hello ";
}
}
}
由于String的不可变性,若没有StringBuilder和StringBuffer的情况下,每次进行+=
操作,都会向字符串常量池不停的创建新的对象,而实际上这些字符串并没什么用处,从而产生性能问题
所以有StringBuilder和StringBuffer的存在,来解决这些问题
来看看上面这段代码实质上的字节码:
可以看到Java自动的把这部分代码做了优化处理,使用了StringBuilder来进行解决String不可变性带来的问题
那么StringBuilder内部是如何解决这一问题的呢?继续往下看…
如何解决String不可变性带来的性能问题
翻开StringBuilder的源代码,查看它的源代码,可以发现StringBuilder中的诸多方法的实现都是采用调取父类的实现方法
查看StringBuilder的继承树
StringBuilder的父类是AbstractStringBuilder
,看看它的源码
可以看到和String类一样,StringBuilder的核心也是一个char数组,在看它的方法
看到了ensureCapacityInternal
,很熟悉!!!ArrayList!!!
没错,StringBuilder本质就是一个数组,对于String类的各项操作也是将字符串转化为一个char类型的数组进行各种操作,就和ArrayList一样,有着扩容,复制等等方法,将不可变的String先读取转化为char数组,进行完一系列修改后最后会执行toString方法重新new一个String对象出去
这样就解决了String类可能存在的问题
StringBuilder和StringBuffer的对比
Java除了提供StringBuilder以外还提供了一个StringBuffer,这两者是什么关系呢?
翻看StringBuffer的源代码
可以看到StringBuffer内部的方法基本和StringBuilder无异,但是每个方法多了synchronized
关键字的修饰,所以我们可以得知StringBuffer和StringBuilder的区别就是,StringBuffer是线程安全的
就和ArrayList与Vector的区别一样
也正是有了synchronized
关键字的修饰,在操作效率上StringBuffer要弱于StringBuilder,但是在多线程环境下,使用StringBuffer来的更加安全
参考
- Java常用类(二)String类详解
- String的不可变性
- String:字符串常量池
- 深入理解Java常用类-----StringBuilder
- 灵魂拷问:为什么 Java 字符串是不可变的?
- Java中String创建对象过程及其运算原理
- String类和常量池内存分析例子以及8种基本类型
- 美图:深入解析String#intern
不开玩笑哦,大佬们写的真的很嗨好啊!!!!再次膜拜😎