一文看完String的前世今生,内容有点多,请耐心看完!

写在开头

String字符串作为一种引用类型,在Java中的地位举足轻重,也是代码中出现频率最高的一种数据结构,因此,我们需要像分析Object一样,将String作为一个topic,单独拿出来总结,这里面涉及到字符串的不可变性,字符串拼接、存储、比较、截取以及StringBuffer,StringBuilder区别等。

String类的源码

源码解读

想要真切的去了解Java中被定义好的一个类,读源码是最直接的方式,以经典的Java8为例(Java9之后,内部的实现数组类型从char改为了byte,目的用来节省内存空间),我们来看看Java中对于String是如何设计的。

public final class String implements java.io.Serializable, 
Comparable<String>, CharSequence {
    private final char value[];
  //...
}

我们从源码中可以总结出如下几点内容:

1. String类被final关键字修饰,意味着它不可被继承;;
 2. String的底层采用了final修饰的char数组,意味着它的不可变性;
 3. 实现了Serializable接口意味着它支持序列化;
 4. 实现了Comparable接口,意味着字符串的比较可以采用compareTo()方法,而不是==号,并且Sring类内部也重写了Object的equals()方法用来比较字符串相等。

String如何实现不可变得性?

从过源码我们可以看到类和char[]数组均被final关键字修饰,且数组的访问修饰符为private,访问权限仅限本类中。
final关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。但光用final修饰只能保证不被子类继承,不存在子类的破坏,char数组中的字符串仍然是可以改变的。
但,当底层实现的这个char[]被private修饰后,代表着它的私有化,且String没有对外提供修改这个字符串数组的方法,这才导致了它的不可变!

String如为什么要不可变?

那么问题来了,String为什么要设计成不可变的呢?我们都知道,不可变意味着,每次赋值其实就是创建一个新的字符串对象进行存储,这无疑带来了诸多不便。但相比于以下2点,那些不便似乎无关紧要了
1、String 类是最常用的类之一,为了效率,禁止被继承和重写
2、为了安全。String 类中有很多调用底层的本地方法,调用了操作系统的API,
如果方法可以重写,可能被植入恶意代码,破坏程序。其实Java 的安全性在这里就有一定的体现啦。

String类的方法

因为使用频率非常高,所以String内部提供很多操作字符串的方法,常用的如下:

equals:字符串是否相同
equalsIgnoreCase:忽略大小写后字符串是否相同
compareTo:根据字符串中每个字符的Unicode编码进行比较
compareToIgnoreCase:根据字符串中每个字符的Unicode编码进行忽略大小写比较
indexOf:目标字符或字符串在源字符串中位置下标
lastIndexOf:目标字符或字符串在源字符串中最后一次出现的位置下标
valueOf:其他类型转字符串
charAt:获取指定下标位置的字符
codePointAt:指定下标的字符的Unicode编码
concat:追加字符串到当前字符串
isEmpty:字符串长度是否为0
contains:是否包含目标字符串
startsWith:是否以目标字符串开头
endsWith:是否以目标字符串结束
format:格式化字符串
getBytes:获取字符串的字节数组
getChars:获取字符串的指定长度字符数组
toCharArray:获取字符串的字符数组
join:以某字符串,连接某字符串数组
length:字符串字符数
matches:字符串是否匹配正则表达式
replace:字符串替换
replaceAll:带正则字符串替换
replaceFirst:替换第一个出现的目标字符串
split:以某正则表达式分割字符串
substring:截取字符串
toLowerCase:字符串转小写
toUpperCase:字符串转大写
trim:去字符串首尾空格

方法有很多,无法一一讲解,我们挑选几个聊一聊哈

方法1、hashCode

由于Object中有hashCode()方法,所以所有的类中都有对应的方法,在String中做了如下的实现:

private int hash; // 缓存字符串的哈希码

