前言
在 Java 中,String 类是一个非常重要的内置类,它用于处理字符串数据。字符串是不可变的(immutable),这意味着一旦创建,字符串的内容不能被修改。这篇博客将详细介绍 String 类的特性、常用方法及一些性能注意事项。
1.String 类的常用方法
1.1 创建字符串
// 方式1 直接赋值
String str = "Hello, World!";
// 方式2 使用new关键字
String str2 = new String("Hello, World!");
// 方式3 字符数组转String
char[] array = {'a', 'b', 'c'};
String str3 = new String(array);
例1:赋值str1,str2指向str1
String str1 = "Hello";
String str2 = str1;
System.out.println(str1); // Hello
System.out.println(str2); // Hello
内存结构如图
例2:修改str1为新的值
String str1 = "Hello";
String str2 = str1;
str1 = "World"
System.out.println(str1); // World
System.out.println(str2); // Hello
内存结构如图
1.2 字符串比较
例1:str1,str2相同值,str3不同值
String str1 = "Hello";
String str2 = "Hello";
String str3 = "world";
System.out.println(str1 == str2); // true 因为 str1 和 str2 引用的是同一个对象
System.out.println(str1 == str3); // false 因为 str1 和 str3 的内容不同,引用的对象也不同
内存结构如图
例2:str1,str2使用new关键字创建相同值
String str1 = new String("Hello");
String str2 = new String("Hello");
System.out.println(str1 == str2); // false
// str1 和 str2 是通过 new 关键字创建的两个不同的 String 对象,因此它们的引用不同,比较结果是 false
// 如果需要比较内容是否相同,应该使用 equals 方法
System.out.println(str1.equals(str2)); // true
1.3 获取长度
String str = "Hello, World!";
int length = str.length(); // 返回字符串的长度
1.4 字符串比较
String str = "Hello, World!";
boolean isEqual = str.equals("Hello, World!"); // 比较内容
boolean isEqualIgnoreCase = str.equalsIgnoreCase("hello, world!"); // 忽略大小写比较
1.5 子字符串
String str = "Hello, World!";
String subStr = str.substring(7, 12); // 返回 "World"
1.6 替换
String str = "Hello, World!";
String replaced = str.replace("World", "Java"); // 返回 "Hello, Java!"
1.7 字符串查找
String str = "Hello, World!";
int index = str.indexOf("World"); // 返回 7
boolean contains = str.contains("Hello"); // 返回 true
2. 字符串常量池
2.1 什么是字符串常量池
String类的设计使用了共享设计模式,在JVM底层实际上会自动维护一个对象池(字符串常量池),字符串常量池是 Java 中一个特殊的内存区域,用于存储字符串字面量和一些常量字符串,以优化内存使用和提高性能。
- 如果采用了直接赋值的模式进行String类的对象实例化操作,那么该实例化对象(字符串内容)将自动保存到这个对象池之中
- 如果下次继续使用直接赋值的模式声明String类对象,此时对象池之中如若有指定内容,将直接进行引用
- 如若没有,则开辟新的字符串对象而后将其保存在对象池之中以供下次使用
2.2 直接使用new String()方法
- 使用 new 关键字时,如果常量池中已经存在 “ab”,不会再常量池中重复创建,会在堆内存中创建一个新的 String 对象
- 如果常量池中不存在 “ab”,则在常量池创建,且在堆内存中创建一个新的 String 对象
2.3 intern方法
intern() 方法是 Java 中 String 类的一个方法,用于获取字符串的规范表示。在调用 intern() 方法时,如果常量池中已经存在一个与该字符串内容相同的字符串,则返回该字符串的引用;如果不存在,则将该字符串添加到常量池中,并返回这个新字符串的引用。所以我们可以使用 String 的 intern 方法来手动把 String 对象加入到字符串常量池中。
优点
- 内存优化:使用 intern() 可以减少内存使用,因为相同内容的字符串只在常量池中存在一份。
- 性能:在字符串比较时,如果使用 == 比较常量池中的字符串会比使用 equals 方法更快,因为它只比较对象引用。
String str1 = new String("Hello");
String str2 = str1.intern(); // 将 str1 的内容添加到常量池中
String str3 = "Hello"; // 直接从常量池中获取
System.out.println(str2 == str3); // 输出 true,str2 和 str3 引用的是常量池中的同一个对象
3.String的不可变
3.1 不可变
String对象一旦创建,其内容不能被更改。如果你对字符串进行修改,比如连接、替换等操作,实际上会生成一个新的字符串对象。例如给一个已有字符串"abcd"第二次赋值成"abcedl",不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。
3.2 String为什么不可变
点开String源码发现
String 类中使用 final 关键字修饰字符数组value来保存字符串,但是如果能直接对数组元素进行操作就可以修改数组中的内容。
final int[] value={1,2,3};
value[2]=100; //这时候数组里已经是{1,2,100}
String 真正不可变有下面几点原因:
- 保存字符串的数组被 final 修饰且为私有(private)的,并且String 类没有提供/暴露修改这个字符串的方法。
- String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。
3.3 String不可变的好处
-
安全性:由于字符串对象一旦创建后不可修改,避免了意外修改内容的风险。这在多线程环境中特别重要,因为多个线程可以安全地共享同一个字符串对象,而不必担心数据被修改。
-
内存效率:不可变字符串可以被共享。比如,当你在常量池中创建一个字符串字面量时,后续对同一内容的引用会直接指向常量池中的对象,而不是创建新的对象。这减少了内存消耗。
-
哈希码缓存:由于字符串不可变,它们的哈希码只计算一次,并可以被缓存。这使得字符串在作为哈希表的键时更加高效,因为不需要在每次查找时重新计算哈希值。
4.性能注意事项
4.1 StringBuilder 和 StringBuffer
由于 String 的不可变性,在频繁修改字符串的情况下,使用 String 可能会导致性能问题。此时,可以考虑使用 StringBuilder(非线程安全)或 StringBuffer(线程安全)来进行字符串操作。
StringBuilder sb = new StringBuilder("Hello");
sb.append(", World!"); // 更高效的字符串拼接
String result = sb.toString(); // 转换为 String
4.2 不要在循环中使用 + 拼接
在循环中使用 + 进行字符串拼接可能导致性能问题,最好使用 StringBuilder。
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append("Hello ");
}
String result = sb.toString();
对于三者使用的总结:
- 操作少量的数据: 适用 String
- 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer