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

news2024/9/29 15:25:28

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

字符串不可变性和字符串常量池

  • 一.字符串的不可变性
    • 1.如何保证字符串不可变
    • 2.对字符串的修改
    • 3.为什么字符串要设置不可变
  • 二.字符串常量池
    • 1.字符串常量对象的创建
    • 2.了解字符串常量池
    • 3.分析上述代码运行结果附图解
    • 4.将字符串手动添加到字符串常量池
  • 三.总结

一.字符串的不可变性

在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关键字的使用)

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/486019.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

考研数学经验分享

考研数二经验 先说一下自我情况吧&#xff0c;我是23计算机专硕考研&#xff0c;本科是河北一本双非&#xff0c;考的是数二英二和408。相对其他专业来说&#xff0c;计算机的专业课408由于要学四本书&#xff0c;所以会占用大部分时间&#xff0c;因此也会挤掉一些数学和英语的…

文本识别、截图识别保存和多文件识别

一、源码 github源码 二、介绍 采用Tesseract OCR识别 采用多线程进行图片识别 界面 选择 文件是可以识别本地的多张图片文件夹是识别文件夹里面的所有图片的内容截图 可以复制到剪切板、可以识别也可以直接保存 重置 是清除选择的图片和识别结果语言选择 是选择不同的模型…

AX7A200教程(7): 基于DDR3的串口发送和接收(64次突发)(二)

本章节主要使用ddr3做为缓存&#xff0c;串口接收的数据通过ddr缓存后通过发送模块发送出去。我们之前的串口发送数据通过ddr缓存是一个突发长度&#xff0c;本篇文章将会传输64个突发长度。整体的功能框图所下图所示 因本博文使用的是上个章节的图片数据&#xff0c;所以数据大…

微服务学习——分布式搜索

初识elasticsearch 什么是elasticsearch elasticsearch是一款非常强大的开源搜索引擎&#xff0c;可以帮助我们从海量数据中快速找到需要的内容。 elasticsearch结合kibana、Logstash、Beats&#xff0c;也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域…

Nmap入门到高级【第十一章】

预计更新第一章. Python 简介 Python 简介和历史Python 特点和优势安装 Python 第二章. 变量和数据类型 变量和标识符基本数据类型&#xff1a;数字、字符串、布尔值等字符串操作列表、元组和字典 第三章. 控制语句和函数 分支结构&#xff1a;if/else 语句循环结构&#…

linux系统下如何调试C/C++程序【越早知道越好】

前言 研发语言是C或者C的小伙伴&#xff0c;无论是从事服务端开发&#xff0c;还是客户端开发&#xff0c;都需要掌握linux系统下关于C/C代码的调试能力。 正文 入门示例 首先&#xff0c;我们通过一段存在问题的代码来演示整个调试过程以及问题分析定位的方法和手段。 示例代码…

如何使用 ChatGPT 生成 Stable diffusion 提示词

chatgpt免费体验入口网址&#xff1a; http://chat.xutongbao.top 一、教chatgpt写提示词 StableDiffusion是一款利用深度学习的文生图模型&#xff0c;支持通过使用提示词来产生新的图像&#xff0c;描述要包含或省略的元素。 我在这里引入StableDiffusion算法中的Prompt概念…

mysql 8.0安装全过程(linux上二进制包安装,非root用户)

目录 介绍 安装 0、删除随系统安装的mariadb 1、下载 2、解压 3、创建用户 4、创建数据目录 5、核心启动配置文件 6、安装依赖 7、初始化mysql文件 8、启动服务 9、配置环境变量 10、改root登录密码 可能遇到错误&#xff1a; 1&#xff09; libaio.so.1 缺少 2…

ASP.NET Core MVC 从入门到精通之HttpContext

随着技术的发展&#xff0c;ASP.NET Core MVC也推出了好长时间&#xff0c;经过不断的版本更新迭代&#xff0c;已经越来越完善&#xff0c;本系列文章主要讲解ASP.NET Core MVC开发B/S系统过程中所涉及到的相关内容&#xff0c;适用于初学者&#xff0c;在校毕业生&#xff0c…

kafka常见问题QA(六)

六、常见问题QA 6.1 无消息丢失如何配置 producer 调用方式 &#xff08;1&#xff09;网络抖动导致消息丢失&#xff0c;Producer 端可以进行重试。 &#xff08;2&#xff09;消息大小不合格&#xff0c;可以进行适当调整&#xff0c;符合 Broker 承受范围再发送。 不要使用…

