🔍 一、String核心特性:不可变的艺术
1.1 String的特性
概述
- String 类位于 java.lang 包中,无需额外导入。
- String 对象的字符内容是存储在一个字符数组 value[]中的。“abc” 等效于 char[]
data={‘h’,‘e’,‘l’,‘l’,‘o’}。
特点
- 不可变性:String 是 final 类,创建后值不可更改。
- 共享性:相同的字符串可以被多个 String 对象共享。
- 底层实现:String 实际上类似于字符数组,但在 JDK 9 及以后使用字节数组。
- 序列化支持:实现了 Serializable 接口。
- 可比较性:实现了 Comparable 接口,可以比较大小。
不可变性:
- 重新赋值:对字符串重新赋值时,会在新的内存区域创建字符串,不能使用原有的值。
- 连接操作:连接字符串时,会生成新的字符串对象,而不是修改原有的值。
- 替换操作:使用 replace() 方法时,也会创建新的字符串,而不改变原有值。
jdk8 中的 String 源码:
public final class String
implements java.io.Serializable, Comparable<String>, CharSe
quence {
/** The value is used for character storage. */
private final char value[]; //String 对象的字符内容是存储在此
数组中
/** Cache the hash code for the string */
private int hash; // Default to 0
说明:
- JDK8基于char[]存储,JDK9+优化为byte[],均被final修饰
- private 限制外部访问字符数组,且没有提供 getter 和 setter 方法。
- final 表示字符数组引用不可变,无法修改其内容。因此,String 是不可变的,一旦修改会产生新对象。
1.2 String的内存结构
因为字符串对象设计为不可变,所以字符串有常量池来保存很多常量对象。
字符串常量池(String Table)
JDK版本 | 常量池位置 | 特点 |
---|---|---|
≤1.6 | 方法区(永久代) | Full GC时回收 |
1.7+ | 堆内存 | 被所有线程共享,GC管理 |
举例说明:
- 以" "方式给出的字符串【字面量的定义方式】,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM都只会建立一个String 对象,并在字符串池中维护
string s3 = "abc" ;
string s4 = "abc" ;
针对第一行代码,JVM会建立一个String对象放在字符串池中,并给s3参考;第二行则让s4直接参考字符串池中的String对象,也就是说它们本质上是同一个对象
注:通过字面量赋值时,字符串会存储在字符串常量池中,避免重复内容。
- 通过 new 创建:每次使用 new 创建字符串时,都会申请新的内存空间,即使内容相同。
char[] chs = {'a', 'b ','c'};
string s1 = new string(chs);
string s2 = new string (chs);
JVM会首先创建一个字符数组,然后每一次new的时候都会有一个新的地址,只不过s1和s2参考的字符串内容是相同的。
面试题: String s = new String(“abc”); 创建对象,在内存中创建了几个对象?
- 两个。一个是堆空间中new结构,另一个 是char[]对应的常量池中的数据: “abc”
- intern() 方法
String s4 = s1.intern();
调用 intern() 后,如果常量池中存在相同字符串,则 s4 将引用常量池中的字符串。
内存结构分配图示:
1.3 字符串的比较
使用==做比较
- 基本类型:比较的是数据值是否相同
- 引用类型:比较的是地址值是否相同
字符串是对象,它比较内容是否相同,是通过一个方法来实现的,这个方法叫: equals(public boolean equals(Object anObject):将此字符串与指定对象进行比较。由于我们比较的是字符串对象,所以参数直接传递一个字符串。
练习示例
String s1 = "javaEE";
String s2 = "javaEE";
String s3 = new String("javaEE");
String s4 = new String("javaEE");
System.out.println(s1 == s2);//true
System.out.println(s1 == s3);//false
System.out.println(s1 == s4);//false
System.out.println(s3 == s4);//false
练习:
String s1 = "hello";
String s2 = "world";
String s3 = "hello" + "world";
String s4 = s1 + "world";
String s5 = s1 + s2;
String s6 = (s1 + s2).intern();
System.out.println(s3 == s4); false
System.out.println(s3 == s5); false
System.out.println(s4 == s5); false
System.out.println(s3 == s6); true
结论:
(1)常量+常量:结果是常量池。且常量池中不会存在相同内容的常量。
(2)常量与变量 或 变量与变量:结果在堆中
(3)拼接后调用 intern 方法:返回值在常量池中
(4)concat 方法拼接,哪怕是两个常量对象拼接,结果也是在堆。
⚡二、String类的常用方法
构造器
public String() :初始化新创建的 String 对象,以使其表示空字符序列。
String(String original): 初始化一个新创建的 String 对象,使其表示一个与参数相同的字符序列;换句话说,新创建的字符串是该参数字符串的副本。
public String(char[] value) :通过当前参数中的字符数组来构造新的String。
public String(char[] value,int offset, int count) :通过字符数组的一部分来构造新的 String。
public String(byte[] bytes) :通过使用平台的默认字符集解码当前参数中的字节数组来构造新的 String。
public String(byte[] bytes,String charsetName) :通过使用指定的字符集解码当前参数中的字节数组来构造新的 String。
系列 1:常用方法(基础操作)
boolean isEmpty():字符串是否为空
int length():返回字符串的长度
String concat(xx):拼接
boolean equals(Object obj):比较字符串是否相等,区分大小写
boolean equalsIgnoreCase(Object obj):比较字符串是否相等,不区分大小写
int compareTo(String other):比较字符串大小,区分大小写,按照 Unicode 编码值比较大小
int compareToIgnoreCase(String other):比较字符串大小,不区分大小写
String toLowerCase():将字符串中大写字母转为小写
String toUpperCase():将字符串中小写字母转为大写
String trim():去掉字符串前后空白符
public String intern():结果在常量池中共享
系列 2:查找
boolean contains(xx):是否包含 xx
int indexOf(xx):从前往后找当前字符串中 xx,即如果有返回第一次出现的下标,要是没有返回-1
int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始
int lastIndexOf(xx):从后往前找当前字符串中 xx,即如果有返回最后一次出现的下标,要是没有返回-1
int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索。
系列 3:字符串截取
String substring(int beginIndex) :返回一个新的字符串,它是此字符串
的从 beginIndex 开始截取到最后的一个子字符串。
String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字符串从beginIndex开始截取到 endIndex(不包含)的一个子字符串。
系列 4:和字符/字符数组相关
char charAt(index):返回[index]位置的字符
char[] toCharArray(): 将此字符串转换为一个新的字符数组返回
static String valueOf(char[] data) :返回指定数组中表示该字符序列的 String
static String valueOf(char[] data, int offset, int count) : 返回指定数组中表示该字符序列的 String
static String copyValueOf(char[] data): 返回指定数组中表示该字符序列的 String
static String copyValueOf(char[] data, int offset, int count):返回指定数组中表示该字符序列的 String
系列 5:开头与结尾
boolean startsWith(xx):测试此字符串是否以指定的前缀开始
boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始
boolean endsWith(xx):测试此字符串是否以指定的后缀结束
系列 6:替换
String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。 不支持正则。
String replace(CharSequence target, CharSequence replacement):使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。
String replaceAll(String regex, String replacement):使用给定的replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
String replaceFirst(String regex, String replacement):使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。
🔥 三、String vs StringBuilder vs StringBuffer
字符串相关类之可变字符序列:StringBuffer、StringBuilder
因为 String 对象是不可变对象,虽然可以共享常量对象,但是对于频繁字符串的修改和拼接操作,效率极低,空间消耗也比较高。因此,JDK 又在 java.lang包提供了可变字符序列 StringBuffer 和 StringBuilder 类型。
StringBuffer 与 StringBuilder 的理解
- java.lang.StringBuffer 代表可变的字符序列,JDK1.0 中声明,可以对字符串内容进行增删,此时不会产生新的对象。
- StringBuilder 和 StringBuffer 非常类似,均代表可变的字符序列,而且提供相关功能的方法也一样。
核心差异对比
特性 | String | StringBuilder | StringBuffer |
---|---|---|---|
底层数据结构 | char[]数组存储 (JDK8.0 中) | char[]数组存储 (JDK8.0 中) | char[]数组存储 (JDK8.0 中) |
可变性 | ❌ 不可变 | ✅ 可变 | ✅ 可变 |
线程安全 | - | ❌ 不安全 | ✅ 安全(synchronized) |
性能 | 低 (频繁操作时) | 最高 | 中等 |
适用场景 | 静态数据、常量 | 单线程字符串操作 | 多线程字符串操作 |
StringBuilder、StringBuffer 的 API
1、常用 API
StringBuffer append(xx):提供了很多的 append()方法,用于进行字符串追加的方式拼接
StringBuffer delete(int start, int end):删除[start,end)之间字符
StringBuffer deleteCharAt(int index):删除[index]位置字符
StringBuffer replace(int start, int end, String str):替换[start,end)范围的字符序列为 str
void setCharAt(int index, char c):替换[index]位置字符
char charAt(int index):查找指定 index 位置上的字符
StringBuffer insert(int index, xx):在[index]位置插入 xx
int length():返回存储的字符数据的长度
StringBuffer reverse():反转
注:当 append 和 insert 时,如果原来 value 数组长度不够,可扩容。
2. 其它 API
int indexOf(String str):在当前字符序列中查询 str 的第一次出现下标
int indexOf(String str, int fromIndex):在当前字符序列[fromIndex,最后]中查询 str 的第一次出现下标
int lastIndexOf(String str):在当前字符序列中查询 str 的最后一次出现下标
int lastIndexOf(String str, int fromIndex):在当前字符序列[fromIndex,最后]中查询 str 的最后一次出现下标
String substring(int start):截取当前字符序列[start,最后]
String substring(int start, int end):截取当前字符序列[start,end)
String toString():返回此序列中数据的字符串表示形式
void setLength(int newLength) :设置当前字符序列长度为 newLength
扩容问题:
- 如果要添加的数据底层数组盛不下了,那就需要扩容底层的数组。默认情况下,扩容为原来容量的2倍 + 2。
- 同时将原有数组中的元素复制到新的数组中。
指导意义:开发中建议大家使用: StringBuffer(int capacity) 或 StringBuilder(int capacity)
🛠 四、字符串转换技巧
字符串与基本数据类型、包装类之间的转换
字符串 --> 基本数据类型、包装类:
• Integer 包装类的 public static int parseInt(String s):可以将由“数字”字符组成的字符串转换为整型。
• 类似地,使用 java.lang 包中的 Byte、Short、Long、Float、Double 类调相应的类方法可以将由“数字”字符组成的字符串,转化为相应的基本数据类型。
基本数据类型、包装类 --> 字符串:
- 调用 String 类的 public String valueOf(int n)可将 int 型转换为字符串
- 相应的 valueOf(byte b)、valueOf(long l)、valueOf(float f)、valueOf(double d)、valueOf(boolean b)可由参数的相应类型到字符串的转换。
字符数组与字符串的转换
字符数组 --> 字符串:
- String 类的构造器:String(char[]) 和 String(char[],int offset,int length) 分别用字符数组中的全部字符和部分字符创建字符串对象。
字符串 --> 字符数组:
- public char[] toCharArray():将字符串中的全部字符存放在一个字符数组中的方法。
- public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin):提供了将指定索引范围内的字符串存放到数组中的方法。
字符串与字节数组的转换
- 编码:字符串–>字节(看得懂—>看不懂的二进制数据)
- 解码:编码的逆过程,字节–>字符串(看不懂的二进制数据—>看得懂)
字符串 --> 字节数组:(编码)
public byte[] getBytes() :使用平台的默认字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中。
- public byte[] getBytes(String charsetName) :使用指定的字符集将此 String 编码到byte 序列,并将结果存储到新的 byte 数组。
字节数组 --> 字符串:(解码)
- String(byte[]):通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的String。
- String(byte[],int offset,int length) :用指定的字节数组的一部分,即从数组起始位置 offset 开始取 length 个字节构造一个字符串对象。
- String(byte[], String charsetName ) 或 new String(byte[], int, int,String charsetName ):解码,按照指定的编码方式进行解码。
注意:解码时,字符集必须与编码时一致,否则会出现乱码。
💡 五、开发避坑指南
- 避免无意识的对象创建
// 低效写法(每次循环创建新对象)
for (int i=0; i<100; i++) {
String s = "Count: " + i;
}
- 慎用intern()方法
- 优点:复用常量池对象节省内存
- 风险:不当使用可能导致PermGen OOM(JDK8前)
- 正则表达式预编译
// 错误:每次调用都编译正则
boolean matches = "a1b2c3".matches("[a-z]\\d");
// 正确:预编译Pattern
private static final Pattern PATTERN = Pattern.compile("[a-z]\\d");
boolean matches = PATTERN.matcher(input).matches();
掌握String核心机制,合理选择字符串处理类,让你的Java代码效率飞升! 🚀
参考资料:尚硅谷Java学习视频