Скільки рядків створюється в пам'яті при об'єднанні рядків на Java?


17

Мене запитали про незмінні рядки на Java. Мені було доручено написати функцію, яка об'єднала ряд "a" s у рядок.

Що я написав:

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

Потім мене запитали, скільки рядків генерує ця програма, припускаючи, що збирання сміття не відбувається. Мої думки за n = 3 були

  1. ""
  2. "а"
  3. "а"
  4. "аа"
  5. "а"
  6. "ааа"
  7. "а"

По суті створюються 2 рядки в кожній ітерації циклу. Однак відповідь була n 2 . Які рядки будуть створені в пам'яті за допомогою цієї функції і чому саме так?


15
Якщо вам запропонують цю роботу, біжіть, бігайте дуже швидко .......
mattnz

@mattnz з кількох причин (і не лише через написаний код).

3
Це вимагає виконання O (n ^ 2) часу, якщо JIT не оптимізує цикл, але він не створює n ^ 2 рядків.
user2357112 підтримує Моніку

Відповіді:


26

Потім мене запитали, скільки рядків генерує ця програма, припускаючи, що збирання сміття не відбувається. Мої думки за n = 3 були (7)

Строки 1 ( "") та 2 ( "a") - константи програми, вони не створюються як частина речей, а є "інтернованими", оскільки вони є константами, про які знає компілятор. Детальніше про це читайте у String interning у Вікіпедії.

Це також видаляє рядки 5 і 7 з рахунку, оскільки вони такі ж, "a"як і рядки №2. Це залишає рядки №3, №4 та №6. Відповідь "3 рядки створюються для n = 3" за допомогою вашого коду.

Кількість п 2 , очевидно , неправильно , так як при п = 3, то це буде 9 , і навіть вашим гіршому випадку відповідь, який був тільки 7. Якщо не-інтерновані рядки було правильно, то відповідь має бути 2n + 1.

Таким чином, питання про те , як повинні ви зробити це?

Оскільки String є незмінним , ви хочете змінити щось, що можна змінити, не створюючи нових об'єктів. Це StringBuilder .

Перше, на що слід звернути увагу - це конструктори. У цьому випадку ми знаємо, як довга буде строка, і є конструктор, StringBuilder(int capacity) який означає, що ми виділяємо рівно стільки, скільки нам потрібно.

Далі, "a"не потрібно бути рядком , а скоріше це може бути персонаж 'a'. Це має незначне підвищення продуктивності при виклику append(String)vs append(char)- з append(String)методом потрібно з'ясувати, як довго триває 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"не створюються кожного разу, це інтерновані рядки . Оскільки струни незмінні, вони можуть вільно ділитися; це відбувається зі струнними константами.

Щоб ефективно об'єднати рядки, використовуйте StringBuilder.


Люди на співбесіді насправді дискутували над тим, чи є література чи ні, і вирішили, що літерали створюються кожного разу. Але це має більше сенсу.
ахальберт

6
Як ви "дискутуєте" про те, що робить мова, ви обов'язково читаєте специфікацію і точно знаєте, чи її не визначено, а отже, немає правильної відповіді .....
mattnz

@mattnz Можливо, буде цікаво дізнатися, що робить компілятор / час виконання, який ви використовуєте, навіть якщо мова йде про деталі реалізації. Особливо це стосується продуктивності.
svick

1
@svick: Ви можете багато чого заробляти, роблячи припущення, тоді компілятор удосконалюється, оптимізація змінюється і т. д. Поведінка змінюється, викликаючи помилки, оскільки ви покладалися на не визначене поведінку, а не на певну поведінку. Ви знаєте, що вони кажуть про оптимізацію - а) залиште це експертам і б) ваш ще не експерт. :) Якщо опора залежить лише від продуктивності, але все-таки до мовної специфікації, ви лише втрачаєте продуктивність. Я багато разів бачив код, який спирався на невказане або специфічне поведінку компілятора, розбиваючись несподівано (в основному C і C ++).
mattnz

@mattnz Отже, як ви пропонуєте приймати рішення, пов'язані з продуктивністю? Зазвичай, найкраще, що ви можете отримати із специфікації / документації, - це складні комплекси, але цього недостатньо. У будь-якому випадку ефективність завжди буде залежати від реалізації, тому я думаю, що нормально покладатися на деталі впровадження, коли мова йде про ефективність.
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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.