Java String源码剖析+面试题整理

news2024/11/24 7:18:08

由于字符串操作是计算机程序中最常见的操作之一,在面试中也是经常出现。本文从基本用法出发逐步深入剖析String的结构和性质,并结合面试题来帮助理解。

String基本用法

在Java中String的创建可以直接像基本类型一样定义,也可以new一个

String s1 = "Hello World";
String s2 = new String("Hello World");

String可以通过+实现合并

String s = "Hello";
s += "World";

String包装方法

为了方便操作,String包装了一堆操作,大多可以看名字直接使用

public boolean isEmpty()//判断字符串是否为空
public int length() //获取字符串长度
public String substring(int beginIndex) //取子字符串
public String substring(int beginIndex, int endIndex) //取子字符串
public int indexOf(int ch)//查找字符,返回第一个找到的索引位置,没找到返回-1
public int indexOf(String str)//查找子串,返回第一个找到的索引位置,没找到返回-1
public int lastIndexOf(int ch)//从后面查找字符
public int lastIndexOf(String str)//从后面查找子字符串
public boolean contains(CharSequence s)//判断字符串中是否包含指定的字符序列
public boolean startsWith(String prefix) //判断字符串是否以给定子字符串开头
public boolean endsWith(String suffix) //判断字符串是否以给定子字符串结尾
public boolean equals(Object anObject) //与其他字符串比较,看内容是否相同
public boolean equalsIgnoreCase(String anotherString)//忽略大小写比较是否相同
public int compareTo(String anotherString) //比较字符串大小
public int compareToIgnoreCase(String str) //忽略大小写比较
public String toUpperCase()//所有字符转换为大写字符,返回新字符串,原字符串不变
public String toLowerCase()//所有字符转换为小写字符,返回新字符串,原字符串不变
public String concat(String str) //字符串连接,返回当前字符串和参数字符串合并结果
public String replace(char oldChar, char newChar) //字符串替换,替换单个字符
public String replace(CharSequence target, CharSequence replacement)//字符串替换,替换字符序列,返回新字符串,原字符串不变
public String trim()//删掉开头和结尾的空格,返回新字符串,原字符串不变
public String[] split(String regex)//分隔字符串,返回分隔后的子字符串数组

String内部结构

String类内部用一个字符数组表示字符串,实例变量定义为:

private final char value[];

String有两个使用字符数组的构造方法,会根据参数新创建一个数组,并复制内容,而不会直接用参数中的字符数组。

public String(char value[])
public String(char value[], int offset, int count)

可以看出String底层是由字符数组实现,并结合构造方法实现常用方法:

length()方法返回的是这个数组的长度

indexOf()方法查找字符或子字符串时是在这个数组中进行查找

substring()方法是根据参数,调用构造方法String(char value[], int offset, int count)新建了一
个字符串

String不可变性

与包装类类似,String类也是不可变类,即对象一旦创建,就没有办法修改了。根据上面的源码,String类也声明为了final,不能被继承,内部char数组value也是final的,保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

String类中提供了很多看似修改的方法,其实是通过创建新的String对象来实现的,原来的String对象不会被修改。比如,concat()方法的代码,通过Arrays.copyOf方法创建了一块新的字符数组,复制原内容,然后通过new创建了一个新的String。

public String concat(String str) {
       int otherLen = str.length();
       if(otherLen == 0) {
           return this;
       }
       int len = value.length;
       char buf[] = Arrays.copyOf(value, len + otherLen);
       str.getChars(buf, len);
       return new String(buf, true);
}

与包装类类似,定义为不可变类,程序可以更为简单、安全、容易理解。但如果频繁修改字符串,而每次修改都新建一个字符串,那么性能太低,这时,应该考虑Java中的另两个类StringBuilder和StringBuffer。

字符串常量

Java中的字符串常量是非常特殊的,除了可以直接赋值给String变量外,它自己就像一个String类型的对象,可以直接调用String的各种方法。

System.out.println("a".length());

实际上,这些常量就是String类型的对象,在内存中,它们被放在一个共享的地方,这个地方称为字符串常量池,它保存所有的常量字符串,每个常量只会保存一份,被所有使用者共享。当通过常量的形式使用一个字符串的时候,使用的就是常量池中的那个对应的String类型的对象。