【Linux Network】网络编程套接字

目录 1. 源IP地址与目的IP地址的认识 2. 端口号的认识 3. 套接字socket 4. TCP协议和UDP协议 5. 网络字节序 6. socket编程 7. socket编程接口 8. 使用UDP协议跨网络通信程序 Linux网络编程✨ 1. 源IP地址与目的IP地址的认识 在因特网上&#xff0c;一台主机和一个IP地址往往是…

【VM服务管家】VM4.2平台SDK_6.1 环境配置类

目录 2.1.1 环境配置&#xff1a;基于Csharp二次开发环境配置方法2.1.2 环境配置&#xff1a;基于MFC二次开发环境配置方法2.1.3 环境配置&#xff1a;基于Qt二次开发环境配置方法2.1.4 用户权限&#xff1a;普通以EXE方式启动Server的方法2.1.5 环境配置&#xff1a;程序启动后…

基于类别级正则化的无监督域自适应眼底图像分割

文章目录 Unsupervised Domain Adaptive Fundus Image Segmentation with Category-Level Regularization摘要方法Inter-domain Category RegularizationSource Domain Category RegularizationTarget Domain Category Regularization总损失 Unsupervised Domain Adaptive Fund…

总结目前敏捷开发框架(持续更新....)

文章目录 0 敏捷开发1 类型分类1.1. Scrum1. 2.极限编程&#xff08;XP&#xff09;1. 3. 快速应用程序开发 (RAD)1. 4. 动态系统开发方法&#xff08;DSDM&#xff09;1.5.统一流程&#xff08;UP&#xff09;1. 6. 精益方法1. 7. 看板1. 8.FDD&#xff08;功能驱动开发&#…

2023年最新版【接口自动化测试,web自动化测试,app自动化测试】全套自动化测试面试题

前言&#xff1a; 自动化测试是软件测试中的一个重要领域&#xff0c;它可以帮助企业提高软件开发质量、缩短测试周期和降低测试成本。随着信息技术的不断发展&#xff0c;自动化测试也在不断地创新和发展。本篇文章收集了2023年最新版的接口自动化测试、Web自动化测试和App自…

通俗理解CNN感受野的计算方法

x o u t x i n − k s 1 x_{out} \frac{x_{in} - k}{s} 1 xout​sxin​−k​1 如果不考虑padding&#xff0c;卷积输出的feature map的计算公式如上&#xff0c;那么 x i n ( x o u t − 1 ) ∗ s k x_{in} (x_{out} - 1) * s k xin​(xout​−1)∗sk。因此计算模型的感…

springbooot使用google验证码

springbooot使用google验证码 1、使用场景2、springboot使用google验证码1、引入依赖2、编写配置类3、编写控制层4、前端实现 1、使用场景 由于需要做一个前后端分离的项目&#xff0c;想着使用google验证码&#xff0c;由于年龄大了&#xff0c;这些知识啊&#xff0c;用完就…

防伪标志使用的全息薄膜,竟可合成大自然的“结构色”

大自然为生物赋予了各种各样的色彩&#xff0c;除了常见的赤橙黄绿青蓝紫外&#xff0c;还有反光效果很好的金属色等等。有趣的是&#xff0c;一些生物身上的颜色也能像金属那样闪闪发光&#xff0c;在不同光线下颜色甚至还会有变化。实际上&#xff0c;大自然很多色彩由色素产…

全注解下的SpringIoc 续6-多环境配置

我们都知道&#xff0c;在企业开发过程中&#xff0c;一个项目往往都会有开发、测试、仿真、生产等环境配置&#xff0c;除了使用配置中心&#xff08;比如Apollo等&#xff09;之外&#xff0c;Spring Boot也提供了不同环境之间切换的机制&#xff0c;下面让我们来一起看看。 …

Docker安全最佳实践

目录 1、探测容器开放端口和服务漏洞 2、宿主机、网络、镜像、DockerApi安全 3、更新Docker、日志、事件 4、Docker安全测试 5、Docker安全最佳实践 1、探测容器开放端口和服务漏洞 使用Nmap扫描Docker容器中的开放端口 使用docker ps命令获取正在运行的容器ID或名称。在…