上篇文章我们讲解了基本类型低层的那些事,今天我们来聊一聊日常开发中用的最多的String。
一、String是什么?
String 是 Java 中的一个引用类型,用于表示文本字符串,它具有不可变性,一旦创建,就无法修改。
二、源码探究
java21源码:
arduino
// 存储字符串的字节数组(不可变)
private final byte[] value;
// 编码标识:0=LATIN1,1=UTF16(JDK 9+ 新增)
private final byte coder;
// 哈希值缓存(懒加载)
private int hash;
// 哈希值是否为 0 的标记(优化判断)
private boolean hashIsZero;
// 控制是否启用紧凑字符串(JVM 注入,默认 true)
static final boolean COMPACT_STRINGS;
1、Java 9 之前,String 内部是用一个 char[] 来存储字符串的,char 类型占2个字节。
2、Java 9之后用byte[]存储,同时 引入了 Compact Strings ,核心思想是:根据字符串的内容,选择不同的编码方式来存储,以达到节省空间的目的。
3、如果字符串只包含 Latin-1 字符,那么 value 数组的长度就等于字符串的长度。与之前的 char[] 相比,内存占用直接减半。
4、如果字符串包含了非 Latin-1 的字符(例如中文、日文、特殊符号等),那么它会使用 UTF-16 编码来存储,value 数组的长度是字符串 UTF-16 编码后字节数的总和。
5、value、coder、COMPACT_STRINGS均被final修饰,保证了字符串的不可变性。
三、为什么 String 是不可变的?
1. 安全性
- 字符串常被用作参数(如数据库连接、网络请求 URL),如果可变,容易被恶意篡改。
- 多线程环境下,不可变对象天然线程安全。
2. 缓存哈希值
String的hashCode()被频繁使用(如 HashMap 的 key)。不可变意味着哈希值可缓存,提升性能。
3. 字符串常量池优化
- JVM 会复用相同的字符串字面量,节省内存。
四、字符串常量池
JVM 维护的一块特殊区域,避免重复创建相同内容的字符串。
ini
String a = "hello";
String b = "hello";
System.out.println(a == b); // true!因为都指向常量池中的同一个对象
String c = new String("hello");
String d = new String("hello");
System.out.println(c == d); // false!new 创建的是堆中的新对象
ini
//intern()的作用:确保返回一个与当前字符串内容相同的、来自字符串常量池的引用。
String e = new String("hello").intern();
System.out.println(a == e); // true!.intern() 会将字符串加入常量池并返回引用---
五、== 和 equals()的区别
这是新手最容易踩的坑!
==:比较引用地址(是否是同一个对象)equals():比较内容是否相等
ini
String x = "java";
String y = new String("java");
System.out.println(x == y); // false
System.out.println(x.equals(y)); // true
✅ 最佳实践:永远用
.equals()比较字符串内容!
六、String、StringBuilder 与 StringBuffer三者的区别
高频面试点:
| 类型 | 可变性 | 线程安全 | 性能 | 适用场景 |
|---|---|---|---|---|
String |
不可变 | 是 | 低(频繁拼接会创建大量对象) | 少量字符串操作 |
StringBuilder |
可变 | 否 | 高 | 单线程下大量拼接 |
StringBuffer |
可变 | 是 | 中 | 多线程下拼接 |
ini
// 错误示范(性能差):
String s = "";
for (int i = 0; i < 10000; i++) {
s += "a"; // 每次都新建对象!
}
// 正确做法:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append("a");
}
String result = sb.toString();
七、手撸简版String类
java
package org.example.javase;
public class MyString {
// 简化版,我们仍用char[]存储,内部存储字符的数组,final 保证引用不可变
private final char[] value;
/**
* 构造函数
* @param value 字符数组
*/
public MyString(char[] value) {
// 进行防御性拷贝,防止外部数组修改影响内部状态
this.value = new char[value.length];
System.arraycopy(value, 0, this.value, 0, value.length);
}
/**
* 返回字符串长度
* @return 长度
*/
public int length() {
return value.length;
}
/**
* 返回指定索引处的字符
* @param index 索引
* @return 字符
* @throws StringIndexOutOfBoundsException 如果索引越界
*/
public char charAt(int index) {
if (index < 0 || index >= value.length) {
throw new StringIndexOutOfBoundsException("Index: " + index + ", Length: " + value.length);
}
return value[index];
}
/**
* 比较字符串内容是否相等
* @param anObject 要比较的对象
* @return 如果相等则为 true,否则为 false
*/
@Override
public boolean equals(Object anObject) {
// 1. 引用相等,直接返回 true
if (this == anObject) {
return true;
}
// 2. 类型不同,返回 false
if (!(anObject instanceof MyString)) {
return false;
}
// 3. 类型相同,比较内容
MyString anotherString = (MyString) anObject;
int n = value.length;
// 先比较长度,长度不同则内容一定不同
if (n != anotherString.value.length) {
return false;
}
// 逐字符比较
for (int i = 0; i < n; i++) {
if (value[i] != anotherString.value[i]) {
return false;
}
}
return true;
}
/**
* 计算哈希值
* 算法参考:s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
* @return 哈希值
*/
@Override
public int hashCode() {
int h = 0;
if (value.length > 0) {
char[] val = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
}
return h;
}
/**
* 返回字符串本身
* @return 字符串
*/
@Override
public String toString() {
// 直接利用 Java 原生 String 的构造函数来创建一个字符串并返回
return new String(value);
}
/**
* 拼接字符串
* @param str 要拼接的字符串
* @return 拼接后的新字符串
*/
public MyString concat(MyString str) {
if (str == null || str.length() == 0) {
return this; // 如果拼接的是空字符串,返回原对象
}
int len1 = this.value.length;
int len2 = str.value.length;
// 创建一个新的字符数组来存储拼接后的结果
char[] buf = new char[len1 + len2];
// 复制当前字符串的内容
System.arraycopy(this.value, 0, buf, 0, len1);
// 复制要拼接的字符串的内容
System.arraycopy(str.value, 0, buf, len1, len2);
// 返回一个新的 MyString 对象
return new MyString(buf);
}
/**
* 截取子字符串
* @param beginIndex 开始索引(包含)
* @return 截取后的新字符串
* @throws StringIndexOutOfBoundsException 如果索引越界
*/
public MyString substring(int beginIndex) {
if (beginIndex < 0 || beginIndex > value.length) {
throw new StringIndexOutOfBoundsException("beginIndex: " + beginIndex);
}
int subLen = value.length - beginIndex;
// 如果从0开始截取,且长度不变,直接返回原对象
if (beginIndex == 0 && subLen == value.length) {
return this;
}
// 创建新的字符数组
char[] subValue = new char[subLen];
System.arraycopy(value, beginIndex, subValue, 0, subLen);
return new MyString(subValue);
}
public static void main(String[] args) {
char[] chars1 = {'H', 'e', 'l', 'l', 'o'};
MyString str1 = new MyString(chars1);
System.out.println("str1: " + str1.toString());
System.out.println("str1.length(): " + str1.length());
System.out.println("str1.charAt(1): " + str1.charAt(1));
char[] chars2 = {'W', 'o', 'r', 'l', 'd'};
MyString str2 = new MyString(chars2);
MyString concatenated = str1.concat(str2);
System.out.println("str1.concat(str2): " + concatenated.toString());
MyString substr = concatenated.substring(5);
System.out.println("concatenated.substring(5): " + substr.toString());
MyString str3 = new MyString(chars1);
System.out.println("str1.equals(str3): " + str1.equals(str3));
System.out.println("str1.equals(str2): " + str1.equals(str2));
System.out.println("str1.hashCode(): " + str1.hashCode());
System.out.println("str3.hashCode(): " + str3.hashCode());
// 尝试修改原字符数组,验证不可变性
chars1[0] = 'X';
System.out.println("After modifying chars1, str1: " + str1.toString());
}
}
七、思考题
下边的代码输出什么?评论区说出你的答案。
ini
String s1 = "ab";
String s2 = "a" + "b";
System.out.println(s1 == s2); // ?
String a = "a";
String s2 = a + "b"; // 运行时拼接
System.out.println(s1 == s2); // ?
觉得写的不错的小伙伴 ,点个关注,更新不迷路