【问题标题】:StringBuffer and String usage scenario in JavaJava中StringBuffer和String的使用场景
【发布时间】:2014-08-18 21:45:00
【问题描述】:

我可以通过两种方式实现单一方法,

1.

public String getTestMessage()
{
    return "Hello" + "World..!";
}

2.

public String getTestMessage()
{
    return new StringBuffer("Hello").append("World..!").toString();
}

在第一种情况下,将创建两个新的String 对象。
在第二种情况下,还会创建两个新对象,但会有一个 StringStringBuffer。现在方式会很快从他们那里?我有点困惑。

【问题讨论】:

  • 在第一种情况下,可能在编译后的字节码中实际上只有一个String,并且您没有创建新对象。而且您不必每次调用它时都创建一个新对象。
  • 你只考虑字符串常量连接吗?还是字符串变量连接?
  • 你应该考虑使用最新的字符串连接:StringBuilder

标签: java performance core


【解决方案1】:

在您的两种情况下,选项 1 每次都会更快,除非 JITC 做了我没想到的事情。我真的不希望它会有所作为,除非你非常频繁地调用这些方法。

为什么?因为您实际上使用选项 1 创建新对象。编译器应该执行常量折叠,将"Hello" + "World..!" 转换为"HelloWorld..!"。而且因为这是一个编译时常量String,它会在VM 启动时自动嵌入到String 池中。因此,每次调用该方法时,您都会得到对该“规范”String 的引用。不执行对象创建。

在选项 2 中,您始终创建多个对象——StringBuffer(顺便说一下,您应该使用StringBuilder)、支持char[],以及结果String(至少) .而且在一个紧密的循环中这样做并不是很有效。

此外,选项 1 更具可读性,这始终是您在编写代码时应该考虑的问题。

证明:

鉴于此测试代码:

public class Test {
    public String getTestMessageA() {
        return "Hello" + "World..!";
    }

    public String getTestMessageB() {
        return new StringBuffer("Hello").append("World..!").toString();
    }
}

使用javac -XD-printflat 编译向我们展示了此代码在编译为字节码之前被处理的内容:

public class Test {

    public Test() {
        super();
    }

    public String getTestMessageA() {
        return "HelloWorld..!";
    }

    public String getTestMessageB() {
        return new StringBuffer("Hello").append("World..!").toString();
    }
}

注意"Hello" + "World..!" 如何在编译时 转换为单个 String。所以String 连接不是第一个选项中发生的事情。

现在让我们看看字节码。这是常量池:

Constant pool:
   #1 = Methodref          #10.#20        //  java/lang/Object."<init>":()V
   #2 = String             #21            //  HelloWorld..!
   #3 = Class              #22            //  java/lang/StringBuffer
   #4 = String             #23            //  Hello
   #5 = Methodref          #3.#24         //  java/lang/StringBuffer."<init>":(Ljava/lang/String;)V
   #6 = String             #25            //  World..!
   #7 = Methodref          #3.#26         //  java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
   #8 = Methodref          #3.#27         //  java/lang/StringBuffer.toString:()Ljava/lang/String;

这是选项 1 的字节码:

  public java.lang.String getTestMessageA();
    Code:
       0: ldc           #2                  // String HelloWorld..!
       2: areturn 

嗯,这很短。如您所见,JVM 从池中加载一个常量 (ldc) 并返回它。没有直接在方法中创建对象。

下面是选项 2 的字节码:

public java.lang.String getTestMessageB();
    Code:
       0: new           #3                  // class java/lang/StringBuffer
       3: dup           
       4: ldc           #4                  // String Hello
       6: invokespecial #5                  // Method java/lang/StringBuffer."<init>":(Ljava/lang/String;)V
       9: ldc           #6                  // String World..!
      11: invokevirtual #7                  // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
      14: invokevirtual #8                  // Method java/lang/StringBuffer.toString:()Ljava/lang/String;
      17: areturn   

