在Java中串联字符串时,在内存中创建了多少个字符串?


17

有人问我有关Java中的不可变字符串。我的任务是编写一个将多个“ a”连接到一个字符串的函数。

我写的是:

public String foo(int n) {
    String s = "";
    for (int i = 0; i < n; i++) {
        s = s + "a"
    }
    return s;
}

然后我被问到这个程序将生成多少个字符串,假设不会发生垃圾回收。我对n = 3的想法是

  1. “一种”
  2. “一种”
  3. “ aa”
  4. “一种”
  5. “啊!”
  6. “一种”

在循环的每次迭代中,基本上都会创建2个字符串。但是,答案是n 2。此函数将在内存中创建哪些字符串,为什么呢?


15
如果您得到这份工作,请逃跑,然后跑得很快……
。– mattnz

@mattnz出于多种原因(而不仅仅是因为编写代码)。

3
除非JIT优化循环,否则这需要O(n ^ 2)运行时,但不会创建n ^ 2字符串。
user2357112支持Monica

Answers:


26

然后我被问到这个程序将生成多少个字符串,假设不会发生垃圾回收。我对n = 3的想法是(7)

字符串1("")和2("a")是程序中的常量,它们不是作为事物的一部分创建的,而是被“ interned”的,因为它们是编译器知道的常量。在Stringped in Wikipedia上了解有关此内容的更多信息。

这也会从计数中删除字符串5和7,因为它们与"a"字符串#2 相同。这样就留下了字符串#3,#4和#6。答案是使用您的代码“为n = 3创建了3个字符串”。

N的计数2显然是错误的,因为在n = 3,这将是9,甚至你的最坏情况下的答案,这只是7.如果你非实习字符串是正确的,答案应该已经2N + 1。

所以,问题是如何应该这样做?

由于String是不可变的,因此您需要一个可变的东西-无需创建新对象即可更改的东西。那就是StringBuilder

首先要看的是构造函数。在这种情况下,我们知道字符串将有多长,并且有一个构造函数StringBuilder(int capacity) ,这意味着我们将完全分配所需的数量。

接下来,"a"不必是String,而可以是character 'a'。这有一些小的性能打电话时提高append(String)VS append(char)-同append(String),该方法需要找出字符串是如何长,做一些工作。另一方面,char总是正好一个字符长。

可以在StringBuilder.append(String)StringBuilder.append(char)处看到代码差异。不必在意,但是如果您想打动雇主,最好使用最佳实践。

那么,当您将其放在一起时,外观如何?

public String foo(int n) {
    StringBuilder sb = new StringBuilder(n);
    for (int i = 0; i < n; i++) {
        sb.append('a');
    }
    return sb.toString();
}

已创建一个StringBuilder和一个String。无需插入额外的字符串。


在Eclipse中编写其他一些简单程序。安装pmd并在您编写的代码上运行它。注意它抱怨什么并修复这些问题。它将发现在一个循环中用+修改了一个String,如果将其更改为StringBuilder,它可能找到初始容量,但肯定会捕捉到.append("a")和之间的区别。.append('a')


9

在每次迭代中,操作员String都会创建一个新+变量并将其分配给s。返回后,除最后一个以外的所有变量都将被垃圾回收。

并非每次都会创建""和类似的字符串常量"a",它们是被嵌入的string。由于字符串是不可变的,因此可以自由共享它们。这发生在字符串常量上。

要有效地连接字符串,请使用StringBuilder


面试的人实际上就是否是原义进行辩论,并决定每次都创建原义。但这更有意义。
ahalbert13年

6
您如何“辩论”一种语言的作用,您一定阅读了规范并确定知道,或者未定义,因此没有正确的答案.....
mattnz

@mattnz知道您正在使用的编译器/运行时的功能可能会很有趣,即使涉及实现细节。这尤其适用于性能。
svick

1
@svick:您可以通过做很多假设,然后升级编译器,更改优化等。行为的改变会引起错误,因为您依赖未指定的行为而不是已定义的行为。您知道他们对优化的看法-a)交给专家,b)还不是专家。:)如果依赖仅基于性能,但仍取决于语言规范,则您只会失去性能。很多时候,我看到依赖未指定或特定于编译器的行为的代码以意外的方式中断(主要是C和C ++)。
mattnz

@mattnz那么,您如何建议做出与绩效相关的决策?通常,从规范/文档中获得的最好的好处是big-O复杂性,但这还不够。无论如何,性能始终取决于实现,因此,我认为在性能方面可以依赖实现细节。
svick

4

正如MichaelT在回答中所解释的那样,您的代码分配了O(n)个字符串。但是它还会分配O(n 2)字节的内存,并在O(n 2)时间内运行。

它分配O(n 2)个字节,因为要分配的字符串的长度为0、1、2,…,n-1,n,总和为(n 2 + n)/ 2 = O(n 2)。

时间也是O(n 2),因为分配第i个字符串需要复制长度为i-1的第(i-1)个字符串。这意味着必须复制每个分配的字节,这将花费O(n 2)时间。

也许这就是面试官的意思?


如果不是公式是(N ^ 2 + N)/ 2,喜欢这里
HeyJude
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.