专业的JAVA编程教程与资源

网站首页 > java教程 正文

Java中String 类型的变量和常量做“+”运算时发生了什么?

temp10 2024-10-18 13:50:18 java教程 10 ℃ 0 评论

Java 中的字符串拼接是一个非常常见的操作。表面上看,使用 + 运算符拼接字符串似乎很简单,但实际上其背后隐藏了许多复杂的实现细节和优化机制。

Java中String 类型的变量和常量做“+”运算时发生了什么?


字符串拼接的基本示例


让我们从一个简单的示例开始:


java


public class StringConcatExample {
    public static void main(String[] args) {
        String str1 = "str";
        String str2 = "ing";
        String str3 = "str" + "ing";
        String str4 = str1 + str2;
        String str5 = "string";

        System.out.println(str3 == str4); // false
        System.out.println(str3 == str5); // true
        System.out.println(str4 == str5); // false
    }
}



在这个示例中,我们使用了不同的方式拼接字符串,并通过 == 运算符比较它们的引用是否相同。接下来,我们将深入分析这些拼接操作的底层实现。


编译期的常量折叠


对于编译期可以确定值的字符串,即常量字符串,JVM 会将其存入字符串常量池。而字符串常量拼接得到的字符串常量在编译阶段就已经被存放到字符串常量池。这归功于编译器的优化——常量折叠(Constant Folding)


常量折叠是一种编译期优化技术,它会将常量表达式的值直接计算出来,并替换为具体的常量。在 Java 编译器中,常量折叠是对源代码进行的少数优化之一。


让我们通过一个具体的例子来理解这一点:


String str3 = "str" + "ing";



上述代码在编译阶段会被优化成:


String str3 = "string";



这意味着 str3 直接指向常量池中的字符串 "string"。我们可以通过反编译工具(如 javap)查看编译后的字节码,验证这一点。


javap -c StringConcatExample



反编译结果显示,str3 直接被初始化为常量 "string"。


引用类型的字符串拼接


对于引用类型的字符串拼接,编译器无法在编译期确定其值,此时会使用 StringBuilder 进行拼接。


String str4 = str1 + str2;



上述代码在编译后实际变成了:


java


String str4 = new StringBuilder().append(str1).append(str2).toString();



因此,str4 是在堆上新创建的一个对象,而不是常量池中的对象。这也是为什么 str3 == str4 会返回 false。


使用 final 修饰的字符串变量


如果字符串变量被 final 修饰,编译器会将其视为常量进行处理,从而在编译期确定其值。


java


final String str1 = "str";
final String str2 = "ing";
String c = "str" + "ing"; // 常量池中的对象
String d = str1 + str2;  // 常量池中的对象

System.out.println(c == d); // true



被 final 修饰的字符串变量会被编译器当作常量处理,其拼接结果在编译期就确定了,因此 c 和 d 指向同一个常量池中的对象。


运行时确定值的字符串拼接


如果字符串的值在运行时才能确定,那么编译器无法对其进行优化,此时会在堆上创建新的对象。


java


final String str1 = "str";
final String str2 = getStr();
String c = "str" + "ing"; // 常量池中的对象
String d = str1 + str2;   // 在堆上创建的新的对象

System.out.println(c == d); // false



在上述代码中,由于 str2 的值在运行时才能确定,因此 d 是在堆上新创建的对象。


java


public static String getStr() {
    return "ing";
}



性能对比


为了避免频繁创建字符串对象,建议在需要频繁拼接字符串时使用 StringBuilder 或 StringBuffer。以下是一个简单的性能对比示例:


java


public class StringPerformanceTest {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        String result = "";
        for (int i = 0; i < 10000; i++) {
            result += "test";
        }
        long endTime = System.currentTimeMillis();
        System.out.println("String: " + (endTime - startTime) + " ms");

        startTime = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 10000; i++) {
            sb.append("test");
        }
        result = sb.toString();
        endTime = System.currentTimeMillis();
        System.out.println("StringBuilder: " + (endTime - startTime) + " ms");
    }
}



运行结果可能如下:


txt


String: 500 ms
StringBuilder: 5 ms



可以看出,使用 StringBuilder 拼接字符串的性能远优于直接使用 + 运算符。这是因为每次使用 + 运算符拼接字符串时,都会创建一个新的 StringBuilder 对象,然后将其转换为字符串,造成大量不必要的对象创建和销毁。


深入源码解析


为了更详细地理解字符串拼接的底层实现,我们可以深入查看 StringBuilder 和 String 类的源码。


StringBuilder 的 append 方法


java


public StringBuilder append(String str) {
    if (str == null) str = "null";
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}



StringBuilder 的 append 方法会确保内部字符数组有足够的容量,然后将待拼接的字符串复制到字符数组中。


String 的 toString 方法


java


public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}



StringBuilder 的 toString 方法会创建一个新的 String 对象,并将内部字符数组的内容复制到新的 String 对象中。


总结


Java 中字符串拼接的实现机制。主要内容包括:


  1. 编译期的常量折叠优化。
  2. 引用类型字符串拼接的底层实现。
  3. 使用 final 修饰字符串变量的优化。
  4. 运行时确定值的字符串拼接。
  5. 性能对比及优化建议。
  6. 深入源码解析 StringBuilder 和 String 的实现细节。

?

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表