《Java基础知识点梳理》不会吧,不会真有人连字符串都不懂吧?(狗头)

前言

字符串就是因为它太常用,用起来也简单,所以总是会被忽视,所以本文就是带你了解这个你“很懂”的String类

思维导图

String类的组成

String类在java的lang包下,通过源码(java8)可以知道String类存对象的本质使用的是char数组

image-20210826154431138

不可变性

不可变性是Java中String类的一个很重要的特性,这里有两个问题:

  • 那么为什么说它是不可变的,Java是如何实现的?
  • 为什么Java要把String类设计成不可变的?

如何实现不可变

Java的String类由final关键字修饰,意味着String类不可继承

同时String类的本质是char数组,这个私有数组且是被final关键字修饰,意味着此数组无法被修改引用

image-20210826155439807

并且String类中没有提供任何方法如setValue这类方法修改value的值

除了上面几点以外,还有一点就是,String类内部使用一些操作String的方法实质上都是会创建一个新的String对象的,然后修改指针让用户看起来是在修改原String

如String的subString方法,最后还是调用了new String重新生成一个新的String对象

还比如concat方法,也是生成一个新的String对象返回

所以Java的String是不可变的

为什么要设计成不可变

设计成如此主要有几个好处:

  1. 只有String设计成不可变的,才能实现字符串常量池,如果String可变,那么多个引用常量池中同一常量的变量在修改时就会互相影响!
  2. String在Java中大量使用,不可变性可以保证数据不被篡改
  3. 不可变的String,hash code也是固定的,所以String类中有hash的成员变量,涉及到hash运算的时候就不用重新计算,提高了效率
  4. 不可变的String是线程安全的,因为任意线程都无法修改String对象,它是固定的不可变的

有没有办法强行修改

那必须的,因为Java有反射的存在,反射太imba了!

字符串常量池

为什么要设计字符串常量池

String在Java中使用非常频繁,如果设计上它和其他对象一样,那么频繁的创建对象势必会造成严重的性能问题

所以Java设计实现了字符串常量池,相当于一个字符串的缓存池。在创建字符串对象前会先去字符串常量池查找是否有此字符串,如果有,就直接返回引用,否则就创建一个字符串常量丢到池中再返回引用

这样就大大提高了字符串的利用率,减少了频繁的对象创建

下面来看看Java中String的创建过程

字符串对象创建过程

Java中字符串创建的最基本的流程就是:先查常量池,有则直接返回引用,无则创建再返回引用

这里存在一个重要的设计模式享元模式

但这不是全部,实际的过程会不同情况会有些许的变化,这是一个基础,知道了这个流程我们再去看看一些实际的例子,加深一些印象

String a = “Hello World”

流程:

  1. 先在字符串常量池中查找,有没有"Hello World"字符串缓存
  2. 有则直接返回"Hello World"对象的引用
  3. 没有则在字符串常量池中创建"Hello World"对象,再将引用返回

image-20210827113943222

String a = new String(“Hello World”)

new String这个过程就和上面有点不同,它调用了构造方法,会去堆空间创建一个String对象出来

创建了几个对象呢?

  • 当常量池存在"Hello World",创建一个String对象,在堆中,value直接复制常量池缓存
  • 当常量池不存在"Hello World",先在堆中创建String对象value是"Hello World",然后缓存"Hello World"到常量池

image-20210827114428457

理解了上面两个最基础的再往下看

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

查看这段代码的字节码文件

image-20210826171357625

神奇的发现,此过程Java会自动转化成StringBuilder来字符串的运算

所以过程变成了:

先创建"a","b"两个常量,然后返回引用

再在堆中创建StringBuilder对象,调用StringBuilder的append方法进行拼接操作,最后会调用toString,生成新对象

这里有一个非常重要的知识点,就是StringBuidler的toString方法

看看它的源代码

image-20210827115830198

它调用的是new String(value, 0 ,count)

image-20210827115902454

而这一段的本质只是创建一个新的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

为什么会这样呢?我们将代码分为上下两部分,逐行分析

上部分:

image-20210827145020114

下部分:

image-20210827150352936

再回头看看前面的概念:

在6及其以前作用是:判断常量池有没有"ab",如果有直接返回常量池中"ab"的引用,否则就缓存一个到"ab"到常量池再返回常量池引用

在7及其以后,变成了:判断常量池有没有"ab",如果有直接返回常量池中"ab"的引用,否则在常量池中放入一个引用,指向堆中的String对象

相信你能理解了?如果你还是不能理解,那我就要祭出我找到的宝藏博文了,此处顶礼膜拜大佬,大佬666,写的我这种菜鸡看完就懂了

ok到这里intern就介绍完了:)

StringBuilder和StringBuffer

前文写了很多介绍Java中的String类,这是Java中使用最最频繁的类,它已经可以完成各种各样的字符串操作了,但Java还是提供了StringBuilder和StringBuffer这两个类,用于操作字符串,同样这里就有几个疑问了:

  1. 为什么Java已经有了String类还是要提供StringBuilder和StringBuffer
  2. StringBuilder和StringBuffer是如何解决String不可变性带来的性能问题的
  3. 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来的更加安全

参考

不开玩笑哦,大佬们写的真的很嗨好啊!!!!再次膜拜😎

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值