Java String类:不可变性的核心奥秘
目录
一、源码
1、String类声明核心代码
2、核心方法源码
①length()方法
②charAt()方法
③substring()方法
④replace()方法
3、构造函数源码
4、总结
二、常用构造方法
三、常量池
四、常用方法
1、length()
2、equals()
3、charAt()
4、toCharArray()
5、split()
6、replace()
7、substring()
String 类是 java 中最古老、最重要的类之一
它属于 java.util 包下,所以使用时不需要导包
它的设计体现了 java 的核心哲学:不可变性
一、源码
在学习之前,我们先来通过源码看一下不可变性体现在哪里:
1、String类声明核心代码
// JDK 8 中的 String 类声明
public final class Stringimplements java.io.Serializable, Comparable<String>, CharSequence {// 核心存储结构 - 字符数组private final char value[];// 缓存哈希码,提高性能private int hash;
}
2、核心方法源码
①length()方法
public int length() {return value.length; // 直接返回数组长度,不修改任何内容
}
②charAt()方法
public char charAt(int index) {if ((index < 0) || (index >= value.length)) {throw new StringIndexOutOfBoundsException(index);}return value[index]; // 只读取,不修改
}
③substring()方法
public String substring(int beginIndex) {int subLen = value.length - beginIndex;if (beginIndex < 0) {throw new StringIndexOutOfBoundsException(beginIndex);}if (subLen < 0) {throw new StringIndexOutOfBoundsException(subLen);}// 返回新的String对象,共享原数组(JDK 8中)return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}public 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);}// 返回新的String对象return ((beginIndex == 0) && (endIndex == value.length)) ? this: new String(value, beginIndex, subLen);
}
④replace()方法
public String replace(char oldChar, char newChar) {// 如果新旧字符相同,直接返回原对象if (oldChar != newChar) {int len = value.length;int i = -1;// 查找第一个需要替换的字符while (++i < len) {if (value[i] == oldChar) {break;}}// 如果找到了需要替换的字符if (i < len) {char buf[] = new char[len];// 复制前面不需要替换的部分for (int j = 0; j < i; j++) {buf[j] = value[j];}// 替换并复制剩余部分for (; i < len; i++) {char c = value[i];buf[i] = (c == oldChar) ? newChar : c;}// 创建新的String对象return new String(buf, true);}}return this; // 没有找到需要替换的字符,返回原对象
}
3、构造函数源码
// 包级私有构造函数,用于优化substring等操作
String(char[] value, boolean share) {// assert share : "unshared not supported";this.value = value; // 直接引用传入的数组
}// 公共构造函数,复制数组确保不可变性
public String(char value[]) {this.value = Arrays.copyOf(value, value.length); // 创建副本
}
4、总结
①类:final 修饰类,防止继承破坏不可变性
②数据:final 修饰 value 数组,防止引用改变
③方法:所有方法都返回新对象或原始值,不修改原对象
④构造函数:通过数组复制确保外部无法通过原数组修改 String 内容
⑤缺少修改方法:没有提供任何修改内部状态的方法
二、常用构造方法
方法名 | 说明 |
public String() | 创建一个空白字符串对象,不含有任何内容 |
public String(char[] arr) | 根据字符数组的内容,创建字符串对象 |
public String(String original) | 根据传入的字符串内容,创建字符串对象 |
String s = "abc"; | 直接赋值的方式创建字符串对象 |
示例:
public class Test{public static void main(String[] args){// public String():// 创建一个空白字符串对象,不含有任何内容String s1 = new String();System.out.println(s1);// public String(char[] chs):// 根据字符数组的内容,来创建字符串对象char[] chs = {'a','b','c'};String s2 = new String(chs);System.out.println(s2);// public String(String original) : //根据传入的字符串内容,来创建字符串对象String s3 = new String("123");System.out.println(s3);String s4 = "hello";System.out.println(s4);}
}
字符串对象创建之后,堆空间中字符串的 内容 和 内存地址 不可以被修改, 对象引用 可以修改
public class ReferenceMutability {public static void main(String[] args) {// 引用str1指向堆中的"Hello"对象String str1 = "Hello";// 引用str1现在指向堆中的"World"对象// 这是引用的改变,不是对象内容的改变str1 = "World";// 引用str1现在指向堆中的"Java"对象str1 = "Java";System.out.println(str1); // 输出: Java// 演示多个引用指向同一对象String str2 = "Hello";String str3 = "Hello";// str2和str3指向同一个对象(字符串常量池)System.out.println("str2 == str3: " + (str2 == str3)); // true// 但当我们"修改"时,实际上是创建新对象str2 = str2 + " World";// 现在str2指向新对象,str3仍指向原对象System.out.println("str2: " + str2); // Hello WorldSystem.out.println("str3: " + str3); // HelloSystem.out.println("str2 == str3: " + (str2 == str3)); // false}
}
三、常量池
那么既然堆内存中的值不可变
它又要频繁创建
有没有什么好的办法呢?
JVM 为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化:
为字符串开辟了一个 字符串常量池,类似于缓存区
创建字符串常量时,首先会检查字符串常量池中是否存在该字符串,如果存在,则返回该实例的引用;如果不存在,则实例化创建该字符串,并放入池中。
java 8 之前,字符串常量池在方法区中
java 8 及之后,字符串常量池位于堆内存中,方便调整字符串常量池大小,并且可以享受到垃圾回收器对堆内存的优化
java 将字符串放入 String 常量池的方法:
1、直接赋值:
eg:String str = "Hello";
2、调用 String 类提供的 intern() 方法:
eg:String str = new String("World").intern();
该方法作用:
若字符串常量池中存在内容相同字符串,则返回池中的引用
若不存在,则将当前字符串放入池中并返回池中的引用
注意!
通过 new 关键字创建的字符串对象不会放入常量池中,也不会从池中取
而是从堆内存中创建一个新对象
示例:
public class Test{public static void main(String[] args){String s1 = "Hello"; // 字符串常量,放入常量池String s2 = "Hello"; // 直接引用常量池中的字符串对象System.out.println(s1 == s2); // true,引用相同// 直接new String对象,不会将'World'放入常量池String s3 = new String("World");// 调用intern()方法,将'World'放入常量池,并返回常量池中的引用String s4 = new java.lang.String("World").intern();String s5 = "World";System.out.println(s3 == s4); // false,引用不同System.out.println(s4 == s5); // true,引用相同}
}
public class Test{public static void main(String[] args){String a = 'a';String b = 'b';// 常量优化机制:"a" 和 "b"都是字面值常量,借助 + 连接,其结果 "ab" 也被当作常量String s3 = "a" + "b";String s4 = "ab";System.out.println(s3.equals(s4)); // trueSystem.out.println(s3 == s4); // trueSystem.out.println("-------------");String s5 = s1 + s2;System.out.println(s4.equals(s5)); // trueSystem.out.println(s4 == s5); // falseSystem.out.println("-------------");String s6 = (s1 + s2).intern();System.out.println(s4.equals(s6)); // trueSystem.out.println(s4 == s6); // true}
}
四、常用方法
1、length()
public int length()
调用者:String类型对象调用
参数:无参
返回值:int 类型当前字符串长度
作用:返回 int 类型当前字符串长度
会不会改变原始值:不会
public class StringLengthDemo {public static void main(String[] args) {String story = "在遥远的东方,有一条龙";System.out.println("故事长度:" + story.length()); // 输出:11// 小知识:这个方法的时间复杂度是 O(1),不是 O(n)// 因为长度在对象创建时就已经计算好了}
}
2、equals()
public boolean equals(Object obj)
调用者:String类型对象调用
参数:Object obj 要比较的字符串对象
返回值:boolean
作用:判断两个字符串是否相等
会不会改变原始值:不会
public class StringComparison {public static void main(String[] args) {String a = new String("Hello");String b = new String("Hello");String c = "Hello";String d = "Hello";System.out.println(a == b); // false - 比较引用System.out.println(a.equals(b)); // true - 比较内容System.out.println(c == d); // true - 字符串常量池的魔法}
}
3、charAt()
public char charAt(int index)
调用者:String类型对象调用
参数:索引值
返回值:char
作用:返回指定索引值位置的char值
会不会改变原始值:不会会不会改变原始值:不会
其他作用:for循环遍历,将字符串变为字符数组
注意:
索引超出长度 运行时错误:java.lang.StringIndexOutOfBoundsException
索引正常范围
索引为负数 运行时错误:java.lang.StringIndexOutOfBoundsException
public class CharacterExplorer {public static void main(String[] args) {String password = "Java123!";// 安全的字符遍历方式for (int i = 0; i < password.length(); i++) {char ch = password.charAt(i);System.out.println("位置 " + i + " 的字符是:" + ch);}// 注意:负数索引会抛出 StringIndexOutOfBoundsExceptiontry {char error = password.charAt(-1);} catch (StringIndexOutOfBoundsException e) {System.out.println("索引越界啦!这是 Java 的保护机制。");}}
}
4、toCharArray()
public char[] toCharArray()
调用者:String类型对象调用
参数:无参
返回值:char[]
作用:字符串改为数组
会不会改变原始值:不会
public class ToCharArrayDemo {public static void main(String[] args) {String str = "Hello World";// 将字符串转换为字符数组char[] charArray = str.toCharArray();System.out.println("原字符串: " + str);System.out.print("字符数组: ");// 遍历字符数组for (char ch : charArray) {System.out.print("'" + ch + "' ");}System.out.println();// 修改字符数组不会影响原字符串charArray[0] = 'h';System.out.println("修改数组后原字符串: " + str); // 仍然是 "Hello World"System.out.println("修改后的数组第一个字符: " + charArray[0]); // 变为 'h'}
}
5、split()
public String[] split(String regex)
public String[] split(String regex, int limit)
调用者:String类型对象调用
参数:正则表达式(规则)
返回值:String[]
作用:将字符串按照正则表达式进行切割,返回字符串数组
注意:特殊符号要加转义字符
会不会改变原始值:不会
public class SplitDemo {public static void main(String[] args) {// 基本用法String sentence = "Java is awesome and powerful";String[] words = sentence.split(" ");System.out.println("原句子: " + sentence);System.out.print("分割结果: ");for (String word : words) {System.out.print("[" + word + "] ");}System.out.println();// 使用正则表达式分割String data = "apple,banana;orange:grape";String[] fruits = data.split("[,;:]"); // 使用字符类分割System.out.print("多种分隔符分割: ");for (String fruit : fruits) {System.out.print("[" + fruit + "] ");}System.out.println();// 特殊字符需要转义String path = "user\\documents\\file.txt";String[] pathParts = path.split("\\\\"); // 转义用“\\”或“[]”System.out.print("路径分割: ");for (String part : pathParts) {System.out.print("[" + part + "] ");}System.out.println();// 限制分割次数String numbers = "1,2,3,4,5,6";String[] limited = numbers.split(",", 3); // 最多分割成3部分System.out.print("限制分割次数: ");for (String num : limited) {System.out.print("[" + num + "] ");}System.out.println();}
}
多个需要转义的分隔符的话:
用“\\”:每个之间需要加上“|”(或)
用“[]”:直接都写在方括号里即可
注意:
在 [ ] 中仍需转义的字符:
- 反斜杠
\
:由于其在正则中始终是转义字符,需双重转义为\\\\
13。- 右方括号
]
:作为字符组结束符,需转义为\\]
310。- 脱字符
^
:仅在字符组开头表示取反时需转义(\\^
),否则无需转义 10。- 连字符
-
:仅在表示范围(如a-z
)时需转义为\\-
;位于字符组开头或结尾时可省略转义 10。
6、replace()
public String replace(char oldChar, char newChar)
调用者:String类型对象调用
参数:两个char类型的参数,一个旧的值,一个新的值
返回值:新的String
作用:将字符串中的旧字符替换成新的字符
注意:所有满足条件的旧值都会被替换成新的值
所有都不满足,返回原始字符串
public String replace(CharSequence target, CharSequence replacement)
调用者:String类型对象调用
参数:两个CharSequence类型的参数(字符串)
返回值:新的String
作用:将字符串中的旧字符替换成新的字符
public class ReplaceDemo {public static void main(String[] args) {// replace(char oldChar, char newChar)String text1 = "Hello World";String result1 = text1.replace('l', 'L');System.out.println("原字符串: " + text1);System.out.println("替换字符后: " + result1); // HeLLo WorLd// replace(CharSequence target, CharSequence replacement)String text2 = "Java is great, Java is powerful";String result2 = text2.replace("Java", "Python");System.out.println("原字符串: " + text2);System.out.println("替换字符串后: " + result2); // Python is great, Python is powerful// 没有匹配项的情况String text3 = "Hello World";String result3 = text3.replace('x', 'y');System.out.println("无匹配项: " + result3); // 仍然是 "Hello World"// 部分匹配的情况String text4 = "banana";String result4 = text4.replace("ana", "XXX");System.out.println("部分替换: " + result4); // bXXXna// 替换空字符串String text5 = "Hello World"; // 两个空格String result5 = text5.replace(" ", " "); // 替换为一个空格System.out.println("空格替换: '" + result5 + "'"); // 'Hello World'}
}
7、substring()
public String substring(int beginIndex)
调用者:String类型对象调用
参数:int下标,代表开始截取的位置(包含开始的位置)
返回值:String(截取后的字符串)
作用:字符串截取
注意:下标越界
会不会改变原始值:不会
public String substring(int beginIndex, int endIndex)
调用者:String类型对象调用
参数:int下标,代表开始截取的位置(包含开始位置)int下标,代表结束截取的位置(不包含结束位置)
返回值:String(截取后的字符串)
作用:字符串截取
注意:下标越界
会不会改变原始值:不会
public class SubstringDemo {public static void main(String[] args) {String email = "user@example.com";// substring(int beginIndex) - 从指定位置到末尾String username = email.substring(0, email.indexOf('@'));String domain = email.substring(email.indexOf('@') + 1);System.out.println("原邮箱: " + email);System.out.println("用户名: " + username);System.out.println("域名: " + domain);// substring(int beginIndex, int endIndex) - 指定起始和结束位置String str = "Hello World Java";String sub1 = str.substring(6, 11); // 从索引6到10(不包含11)String sub2 = str.substring(12); // 从索引12到末尾System.out.println("原字符串: " + str);System.out.println("substring(6,11): " + sub1); // WorldSystem.out.println("substring(12): " + sub2); // Java// 边界情况演示try {String error = str.substring(20); // 索引越界} catch (StringIndexOutOfBoundsException e) {System.out.println("索引越界异常: " + e.getMessage());}try {String error = str.substring(5, 2); // 起始索引大于结束索引} catch (StringIndexOutOfBoundsException e) {System.out.println("索引范围错误: " + e.getMessage());}// 空字符串处理String empty = "";try {String result = empty.substring(0);System.out.println("空字符串substring(0): '" + result + "'");} catch (StringIndexOutOfBoundsException e) {System.out.println("空字符串异常: " + e.getMessage());}}
}