因此,这段代码创建了一个新的StringBuffer,从字符串池中加载适当的常量,为每个常量调用append() 方法,然后在缓冲区上调用toString() 方法。 toString() 是这样实现的:

@Override
public synchronized String toString() {
    if (toStringCache == null) {
        toStringCache = Arrays.copyOfRange(value, 0, count);
    }
    return new String(toStringCache, true);
}

所以选项 2 每次调用时都会创建两个新对象,并且还会执行更多指令。因此,选项 1 会更快。

【讨论】:

  • 请查看我的回答。
  • @M.Sharma 您的回答不适用于此处。第一个选项涉及String 连接,因为它们是编译时常量。
  • @M.Sharma 添加了更多代码来显示正在发生的事情。虽然您的文章没有错误,但它不适用于 OP 的问题。
【解决方案2】:

在性能方面,StringBuffer 在执行连接时更快。这是因为当您连接一个 String 时,由于 String 是不可变的,因此每次都在(内部)创建一个新对象。

对于较小的字符串(附加一个或两个字符串)您的第一种方法没有问题,考虑较大这会降低性能。

来到这个场景:

如果你添加

     "value" + 5

Java 必须首先将 5 转换为字符串。如果查看字节码,Java 实际上调用了 String.valueOf(5)。查看该方法,您会发现 Java 创建了一个 char 数组和一个 String(2 个对象)来构建该值。

当 Java 追加 5 时,它只是在 StringBuilder 上调用 append(int)。该方法只是将值复制到内部字符缓冲区中,而不创建任何额外的对象。

内附

if (str == null) str = "null";
    int len = str.length();
if (len == 0) return this;
int newCount = count + len;
if (newCount > value.length)
    expandCapacity(newCount);
str.getChars(0, len, value, count);
count = newCount;
return this;

这里使用的是原生方法 System.arrayCopy(),这里是http://www.cafeaulait.org/course/week2/50.html

看到这个What is the difference between String and StringBuffer in Java?

【讨论】:

  • 性能方面,第一个选项可能没有首先要执行的串联。
  • 您的回答是否适用于 OP 的问题?
  • 不完全正确:stackoverflow.com/questions/1532461/…。串联可以转化成StringBuilder,比StringBuffer快(因为没有同步à
  • OP 显然使用该场景作为样本,因此不应将其视为真实数据。他也可能会考虑字符串变量的连接,而不仅仅是字符串常量,在这种情况下,这个答案是有效的。
  • @RobertoLinares 我不知道。他们正在谈论这两种情况。
【解决方案3】:

很想知道我遇到this article 的幕后情况,这可以证明我们完全错了。

文章说+ 运算符看似无辜,但生成的字节码却产生了一些意外。使用StringBuffer 进行连接实际上可以生成比使用String 快得多的代码。

String 连接生成的字节码又会创建一个StringBuffer 对象,然后调用其append 方法。在StringBuffer 对象上执行连接后,必须将其转换回String。这是通过调用toString 方法完成的。此方法从临时的StringBuffer 对象创建一个新的String 对象。

综上所述,字符串连接依次创建了三个对象:

一个字符串对象, 一个 StringBuffer 对象, 一个字符串对象。

但是,第二种情况不需要创建临时的StringBuffer

请参阅article link 以获取完整信息

【讨论】:

  • 您实际上可以编译上面的代码并检查字节码。您的描述不适用于问题中的场景。
【解决方案4】:

这个

"Hello" + "World..!"

是一个常量表达式。编译器将进行连接并将其作为单个String 常量添加到字节码中。因此 JVM 只会为它创建一个 String 对象。

return new StringBuffer("Hello").append("World..!").toString();

有两个 String 字面量。 JVM 将为其中的每一个创建一个对象。然后它将创建一个StringBuffer 对象和所有相应的支持char[],最后为toString() 方法返回值创建String 对象。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-01-27
    • 2011-02-27
    • 1970-01-01
    • 2014-12-05
    • 2011-02-01
    • 2014-09-23
    相关资源
    最近更新 更多