Java 之深入理解 String、StringBuilder、StringBuffer

news2024/10/4 3:34:10

前言

        由于发现 String、StringBuilder、StringBuffer 面试的时候会经常问到,这里就顺便总结一下:本文重点会以这三个字符串类的性能、线程安全、存储结构这三个方面进行分析


 ✨上期回顾:Java 哈希表


 ✨目录

前言

 String 介绍

String 的不可变性

String 在字符串常量池中的表示

字符串常量池没有该字符串:

字符串常量池有该字符串:

总结

StringBuilder 与 StringBuffer

 效率的比较

线程安全的比较

模拟面试

 String 介绍

String 的不可变性

        跳转到 String 的实现就会发现:

String 类 不能被继承:该类被 final 修饰
String 类是不可变的:value[ ] 被 final 修饰,表明 value[ ] 自身的值不能改变
String 类可以序列化,可以和其他 String 比较其大小:实现了 Comparable 接口

   通过下述代码,你就会发现 String 类中每一个看起来会修改 String 值的方法,实际上都是创建了一个全新的 String 对象包含修改后的字符串内容。而最初的 String 对象则丝毫未动 

class Test{
    static String Func(String s){
        return s.toUpperCase();
    }

    public static void main(String[] args) {
        String str = "hello world";
        System.out.println(str);
        String ret = Func(str);
        System.out.println(ret);
        System.out.println(str);
    }
}

//

输出结果:
hello world
HELLO WORLD
hello world

         当把 str 传给 Func 方法时,实际传递的是引用的一个拷贝。其实,每当把 String 对象作为方法的参数时,都会复制一份引用,而该引用所指的对象其实一直待在单一的物理位置上,从未动过

String 在字符串常量池中的表示

        先从一段代码开始吧,以下这行代码总共创建了几个对象呢?

String str = new String("Hello");

        我想很多人看到会不暇思索的回答:“这不是一个吗?”,其实并不然它创建了两个对象:

字符串常量池没有该字符串:

        如果字符串常量池中没有 Hello 这个字符,先在字符串常量池中创建一个 ‘Hello’ 的字符串对象,然后再在堆中创建一个 ‘Hello’ 的字符串对象,然后将堆中这个 ‘Hello’ 的字符串对象地址返回赋值给变量 str。因此需要创建个对象。


字符串常量池有该字符串:

        String str = new String("Hello World");
        String ret = new String("Hello World");

        Java 虚拟机会先在字符串常量池中查找有没有 ‘Hello’ 这个字符串对象,如果有,就不会在字符串常量池中创建 ‘Hello’ 这个对象了,直接在堆中创建一个‘Hello’ 的字符串对象,然后将堆中这个 ‘Hello’ 的对象地址返回赋值给变量 str。因此只需要创建个对象。

(注意:ret 所指向的字符是 Hello,不是HHllo,画到最后没存档回不去了,将就着看吧)

        为什么要先在字符串常量池中创建对象,然后再在堆上创建呢

        由于字符串的使用频率实在是太高了,所以 Java 虚拟机为了提高性能和减少内存开销,在创建字符串对象的时候进行了一些优化,特意为字符串开辟了一块空间 -- 也就是字符串常量池

        通常情况下我们会采用双引号的方式来创建字符串对象,而不是通过 new 关键字的方式,因为 new 会强制创建对象会对资源造成浪费。

        如果我们采用双引号创建对象,如下图所示:

String str1 = "Hello";

String str2 = "World";

        Java 虚拟机会先在字符串常量池中查找是否存在该字符串,如果存在则不创建任何对象直接返回常量池中的对象引用如果不存在,则在常量池中创建该字符串,并返回对象引用。这样做的好处是避免了重复创建多个相同的字符串对象,减少了内存的开销。

        接下来我们来研究一个经典的面试问题:

    public static void main(String[] args) {
            String a = "abc";
            String b = new String("abc");
            String c = new String("abc");
            String d = b.intern();
            System.out.println(a == b);
            System.out.println(b == c);
            System.out.println(a == d);
        }

     请问上述程序打印的结构是什么?

// 打印结构为:
false
false
true

        通过 String a = "abc" 这样创建一个字符串对象时,JVM会首先在字符串常量池中寻找这个字符串,我们发现 "abc" 不存在,则在常量池中创建该字符串并将 a 指向它


        通过 String b = new String("abc") 这样创建字符串时,情况就不一样了,同样先在字符串常量池中寻找这个字符串,我们发现 "abc" 存在。它会在堆区创建该字符串对象并使 b 指向它,同样调用 String c = new String("abc") 时,也会在堆区再创建一个 String 对象并使 c 指向它。由于我们字符串中 “==” 比较的是地址,而我们的 b、c 创建的是两个不同的对象所以返回 false。a、b 同理返回 false。


        当调用 String d = b.intern() 时,intern方法(该方法为 native 方法)会在字符串常量池中查找是否存在该字符串对象,如果存在,则将 d 指向该常量池中的字符串对象,如果不存在则在常量池中创建该字符串并指向它,所以 a == d 返回 true。

