Java字符串连接原理

本文主要参考黑马程序员的Java面试宝典上的内容

我们都知道,在Java中字符串可以用+连接,也可以使用StringBuilder或StringBuffer连接。

String str = "abc"+"xyz";

那么这几种方式由什么区别呢。当然你可能会知道以下几点

  • String是只读字符串,String引用的字符串内容是不能被改变的
  • StringBuffer/StringBuilder 表示的字符串对象可以直接进行修改
  • StringBuffer是线程安全的,他的方法都被synchronized修饰过,StringBuilder 是线程不安全的,通常效率要比StringBuffer要高一点

但是现在需要对String的+进行深层次的探索。

下面一段代码

public class Main {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = "xxx" + s1 + "zzz" +1;
        System.out.println(s2);
    }
}

从表面上看,对字符串和整型使用"+"号并没有什么区别,但实际上看看这段代码的本质,你就会发现其中奥秘。

使用反编译工具jad对代码进行反编译, 这里分享一个jad百度云下载地址https://pan.baidu.com/s/1_QDofa8t5thVAiPc5C5mzg 提取码: 9fn3

jad -o -a -s java Main.class

其中 -o覆盖输出文件无需确认,-a生成JVM指令作为注释,-s 输出文件后缀名(默认是.jad)。这里生成JVM指令作为参考。

import java.io.PrintStream;

public class Main
{

    public Main()
    {
    //    0    0:aload_0         
    //    1    1:invokespecial   #1   <Method void Object()>
    //    2    4:return          
    }

    public static void main(String args[])
    {
        String s1 = "abc";
    //    0    0:ldc1            #2   <String "abc">
    //    1    2:astore_1        
        String s2 = (new StringBuilder()).append("xxx").append(s1).append("zzz").append(1).toString();
    //    2    3:new             #3   <Class StringBuilder>
    //    3    6:dup             
    //    4    7:invokespecial   #4   <Method void StringBuilder()>
    //    5   10:ldc1            #5   <String "xxx">
    //    6   12:invokevirtual   #6   <Method StringBuilder StringBuilder.append(String)>
    //    7   15:aload_1         
    //    8   16:invokevirtual   #6   <Method StringBuilder StringBuilder.append(String)>
    //    9   19:ldc1            #7   <String "zzz">
    //   10   21:invokevirtual   #6   <Method StringBuilder StringBuilder.append(String)>
    //   11   24:iconst_1        
    //   12   25:invokevirtual   #8   <Method StringBuilder StringBuilder.append(int)>
    //   13   28:invokevirtual   #9   <Method String StringBuilder.toString()>
    //   14   31:astore_2        
        System.out.println(s2);
    //   15   32:getstatic       #10  <Field PrintStream System.out>
    //   16   35:aload_2         
    //   17   36:invokevirtual   #11  <Method void PrintStream.println(String)>
    //   18   39:return          
    }
}

从反编译的代码中可以看出,String的+拼接,实际上是StringBuilder拼接然后转为String的,因此,我们可以得出结论,在 Java 中无论使用何种方式进行字符串连接,实际上都使用的是 StringBuilder ,那这样是不是就表示String的+拼接和StringBuilder的效果是一样的呢?从运行结果上看,两者是等效的。但是从效率和资源消耗上看,两者区别很大。当使用简单字符串相加使,没有太大区别,但是在循环字符串中,这两者的差距就很大。

看下面一段代码,在循环中使用+拼接字符串

public class Main2 {
    public static void main(String[] args) {
        String s = "";
        for (int i = 0; i < 10; i++) {
            s = s + i + " ";
        }
        System.out.println(s);
    }
}

使用jad.exe -o -s java .\Main2.class反编译,这里不生成JVM指令.

import java.io.PrintStream;

public class Main2
{

    public Main2()
    {
    }

    public static void main(String args[])
    {
        String s = "";
        for(int i = 0; i < 10; i++)
            s = (new StringBuilder()).append(s).append(i).append(" ").toString();

        System.out.println(s);
    }
}

可以看出在循环中每次都创建了一个新的StringBuilder对象,占用大量资源。对此我们将其改进一下,使用StringBuilder连接

public class Main3 {
    public static void main(String[] args) {
        StringBuilder s = new StringBuilder();
        for (int i = 0; i < 10; i++) {
            s.append(i);
            s.append(" ");
        }
        System.out.println(s);
    }
}

