String的那些事儿

news2024/12/24 21:38:28

String作为我们最常用的Java类之一,在日常开发过程中充当着重要角色?那么大家真的了解String吗?让我们一起看看下面的问题:

  • String内存结构?对象存储在堆上还是栈上?
  • 一个String有多长?占内存多大?
  • 字符串拼接过程中内存对象是怎么管理的?
  • String.intern是用来干嘛的?
  • String底层数据结构是什么?
  • String为什么设计成final类?
  • 请简单实现下String的equals方法
  • String的hashCode方法有了解吗?有没有可能两个String的值不同,但是hashCode相同?这种情况被称为什么?有什么解决方案?

String内存分配与最大长度

要谈String的内存分配与最大大小,我们首先应该清楚String中Java中有两种创建方式,如下所示:

// 直接使用""定义
String hello = "Hello";

// 使用new关键词创建
String nextHello = new String("nextHello");

那么这两种创建的String有什么不同呢?我们一起来看下,编写测试代码如下:

public class StringTest {
    public void testString(){
        String s1 = "Hello";
        String s2 = new String("nextHello");
        System.out.println(s1);
        System.out.println(s2);
        s1 = "World";
        System.out.println(s1);
    }
}

查看StringTest类字节码,如下所示:

1-3-5-1

1-3-5-2

1-3-5-3

可以看出使用"“创建的Hello和World字符串均声明在常量池中,而常量池中一个字符串的最大长度为65535个字符,故使用”"创建的字符串最大长度为65535.最大内存大小为65535*2 byte 约等于 128 KB。

继续查看StringTest的字节码文件,可以看到String s2 = new String("nextHello");对应的字节码如下所示:

1-3-5-4

可以看出这里首先在常量池中生成nextHello字符串,随后在堆上创建String对象,通过astore_2指令将新创建的对象引用赋值给s2。

ldc,astore_2这两个指令的说明也能印证上述结论,两个指令的说明如下所示:

1-3-5-6

1-3-5-5

那么通过new String创建的字符串最大长度是多少呢?由于String底层使用char数组实现,故其最大长度为Integer.MAX_VALUE个字符(理论上),最大内存大小为2*Integer.MAX_VALUE byte 约等于 4GB。

String为什么被设计成final?

从final那些事儿一文中可以知道,final修饰类时,则该类不可被继承,也就意味着其内部的实现和依赖是不可更改的,进而导致不论如何创建的String实例,其必然关联的是String对象,而不可能是其他类的对象,String不可变,那么什么是String不可变,为什么要设计成不可变的呢?

String不可变

以下述代码为例:

String s  = "abc";
s = "abcd";

其执行顺序如下,常量池创建"abc"字符串,变量s指向常量池新创建的字符串,随后在常量池中创建字符串"abcd",再次将s指向"abcd"所在地址,而不是将字符串"abc"修改为字符串"abcd"。

为什么String不可变

在源码中可以看到,为了实现String不可变,在String内部,底层数据结构是char数组,该数组同样final修饰,从final那些事儿一文可知,如果final修饰数组,只要数组指向地址不变,其内部元素值可以发生改变,故而进一步也用final修饰了String类,同样在该类并没有暴露任何访问内部成员字段的接口,进而最终实现String不可变。

当然反射修改String底层数组中的元素值仍然是可行的,但没有必要

String不可变的优势

String不可变主要有以下优势:

  • 安全:在Java中,函数传参使用引用传递,String作为广泛应用的类型,如果其可变,在多个函数的层次传递过程中,极有可能被修改导致异常,同时HashMap,HashSet等类都可以使用String作为key,如果String可变,则键值,整个存储结构就乱套了。
  • 内存利用率高,提升效率:在大量使用字符串的场景下,字符串重用,减少内存开销,避免频繁GC

String拼接原理

在日常开发中,我们经常使用 " + " 号进行字符串拼接,那么其底层是如何实现的呢?编写测试代码如下:

public class Person {
    private int mAge = 100;

    public Person(int mAge) {
        this.mAge = mAge;
    }

    public int getAge() {
        return mAge;
    }
}

public class StringTest {
    public void testString() {
        Person person = new Person(200);
        String s1 = "String Test";
        int a = 300;
        System.out.println("Test String append:" + s1 + ",person.Age:" + person.getAge() + ",a:" + a);
    }
}

public class Main {
    public static void main(String[] args) {
        StringTest stringTest = new StringTest();
        stringTest.testString();
    }
}