总结

使用双引号声明的字符串对象会保存在字符串常量池中
使用 new 关键字创建的字符串对象会先从字符串常量池中找,如果没找到就创建一个,然后再在堆中创建字符串对象;如果找到了,就直接在堆中创建字符串对象
在存在字符串常量池的前提下,使用 new 关键字但是不想创建对象,可以使用 intern 方法直接获取字符串常量池的引用

StringBuilder 与 StringBuffer

 效率的比较

        通过以上内容,相信你已经对 String 有一定了解。由于字符串是不可变的,所以当遇到字符串的拼接(尤其是使用 + 号操作符)的时候,就需要考量性能的问题,你不能毫无顾虑地生产太多 String 对象,对珍贵的内存造成不必要的压力

        于是 Java 就设计了两个专门用来解决此问题的 StringBuilder、StringBuffer 类 ~

        可能有人会问 String 能做的事情干嘛还要用 StringBuilder、StringBuffer 呢?我们可以对一个字符进行多次拼接查看程序的运行效率,如下述代码:

    public static void main(String[] args) {
            String s = "";
            long st = System.currentTimeMillis();
            for(int i = 0; i < 100000; i++) {
                s += "a";
            }
            long ed = System.currentTimeMillis();
            System.out.println("String时间:" + (ed - st) + "毫秒");

            st = System.currentTimeMillis();
            StringBuilder sb = new StringBuilder();
            for(int i = 0; i < 100000; i++) {
                sb.append("a");
            }
            ed = System.currentTimeMillis();
            System.out.println("StringBuilder时间:" + (ed - st) + "毫秒");

            st = System.currentTimeMillis();
            StringBuffer sf = new StringBuffer();
            for(int i = 0; i < 100000; i++) {
                sf.append("a");
            }
            ed = System.currentTimeMillis();
            System.out.println("StringBuffer时间:" + (ed - st) + "毫秒");

        }

代码运行结果:

String时间:827毫秒
StringBuilder时间:1毫秒
StringBuffer时间:3毫秒

        可以看出,在大量对字符串进行连接操作的情况下,StringBuilder、StringBuffer 优势非常明显。因为 String 拼接会产生大量对象,而 StringBuilder、StringBuffer 无论是创建、拼接、修改、删除都是直接作用于原字符串,并不会产生多余的对象。其次 StringBuilder 比 StringBuffer 的效率稍微高一点也是有原因的:这就涉及到线程安全问题,待会再讲。

 

线程安全的比较

        我们可以来对比一下它们的底层源码,再来做分析:

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


//StringBuilder
public final class StringBuilder
    extends AbstractStringBuilder
    implements Serializable, CharSequence
{
    @Override
    public StringBuilder append(Object obj) {}
    @IntrinsicCandidate
    public String toString() {...}
    ...
}


//StringBuffer
public final class StringBuffer
    extends AbstractStringBuilder
    implements Serializable, CharSequence
{
    //方法有synchronized关键字
    @Override
    public synchronized StringBuffer append(Object obj){...}
    @IntrinsicCandidate
    public synchronized String toString() {...}
    ...
}

//AbstractStringBuilder 
abstract sealed class AbstractStringBuilder implements Appendable, CharSequence permits StringBuilder, StringBuffer {
    byte[] value;
    ...
}

 

        <1> 我们可以看到在 String 中,value 是被 final 修饰的是不可变的;StringBuilder、StringBuffer 都继承于 AbstractStringBuilder 这个类,而这个类中 的 value 是可变数组,所以我们进行拼接等操作时是直接作用于原字符串实现的,这就是效率高的由来。

 

        <2> 我们观察 StringBuilder、StringBuffer  的 toString、append 方法:由于 StringBuffer 操作字符串的方法加了synchronized 进行了同步,所以每次操作字符串时都会加锁,所以线程安全、但是性能低。这就是 StringBuilder 比 StringBuffer 运行效率略高的原因。

总结:

String 类

不可变性:一旦创建,内容不可改变
线程安全:由于不可变性,String 对象天生线程安全
性能:频繁的字符串操作会导致大量的对象创建和内存消耗

StringBuilder 类

可变性:内容可以被改变
非线程安全:适用于单线程环境
性能:比 String 更适合频繁的字符串操作,因为不会创建大量的中间对象

StringBuffer 类

