String详解
String对象的不可变原因,String对象的内存布局,及String对象之间的执行==
,equals
,+
运算时的分析。
Author: Msuenb
Date: 2023-02-16
java.lang.String
类代表字符串。String 对象用于保存字符串,也就是一组字符序列。字符串是常量,它们的值在创建后不能更改。
String 的特点
-
String 是 final 类,不能被其他的类继承
-
String 类实现了 Comparable 接口,String 对象可以比较大小
-
String 对象内部是用字符数组保存字符串内容的
JDK9之前是
private final char value[]
数组,JDK9之后是 byte[] 数组"hello"
等效于char[] value = {'h', 'e', 'l', 'l', 'o'}
-
String 对象是不可变对象,一旦进行修改,就会产生新对象
注意:
- String 类的 char[] value 数组是 final 修饰的,说明 value 数组地址不可变,不是数组元素不可变
- 由于 value 数组是 private 的,所以在 String 类外无法直接修改 value 数组的元素值(除非用反射)
- String 类中的方法涉及到 value 数组长度变化,或修改元素值,都是用用新对象来表示修改后内容
final char[] value = {'h', 'e', 'l', 'l', 'o'}; // value = new char[5]; // value 的地址不可修改 value[0] = 'H'; // 可以修改 value 元素值 String str = "hello"; str = "world"; // 不会修改 "hello", 会在池中创建一个新的String对象接受 "world" str指向这个新对象 共会创建两个对象
-
String 对象不可变,可以共享(节省内存)。Java 中把需要共享的字符串常量对象存放在常量池中。
String s1 = "hello"; String s2 = "hello"; System.out.println(s1 == s2); // true // 内存中只有一个 "hello" 对象被创建,同时被s1和s2共享
创建String对象
总的来说,String 对象的创建方式有两种:直接赋值 和 使用构造器,这两种方式的机制是不一样的
- 直接赋值:
String str1 = "hello";
- 先从常量池中查看是否有 "hello" 数据空间,如果有,直接指向;如果没有,重新创建,然后指向。
- str1 最终指向的是常量池的空间地址
- 调用构造器:
String str2 = new String("hello")
- 先在堆中创建空间,里面维护了
char[] value
属性。 - 如果常量池中有 "hello" 数据空间,value 直接指向 "hello" 空间;如果没有,重新创建,然后指向。
- str2 最终指向的是堆中的空间地址;value 才是指向常量池的空间地址
- 先在堆中创建空间,里面维护了
String 内存分析
针对下面两行代码分析 String 对象的创建过程和内存布局
String str1 = "hello";
String str2 = new String("hello");
String 内存布局:
![](https://s1.ax1x.com/2023/02/16/pSHdsN4.png)
String 对象创建过程:
String str1 = "hello";
- 先去查看常量池中是否有 "hello" 数据空间,没有,创建 "hello" 数据空间
- 将 "hello" 数据空间的地址返回给 String 对象引用 str
String str2 = new String("hello");
3. 先在堆中开辟空间,里面维护了 value 属性,str2 指向这片空间
4. 检查常量池中是否有 "hello" 数据空间,有,value 直接指向 "hello" 数据空间
哪些字符串对象地址放入字符串常量池:
需要共享的字符串地址记录到字符串常量池的table表中,不需要共享的字符串对象其地址值不需要记录到字符串常量池的table表中。
除了以下2种,其他的都不放入字符串常量池:
1. "..." 字符串字面量
2. 字符串对象.intern()的结果
其他:
1. 直接new
2. valueOf,copyValueOf等
3. 字符串对象拼接:concat拼接 以及 +左右两边出现 非字符串字面量拼接
4. toUpperCase,toLowerCase,substring,repalce等各种String方法得到的字符串
其实下面这些方式,本质都是新new的,其地址都是指向堆空间。
String对象的创建形式
String对象的创建方式有两种,使用其它形式生成String对象,像valueOf()
或toString()
,它们在底层也是调用String的构造方法
-
直接赋值
String str = "hello";
-
构造方法
用于创建 String 对象的构造方法有很多,比较常用的有以下几个:
public String()
:其表示创建空字符序列。String(String original)
:其表示创建一个与参数相同的字符序列;也即新创建的字符串是该参数字符串的副本。public String(char[] value)
:通过当前参数中的字符数组来构造新的String。public String(byte[] bytes)
:通过使用默认字符集解码当前参数中的字节数组来构造新的String。也可以指定字符集
String str1 = new String(); // 创建一个空字符串 String str2 = new String("hello"); System.out.println(str1.equals("")); // true char[] value = {'h', 'e', 'l', 'l', 'o'}; // 字符数组 String str3 = new String(value); byte[] bytes = {104, 101, 108, 108, 111}; // 字节数组 String str5 = new String(bytes); // hello
-
valueOf
和copyValueOf
方法:-
static String copyValueOf(char[] data): 返回指定数组中表示该字符序列的 String
-
static String valueOf(char[] data) : 返回指定数组中表示该字符序列的 String
-
static String valueOf(xx value):xx支持各种数据类型,返回各种数据类型的value参数的字符串表示形式。
char[] data = {'h','e','l','l','o','j','a','v','a'}; String s1 = String.copyValueOf(data); String s2 = String.copyValueOf(data, 0, 5); // hello String s3 = String.valueOf(237); // 237
-
-
连接运算
+
任意数据类型与"字符串"进行拼接,结果都是字符串类型
int num = 123456; String s = num + ""; Student stu = new Student(); String s2 = stu + ""; // 自动调用对象的toString(),然后与 "" 进行拼接
-
toString
方法Object 类中声明了 toString() 方法,因此任意对象都可以调用 toString 方法,转为字符串类型。
LocalDate today = LocalDate.now(); String str = today.toString(); System.out.println(str);
String对象比较
== 与 equals
==
:比较的是两个字符串对象的地址boolean equals(Object obj)
方法:比较是两个字符串对象的内容(String 类重写了 equals 方法)
String str1 = "hello";
String str2 = "hello";
String str3 = new String("hello");
String str4 = new String("hello");1
System.out.println(str1 == str2); // true str1 和 str2 都指向池中 "hello" 数据空间
System.out.println(str1 == str3); // false str3 指向堆中
System.out.println(str1.equals(str3)); // true str1 与 str3 内容都是 "hello"
System.out.println(str3 == str4); // false str3 与 str3 指向堆中不同的内存空间
System.out.println(str3.equals(str4)); // true
System.out.println(str1 == str3.intern()); // true
System.out.println(str3 == str3.intern()); // false
// 常量池中存在 "hello" 数据空间 str2.intern() 返回 "hello" 的引用 与str1相同
intern() 方法说明:执行
str2.intern()
时,会返回常量池中与 str2 内容相同的字符串常量的地址;若池中没有,则将 str2 添加到池中,并返回 str2 的引用
内存布局分析:
![](https://s1.ax1x.com/2023/02/16/pSHRN8K.png)
str1 和 str2 中的 value 都指向常量池中的 "hello" 数据空间,str1.value == str2.value。
compareTo方法
int compareTo(String str)
方法:String 类型实现了 Comparable 接口, 重写了 compareTo 方法,即 String 对象支持自然排序。
int compareTo(String str) 方法按照字符的 Unicode 编码值进行比较大小的
@Test
public void test07() {
String[] arr = {"java","linux","hadoop","hive","shell","flink","spark"};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
}
+ 与 concat 的区别
拼接两个字符串的方式有两种:连接运算符+
和concat()
方法
String str1 = "hello,";
String str2 = "world";
String str3 = str1 + str2;
String str4 = str1.concat(str2);
System.out.println(str3); // helloworld
System.out.println(str4); // helloworld
-
str1 + str2
:"..."
字符串字面量拼接,编译器直接优化为拼接后的字符串常量值。如,"abc" + "def"
优化等价于"abcdef"
- 非
"..."
字符串字面量拼接,编译器优化为 StringBuilder 的 append 方法,然后再把结果 toString。
String str1 = "hello"; String str2 = "world"; String str3 = "helloworld"; String str4 = str1 + str2; // 非 "..." 字符串字面量拼接 String str5 = str1 + "world"; // 非 "..." 字符串字面量拼接 String str6 = "hello" + "world"; // "..." 字符串字面量拼接 等价于: String str6 = "helloworld"; System.out.println(str3 == str4); // false System.out.println(str3 == str5); // false System.out.println(str3 == str6); // true
现在使用
debug
方式追一下String str5 = str1 + "world";
的执行过程,如下:-
先创建一个 StringBuilder 对象 sb,容量为16,初始为空。
-
执行
sb.append("hello");
-
执行
sb.append("world");
-
调用
sb.toString();
方法,返回 String 对象
最后 str5 会指向堆中的 String 对象,value[] 指向常量池中的 "helloworld" 数据空间。
注意:final String str1 = "hello"; // 此时 str1 完全等价于"hello"
final String s1 = "hello";//此时s1完全等价于"hello" final String s2 = "world";//此时s2完全等价于"world" String s3 = "helloworld"; String s4 = s1 + s2; String s5 = s1 + "world"; String s6 = "hello" + "world"; System.out.println(s3 == s4);//true System.out.println(s3 == s5);//true System.out.println(s3 == s6);//true
-
str1.concat(str2)
:只要拼接的不是空字符串,每次都 new 一个 StringString str1 = "hello"; String str2 = "world"; String str3 = "helloworld"; String str4 = str1.concat(str2); // str2 不是空串 创建一个新对象接受连接后的内容 String str5 = str1.concat("world"); String str6 = "hello".concat("world"); System.out.println(str3 == str4); // false System.out.println(str3 == str5); // false System.out.println(str3 == str6); // false
concat(String)
方法源码:
可变字符序列
因为 String 对象是不可变对象,虽然可以共享常量对象,但是对于频繁字符串的修改和拼接操作,效率极低。因此 Java 提供了可变字符序列 StringBuilder 和 StringBuffer 类型。
-
StringBuffer
:线程安全的(方法有 synchronized 修饰) -
StringBuilder
:线程不安全的
StringBuffer和StringBuilder
StringBuffer 与 StringBuilder 最大区别就是在于 StringBuffer 是线程安全的,支持多线程访问;StringBuilder是线程不安全的,在单线程下使用。
StringBuilder 的效率要高于 StringBuffer,通常情况先建议使用 StringBuilder,但在要求线程安全的情况下需要使用String Buffer。
其他方面 StringBuffer 与 StringBuilder 相似:
-
StringBuffer 和 StringBuilder 都是 final 类,不能被继承
-
StringBuffer 和 StringBuilder 都继承自 AbstractStringBuilder,有属性
char[] value
,用于存放字符串内容,由于 value 不是 final 类型,因此 value 数组里的内容存放在堆空间,而不是常量池。
-
StringBuffer 和 StringBuilder 都不用每次都需要创建新对象,所有效高于 String
-
StringBuffer 和 StringBuilder 有相同扩容机制:初始容量默认
capacity = str.length + 16
,每次扩容为capacity = 2 * capacity + 2
常用的API,StringBuilder、StringBuffer是完全一致的:
-
StringBuffer append(xx):拼接,追加
-
StringBuffer insert(int index, xx):在 [index] 位置插入 xx
-
StringBuffer deleteCharAt(int index):删除[index]位置字符
-
void setCharAt(int index, xx):替换[index]位置字符
-
StringBuffer reverse():反转
-
int indexOf(String str):在当前字符序列中查询str的第一次出现下标
@Test
public void test1(){
StringBuilder s = new StringBuilder();
s.append("hello").append(true).append('a').append(12).append("world");
System.out.println(s);
System.out.println(s.length());
}
@Test
public void test2(){
StringBuilder s = new StringBuilder("helloworld");
s.insert(5, "java");
System.out.println(s);
}
@Test
public void test3(){
StringBuilder s = new StringBuilder("helloworld");
s.deleteCharAt(4);
System.out.println(s);
}
@Test
public void test4(){
StringBuilder s = new StringBuilder("helloworld");
s.reverse();
System.out.println(s);
}
@Test
public void test5(){
StringBuilder s = new StringBuilder("helloworld");
s.setCharAt(2, 'a');
System.out.println(s);
}
@Test
public void test6(){
StringBuilder s = new StringBuilder("helloworld");
int index = s.indexOf("owo");
System.out.println(index);
}
String,StringBuffer,StringBuilder效率测试
import org.junit.jupiter.api.Test;
public class EfficiencyTest {
@Test
public void test() {
long start = System.currentTimeMillis();
stringTest();
// stringBufferTest();
// stringBuilderTest();
long end = System.currentTimeMillis();
long memory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
System.out.println("String拼接+用时:" + (end - start));
System.out.println("String拼接+占用内存: " + memory);
// String StringBuffer StringBuilder
// time 5986 5 3
// memory 57263088 43127632 43127912
}
public void stringTest() {
String str = new String("0");
for (int i = 0; i < 50000; i++) {
str += i;
}
}
public void stringBufferTest() {
StringBuffer sb = new StringBuffer("0");
for (int i = 0; i < 50000; i++) {
sb.append(i);
}
}
public void stringBuilderTest() {
StringBuilder sb = new StringBuilder("0");
for (int i = 0; i < 50000; i++) {
sb.append(i);
}
}
}