2.Java SDK源码分析系列笔记-String系列
1. String
1.1. 是什么
不可变、线程安全的字符串
1.2. 使用
public class StringTest
{
public static void main(String[] args)
{
String val = new String("test1");
String val1 = new String("test1");
System.out.println(val == val1);//false。上面的代码会在堆中两块不同的地方创建字符串
String val2 = "test2";
String val3 = "test2";
System.out.println(val2 == val3);//true。上面的代码在编译期间已经确定,那么会把"test2"保存在常量池(不是堆中)
String val4 = "te" + "st2";
System.out.println(val2 == val4);//true。虽然val4是通过+拼接的,但是这个也是可以在编译期确定的,所以使用的仍是常量池中的字符串
String val5 = String.valueOf("test3");
String val6 = String.valueOf("test3");
System.out.println(val5 == val6);//true。"test3"在编译期间已经确定,放入常量池中。String.valueOf返回的是常量池中的字符串
String aa = new String("1111");
String bb = new String("1111");
String val9 = String.valueOf(aa);
String val10 = String.valueOf(bb);
System.out.println(val9 == val10);//false。两个"1111"分别在堆中创建,String.valueOf返回的是堆中不同的对象
String val7 = new String("test4");
String val8 = "test4";
String val7Intern = val7.intern();
System.out.println(val8 == val7);//false。val7在堆中,val8在常量池中,自然不相等
System.out.println(val8 == val7Intern);//true。intern方法的作用是在运行时往常量池中增加字符串,如果常量池池中已有,那么把常量池中的对象返回
System.out.println(val8 == val7);//false。再试验一次说明intern方法不是把堆中的地址塞到常量池中
}
}
1.3. 源码分析
1.3.1. 类的定义
//final表示不能被继承
public final class String
//可比较,可序列化
implements java.io.Serializable, Comparable<String>, CharSequence
{
/** The value is used for character storage. */
//底层是通过char数组实现的,final表示引用不能修改,但并不表示char数组里的值不能修改
//那为什么String还是不可变的呢?因为String并没有提供修改value数组值的方法,所以自然就不可变
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
}
String是不可变的
- 类使用final修饰
- 内部属性char value[]使用final修饰,说明引用不能改变
- 且内部没有对外提供修改内部属性char value[]的方法
1.3.2. 构造方法
//无参构造方法
public String() {
//会创建一个空串
this.value = "".value;
}
//使用String构造
public String(String original) {
//直接把引用指向同一个字符数组?因为String内部的char数组是不可以改变的,所以可以共享
this.value = original.value;
this.hash = original.hash;
}
//使用char数组构造
public String(char value[]) {
//外部传递过来的char数组可能被改变,所有需要复制数组
this.value = Arrays.copyOf(value, value.length);
}
//使用StringBuffer构造
public String(StringBuffer buffer) {
//线程安全的StringBUffer需要加锁并且复制数组
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
//使用StringBuilde构造
public String(StringBuilder builder) {
//复制数组
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
//使用char数组带下标的构造
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near 1>>>1.
if (offset > value.length count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
//复制char数组
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
1.3.2.1. 解释new String("test1") != new String("test1")
String val = new String("test1");
String val1 = new String("test1");
System.out.println(val == val1);//false。上面的代码会在堆中两块不同的地方创建字符串
我们查看字节码,结果如下:
调用的字节码时NEW,会在堆中创建字符串,所以两者不同
1.3.3. 常量池
英文名叫constant pool,指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量
1.3.3.1. 解释"test2"=="test2"
String val2 = "test2";
String val3 = "test2";
System.out.println(val2 == val3);//true。上面的代码在编译期间已经确定,那么会把"test2"保存在常量池(不是堆中)
String val4 = "te" + "st2";
System.out.println(val2 == val4);//true。虽然val4是通过+拼接的,但是这个也是可以在编译期确定的,所以使用的仍是常量池中的字符串
我们查看字节码,结果如下:
可以看出上面三行都调用了LDC
字节码,他表示在常量池中加载字符串,而"test2"
这个字符串在编译器会存入.class文件中,因此三者相等
1.3.4. 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;
//从后往前比较value是否相等
while (n != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
1.3.5. toCharArray方法
public char[] toCharArray() {
// Cannot use Arrays.copyOf because of class initialization order issues
//创建一个新的char数组
char result[] = new char[value.length];
//调用arraycopy函数把value的值复制到新的char数组返回(防止外界改变char数组的值)
System.arraycopy(value, 0, result, 0, value.length);
return result;
}
1.3.6. toString
public String toString() {
//直接返回自己
return this;
}
1.3.7. valueOf
public static String valueOf(Object obj) {
//为null的话返回“null”,否则调用obj的toString
return (obj == null) ? "null" : obj.toString();
}
1.3.7.1. 解释String.valueOf("test3") == String.valueOf("test3")
String val5 = String.valueOf("test3");
String val6 = String.valueOf("test3");
System.out.println(val5 == val6);//true。"test3"在编译期间已经确定,放入常量池中。String.valueOf返回的是常量池中的字符串
对于String val5 = String.valueOf("test3")
这种代码,编译器首先会把他当作String val5 = "test3"
处理,把"test3"
放入常量池中,然后调用String.valueOf
方法返回常量池中的"test3"
字符串,所以两者相等。
- 再看一个例子
String aa = new String("1111");
String bb = new String("1111");
String val9 = String.valueOf(aa);
String val10 = String.valueOf(bb);
System.out.println(val9 == val10);//false。两个"1111"分别在堆中创建,String.valueOf返回的是堆中不同的对象
String aa = new String("1111")
这种先在堆中创建字符串"1111"
,然后String val9 = String.valueOf(aa)
返回的是堆中的字符串,所以两者不等
1.3.8. intern方法
//运行时往常量池增加字符串
//调用intern方法的时候,如果常量池中已经存在一个字符串与这个字符串相等,那么返回常量池的中字符串。
//没有的话会在常量池中创建这个字符串,然后才返回。
public native String intern();
1.3.8.1. 解释new String("test4").intern() == "test4"
String val7 = new String("test4");
String val8 = "test4";
String val7Intern = val7.intern();
System.out.println(val8 == val7);//false。val7在堆中,val8在常量池中,自然不相等
System.out.println(val8 == val7Intern);//true。intern方法的作用是在运行时往常量池中增加字符串,如果常量池池中已有,那么把常量池中的对象返回
System.out.println(val8 == val7);//false。再试验一次说明intern方法不是把堆中的地址塞到常量池中
String val7 = new String("test4")
是堆中的字符串"test4"
,String val8 = "test4"
是常量池中的"test4"
,String val7Intern = val7.intern()
intern首先检查常量池中是否有"test4"
,发现有直接返回
1.3.9. subString
String substring(int beginIndex, int endIndex) {
//下标越界判断
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
//返回自己或者调用使用char数组带下标的构造函数
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
1.4. 常见问题
1.4.1. toString和valueOf的区别
String aa = null;
//System.out.println(aa.toString());//抛出异常
System.out.println(String.valueOf(aa));//null
前者没有做为空判断,后者做了。
1.4.2. String的不可变性
String这个类是由final修饰的,意味着不能被继承
String内部通过char数组实现,而这个数组是用final修饰的。意味着一旦赋值就不能改变引用,而且String也没有提供修改字符数组内容的方法
用下面的例子解释:
String a = "aaa";
a = "bbb";//这里的可变String类型的引用改变了,但是原有的值没有变化
//这种看似修改的方法实际上返回的是一个新的String对象
String c= a.subString(1,2);
1.4.3. 线程安全
因为不可变所以线程安全
public class TestString
{
public static void main(String[] args) throws InterruptedException
{
String string = "0";
TestThread testThread = new TestThread(string);//因为不可变,所以传递进去无论做了什么操作都不影响
testThread.start();
testThread.join();
System.out.println(string);//0
}
}
class TestThread extends Thread
{
private String string;
public TestThread(String string)
{
this.string = string;
}
@Override
public void run()
{
this.string += "test";
System.out.println(Thread.currentThread().getName() + ":" + this.string);
}
}
1.4.4. String对+的重载
实际上使用的StringBuilder,并且调用append方法,最后调用toString方法
普通+
循环+
1.4.5. replaceFirst、replaceAll、replace区别
String replaceFirst(String regex, String replacement)
基于正则的替换,替换第一个String replaceAll(String regex, String replacement)
基于正则的替换,替换全部String replace(Char Sequencetarget, Char Sequencereplacement)
普通的比较替换,替换全部
1.4.6. String s = new String("abc")创建了几个字符串对象
- 当加载类时,"abc"被创建并驻留在了字符创常量池中(如果先前加载中没有创建驻留过)。
- 当执行此句时,因为"abc"对应的String实例已经存在于字符串常量池中,所以JVM会将此实例复制到会在堆(heap)中并返回引用地址
2. StringBuilder
2.1. 是什么
线程安全的、可变字符串
其实就是在StringBuilder的基础上加了synchronized关键字
2.2. 如何使用
public class TestStringBuilder
{
public static void main(String[] args) throws InterruptedException
{
StringBuffer stringBuffer = new StringBuffer();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5000; i++)
{
stringBuffer.append("aaaa");
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5000; i++)
{
stringBuffer.append("aaaa");
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(stringBuffer.toString());
System.out.println(stringBuffer.length() == 5000 * 2 * 4);//true
}
}
2.3. 原理分析
2.3.1. 构造函数
public final class StringBuffer//一样是final的
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
public StringBuffer() {
//跟StringBuilder一样调用AbstractStringBuilder的构造方法
super(16);//默认容量16个
}
}
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
}
2.3.2. append方法
//加了synchronized修饰
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
2.3.3. toString
//加了synchronized修饰
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
2.3.4. subString
public synchronized String substring(int start, int end) {
return super.substring(start, end);
}
3. StringBuffer
3.1. 是什么
线程安全的、可变字符串
其实就是在StringBuilder的基础上加了synchronized关键字
3.2. 如何使用
public class TestStringBuilder
{
public static void main(String[] args) throws InterruptedException
{
StringBuffer stringBuffer = new StringBuffer();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5000; i++)
{
stringBuffer.append("aaaa");
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5000; i++)
{
stringBuffer.append("aaaa");
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(stringBuffer.toString());
System.out.println(stringBuffer.length() == 5000 * 2 * 4);//true
}
}
3.3. 原理分析
3.3.1. 构造函数
public final class StringBuffer//一样是final的
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
public StringBuffer() {
//跟StringBuilder一样调用AbstractStringBuilder的构造方法
super(16);//默认容量16个
}
}
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
}
3.3.2. append方法
//加了synchronized修饰
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
3.3.3. toString
//加了synchronized修饰
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
3.3.4. subString
public synchronized String substring(int start, int end) {
return super.substring(start, end);
}
4. StringBuilder vs StringBuffer vs String
String | StringBuffer | StringBuilder | |
---|---|---|---|
是否线程安全 | √ | √ | × |
是否可变 | × | √ | √ |
5. 参考链接
- Java 源码分析 — String 的设计 - 掘金
- String源码分析 - 掘金
- Java中String对"+"的"重载" - 掘金
- java 中为什么说,String是线程安全的?为什么说StringBuilder是线程不安全的?分别举例证明。 - OSCHINA
- 为什么String被设计为不可变?是否真的不可变? - Jessica程序猿 - 博客园
- Java提高篇——理解String 及 String.intern() 在实际中的应用 - 萌小Q - 博客园
2.Java SDK源码分析系列笔记-String系列的更多相关文章
- Java 集合源码分析(一)HashMap
目录 Java 集合源码分析(一)HashMap 1. 概要 2. JDK 7 的 HashMap 3. JDK 1.8 的 HashMap 4. Hashtable 5. JDK 1.7 的 Con ...
- Java Reference 源码分析
@(Java)[Reference] Java Reference 源码分析 Reference对象封装了其它对象的引用,可以和普通的对象一样操作,在一定的限制条件下,支持和垃圾收集器的交互.即可以使 ...
- element-ui 组件源码分析整理笔记目录
element-ui button组件 radio组件源码分析整理笔记(一) element-ui switch组件源码分析整理笔记(二) element-ui inputNumber.Card .B ...
- element-ui Carousel 走马灯源码分析整理笔记(十一)
Carousel 走马灯源码分析整理笔记,这篇写的不详细,后面有空补充 main.vue <template> <!--走马灯的最外层包裹div--> <div clas ...
- java集合源码分析(三):ArrayList
概述 在前文:java集合源码分析(二):List与AbstractList 和 java集合源码分析(一):Collection 与 AbstractCollection 中,我们大致了解了从 Co ...
- java集合源码分析(六):HashMap
概述 HashMap 是 Map 接口下一个线程不安全的,基于哈希表的实现类.由于他解决哈希冲突的方式是分离链表法,也就是拉链法,因此他的数据结构是数组+链表,在 JDK8 以后,当哈希冲突严重时,H ...
- Java集合源码分析(三)LinkedList
LinkedList简介 LinkedList是基于双向循环链表(从源码中可以很容易看出)实现的,除了可以当做链表来操作外,它还可以当做栈.队列和双端队列来使用. LinkedList同样是非线程安全 ...
- Java Collections 源码分析
Java Collections API源码分析 侯捷老师剖析了不少Framework,如MFC,STL等.侯老师有句名言: 源码面前,了无秘密 这句话还在知乎引起广泛讨论. 我对教授程序设计的一点想 ...
- java HashMap源码分析(JDK8)
这两天在复习JAVA的知识点,想更深层次的了解一下JAVA,所以就看了看JAVA的源码,把自己的分析写在这里,也当做是笔记吧,方便记忆.写的不对的地方也请大家多多指教. JDK1.6中HashMap采 ...
- Java集合源码分析(六)TreeSet<E>
TreeSet简介 TreeSet 是一个有序的集合,它的作用是提供有序的Set集合.它继承于AbstractSet抽象类,实现了NavigableSet<E>, Cloneable, j ...
随机推荐
- 区块链特辑——solidity语言基础(四)
Solidity语法基础学习 七.事件: 事件 Event ·日志(log),是用来快速索引并查询过往资料的手段. ·而solidity是透过"事件"在区块链上写下日志,使用者或由 ...
- 【Java】修饰符
修饰符(Modifier):是用于限定类型以及类型成员的声明的一种符号. 其用来定义类.方法或者变量,通常放在语句的最前端. 例子: public class Person { default Str ...
- 必须添加对程序集"System.Core"的引用
异常波浪线 解决办法 <system.web> <compilation> <assemblies> <add assembly="System.C ...
- C#/.NET/.NET Core技术前沿周刊 | 第 32 期(2025年3.24-3.31)
前言 C#/.NET/.NET Core技术前沿周刊,你的每周技术指南针!记录.追踪C#/.NET/.NET Core领域.生态的每周最新.最实用.最有价值的技术文章.社区动态.优质项目和学习资源等. ...
- DAY1--ROS基本认知
1.ROS基本框架 ROS架构如下图所示,可以将其分为三个层次:OS层.中间层和应用层. 1.1 应用层 应用层是用户直接交互的部分,包含以下核心组件: Master: ROS的核心协调者,负责节点( ...
- Eclipse 中 JAVA AWT相关包不提示问题(解决)
原因: 由于在2021年7月15日 OpenJDK管理委员会全票通过批准成立由Phil Race担任初始负责人的 Client Libraries Group(客户端类库工作组). 新的工作组将继续赞 ...
- kettle介绍-Step之CSV Input
CSV Input/CSV 文件输入介绍 CSV 文件输入步骤主要用于将 CSV 格式的文本文件按照一定的格式输入至 流中 Step name:步骤的名称,在单一转换中,名称必须唯一 Filename ...
- FastAPI与Tortoise-ORM实现关系型数据库关联
title: FastAPI与Tortoise-ORM实现关系型数据库关联 date: 2025/04/21 10:51:41 updated: 2025/04/21 10:51:41 author: ...
- 题解:AT_abc369_d [ABC369D] Bonus EXP
题目大意: 有 nnn 个怪物,每个怪物有一个战力值 aia_iai ,你可以选择击败他或放走他,放走他没有经验值,击败他可以获得 aia_iai 的经验值,如果击败的数量是偶数,则还可以获得 a ...
- 2.3K star!5分钟搭建专属网课平台?这个开源项目强得离谱!
嗨,大家好,我是小华同学,关注我们获得"最新.最全.最优质"开源项目和高效工作学习方法 在线视频会议,在线教育和辅导变得越来越普及.而一款优秀的视频会议系统对于在线辅导来说至关重要 ...