可变性:内容可以被改变
线程安全:所有方法都是同步的,适用于多线程环境
性能:由于同步机制,性能略低于 StringBuilder

模拟面试

如果HR问你:String、StringBuffer、StringBuilder 的区别?(你会怎么回答)

答:关于String、StringBuffer、StringBuilder的区别,我有四个方面来说:

        第一个是可变性,String 内部的 value 是 final 修饰的,所以它是一个不可变的类,所以每次修改 String 的值的时候都会产生一个新的对象。而 StringBuffer、StringBuilder 是一个可变类,字符串的变更不会产生新的对象。

        第二个是线程的安全性,因为 String 是一个不可变的类,所以它是线程安全的;而 StringBuffer 也是线程安全的,因为它的每个操作方法中都有一个 synchronized  一个同步关键字;StringBuilder 不是线程安全的,所以在多线程环境下对字符串进行操作的时候我们应该使用 StringBuffer 否者使用 StringBuilder。

        第三个是性能方面,String 效率是最低的,因为其不可变性导致做字符串的拼接或者修改的时候,我们需要创建新的对象,以及分配内存;其次是 StringBuffer  比 String 的效率更高一点,因为它的可变性意味值字符串可以直接被修改;最后性能最高的是 StringBuilder ,因为 StringBuilder 比 StringBuffer 的性能要高,因为 StringBuffer 加了同步锁意味着对性能产生了影响。

        第四个是存储方面,String 存储在字符串常量池中,而 StringBuffer、StringBuilder 则是存储在堆的内存空间。

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

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

相关文章

2024/10/3 408数据结构大题打卡

最短路径复习&#xff1a; bfs&#xff1a;只能解决无权图

【LeetCode每日一题】——17.电话号码的字母组合

文章目录 一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【题目提示】七【解题思路】八【时间频度】九【代码实现】十【提交结果】 一【题目类别】 回溯 二【题目难度】 中等 三【题目编号】 17.电话号码的字母组合 四【题目描述】 给定一个…

redis 5的安装及启动(window)

最近看大模型的时候发现入手redis的同学没有练手的&#xff0c;而且大部分redis的文章要钱才能看&#xff0c;在这里我把路径和环境配置&#xff0c;启动给大家说一下 下载 redis5的获取链接在下面&#xff08;为什么是redis5&#xff0c;因为上个模型用的就是redis5&#xff…

pipe函数的例子

代码&#xff1a; #include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<sys/types.h> int main(void) {int result -1;int fd[2],nbytes;pid_t pid;char string[80]"ni hao, pipe!";char readbuff…

linux信号 | 学习信号四步走 | 一篇文章教你理解信号如何保存

前言&#xff1a; 本节内容是信号的保存。 学习信号&#xff0c; 我们首先了解了信号的概念&#xff0c; 然后学习了信号的产生方式。 现在就开始讲解信号在时间窗口内是如何保存在进程内部的。 ps&#xff1a;本节内容需要了解信号的概念&#xff0c; 希望友友们了解一些信号…

实用技能分享!推荐最适合论文写作的5款ai工具

在当今学术研究和教育领域&#xff0c;AI工具的应用已经变得越来越普遍。这些工具不仅能够提高写作效率&#xff0c;还能帮助生成高质量的文稿。对于教师而言&#xff0c;选择合适的AI工具可以显著提升论文写作的效率和质量。本文将重点推荐五款最适合教师论文写作的AI工具&…

Linux聊天集群开发之环境准备

一.windows下远程操作Linux 第一步&#xff1a;在Linux终端下配置openssh&#xff0c;输入netstate -tanp,查看ssh服务是否启动&#xff0c;默认端口22.。 注&#xff1a;如果openssh服务&#xff0c;则需下载。输入命令ps -e|grep ssh, 查看如否配有&#xff0c; ssh-agent …

【重学 MySQL】四十六、创建表的方式

【重学 MySQL】四十六、创建表的方式 使用CREATE TABLE语句创建表使用CREATE TABLE LIKE语句创建表使用CREATE TABLE AS SELECT语句创建表使用CREATE TABLE SELECT语句创建表并从另一个表中选取数据&#xff08;与CREATE TABLE AS SELECT类似&#xff09;使用CREATE TEMPORARY …

【重学 MySQL】五十四、整型数据类型

【重学 MySQL】五十四、整型数据类型 整型类型TINYINTSMALLINTMEDIUMINTINT&#xff08;或INTEGER&#xff09;BIGINT 可选属性UNSIGNEDZEROFILL显示宽度&#xff08;M&#xff09;AUTO_INCREMENT注意事项 适合场景TINYINTSMALLINTMEDIUMINTINT&#xff08;或INTEGER&#xff0…

