百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Java 从底层与接口实现了解String、StringBuffer、StringBuilder

zhezhongyun 2025-09-19 06:31 36 浏览

String、StringBuffer 和 StringBuilder的接口实现关系:

String:字符串常量,字符串长度不可变。Java中String 是immutable(不可变)的。用于存放字符的数组被声明为final的,因此只能赋值一次,不可再更改。

StringBuffer:宣称线程安全的字符串变量(Synchronized,即线程安全,multiple threads cannot access it simultaneously,可将字符串缓冲区安全地用于多个线程)。如果要频繁对字符串内容进行修改,出于效率考虑最好使用 StringBuffer,如果想转成 String 类型,可以调用 StringBuffer 的 toString() 方法。

StringBuilder:字符串变量(非线程安全,但效率更高)。在内部 StringBuilder 对象被当作是一个包含字符序列的变长数组。

三类字符串使用建议:

如果要操作少量的数据用 String ;

单线程操作大量数据用StringBuilder ;

多线程操作大量数据,用StringBuffer。

1 关于 String 为啥是不可改变的

字符串实际上就是一个 char 数组,并且内部就是封装了一个 char 数组。

并且这里 char 数组是被 final 修饰的:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

并且 String 中的所有的方法,都是对于 char 数组的改变,只要是对它的改变,方法内部都是返回一个新的 String 实例。

对于字符串的加运算,当编译成 class 文件时,会自动编译为 StringBuffer 来进行字符串的连接操作。

同时对于字符串常量池:

当一个字符串是一个字面量时,它会被放到一个常量池中,等待复用。

String 类的常见面试问题:

面试题一:

String s1 = "abc"; // 常量池
String s2 = new String("abc"); // 堆内存中
System.out.println(s1==s2); // false,两个对象的地址值不一样。
System.out.println(s1.equals(s2)); // true

面试题二:

String s1="a"+"b"+"c";
String s2="abc";
System.out.println(s1==s2);
System.out.println(s1.equals(s2));
// java 中常量优化机制,编译时 s1 已经成为 abc 在常量池中查找创建,s2 不需要再创建。

面试题三:

String s1="ab";
String s2="abc";
String s3=s1+"c";
System.out.println(s3==s2); // false
System.out.println(s3.equals(s2)); // true

先在常量池中创建 ab ,地址指向 s1, 再创建 abc ,指向 s2。对于 s3,先创建StringBuilder(或 StringBuffer)对象,通过 append 连接得到 abc ,再调用 toString() 转换得到的地址指向 s3。故 (s3==s2) 为 false。

2 StringBuffer 类

在使用 StringBuffer 类时,每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象。

Java.lang.StringBuffer 是宣称线程安全的可变字符序列。在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容。

3 StringBuilder 类

StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)。

由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。

关于线程安全,即使你真的遇到了这样的场景,很不幸的是,恐怕你仍然有 99.99....99% 的情况下没有必要选择 stringbuffer,因为 stringbuffer 的线程安全,仅仅是保证 jvm 不抛出异常顺利的往下执行而已,它可不保证逻辑正确和调用顺序正确。大多数时候,我们需要的不仅仅是线程安全,而是锁。

最后,为什么会有 stringbuffer 的存在,如果真的没有价值,为什么 jdk 会提供这个类?答案太简单了,因为最早是没有 stringbuilder 的,sun 的人不知处于何种愚蠢的考虑,决定让 stringbuffer 是线程安全的,然后大约 10 年之后,人们终于意识到这是一个多么愚蠢的决定,意识到在这 10 年之中这个愚蠢的决定为 java 运行速度慢这样的流言贡献了多大的力量,于是,在 jdk1.5 的时候,终于决定提供一个非线程安全的 stringbuffer 实现,并命名为 stringbuilder。顺便,javac 好像大概也是从这个版本开始,把所有用加号连接的 string 运算都隐式的改写成 stringbuilder,也就是说,从 jdk1.5 开始,用加号拼接字符串已经没有任何性能损失了。

