String类的学习笔记(中):介绍字符串的不可变性和字符串常量池

本文介绍了String类字符串的不可变性和字符串常量池,主要包括 如何保证字符串不可变, 如何对字符串的修改. 为什么字符串要设置不可变, 字符串常量池的创建和了解,简单的字符串常量池图, 以及如何将字符串手动添加到字符串常量池

一.字符串的不可变性

在Java中String类实例化的String对象实际是一种不可变对象. 字符串中的内容是不可改变。即其字符串在创建后就不可被修改

在这里插入图片描述
在String源码界面也已经标明,Java程序所有字符串文字都作为此类的实例
且字符串当做常量,即其值在创建后不能进行更改!!!

1.如何保证字符串不可变

仅凭一些文字描述限制字符串不能被修改是不现实的~ 在String类中又是如何做到创建的对象其内容不能被修改呢?

在String类中实际上是封装了两个个成员变量
在这里插入图片描述
实际上String对象内表示的字符串实际上就是一个字符数组存储的,故创建一个String对象 还会创建一个字符数组对象,字符串内容即每个字符都存放在字符数组对象内,然后被value数组引用接受
故value数组是用来存放字符串内容的…

而hash变量其涉及到字符串常量池 初始化默认值是0 ,在下面再介绍…

我们可以看到value是被private和final修饰的,而hash被private修饰的
value成员变量被final修饰即表示在实例化字符串对象时,其内一定会被初始化即会指向一个字符数组,

因为value被final修饰则实例化完后value可以看成是一个常量.即其对字符数组对象的指向不能再被更改,但是value指向的字符数组的内容仍然可以被修改

而hash没有被final修饰即它在后续是可以被修改的~

而二者都被private修饰,故只能在String类中被访问,在类外是无法拿到这两个属性的
在类内只有通过成员方法才能访问这两个属性,但是类里面对外提供的所有方法没有能对value和hash进行修改的操作
故由此设计,外部无法访问修改value指向的数组内容即无法修改String字符串内容!

字符串内容是一个字符数组其被value指向
字符串内部通过权限修饰符private保证字符串内容不能被外部访问修改,且对外提供的方法也没有对value指向的数组内容进行修改

2.对字符串的修改

上面说到字符串在Java中都是用String类实例表示而内部被value维护,且其不能被修改,那么上面那些常用的字符串操作方法又是怎么修改字符串的呢?

在这里插入图片描述
上面那些常用的操作方法,但凡涉及到对字符串修改的,随便看一个源码就会发现,只要对字符串进行修改,那么就一定是创建一个新的修改后的字符串对象返回!!!
原字符串没有发生任何变化!!!

所有对String对象的修改,都是创建了一个新的字符串对象进行修改 最后返回新对象的地址

而一般String内容没有被修改时返回的是原字符串对象的地址
如:字符串内字符全是大写 调用toUpperCase没有发生变化 返回的仍然是原字符串

3.为什么字符串要设置不可变

我们使用String类对象存放常量字符串时,会涉及到将字符串对象放到字符串常量池中
字符串常量池可以看成是一个字符串资源区

当我们存放一个字符串对象在字符串常量池内,下一次在使用这个字符串对象,就可以直接从常量池内拿出来使用,可以节省创建新字符串对象所浪费的时间和空间

但如果能够修改字符串的内容,那么意味着修改后的内容在字符串常量池内可能就要换个位置存放,如果不换在后续存放时就会发生存放重复字符串的情况
而如果选择换位置,那每对字符串常量池中的对象内容进行修改就要重新换位置,这样又复杂又降低了性能

而字符串中的hash即是定位字符串对象在字符串常量池中位置,如果字符串内容能被更改,那么其hash每次也要进行修改,其每次还要重新计算hash值

而在多线程情况下,当字符串对象能修改那么每个线程都可以对字符串进行修改,可能就会发生多个线程同时对字符串内容进行修改的情况,是线程不安全的

故设计字符串的内容不可以被修改:

  1. 方便实现字符串对象池. 如果 String 可变, 那么对象池就需要考虑写时拷贝的问题了.
  2. 不可变对象是线程安全的
  3. 不可变对象更方便缓存 hash code, 作为 key 时可以更高效的保存到 常量池 中

二.字符串常量池

什么是池?

“池” 是编程中的一种常见的, 重要的提升效率的方式, 我们会在未来的学习中遇到各种 “内存池”, “线程池”, “数据库连接池”
… 比如:家里给大家打生活费的方式

  1. 家里经济拮据,每月定时打生活费,有时可能会晚,最差情况下可能需要向家里张口要,速度慢
  2. 家里有矿,一次性打一年的生活费放到银行卡中,自己随用随取,速度非常快 方式2,就是池化技术的一种示例,钱放在卡上,随用随取,效率非常高。常见的池化技术比如:数据库连接池、线程池等.

