Avatar ·

В чем разница между StringBuilder и StringBuffer в Java?

Всем привет! Я учу Java и пишу небольшой серверный модуль для обработки логов. Мне нужно собирать строки из множества фрагментов, и я знаю, что конкатенация через "+" в цикле — это плохо, потому что создается много лишних объектов String. Я начал использовать StringBuffer, но мой тимлид сказал, что лучше перейти на StringBuilder. Я не совсем понял, почему. В интернете пишут, что оба класса делают одно и то же: позволяют изменять строку без создания новых объектов. Зачем тогда два класса? И какой из них выбрать? Вот пример моего кода: ```java public class LogBuilder { public static void main(String[] args) { StringBuffer log = new StringBuffer(); for (int i = 0; i < 1000; i++) { log.append("Событие #").append(i).append(": "); log.append("Обработка завершена\n"); } System.out.println(log.toString()); } } ``` Если я просто заменю StringBuffer на StringBuilder, код продолжит работать. Значит ли это, что они полностью взаимозаменяемы? Или есть какие-то подводные камни? Мне важно понять, как не ошибиться в продакшене, особенно когда речь идет о многопоточности (я читал, что у нас в проекте будет многопоточная обработка запросов). Объясните, пожалуйста, на пальцах, чем они отличаются, и в каких случаях что использовать. С примерами кода, если можно. <<>>

Основное различие: синхронизация

Главное и практически единственное отличие между StringBuilder и StringBuffer заключается в том, что методы StringBuffer являются синхронизированными (thread-safe), а методы StringBuilder — нет. Это означает, что StringBuffer безопасен для использования в многопоточной среде, где несколько потоков могут одновременно обращаться к одному и тому же экземпляру. StringBuilder такой гарантии не дает.

Однако за синхронизацию приходится платить производительностью. StringBuffer работает медленнее, потому что каждый вызов его методов (например, append()) требует захвата и освобождения монитора. В однопоточных сценариях этот оверхед абсолютно бесполезен, поэтому StringBuilder всегда предпочтительнее, если вы не работаете с общим mutable-объектом из нескольких потоков.

Пример, который показывает разницу

Рассмотрим многопоточный сценарий. Допустим, два потока одновременно пишут в один буфер:

public class StringBufferVsBuilder {
    public static void main(String[] args) throws InterruptedException {
        // Тест с StringBuffer
        StringBuffer buffer = new StringBuffer();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                buffer.append("A");
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                buffer.append("B");
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("StringBuffer length: " + buffer.length()); // Всегда 2000

        // Тест с StringBuilder (может упасть или дать неверный результат)
        StringBuilder builder = new StringBuilder();
        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                builder.append("A");
            }
        });
        Thread t4 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                builder.append("B");
            }
        });
        t3.start();
        t4.start();
        t3.join();
        t4.join();
        System.out.println("StringBuilder length: " + builder.length()); // Может быть < 2000 или ArrayIndexOutOfBoundsException
    }
}

В этом примере StringBuffer всегда даст корректный результат (длина 2000), а StringBuilder может либо потерять данные, либо выбросить исключение из-за состояния гонки.

Когда что использовать?

  • StringBuilder — ваш выбор по умолчанию. Используйте его везде, где вы работаете с буфером в рамках одного потока: сборка SQL-запросов, JSON, логов в однопоточном обработчике, конкатенация строк в методах.
  • StringBuffer — используйте только если вы точно знаете, что один и тот же объект будет изменяться из разных потоков. На практике такие ситуации редки, так как обычно каждый поток имеет свой собственный StringBuilder. Если вам нужна потокобезопасность, часто проще использовать локальные переменные или явную синхронизацию вокруг StringBuilder.

Историческая справка

StringBuffer появился в Java 1.0 и был единственным изменяемым строковым классом. Когда в Java 5 осознали, что синхронизация почти всегда излишня и вредит производительности, добавили StringBuilder как несинхронизированную версию. С тех пор StringBuilder стал стандартом для однопоточного использования.

Практический совет для вашего кода

Ваш пример с логами — классический однопоточный сценарий (цикл в одном методе). Поэтому смело заменяйте StringBuffer на StringBuilder. Если в будущем понадобится многопоточный логгер, лучше использовать специализированные библиотеки (например, Logback или Log4j), которые сами управляют синхронизацией, а не полагаться на StringBuffer.

<<>>

Дополнение: производительность и внутреннее устройство

Стоит добавить, что разница в производительности между StringBuilder и StringBuffer может быть существенной в высоконагруженных системах. По разным бенчмаркам, StringBuilder в однопоточном режиме работает в 1.5–2 раза быстрее, так как не тратит время на синхронизацию.

Обратите внимание, что оба класса наследуются от абстрактного класса AbstractStringBuilder. Их внутренняя структура идентична: массив символов (char[]) и счетчик длины. Разница только в том, что методы StringBuffer помечены ключевым словом synchronized. Например:

// StringBuffer
public synchronized StringBuffer append(String str) {
    super.append(str);
    return this;
}

// StringBuilder
public StringBuilder append(String str) {
    super.append(str);
    return this;
}

Ещё один нюанс: в StringBuffer синхронизированы не все методы, а только те, которые изменяют состояние (например, append(), insert(), delete()). Методы чтения, такие как length() или charAt(), тоже могут быть синхронизированы, но это не даёт полной потокобезопасности без внешней синхронизации. Поэтому StringBuffer — это компромисс, который редко бывает оправдан.

Итог: если вы не пишете код, где несколько потоков одновременно модифицируют один и тот же буфер (что случается крайне редко), используйте StringBuilder. Он быстрее, легче, и именно его рекомендуют современные стандарты кодирования.