查看StringTest的testString函数字节码,如下图所示:

1-3-5-7

可以看出,对于System.out.println("Test String append:" + s1 + ",person.Age:" + person.getAge() + ",a:" + a);从其字节码可以看出,“Test String append:”,",person.Age:“以及”,a:"这三个字符串,存储在字符串常量池中,通过ldc指令取出,在整个代码执行流程中,主要是StringBuilder.append来实现字符串拼接的,最终将拼接后的内容输出。

以上例子都是变量参与字符串拼接,那么如果有常量参与,又会怎样呢?

1-3-5-8

可以看出,当常量参与字符串拼接时,编译器会直接替换内容(如变量s3),如果参与拼接的全是常量,则在编译时就会直接完成拼接操作(如变量s4)。

String.intern

查看String类的源码,可以看到String.intern是个native方法,声明如下所示:

/**
 * Returns a canonical representation for the string object.
 * <p>
 * A pool of strings, initially empty, is maintained privately by the
 * class {@code String}.
 * <p>
 * When the intern method is invoked, if the pool already contains a
 * string equal to this {@code String} object as determined by
 * the {@link #equals(Object)} method, then the string from the pool is
 * returned. Otherwise, this {@code String} object is added to the
 * pool and a reference to this {@code String} object is returned.
 * <p>
 * It follows that for any two strings {@code s} and {@code t},
 * {@code s.intern() == t.intern()} is {@code true}
 * if and only if {@code s.equals(t)} is {@code true}.
 * <p>
 * All literal strings and string-valued constant expressions are
 * interned. String literals are defined in section 3.10.5 of the
 * <cite>The Java&trade; Language Specification</cite>.
 *
 * @return  a string that has the same contents as this string, but is
 *          guaranteed to be from a pool of unique strings.
 */
public native String intern();

从函数注释可以看出,当该函数被调用时,如果字符串常量池中已经有一个相同的字符串(equals返回true),则直接返回常量池中的字符串,如果常量池中不存在,则添加字符串到常量池并返回该字符串的引用

对于直接使用""创建的字符串而言,我们知道其创建在常量池中,故而调用其intern方法与直接访问并没有内存上的实际差异,我们重点来看下使用new关键字创建字符串的场景。首先我们来思考下String s2 = new String("Hello") + new String(" String");这行代码会创建几个对象?

其实前文中已有答案,其一共会有6个对象生成,如下图所示:

1-3-5-9

1-3-5-10

了解了new String参与时,相关对象的创建过程,我们接下来看下下述代码:

public void testString() {
    String s1 = new String("Hello  String");
    String s3 = s1.intern();
    String s2 = "Hello String";
    System.out.println(s1 == s2);
    System.out.println(s2 == s3);
}

运行后可以看到 s1 == s2返回false,s2 == s3返回true,也进一步说明String.intern的含义,s1不等于s2主要是因为s1指向的是堆内存的字符串地址,而s2指向的是常量池中的内存地址,s3等于s2,主要是经过s1.intern后,s3指向的是常量池中已经存在的"Hello String"字符串,后面创建s2时,由于常量池中已经存在,故两个变量指向的是同一个内存地址。

那么我们再来看下如下字符串拼接代码:

public class StringTest {
    public void testString() {
        String s1 = new String("Hello") + new String(" String");
        s1.intern();
        String s2 = "Hello String";
        System.out.println(s1 == s2);
    }
}

这里应该输出true还是false呢?这里返回的是true,大家猜对了吗?

查看这段代码对应的字节码如下:

1-3-5-11

可以看出,s1的赋值代码实际上等价于String s1 = StringBuilder.toString()而StringBuilder.toString是通过new String创建一个堆内存对象,此时再执行s1.intern,由于字符串常量池中无该字符串,则会将该字符串的引用添加到常量池中,进而后续创建的s2指向的也是常量池中该字符串的引用地址(内容一致)

String.equals

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

String.equals源码如上所示,可以看出其首先比较的时两个对象的引用地址是否相同,如果相同的话直接返回true,随后逐位比较字符串的内容,如果有一位不相同,则返回false,否则返回true。

String.hashCode

public int hashCode() {
    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;
}

String.hashCode源码如上所示,可以看到其是逐位递归,依此对每一位字符应用算法, 将最后一位的算法取值作为全串的hash值。

最后一位的算法取值 = 31 * 前一位的算法取值 + 当前位的字符编码