"用加号拼接字符串已经没有任何性能损失了"这种说法并不严谨,严格的说,如果没有循环的情况下,单行用加号拼接字符串是没有性能损失的,java 编译器会隐式的替换成 stringbuilder,但在有循环的情况下,编译器没法做到足够智能的替换,仍然会有不必要的性能损耗,因此,用循环拼接字符串的时候,还是老老实实的用 stringbuilder 吧。

4 关于字符串字面量

Like many Java objects, all String instances are created on the heap, even literals. When the JVM finds a String literal that has no equivalent reference in the heap, the JVM creates a corresponding String instance on the heap and it also stores a reference to the newly created String instance in the String pool. Any other references to the same String literal are replaced with the previously created String instance in the heap.

与许多Java对象一样,所有字符串实例都是在堆上创建的,甚至是文字。当JVM发现堆中没有等效引用的字符串文本时,JVM会在堆上创建相应的字符串实例,并在字符串池中存储对新创建的字符串实例的引用。对同一字符串文字的任何其他引用都将替换为堆中先前创建的字符串实例。

class Strings
{
    public static void main (String[] args)
    {
        String a = "alpha";
        String b = "alpha";
        String c = new String("alpha");
        //All three strings are equivalent
        System.out.println(a.equals(b) && b.equals(c));
        //Although only a and b reference the same heap object
        System.out.println(a == b);
        System.out.println(a != c);
        System.out.println(b != c);
    }
}

图示:

When we use double quotes to create a String, it first looks for String with same value in the String pool, if found it just returns the reference else it creates a new String in the pool and then returns the reference.

当我们使用双引号创建字符串时,它首先在字符串池中查找具有相同值的字符串,如果找到,它只返回引用,否则它在池中创建一个新字符串,然后返回引用。

However using new operator, we force String class to create a new String object in heap space. We can use intern() method to put it into the pool or refer to other String object from string pool having same value.

然而,使用new操作符,我们强制String类在堆空间中创建一个新的String对象。我们可以使用intern()方法将其放入池中,或引用字符串池中具有相同值的其他字符串对象。

The String pool itself is also created on the heap. (Version < Java SE 7)

Java SE 7以前的版本的字符串池本身也在堆上创建。

Before Java 7, String literals were stored in the runtime constant pool in the method area of PermGen, that had a fixed size.

在Java 7之前,字符串文本存储在PermGen方法区域的运行时常量池中,该池的大小是固定的。

The String pool also resided in PermGen. (Version ≥ Java SE 7)

Java SE 7 的字符串池也位于PermGen中。

In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application. This change will result in more data residing in the main Java heap, and less data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the String. intern() method will see more significant differences.

在JDK 7中,内部字符串不再分配给Java堆的永久生成(PermGen),而是与应用程序创建的其他对象一起分配给Java堆的主要部分(称为年轻和老年代)。此更改将导致更多数据驻留在主Java堆中,而永久生成中的数据更少,因此可能需要调整堆大小。由于这种变化,大多数应用程序在堆使用方面只会看到相对较小的差异,但较大的应用程序会加载许多类或大量使用字符串。intern()方法将看到更显著的差异。

绝大部分 Java 程序员应该都见过 "
java.lang.OutOfMemoryError: PermGen space "这个异常。这里的 “PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 “PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。

其实,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。

在 JDK 1.8 中, HotSpot 已经没有 “PermGen space”这个区间了,取而代之是一个叫做 Metaspace(元空间) 的东西。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小。

去除永久代的原因:(1)为了HotSpot与JRockit的融合;(2)永久代大小不容易确定,PermSize指定太小容易造成永久代OOM,与老年代没关系。字符串存在永久代中,容易出现性能问题和内存溢出。

ref

https://www.runoob.com/java/java-string.html

https://www.runoob.com/java/java-stringbuffer.html

https://www.cnblogs.com/lixuwu/p/10746125.html

-End-

相关推荐

Python入门学习记录之一:变量_python怎么用变量

写这个,主要是对自己学习python知识的一个总结,也是加深自己的印象。变量(英文:variable),也叫标识符。在python中,变量的命名规则有以下三点:>变量名只能包含字母、数字和下划线...