public int hashCode() {
    int h = hash; // 从缓存中获取哈希码
    // 如果哈希码未被计算过(即为 0)且字符串不为空,则计算哈希码
    if (h == 0 && value.length > 0) {
        char val[] = value; // 获取字符串的字符数组

        // 遍历字符串的每个字符来计算哈希码
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i]; // 使用 31 作为乘法因子
        }
        hash = h; // 缓存计算后的哈希码
    }
    return h; // 返回哈希码
}

先检核是否已计算哈希,若已计算则直接返回,否则根据31倍哈希法进行计算并缓存计算后的哈希值。String中重写后的hashCode方法,计算效率高,随机性强,哈希碰撞概率小,所以常被用作HashMap中的Key。

方法2、equals

我们在之前的文章中曾提到过重写equals方法往往也需要重写hashCode方法,这一点String就做到了,我们来看看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;
    }

这是Java8中的实现,逻辑清晰易懂,首先,通过==判断是否是同一个对象,如果是则直接返回true,否则进入下一轮判断逻辑:判断对象是否为String类型,再判断两个字符串长度是否相等,再比较每个字符是否相等,全部为true最后返回true,其中有任何一个为flase则返回false。

方法3、substring

该方法在日常开发中时常被用到,主要用来截取字符串,源码:

public String substring(int beginIndex) {
    // 检查起始索引是否小于 0,如果是,则抛出 StringIndexOutOfBoundsException 异常
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    // 计算子字符串的长度
    int subLen = value.length - beginIndex;
    // 检查子字符串长度是否为负数,如果是,则抛出 StringIndexOutOfBoundsException 异常
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    // 如果起始索引为 0,则返回原字符串;否则,创建并返回新的字符串
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

使用案例:
注意源码中注释提到的:如果 beginIndex 为 0,说明子串与原字符串相同,直接返回原字符串。否则,使用 value 数组(原字符串的字符数组)的一部分 new 一个新的 String 对象并返回。

String str = "Hello, world!";
String pre = str.substring(0);
System.out.println(pre);
String prefix = str.substring(0, 5);  
System.out.println(prefix);
String suffix = str.substring(7);     
System.out.println(suffix);

输出:

Hello, world!
Hello
world!

方法4、indexOf

indexOf的主要作用是获取目标字符或字符串在源字符串中位置下标,看源码:

/**
     * 由 String 和 StringBuffer 共享的用于执行搜索的代码。这
     * source 是正在搜索的字符数组,目标
     * 是要搜索的字符串。
     *
     * @param正在搜索的字符的来源。
     * @param源字符串的 sourceOffset 偏移量。
     * @param源字符串的 sourceCount 计数。
     * @param定位正在搜索的字符。
     * @param目标字符串的 targetOffset 偏移量。
     * @param目标字符串的 targetCount 计数。
     * @param fromIndex 要开始搜索的索引。
     */
static int indexOf(char[] source, int sourceOffset, int sourceCount,
            char[] target, int targetOffset, int targetCount,
            int fromIndex) {
        if (fromIndex >= sourceCount) {
            return (targetCount == 0 ? sourceCount : -1);
        }
        if (fromIndex < 0) {
            fromIndex = 0;
        }
        if (targetCount == 0) {
            return fromIndex;
        }

        char first = target[targetOffset];
        int max = sourceOffset + (sourceCount - targetCount);

        for (int i = sourceOffset + fromIndex; i <= max; i++) {
            /* Look for first character. */
            if (source[i] != first) {
                while (++i <= max && source[i] != first);
            }

            /* Found first character, now look at the rest of v2 */
            if (i <= max) {
                int j = i + 1;
                int end = j + targetCount - 1;
                for (int k = targetOffset + 1; j < end && source[j]
                        == target[k]; j++, k++);

                if (j == end) {
                    /* Found whole string. */
                    return i - sourceOffset;
                }
            }
        }
        return -1;
    }

使用案例一

String str = "Hello, world!";
int index = str.indexOf("wor");  // 查找 "world" 子字符串在 str 中第一次出现的位置
System.out.println(index);        // 输出 7,字符串下标从0开始,空格也算一位

使用案例二

String str = "Hello, world!";
int index1 = str.indexOf("o");    // 查找 "o" 子字符串在 str 中第一次出现的位置
int index2 = str.indexOf("o", 5); // 从索引为5的位置开始查找 "o" 子字符串在 str 中第一次出现的位置
System.out.println(index1);       // 输出 4
System.out.println(index2);       // 输出 8

方法五、replace与replaceAll

话不多说,直接看码

///replace是字符和字符串的替换操作,基于字符匹配
public String replace(char oldChar, char newChar)
public String replace(CharSequence target, CharSequence replacement)

//replaceAll是基于正则表达式的字符串匹配与替换
public String replaceAll(String regex, String replacement)

使用案例:

String str = "Hello Java. Java is a language.";
//查找原字符串中所有Java子串,并用c进行替换
System.out.println(str.replace("Java", "c"));
//根据正则表达式匹配规则,.代表是任意字符 可以匹配任何单个字符
//所以经过正则匹配后,找出原字符串中所有“Java”+”任意一个字符”的子串,用c进行替换!
System.out.println(str.replaceAll("Java.", "c"));

输出:

Hello c. c is a language.
Hello c cis a language.

String类的使用

学以致用,学习的最终目的就是使用!

字符串常量池

搞清楚字符串常量池之前,我们先看如下这条语句,考你们一下,这行代码创建了几个对象?

String s1 = new String("abc");

这个答案并不是唯一的
第一种情况:若字符串常量池中不存在“abd”对象的引用,则语句会在堆中闯将2个对象,其中一个对象的引用保存到字符串常量池中。
这种情况下的字节码(JDK1.8)
在这里插入图片描述
ldc 命令用于判断字符串常量池中是否保存了对应的字符串对象的引用,如果保存了的话直接返回,如果没有保存的话,会在堆中创建对应的字符串对象并将该字符串对象的引用保存到字符串常量池中。
第二种情况: 如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。

// 字符串常量池中已存在字符串对象“abc”的引用
String s1 = "abc";
// 下面这段代码只会在堆中创建 1 个字符串对象“abc”
String s2 = new String("abc");

看到这里我们大致可以明白什么时字符串常量池,以及它的作用了:

字符串常量池是JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

字符串引用的存储

在上面的内容中,我们了解了字符串常量池,那么Java中是怎么将字符串的引用保存到常量池中的呢,这里我们需要提到String的intern()方法。

String.intern() 是一个native(本地)方法
其作用是将指定的字符串对象的引用保存在字符串常量池中。
若字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用;
若字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。

我们看下面一段代码:

String s1 = new String("Hello") + new String("World");
String s2 = s1.intern();
System.out.println(s1 == s2);

你们觉得返回的是false还是true?如果还不明白,那么请看一下美团团队发布的一篇文章
美团技术团队深入解析 String.intern()

字符串的拼接

你是不是曾用过“+”进行字符串的拼接操作,比如说String res = "str" + "ing"; 。,最终输出的就是string
出现这样的效果的原因是Java编译器的优化功能-常量折叠

对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。

优化前:String res = "str" + "ing";
优化后:String res = "string";
但像对象引用这种情况,无法在编译其进行优化,我们看下面这段

String str1 = "str";
String str2 = "ing";
System.out.println(str1+str2);

字节码(JDK1.8)
在这里插入图片描述
通过字节码我们可以分析出,通过+号将几个对象引用进行拼接,实际上是调用StringBuilder().append(str1).append(str2).toString();来实现的。
但有几个对象引用拼接,就会创建几个StringBuilder对象,浪费资源,因此,在做字符串拼接时直接采用StringBuilder实现!

String、StringBuffer,StringBuilder区别

相同点:

1、都可以储存和操作字符串
2、都使用 final 修饰,不能被继承
3、提供的 API 相似

异同点:

1、String 是只读字符串,String 对象内容是不能被改变的
2、StringBuffer 和 StringBuilder 的字符串对象可以对字符串内容进行修改,在修改后的内存地址不会发生改变
3、StringBuilder 线程不安全;StringBuffer 线程安全

三者区别详解请点击看这篇文章

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

虹科分享 | Redis与MySQL协同升级企业缓存

文章速览&#xff1a; MySQL为什么需要Redis EnterpriseRedis Enterprise带来哪些优势Redis Enterprise与MySQL协同 传统的MySQL数据库在处理大规模应用时已经到了瓶颈&#xff0c;Redis Enterprise怎样助力突破这一瓶颈&#xff1f;Redis Enterprise与MYSQL共同用作企业级缓存…

第二次作业+第三次作业

第二次作业第三次作业 第二次作业 题目&#xff1a; 网站需求&#xff1a; ​ 1.基于域名[www.openlab.com](http://www.openlab.com)可以访问网站内容为 welcome to openlab!!! 2.给该公司创建三个子界面分别显示学生信息&#xff0c;教学资料和缴费网站&#xff0c;基于[ww…

[全连接神经网络]Transformer代餐,用MLP构建图像处理网络

一、MLP-Mixer 使用纯MLP处理图像信息&#xff0c;其原理类似vit&#xff0c;将图片进行分块(patch)后展平(fallten)&#xff0c;然后输入到MLP中。理论上MLP等价于1x1卷积&#xff0c;但实际上1x1卷积仅能结合通道信息而不能结合空间信息。根据结合的信息不同分为channel-mixi…

hash应用

目录 一、位图 1.1、引出位图 1.2、位图的概念 1.3、位图的应用 1.4、位图模拟实现 二、布隆过滤器 2.1、什么是布隆过滤器 2.2、布隆过滤器应用的场景 2.3、布隆过滤器的原理 2.4、布隆过滤器的查找 2.5、布隆过滤器的插入 2.6、布隆过滤器的删除 2.7、布隆过滤器…

深入解析JavaScript中箭头函数的用法

&#x1f9d1;‍&#x1f393; 个人主页&#xff1a;《爱蹦跶的大A阿》 &#x1f525;当前正在更新专栏&#xff1a;《VUE》 、《JavaScript保姆级教程》、《krpano》、《krpano中文文档》 ​ ​ ✨ 前言 箭头函数(Arrow function)是JavaScript ES6中引入的一大特性。箭头函…

数字图像处理期末速成笔记

目录 一、基础知识二、相邻像素间基本关系三、图像增强方法1、直方图求解2、直方图均衡化3、直方图规定化4、图像平滑5、邻域平均法&#xff08;线性&#xff09;6、 中值滤波法&#xff08;分线性&#xff09;7、中值滤波与领域平均的异同8、4-邻域平滑法9、超限像素平滑法10、…

【Linux】yum

个人主页 &#xff1a; zxctsclrjjjcph 文章封面来自&#xff1a;艺术家–贤海林 如有转载请先通知 yum 1. 什么是yum&#xff1f;2. Linux系统(Centos)的生态3. yum的相关操作4. yum的本地配置5. 如何安装软件 1. 什么是yum&#xff1f; yum是一个软件下载安装的一个客户端&a…

逻辑运算

目录 AND OR NOT Oracle从入门到总裁:https://blog.csdn.net/weixin_67859959/article/details/135209645 逻辑运算可以保证连接多个条件&#xff0c;连接主要使用 AND、OR 、NOT完成 AND 1.查询职位不是办事员&#xff0c;但是工资低于 300 的员工信息 这个范例可以理…

学习响应式编程中遇到的奇奇怪怪的问题

spring项目无法启动 Description: Web application could not be started as there was no org.springframework.boot.web.reactive.server.ReactiveWebServerFactory bean defined in the context. Action: Check your application’s dependencies for a supported react…

Visual Studio 设置编辑框(即代码编辑器)的背景颜色

在Visual Studio 中设置编辑框&#xff08;即代码编辑器&#xff09;的背景颜色&#xff0c;可以按照以下步骤进行&#xff1a; 打开Visual Studio。在菜单栏上找到并点击“工具”(Tools)选项。在下拉菜单中选择“选项”(Options)。在“选项”对话框中&#xff0c;导航至“环境…

基于Spring+mybatis+vue的在线课后测试系统(Java毕业设计)

大家好&#xff0c;我是DeBug&#xff0c;很高兴你能来阅读&#xff01;作为一名热爱编程的程序员&#xff0c;我希望通过这些教学笔记与大家分享我的编程经验和知识。在这里&#xff0c;我将会结合实际项目经验&#xff0c;分享编程技巧、最佳实践以及解决问题的方法。无论你是…

Oracle架构_数据库底层原理、机制 (授人以渔)

目录 系统全局区SGA 高速缓存缓冲区(数据库缓冲区) 日志缓冲区 共享池 其他结构 用户连接进程 用户进程User Process Server Process服务进程 程序全局区PGA Oracle的connect连接和session会话与User Process紧密相关 后台进程 数据库写入进程(DBWn) 检查点(CKPT)…

plt.table绘制表格

目录 一&#xff1a;介绍 二&#xff1a;绘制一个表格 一&#xff1a;介绍 plt.table()函数是Matplotlib库中的一个函数&#xff0c;用于在图表中插入一个表格。它提供了创建和定制表格的功能。下面是plt.table()函数的一些常用参数&#xff1a; cellText: 一个二维列表或数…

mp4文件可以转成mp3音频吗

现在是个非常流行刷短视频一个年代&#xff0c;刷短视似乎成了人们休闲娱乐的一种方式&#xff0c;在日常刷短视频过程中&#xff0c;肯定会有很多同学被短视频 bgm 神曲洗脑&#xff0c;比如很多被网红翻唱带火的歌曲&#xff0c;例如其中"不负人间”&#xff0c;就是其中…

elementUI+el-upload 上传、下载、删除文件以及文件展示列表自定义为表格展示

Upload 上传组件的使用 官方文档链接使用el-upload组件上传文件 具体参数说明&#xff0c;如何实现上传、下载、删除等功能获取文件列表进行file-list格式匹配代码 文件展示列表自定义为表格展示 使用的具体参数说明文件大小展示问题&#xff08;KB/MB&#xff09;文件下载代码…

Windows下载并配置Kettle

注意&#xff1a;需要windows配置Java 下载 Kettle 进入官网&#xff1a;https://www.hitachivantara.com/en-us/products/pentaho-plus-platform/data-integration-analytics/pentaho-community-edition.html 下载带有Pentaho Data Integration (Base Install)的文件&#…

智能工厂能耗监测系统

智能工厂能耗监测系统是一种用于监测和管理智能工厂内能耗的系统。它通过实时收集和分析能耗数据&#xff0c;帮助工厂管理者实现能源节约、提高能源使用效率和降低运营成本。本文将详细介绍智能工厂能耗监测系统的原理、组成、优势和应用。 一、系统原理 智能工厂能耗监测系…

AWS 亚马逊云服务专题学习

目录 1. 学习大纲2. 学习地址3. 系列博客4. AWS 初识 1. 学习大纲 AWS 基础知识&#xff1a;IAM、EC2、负载均衡、Auto Scaling、EBS、EFS、Route 53、RDS、ElastiCache、S3、CloudFrontAWS CLI&#xff1a;CLI 设置、在 EC2 上的使用、最佳实践、SDK、高级使用深度数据库比较…

文件名修改方法:批量重命名文件,并将扩展字母统一转换为大写

在日常生活和工作中&#xff0c;我们经常需要处理大量的文件&#xff0c;有时候需要对这些文件进行重命名&#xff0c;或者将它们的扩展名统一转换为大写。虽然这个过程看似简单&#xff0c;但实际上需要一定的技巧和注意事项。本文讲解云炫文件管理器如何批量重命名文件&#…

[C#]winform部署openvino官方提供的人脸检测模型

【官方框架地址】 https://github.com/sdcb/OpenVINO.NET 【框架介绍】 OpenVINO&#xff08;Open Visual Inference & Neural Network Optimization&#xff09;是一个由Intel推出的&#xff0c;针对计算机视觉和机器学习任务的开源工具套件。通过优化神经网络&#xff…