从其算法可以看出,必然会出现两个不同的字符串hash取值一样的情况,这种情况被称为hash冲突,比较常见的hash冲突解决方案有链地址法和开放地址法,链地址法的经典实现就是HashMap,将hash值相同的key以链表形式存储下来,而开放地址法指的是在已有的hash值相同的元素后方,寻找空余位置处存放当前元素,当然也有其他的hash冲突解决方案,我们后续单独介绍。

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

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

相关文章

享元设计模式解读

目录 问题引进 展示网站项目需求 传统方案解决网站展现项目 传统方案解决网站展现项目-问题分析 享元模式基本介绍 基本介绍 享元模式的原理类图 对类图的说明 内部状态和外部状态 享元模式解决网站展现项目 应用实例要求 思路分析和图解(类图) 代码实现 享元模式…

创建一个 vue 3 项目

vue create projectNameVue CLI v5.0.8 ? Please pick a preset: ❯ Default ([Vue 3] babel, eslint)Default ([Vue 2] babel, eslint)Manually select featuresbabel : ES2015 and beyond。Babel 默认使用一组 ES2015 语法转换器&#xff0c;允许你使用新的语法&#xff0c…

vue2数据响应式原理(6) 处理数组特殊遍历

打开一直在写的案例 然后 找到src下的 dataResp.js 这里 我们Observer中 数数组和对象还是要分开处理 因为他们还是有所不同 我们修改 Observer 类代码如下 class Observer{constructor(value) {//相当于 给拿到的对象 其中的__ob__绑定 值为thsi,在类中用this 表示取实例本…

数据结构入门(C语言版)一篇文章教会你手撕八大排序

八大排序 排序的概念常见的排序算法排序算法的实现一、直接插入排序二、希尔排序三、选择排序四、堆排序五、冒泡排序六、快速排序1.递归写法①三位取中函数②hoare版本③挖坑法④前后指针版本⑥快排主函数 2.非递归写法 七、归并排序1.递归写法2.非递归写法 八、非比较排序1.基…

倾斜摄影三维模型顶层合并技术及其实现方法

倾斜摄影三维模型顶层合并技术及其实现方法 倾斜摄影三维模型由于数据量大、结构复杂&#xff0c;常常需要进行顶层合并&#xff0c;以便更好地应用到城市规划、土地管理和文化遗产保护等领域。本文将介绍倾斜摄影三维模型顶层合并技术及其实现方法。 1、什么是顶层合并 倾斜…

【安全与风险】普适计算中的安全与隐私研究

普适计算中的安全与隐私研究 日常生活的数字化无处不在的计算对移动社交媒体的影响讨论更便宜的存储和更强大的处理的影响移动和普适计算的影响有时候&#xff0c;惊奇另一个例子攻击模型贡献是什么&#xff1f;智能家居的案例研究本文的主要内容如何自学? 日常生活的数字化 …

第三章 使用 Maven:命令行环境

第一节 实验一&#xff1a;根据坐标创建 Maven 工程 Maven 核心概念&#xff1a;坐标 ①数学中的坐标 使用 x、y、z 三个**『向量』作为空间的坐标系&#xff0c;可以在『空间』中唯一的定位到一个『点』**。 ②Maven中的坐标 [1]向量说明 使用三个**『向量』在『Maven的仓…

不知道今天吃什么?今天吃什么 API 告诉你

引言 在现代社会&#xff0c;由于生活节奏加快和繁忙的工作日程&#xff0c;越来越多的人感到选择今天吃什么餐点是一项繁琐且令人困扰的任务。为了解决这个问题&#xff0c;许多人会求助于在线菜谱和美食博客等渠道&#xff0c;但是这些选项通常是繁琐和耗时的。 幸运的是&a…

聚观早报 | 马斯克称星舰1-2个月内准备再发射;推特撤下官媒标签

今日要闻&#xff1a;马斯克称星舰1-2个月内准备再发射&#xff1b;推特撤下「官媒」标签&#xff1b;Pixel Fold 折叠机型首次被泄露&#xff1b;蔚来员工曝半年加班500小时&#xff1b;苹果Mac Pro和Mac Studio无缘WWDC 马斯克称星舰1-2个月内准备再发射 美国当地时间 4 月 …

关于GNSS技术介绍(二)

在上期文章中&#xff0c;我们介绍了GNSS技术的发展历程、原理&#xff0c;并对不同类型的定位技术进行了介绍&#xff0c;在本期文章中我们将继续讨论GNSS的优点与应用及其测试方法和解决方案。 GNSS的优点与应用 目前GNSS技术已经成为日常生活不可或缺的一部分&#xff0c;几…

自定义RecyclerView.LayoutManager实现类实现卡片层叠布局的列表效果

一.前言 先看效果&#xff08;大佬们请忽略水印&#xff09;&#xff1a; 卡片层叠列表的实现效果已经发布成插件&#xff0c;集成地址&#xff1a;implementation ‘com.github.MrFishC:YcrCardLayoutHepler:v1.1’&#xff1b; 先讲解如何快速实现&#xff0c;然后再来讲解…

小程序开发费用估算:如何控制项目成本?

在当今数字化的时代&#xff0c;小程序已经成为了很多企业和个人开展业务的重要手段。小程序的开发需要耗费时间和资源&#xff0c;因此在项目初期&#xff0c;了解预计的开发费用是非常重要的。本文将详细介绍如何估算小程序开发费用以及如何控制项目成本。 小程序开发费用 …

2023年重庆经济发展研究报告

第一章 发展概况 1.1 地理和人口发展概况 重庆&#xff08;简称“渝”&#xff09;位于中国西南地区&#xff0c;是中国四个直辖市之一&#xff0c;地处长江中上游&#xff0c;横跨东经10517到11011&#xff0c;北纬2810到3213之间。重庆市地势复杂&#xff0c;地形多山&…

风光场景削减及源荷不确定性的虚拟电厂随机优化调度研究(Matlab代码实现)

&#x1f4a5; &#x1f4a5; &#x1f49e; &#x1f49e; 欢迎来到本博客 ❤️ ❤️ &#x1f4a5; &#x1f4a5; &#x1f3c6; 博主优势&#xff1a; &#x1f31e; &#x1f31e; &#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 …

Ubuntu16.04配置使用robosense雷达18版本的rsview软件记录

目录 依赖安装boost安装下载源码解压编译安装更新一下系统的动态链接库 libpng16-16安装 rsview运行下载运行具体操作可以看doc文件夹内的说明文件 参考文章 依赖安装 boost安装 下载源码 boost1.65官方下载地址 解压 进入下载文件所在的文件夹 tar -zxvf boost_1_65_1.t…

托福高频真词List05 // 附托福TPO阅读真题

目录 4月23日单词 生词 熟词 4月24日真题 4月23日单词 生词 sparsethinly distributedadj 稀疏的sparselythinlyadv 稀疏地congestion / kənˈdʒestʃən / overcrowdingn 拥挤continuallyregularlyadv 持续的eradicateeliminatev 消除facilitatemake easiereasev 使..…

M2下通过docker安装sqlserver

背景&#xff1a;要做这一个需求&#xff0c;将txt文件格式化以后&#xff0c;入库到sqlserver中&#xff0c;没有测试环境&#xff0c;只能自己在本地搭建一个sqlserver的服务器 前提&#xff1a;安装好docker 第一步&#xff1a;在终端下查找sqlserver的镜像文件 我们可以选…

S7-1200通过模拟量的方式实现PID控制恒压供水的具体方法示例

S7-1200通过模拟量的方式实现PID控制恒压供水的具体方法示例 具体方法和步骤可参考以下内容: 创建工艺对象,如下图所示,有2种方式: PID_Compact指令需要在OB30中断组织块中调用,如下图所示, 使用 PID 控制器前,需要对其进行组态设置,分为基本设置、过程值设置、高级设…

Stm32开发环境从0搭建(Clion作为开发软件)

环境安装 1. 下载CLion开发软件和安装 https://www.jetbrains.com/zh-cn/clion/2. 安装msys2开发环境 一直下一步 参考文档 安装OpenOCD工具(烧录下载工具) 将压缩包解压到环境安装目录下: 安装Arm-Gcc编译环境 将此目录拷贝到安装目录&#xff0c;并解压 配置环境变量 …

实测有效!手把手带你将 Docker Image 体积减少 90%

Docker Image 体积越大,那部署要花的时间就越长;假如每个版本都有好几 GB,那并不是一个理想的状态;因此笔者开始动手实作,想看看到底能将 Docker Image 的体积缩小多少! 大纲 ㄧ、先初始化一个简易的 Node.js 专案 二、撰写 Dockefile,了解优化前体积有多大 三、使用 No…