Python 从入门到实战33(使用MySQL)

我们的目标是&#xff1a;通过这一套资料学习下来&#xff0c;通过熟练掌握python基础&#xff0c;然后结合经典实例、实践相结合&#xff0c;使我们完全掌握python&#xff0c;并做到独立完成项目开发的能力。 上篇文章我们讨论了数据库编程接口操作的相关知识。今天我们将学习…

SLF4J(W): Class path contains multiple SLF4J providers.

问题背景 最近在给某AI项目集成阿里的通义千问SDK&#xff0c;发现竟然有个奇怪的报错&#xff0c;仔细一看发现&#xff0c;我类上用的lombok的Slf4j注释&#xff0c;阿里用的是org.slf4j.simple.SimpleServiceProvider&#xff0c;但是lombok用的是LogbackServiceProvider&a…

关于Vben Admin多标签页面缓存不生效的问题

情况说明 笔者在接手一个基于Vben Admin框架改造的vue3后台管理项目&#xff0c;客户要求在切换头部Tab页面时&#xff0c;不要刷新清空已经填写的表单页面或者表格。 然而&#xff0c;笔者根据Vben Admin的官方文档来配置多标签页面缓存后&#xff0c;页面每次切换后&#x…

Linux 应用层协议HTTP

文章目录 一、初始HTTP协议二、URL格式网络中怎么通过URL进行定位资源呢&#xff1f;编码和解码 三、HTTP的请求格式和响应格式HTTP的请求格式HTTP的响应格式HTTP的请求方法GET方法POST方法GET Vs PostHTTP的封装和分用文件流操作浏览器获得一个完整的网页流程 HTTP的状态码对3…

一、Linux下MySQL的安装与使用

文章目录 1. 基于docker安装mysql2. 字符集的相关操作2.1 修改MySQL5.7字符集2.2 各级别的字符集2.3 字符集与比较规则(了解)2.4 请求到响应过程中字符集的变化 3. SQL大小写规范3.1 Windows和Linux平台区别3.2 Linux下大小写规则设置3.3 SQL编写建议 4. sql_mode的合理设置4.1…

知识图谱入门——5:Neo4j Desktop安装和使用手册(小白向:Cypher 查询语言:逐步教程!Neo4j 优缺点分析)

Neo4j简介 Neo4j 是一个基于图结构的 NoSQL 数据库&#xff0c;专门用于存储、查询和管理图形数据。它的核心思想是使用节点、关系和属性来描述数据。图数据库非常适合那些需要处理复杂关系的数据集&#xff0c;如社交网络、推荐系统、知识图谱等领域。 与传统的关系型数据库…

端侧大模型系列 | 端侧AI Agent任务拆解大师如何助力AI手机?(简短版)

引言 简介 模型 实验 意义&前景: 总结 引言 今人不见古时月&#xff0c;今月曾经照古人。 小伙伴们好&#xff0c;我是微信公众号《小窗幽记机器学习》的小编&#xff1a;卖沙茶面的小女孩。 设想一下&#xff0c;你的智能手机不再只是"聪明"&#xff0…

12.梯度下降法的具体解析——举足轻重的模型优化算法

引言 梯度下降法(Gradient Descent)是一种广泛应用于机器学习领域的基本优化算法&#xff0c;它通过迭代地调整模型参数&#xff0c;最小化损失函数以求得到模型最优解。 通过阅读本篇博客&#xff0c;你可以&#xff1a; 1.知晓梯度下降法的具体流程 2.掌握不同梯度下降法…

力扣 中等 129.求根节点到叶子结点数字之和

文章目录 题目介绍解法 题目介绍 解法 法一&#xff1a;有返回值、 class Solution {public int sumNumbers(TreeNode root) {return dfs(root, 0);}public int dfs(TreeNode root, int x) {if (root null) {return 0;}x x * 10 root.val;if (root.left root.right) { //…

LC刷题专题:dfs、哈希表合集

自己刷题缺少分类思想&#xff0c;总是这里刷一道那里刷一道&#xff0c;以后建立几个专辑&#xff0c;然后自己新刷的同类型的题目都会即使更新上。 文章目录 690. 员工的重要性 690. 员工的重要性 2024-10-03 题目描述&#xff1a; 我第一次写并没有考虑到dfs&#xff0c;…

基于Arduino的L298N电机驱动模块使用

一.简介&#xff1a; L298N作为电机驱动芯片&#xff0c;具有驱动能力强&#xff0c;发热量低&#xff0c;抗干扰能力强的特点,一个模块可同时驱动两个直流电机工作&#xff0c;能够控制电机进行正转、反转、PWM调速。 说明&#xff1a; 1&#xff09;12V输入端口接入供电电压…