python变量命名规则——来自小白的总结

python是一个动态编译类编程语言,所以程序在运行前不需要如C语言的先行编译动作,因此也只有在程序运行过程中才能发现程序的问题。基于此,python的变量就有一定的命名规范。python作为当前热门...

Python入门学习教程:第 2 章 变量与数据类型

2.1什么是变量?在编程中,变量就像一个存放数据的容器,它可以存储各种信息,并且这些信息可以被读取和修改。想象一下,变量就如同我们生活中的盒子,你可以把东西放进去,也可以随时拿出来看看,甚至可以换成...

绘制学术论文中的“三线表”具体指导

在科研过程中,大家用到最多的可能就是“三线表”。“三线表”,一般主要由三条横线构成,当然在变量名栏里也可以拆分单元格,出现更多的线。更重要的是,“三线表”也是一种数据记录规范,以“三线表”形式记录的数...

Python基础语法知识--变量和数据类型

学习Python中的变量和数据类型至关重要,因为它们构成了Python编程的基石。以下是帮助您了解Python中的变量和数据类型的分步指南:1.变量:变量在Python中用于存储数据值。它们充...

一文搞懂 Python 中的所有标点符号

反引号`无任何作用。传说Python3中它被移除是因为和单引号字符'太相似。波浪号~(按位取反符号)~被称为取反或补码运算符。它放在我们想要取反的对象前面。如果放在一个整数n...

Python变量类型和运算符_python中变量的含义

别再被小名词坑哭了:Python新手常犯的那些隐蔽错误,我用同事的真实bug拆给你看我记得有一次和同事张姐一起追查一个看似随机崩溃的脚本,最后发现罪魁祸首竟然是她把变量命名成了list。说实话...

从零开始:深入剖析 Spring Boot3 中配置文件的加载顺序

在当今的互联网软件开发领域,SpringBoot无疑是最为热门和广泛应用的框架之一。它以其强大的功能、便捷的开发体验,极大地提升了开发效率,成为众多开发者构建Web应用程序的首选。而在Spr...

Python中下划线 ‘_’ 的用法,你知道几种

Python中下划线()是一个有特殊含义和用途的符号,它可以用来表示以下几种情况:1在解释器中,下划线(_)表示上一个表达式的值,可以用来进行快速计算或测试。例如:>>>2+...

解锁Shell编程:变量_shell $变量

引言:开启Shell编程大门Shell作为用户与Linux内核之间的桥梁,为我们提供了强大的命令行交互方式。它不仅能执行简单的文件操作、进程管理,还能通过编写脚本实现复杂的自动化任务。无论是...

一文学会Python的变量命名规则!_python的变量命名有哪些要求

目录1.变量的命名原则3.内置函数尽量不要做变量4.删除变量和垃圾回收机制5.结语1.变量的命名原则①由英文字母、_(下划线)、或中文开头②变量名称只能由英文字母、数字、下画线或中文字所组成。③英文字...

更可靠的Rust-语法篇-区分语句/表达式,略览if/loop/while/for

src/main.rs://函数定义fnadd(a:i32,b:i32)->i32{a+b//末尾表达式}fnmain(){leta:i3...

C++第五课:变量的命名规则_c++中变量的命名规则

变量的命名不是想怎么起就怎么起的,而是有一套固定的规则的。具体规则:1.名字要合法:变量名必须是由字母、数字或下划线组成。例如:a,a1,a_1。2.开头不能是数字。例如:可以a1,但不能起1a。3....

Rust编程-核心篇-不安全编程_rust安全性

Unsafe的必要性Rust的所有权系统和类型系统为我们提供了强大的安全保障,但在某些情况下,我们需要突破这些限制来:与C代码交互实现底层系统编程优化性能关键代码实现某些编译器无法验证的安全操作Rus...

探秘 Python 内存管理:背后的神奇机制

在编程的世界里,内存管理就如同幕后的精密操控者,确保程序的高效运行。Python作为一种广泛使用的编程语言,其内存管理机制既巧妙又复杂,为开发者们提供了便利的同时,也展现了强大的底层控制能力。一、P...