所以下面的代码会输出true,因为是同一个对象。

String namel = "a";
String name2 = "a";
System.out.println(name1==name2);

需要注意的是,如果不是通过常量直接赋值,而是通过new创建,==就不会返回true了

String namel = new String("a");
String name2 = new String("a");
System.out.println(namel==name2);

这是因为String类中以String为参数的构造方法代码如下,hash是String类中另一个实例变量,表示缓存的hashCode值。

public String(String original) {
       this.value = original.value;
       this.hash = original.hash;
}

hash变量缓存了hashCode方法的值,也就是说,第一次调用hashCode方法的时候,会把结果保存在hash这个变量中,以后再调用就直接返回保存的值。

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; 
}

如果缓存的hash不为0,就直接返回了,否则根据字符数组中的内容计算hash,计算方法是:
s[0]*31^(n-1)+ s[1]*31^(n-2)+ ...+ s[n-1]。使用这个式子,可以让hash值与每个字符的值有关,也与每个字符的位置有关,位置i(i>=1)的因素通过31的(n-i)次方表示。使用31大致是因为两个原因:一方面可以产生更分散的散列,即不同字符串hash值也一般不同;另一方面计算效率比较高,31*h与32*h-h即(h<<5)-h等价,可以用更高效率的移位和减法操作代替乘法操作。

从下图可以看出,通过new创建name1和name2指向两个不同的String对象,只是这两个对象内部的value值指向相同的char数组。所以name1!=name2,但是name1.equals(name2)的值是true。

String的八股考点

string为什么要设计成不可变类

在Java中将 String设计成不可变的是综合考虑到各种因素的结果。主要的原因主要有以下三点:
(1)字符串常量池的需要:字符串常量池是Java堆内存中一个特殊的存储区域,当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象;
(2)允许 String对象缓存 HashCode: Java中String对象的哈希码被频繁地使用,比如在HashMap等容器中。字符串不变性保证了 hash码的唯一性,因此可以放心地进行缓存。这也是一种性能优化手段,意味着不必每次都去计算新的哈希码;
(3)String被许多的 Java类(库)用来当做参数,例如:网络连接地址URL、文件路径path、还有反射机制所需要的 String参数等,假若 String不是固定不变的,将会引起各种安全隐患。

String s1 = new String("abc");这行代码创建了几个对象

如果字符串常量池中不存在字符串对象“abc”的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。

如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。

String a = new String("aa") + "bb";这行代码创建了多少个对象

共创建了3-4个String对象,假设常量池中没有对象“aa”,第一个是常量池中的对象“aa”,如果常量池中没有就创建并添加进常量池中。第二个是new出来的以“aa”为初始值创建的 String对象,第三个“bb”同“aa”,第四个是通过“+”拼接前两个对象,创建出的新String对象。

String的equals() 和 Object的equals() 有何区别

String 中的 equals 方法是被重写过的,比较的是 String 字符串的值是否相等。 Object 的 equals 方法是比较的对象的内存地址。

String对象最多可以存放多少个字符

String在源码中使用 char[]来维护字符序列的,而char[]的长度是 int类型,所以理论上 String的长度最大为2^31-1,占用空间大约为4GB,不过根据实际JVM的堆内存限制,编译时,String长度最多可以是2的16次方减2,运行时长度最多可以是2的31次方减1,意思是可以在编译时定义一些短的字符串,运行时可以进行拼接,长一点也可以。

string常量池放在哪

Java 8以前被放在永久代中,Java 8及以后被放在方法区的元数据 metadata中。

String str = "i" 和 String str = new String("i")有什么区别