为了节省存储空间以及程序的运行效率,Java中引入了:

  1. Class文件常量池:每个.Java源文件编译后生成.Class文件中会保存当前类中的字面常量以及符号信息
  2. 运行时常量池:在.Class文件被加载时,.Class文件中的常量池被加载到内存中称为运行时常量池,运行时常量池每个类都有一份
  3. 字符串常量池

在Java程序中,类似于:1, 2, 3,3.14,“hello”等字面类型的常量经常频繁使用,为了使程序的运行速度更快、更节省内存,Java为8种基本数据类型和String类都提供了常量池
字符串常量池在JVM中是StringTable类,实际是一个固定大小的HashTable哈希表(一种高效用来进行查找的数据结构),不同JDK版本下字符串常量池的位置以及默认大小是不同的:

在这里插入图片描述

1.字符串常量对象的创建

用"" 括起来的0~n个字符被称为字符串常量,这种写法省略了new关键字,也能直接实例化字符串对象给字符串引用接受,而字符串常量的创建过程会经过字符串常量池
来看看下面代码:
思考一下 下面代码会创建多少个String对象以及运行结果.

public static void main(String[] args) {
    
    
        String s1 = "hello";
        String s2 = "hello";
        String s3 = new String("hello");
        String s4 = new String("hello");
        System.out.println(s1 == s2);   
        System.out.println(s2 == s3); 
        System.out.println(s3 == s4);  
        
    }

结果:上述代码最后创建了三个对象, 运行结果为true false false

字符串内容都是hello 但是产生的结果不同,因为这其中字符串常量还经过了字符串常量池这一变数,要想理解上面代码的运行结果就得再深入了解一下字符串常量池~

2.了解字符串常量池

上面说到字符串常量池是一个StringTable类,即是一个哈希表,本质上就是一个链表+数组+红黑树的结构,为了便于理解这里拿链表+数组举例…

其字符串常量池也就是一个数组,数组每个元素是一个链表,链表每个节点有三个域:
一个域 存放字符串对象地址 ,一个域 存放字符串对象的hash值 ,一个域存放下一个节点的地址

对于字符串常量,其在创建前会在字符串常量池寻找是否存在字符串内容相等的字符串对象

如果存着则直接返回其字符串常量池内的字符串对象地址(使用字符串常量池内的字符串对象)

如果不存在,则会创建一个节点,根据字符串常量创建一个对象,节点内存放该字符串对象的地址 hash 和下一个节点的地址 并将字符串对象地址返回.(将创建的字符串常量放到字符串常量池)

创建一个字符串常量如何在字符串常量池确定其位置呢?

在Java中,字符串常量池是由字符串字面量创建的,它们是在编译时确定的,而不是运行时确定的。因此,每个字符串在编译时都会被分配一个唯一的哈希码。这个哈希码是通过使用字符串的内容计算出来的,通常使用的是一种叫做“Jenkins Hash”的哈希算法

故每""括起来的字符串常量在编译时会得到其hash并定位到字符串常量池即链表数组的某个下标
即可在下标所处的链表里查找是否存在一个节点其内指向的字符串常量对象和字符串常量相等