生成的字节码

import java.io.PrintStream;

public class Main3
{

    public Main3()
    {
    }

    public static void main(String args[])
    {
        StringBuilder s = new StringBuilder();
        for(int i = 0; i < 10; i++)
        {
            s.append(i);
            s.append(" ");
        }

        System.out.println(s);
    }
}

可以看出源码和字节码没有区别,也没有生成额外的对象。

但是要注意,使用StringBuilder拼接字符串时,不要和+混用,否则还会生成更多对象

例如

public class Main4 {
    public static void main(String[] args) {
        StringBuilder s = new StringBuilder();
        for (int i = 0; i < 10; i++) {
            s.append(i + " ");
        }
        System.out.println(s);
    }
}

反编译源码

import java.io.PrintStream;

public class Main4
{

    public Main4()
    {
    }

    public static void main(String args[])
    {
        StringBuilder s = new StringBuilder();
        for(int i = 0; i < 10; i++)
            s.append((new StringBuilder()).append(i).append(" ").toString());

        System.out.println(s);
    }
}

可以看出Java会将+连接的字符串通过StringBuilder对象连接,这样还是生成了不必要的对象,造成资源浪费,在IDEA中,如果是使用这种方式拼接字符串,它会提出警告.

通过以上示例,我们明白String通过+拼接字符串的本质,拼接字符串时尽量使用StringBuilder,并且二者不要混用。

以下是一个判断字符串相等的示例,让我们从反编译的角度看原因

public class StringEqualTest {
    public static void main(String[] args) {
        String s1 = "Programming";
        String s2 = new String("Programming");
        String s3 = "Program";
        String s4 = "ming";
        String s5 = "Program" + "ming";
        String s6 = s3 + s4;
        System.out.println(s1 == s2);           //false
        System.out.println(s1 == s5);           //true
        System.out.println(s1 == s6);           //false
        System.out.println(s2 == s6);           //false
        System.out.println(s1 == s2.intern());  //true
        System.out.println(s1 == s6.intern());  //true
        System.out.println(s2 == s2.intern());  //false
    }
}

我们都知道Java中有个常量池,并且符合以下条件

  • 由操作符 new 调用的 String的构造器产生的对象,如 String s = new String(“1”), JVM 会先使用常量池来管理 ,再调用String 类的构造器来创建一个新的 String 对象,新创建的 String 对象被保存在堆内存中 。
  • 字符串常量初始化的对象 (包括在编译时就可以计算出来的字符串值) , 如, String s =“1”; 存到常量池中。
  • 堆中字符串相加的表达式,如:String s3 = new String(“1”) + new String(“1”);, 其结果 s3 仍存到堆中。
  • 字符串相加的表达式中,若包含字符串常量的算子,其结果仍存到常量池中。
  • String对象的intern()方法会得到字符串对象在常量池中对应的版本的引用,如果常量池中没有对应的字符串,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用;

对代码进行反编译

import java.io.PrintStream;

public class StringEqualTest
{

    public StringEqualTest()
    {
    }

    public static void main(String args[])
    {
        String s1 = "Programming";
        String s2 = new String("Programming");
        String s3 = "Program";
        String s4 = "ming";
        String s5 = "Programming";
        String s6 = (new StringBuilder()).append(s3).append(s4).toString();
        System.out.println(s1 == s2);
        System.out.println(s1 == s5);
        System.out.println(s1 == s6);
        System.out.println(s2 == s6);
        System.out.println(s1 == s2.intern());
        System.out.println(s1 == s6.intern());
        System.out.println(s2 == s2.intern());
    }
}
  • s1和s2一个在常量池,一个在堆中,所以是false
  • s5由两个常量池中数据相加,反编译后可以看到,值已经运算出来了,其结果仍保存在常量池中,所以s1==s5
  • s6是两个字符串相加,通过StringBuilder的append连接而成,所以s1肯定不等于s6
  • s2和s6属于两个对象,肯定不相等
  • s2退回常量池后,返回常量池的引用,所以和s1相等
  • s6退回常量池后,返回常量池的引用,所以和s1相等
  • 一个在堆中,一个在常量池中,不相等

在未了解String的+运算符实质之前,对于s1==s6的判断,我也是稍有疑惑,但是在反编译了解其运行原理后就可以很清楚的了解s5,s6的区别。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值