不一样,因为内存的分配方式不一样。String str =“i”的方式,Java虚拟机会将其分配到常量池中;而String str = new String(“i")则会被分到堆内存中。

public class StringTest {

    public static void main(String[] args) {

        String strl = "abc";
        String str2 = "abc";
        String str3 = new String("abc");
        String str4 = new String("abc");
        System.out.println(str1 == str2); // true
        System.out.println(str1 == str3); // false
        System.out.println(str3 = str4); // false
        System.out.println(str3.equals(str4)); // true

在执行String str1=“abc”的时候,JVM会首先检查字符串常量池中是否已经存在该字符串对象,如果已经存在,那么就不会再创建了,直接返回该字符串在字符串常量池中的内存地址;如果该字符串还不存在字符串常量池中,那么就会在字符串常量池中创建该字符串对象,然后再返回。所以在执行Stringstr2=“abc”的时候,因为字符串常量池中已经存在“abc”字符串对象了,就不会在字符串常量池中再次创建了,所以栈内存中str1和str2的内存地址都是指向“abc”在字符串常量池中的位置,所以str1=str2 的运行结果为 true。

而在执行 String str3 = new String(“abc")的时候,JVM会首先检查字符串常量池中是否已经存在“abc”字符串,如果已经存在,则不会在字符串常量池中再创建了;如果不存在,则就会在字符串常量池中创建“abc”字符串对象,然后再到堆内存中再创建一份字符串对象,把字符串常量池中的“abc”字符串内容拷贝到内存中的字符串对象中,然后返回堆内存中该字符串的内存地址,即栈内存中存储的地址是堆内存中对象的内存地址。String str4 = newString(“abc”)是在堆内存中又创建了一个对象,所以 str3 == str4运行的结果是 false。

String修改的实现原理

当用String类型来对字符串进行修改时,其实现方法是首先创建一个StringBuilder,其次调用
StringBuilder的 append()方法,最后调用StringBuilder 的 toString()方法把结果返回。

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

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

相关文章

骑砍MOD天芒传奇-天芒使用方法

骑砍1战团mod天芒传奇-使用红色天芒碎片开P51战斗机_单机游戏热门视频 (bilibili.com)https://www.bilibili.com/video/BV1nm41197iA/ 一.黄色天芒碎片 天芒盒子 野外战斗H键-召唤徐天地 二.绿色天芒碎片 天芒盒子 野外战斗H键-站在巨人肩膀上战斗 三.蓝色天芒碎片 天芒盒…

华为问界M9:全方位自动驾驶技术解决方案

华为问界M9的自动驾驶技术采用了多种方法来提高驾驶的便利性和安全性。以下是一些关键技术&#xff1a; 智能感知系统&#xff1a;问界M9配备了先进的传感器&#xff0c;包括高清摄像头、毫米波雷达、超声波雷达等&#xff0c;这些传感器可以实时监测车辆周围的环境&#xff0…

车载电子电器架构 —— 电子电气系统功能开发

车载电子电器架构 —— 电子电气系统功能开发 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 本就是小人物,输了就是输了,不要在意别人怎么看自己。江湖一碗茶,喝完再挣扎,出门靠自己,四海皆…

几个好用的 iphone 手机模板贴图样机

整理了几个好用的 iphone 手机模板贴图&#xff0c;分享一下。 关注订阅号「设计师工作日常」&#xff0c;发送关键词 iphone mockup ,获取下载链接。 [1] 原文阅读 我是 Just&#xff0c;这里是「设计师工作日常」&#xff0c;求点赞求关注&#xff01;

huggingface学习|用dreambooth和lora对stable diffusion模型进行微调

目录 用dreambooth对stable-diffusion-v1-5模型进行微调&#xff08;一&#xff09;模型下载和环境配置&#xff08;二&#xff09;数据集准备&#xff08;三&#xff09;模型微调&#xff08;四&#xff09;运行微调后的模型 用lora对stable-diffusion-v1-5模型进行微调&#…

windows 下安装gin

go install 执行命令&#xff0c;执行不了的参考一下 https://blog.csdn.net/weixin_42592326/article/details/135946806 Golang 中没法下载第三方包解决办法-CSDN博客 go install github.com/gin-gonic/ginlatest 还是安装不了的话&#xff0c;用手机开热点&#xff0c;电…

在程序中使用日志功能

在应用中&#xff0c;需要记录程序运行过程中的一些关键信息以及异常输出等。这些信息用来排查程序故障或者其他用途。 日志模块可以自己实现或者是借用第三方库&#xff0c;之前写过一个类似的使用Qt的打印重定向将打印输出到文件&#xff1a;Qt将打印信息输出到文件_qt log输…

PyCharm2023.3.2配置conda环境

重点在于Path to conda这一步&#xff0c;需要找到conda.bat这个文件&#xff0c;PyCharm才能识别出现有的conda环境。

配置VMware实现从服务器到虚拟机的一键启动脚本

正文共&#xff1a;1666 字 15 图&#xff0c;预估阅读时间&#xff1a;2 分钟 首先祝大家新年快乐&#xff01;略备薄礼&#xff0c;18000个红包封面来讨个开年好彩头&#xff01; 虽然之前将服务器放到了公网&#xff08;成本增加了100块&#xff0c;内网服务器上公网解决方案…

c语言游戏实战(6):走迷宫之推箱子

前言&#xff1a; 在上一篇文章当中我介绍了一个走迷宫的写法&#xff0c;但是那个迷宫没什么可玩性和趣味性&#xff0c;所以我打算在迷宫的基础上加上一个推箱子&#xff0c;使之有更好的操作空间&#xff0c;从而增强了游戏的可玩性和趣味性。 1. 打印菜单 void menu() {…

【DDD】学习笔记-UML 与彩色建模

如果某个领域已经形成了稳定的分析模式&#xff0c;在设计该领域的分析模型时&#xff0c;这些模式就可以提供有价值的参考。可惜&#xff0c;分析模式需要有人来总结和提炼&#xff0c;最好的分析模式提炼者需要兼具领域知识和软件建模能力。很早以前&#xff0c;Martin Fowle…

nodejs切换版本

sudo n 18.17.0 sudo n然后键盘上下选择

Vue核心基础6:Vue内置指令、自定义指令、生命周期

1 Vue中的内置指令 <script>const vm new Vue({el: #root,data: {n: 1,m: 100,name: Vue,str: <h3>你好</h3>}})</script> 1.1 v-text <div v-text"name"></div>1.2 v-html <div v-html"str"></div> …

SpringCloud-高级篇(二十)

下面我们研究MQ的延迟性问题 &#xff08;1&#xff09;初始死信交换机 死信交换机作用一方面可以向Public的异常交换机一样做异常消息的兜底方案&#xff0c;另一方面&#xff0c;可以处理一些超时消息&#xff0c;功能比较丰富一点 &#xff08;2&#xff09;TTL 上面学习…

Java基础:值传递和引用传递

Java在给方法传递参数时&#xff0c;有值传递和引用传递两种方式。 基本概念 值传递&#xff1a;传递对象的一个副本&#xff0c;即使副本被改变&#xff0c;也不会影响源对象&#xff0c;因为值传递的时候&#xff0c;实际上是将实参的值复制一份给形参。 引用传递&#xf…

猫头虎分享已解决Bug || ValueError: Data cardinality is ambiguous

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通鸿蒙》 …

【Cocos入门】物理系统

物理引擎默认是关闭状态以节省资源开销。开启方法和之前的普通碰撞类似:cc.directorgetPhysicsManager().enabled true但有一个区别&#xff0c;物理引擎的开启必须放在onLoad函数内运行&#xff0c;否则不生效。 开启物理引擎后&#xff0c;游戏运行&#xff0c;会发现添加…

C++多态重难点

CSDN上已经有很多关于C多态方面的一些系统介绍了&#xff0c;但是我看了一下一些有关于多态问题的细节问题文章较少&#xff0c;因此我想要出一片文章重点讲一讲我认为比较重点且容易被遗忘的知识点&#xff0c;一些比较基本的知识这里就不过多赘述了&#xff0c;可以参考其他优…

controller-manager学习三部曲之二:源码学习

欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码)&#xff1a;https://github.com/zq2599/blog_demos 本篇概览 作为《controller-manager学习三部曲》系列的第二篇&#xff0c;前面通过shell脚本找到了程序的入口&#xff0c;接下来咱们来学习controller-mana…

第三百一十八回

文章目录 1. 概念介绍2. 使用方法2.1 本地缓冲2.2 服务器缓冲3. 示例代码4. 内容总结我们在上一章回中介绍了"如何让输入键盘不遮挡屏幕"相关的内容,本章回中将介绍如何有效地缓冲网络图片.闲话休提,让我们一起Talk Flutter吧。 1. 概念介绍 我们在本章回中介绍的…