而在String类里也重写了Object类的hashCode方法 是一种计算字符串的hash值的方法,但此方法不用在字符串常量池处,用于一些在其他需要获取字符串对象的hash如:hashMap

 public int hashCode() {
    
     //String源代码
        int h = hash;
        if (h == 0 && value.length > 0) {
    
    
            char val[] = value;
            for (int i = 0; i < value.length; i++) {
    
    
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

源码分析:

通过调用重写的hashCode方法, 判断当前字符串的hash为0,且字符数组长度大于0的情况下,获取字符数组每个字符 通过每个字符+31*h累计和的形式最后得到一个hash值保存在hash变量中

而当hash值不为0说明已经计算过一次hashCode,因为字符串不可变性,不需要再进行重复计算,直接使用hash变量里计算好的hash值即可,而字符串内容为空即空字符串时其hash值为0返回即可

在这里插入图片描述

以此根据字符串内容的每个字符生成的hash存放到hash变量中

注意:编译时产生的hash和hashCode方法生成的hash是一样的,但是在字符串常量池中,不是经过hashCode生成的hash值,而是使用的编译时生成的hash值
故字符串对象的hash变量在创建对象后仍然会是0,只有在手动调用hashCode时,才会得到计算后的hash并且hash变量才会被更改,

而在常量池里节点的hash会更改为存放的字符串对象的hash,其可以提高在查找和添加字符串的效率

比如后续要再插入字符串常量,直接在字符串常量池中通过比较hash值是否相等,如果相等再比较内容是否相等,

3.分析上述代码运行结果附图解

public static void main(String[] args) {
    
    
        String s1 = "hello";
        String s2 = "hello";
        String s3 = new String("hello");
        String s4 = new String("hello");
        System.out.println(s1 == s2);   
        System.out.println(s2 == s3); 
        System.out.println(s3 == s4);  
    }

第一个"hello" 即会在编译时获取其hash 在字符串常量池里找到对应下标,在节点内寻找是否有字符串对象的内容和hello相等,
而因为"hello"是第一次使用,此时字符串常量池并不存在此对象,则会创建一个hello内容的String对象和其hash值存放在节点中,节点存放在此下标的链表里,同时返回创建的字符串对象地址给s1引用

第二个"hello"获取其hash在字符串常量池内已经有了字符串对象内容和hello相等的对象,此时并不会再创建对象,而返回字符串常量池中指向的hello对象地址给s2引用接收

第三个hello 和前面同理 返回的都是第一个hello创建的对象, 但是 其还有new String()语句,会创建一个新的String对象,将第一个hello对象地址传入其构造方法,最后会使新对象内的value引用指向第一个对象内的value,hash也是第一个对象内的hash
下面是String对应的此构造方法
在这里插入图片描述

为什么上面构造方法不创建新的数组对象而是引用原来的字符数组?
因为创建新数组对象会浪费时间和空间,而由于字符串是不可变的,其value无法被访问修改,所以指向一个字符数组也能满足字符串的基本使用,还节省了资源

第四个hello和上一个同理, 会再创建新的字符串对象,但其内部的value引用是指向第一个字符串对象的value指向的字符数组对象, hash也是第一个字符串对象内的hash

由此可得 s1 和s2 内存放的都是同一个hello对象 ,s3 s4存放的是另外两个不同的字符串对象,但是其内部的value指向的是同一个字符数组 , 此时是创建了三个字符串对象,一个字符数组对象~

而两个引用变量用==比较的是地址,故结果true false false

注意:hash在编译时就获取到了,是否创建字符串对象,取决于在字符串常量池内是否找到要创建的字符串, 在字符串常量池中的hash是根据编译时的方法得到的而不是hashCode
常量池内存放的是根据字符串对象的内容生成的hash值.而创建的字符串对象内本身的hash变量没有发生变化仍然是0

以下是上述代码的简单图解:

在这里插入图片描述

可以看到创建了三个字符串对象,最后s1 s2 指向同一个字符串对象 s3 s4指向不同的字符串对象,但每个对象的value指向的是同一个字符数组对象

4.将字符串手动添加到字符串常量池

使用常量串创建String类型对象会存放在字符串常量池中其效率更高,而且更节省空间。 而也可以将创建的字符串对象通过 intern 方式添加进字符串常量池中。

intern 是一个native方法(Native方法指:底层使用C++实现的,看不到其实现的源代码),该方法的作用是手动将创建的String对象添加到常量池中

分析下面代码 s1. intern 在String s2 上面 出现 和在String s2 下面出现分别有什么不同的结果~

public static void main(String[] args) {
    
    
        char[] ch={
    
    '1','2','3'};
        String s1=new String(ch);  // 实例化一个字符串对象 内部value数组 指向一份拷贝的ch数组
       // s1.intern();   1

        String s2="123";      
//        s1.intern();   2
        System.out.println(s1==s2);
    }

s1.intern在String s2上面 时 s1指向的字符串对象 会获取其编译时生成的hash值映射到字符串常量池中的某个下标,由于字符串常量池中没有"123"对象 则会将其放到字符串常量池中 ,而 再执行String s2=“123”;时, 字符串常量池里已经存在了s1指向的对象,此时返回的是s1指向的对象 结果为true
在这里插入图片描述

s1.intern在String s2下面时, s1指向的对象会被手动添加到字符串常量池中,但是常量池里已经有"123"这个字符串,此时会返回s2指向的字符串,s1字符串并未再添加到字符串常量池,故结果为 false
在这里插入图片描述

但是 如果语句是s1=s1.intern时, s1最后会指向字符串常量池内返回的s2指向的字符串对象, 结果会为true

在这里插入图片描述

注意:在Java6 和 Java7、8中Intern的实现会有些许的差别

三.总结

本文介绍了String类的不可变性,–如何保证String类的不可变(通过private权限封装 和对外公开的方法 )
对字符串进行修改(调用的方法如果对字符串内容进行改变都会创建新的字符串对象更改的是新字符串对象的内容),
为什么字符串对象设置不可变(为了方便字符串常量池的内容不再被修改,hash内容不被改变,使String类线程安全 )

字符串常量池的介绍,为什么存在字符串常量池(提高对常量字符串的利用率,对大量内容相等的常量字符串共用一份对象,节省时间空间资源)
创建字符串常量时的过程加图解,
手动将字符串对象添加到字符串常量池中
(intern关键字的使用)

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/lch1552493370/article/details/130476342
今日推荐