String类对象存储的本质是final修饰的字符数组value[],一旦创建后对象就不可变,当我们看到String对象可以增删改时,实际上底层经过了临时对象的创建和字符数组的拷贝。
在做一道String字符串拼接练习时使用了三种做法:
一、
public class Main {
public static void main(String[] args) {
String str1 = "I love ";
String str2 = "China";
str1 = str1.concat(str2);
str1 = str1.toUpperCase();
System.out.println(str1);
}
}
str1首先指向常量池中的字符串常量"I love"。在调用String类的方法concat()和str2拼接后,结果赋值给str1,这个过程实际上在底层创建了新的字符数组,并创建String的临时对象,将这个临时对象的地址赋给str1。也就是说str1开始指向常量池的对象,拼接字符串后又指向堆中的临时对象(被指向后不再临时)。在改变大小写的操作时,底层同样又在堆中创建了新的对象,str1又改变指向了全大写的字符串对象。最终输出。
二、
public class Main {
public static void main(String[] args) {
String str1 = "I love ";
String str2 = "China";
System.out.println(str1.concat(str2).toUpperCase());
}
}
在这个做法中,没有给新创建的String对象显示的声明一个引用,而是直接输出到控制台。虽然没有将对象赋值给一个变量,但在底层依旧和第一种一样创建了两个新的String类型变量。
三、
public class Main {
public static void main(String[] args) {
String str1 = "I love ";
String str2 = "China";
String str3 = str1 + str2;
System.out.println(str3.toUpperCase());
}
}
第三种做法没有调用concat()方法,而是使用+操作符连接两个指向常量池对象的常量。在此过程中,底层先创建了StringBuilder类的临时对象,并调用append()方法对两个字符串对象进行拼接,最后调用toString()方法创建新的String对象,这个对象包含了拼接后的内容,将这个String对象的地址赋给str3,最后调用大写函数输出。
总结
1、第一种做法直接将新创建的Sting对象赋值给str1,会造成原来str1指向的对象没有指针指向,会造成垃圾对象;
2、第二种做法直接调用了新创建的对象,当输出结束后同样会造成垃圾对象,并有被垃圾回收机制回收的风险;
3、第三种做法
(1)当使用+
操作符来连接(拼接)字符串时,实际上在编译时,如果两个字符串都是编译时常量(即直接在代码中指定的字符串字面量),并且没有与其他变量进行拼接,那么这些字符串在编译时就会被放入字符串常量池中。当使用+
操作符拼接这些字符串时,编译器会计算拼接后的结果,并将这个新的字符串字面量也放入常量池中。在运行时,这个拼接后的字符串直接从常量池中获取,不会有任何额外的内存分配或StringBuilder
的使用。
(2)虽然str1
和str2
都是指向字符串常量池中的字符串,但是当使用str1 + str2
这样的表达式时,由于存在变量,编译器就不能确定字符串的最终值。因此,编译器会生成字节码,这些字节码在运行时会使用StringBuilder
来动态地拼接字符串。这是为了提高性能,因为创建多个中间String
对象(在每次拼接时)会导致不必要的内存分配和垃圾回收。使用StringBuilder
可以避免这种开销,因为它允许在单个字符数组中逐步构建最终的字符串。