StringBuilder vs StringBuffer: 10 главных отличий в Java

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

StringBuilder vs StringBuffer: что выбрать и почему это важно?

Если вы когда-нибудь писали на Java код, который активно работает со строками (например, собирает SQL-запрос, формирует JSON или просто склеивает десятки кусочков текста), то наверняка сталкивались с дилеммой: StringBuilder или StringBuffer? На Stack Overflow этот вопрос собрал сотни тысяч просмотров, и не зря. На первый взгляд классы похожи как близнецы — оба расширяют AbstractStringBuilder, оба используют изменяемый массив символов, оба умеют append(), insert(), delete(). Но дьявол, как всегда, кроется в деталях.

В этой статье мы не просто перечислим отличия. Мы разберём их на реальных примерах, заглянем под капот JVM и ответим на вопросы, которые вам обязательно зададут на собеседовании. Поехали!

1. Главное отличие: синхронизация

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

Давайте посмотрим на код (упрощённая версия из реального JDK):

// StringBuffer (Java 17)
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

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

Одна строчка synchronized — и вся разница. Но эта строчка меняет всё: производительность, область применения, даже архитектуру приложения.

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

  • Вы пишете многопоточное приложение.
  • Один и тот же экземпляр StringBuffer используется из нескольких потоков без внешней синхронизации.
  • Пример: логгер, который собирает сообщения из разных потоков в один буфер.

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

  • Однопоточный код (90% случаев).
  • Локальные переменные внутри метода.
  • Циклы с конкатенацией строк (компилятор сам предложит StringBuilder).

2. Производительность: кто быстрее?

Здесь всё предсказуемо: StringBuilder быстрее StringBuffer. Насколько? Зависит от нагрузки, но в среднем в 2-5 раз. Давайте проверим на простом бенчмарке.

public class PerformanceTest {
    private static final int ITERATIONS = 10_000_000;

    public static void main(String[] args) {
        // StringBuilder
        long start = System.nanoTime();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < ITERATIONS; i++) {
            sb.append("a");
        }
        long sbTime = System.nanoTime() - start;

        // StringBuffer
        start = System.nanoTime();
        StringBuffer sbf = new StringBuffer();
        for (int i = 0; i < ITERATIONS; i++) {
            sbf.append("a");
        }
        long sbfTime = System.nanoTime() - start;

        System.out.println("StringBuilder: " + sbTime / 1_000_000 + " ms");
        System.out.println("StringBuffer:  " + sbfTime / 1_000_000 + " ms");
        System.out.println("Buffer медленнее в " + (sbfTime / (double) sbTime) + " раз");
    }
}

На моём ноутбуке (Intel i7, 16GB RAM, Java 17) результат стабильный: StringBuilder ~180 ms, StringBuffer ~620 ms. Разница почти в 3.5 раза. А если добавить реальную работу (парсинг, форматирование), разрыв может увеличиться. Почему? Потому что каждый вызов synchronized — это проверка монитора, захват блокировки и её освобождение. В однопоточном коде это чистая потеря времени.

Важно: Если ваш метод гарантированно работает в одном потоке, всегда используйте StringBuilder. Никаких «на всякий случай» — это антипаттерн.

3. toStringCache: скрытая оптимизация StringBuffer

Мало кто знает, но у StringBuffer есть ещё одно отличие — поле toStringCache:

// StringBuffer (Java 17)
private transient char[] toStringCache;

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

Что это даёт? При первом вызове toString() создаётся копия массива символов и кешируется. При последующих вызовах (пока буфер не изменится) мы просто возвращаем уже созданную строку. Это ускоряет повторные вызовы, но только в многопоточной среде (где буфер редко меняется после инициализации). В StringBuilder такого нет — каждый вызов toString() создаёт новую копию массива.

На практике это редко имеет значение, но на собеседовании может быть плюсом, если вы упомянете этот нюанс.

4. История и наследование

StringBuffer появился в Java 1.0 (1996 год). Он был единственным изменямым строковым классом. StringBuilder добавили только в Java 5 (2004 год) как лёгкую замену для однопоточных сценариев. Оба класса наследуются от AbstractStringBuilder, но не являются частью одной иерархии с String (String — неизменяемый, final класс).

Интересный факт: в Java 5-8 компилятор использовал StringBuffer для конкатенации строк в циклах. Начиная с Java 9, он переключился на StringBuilder (через invokedynamic). Так что если вы пишете str1 + str2 внутри цикла, компилятор сам создаст StringBuilder. Но только если не используете многопоточность.

5. Частые вопросы с собеседований (и Stack Overflow)

Вопрос 1: Можно ли использовать StringBuilder в многопоточном коде?

Можно, но с осторожностью. Если вы синхронизируете доступ к нему внешним блоком synchronized или используете ReentrantLock, то да. Но зачем? Проще взять StringBuffer или вообще StringWriter. Если забудете синхронизацию — получите race condition: один поток пишет, другой читает, третий удаляет — итог: ArrayIndexOutOfBoundsException или мусор в буфере.

Вопрос 2: Какой класс быстрее для больших строк?

StringBuilder. Всегда. Исключение — если вы пишете в 10 потоков в один буфер. Тогда StringBuffer будет быстрее, чем StringBuilder с внешней синхронизацией, потому что его синхронизация тоньше (блокировка на уровне метода).

Вопрос 3: Почему StringBuffer не устарел?

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

Блоги

Рекомендаций по книгам