String неизменяемый, любая операция над ним создает новый объект. StringBuilder и StringBuffer изменяемые, они правят свое содержимое на месте. Между собой эти двое отличаются ровно одним: методы StringBuffer синхронизированы и потому потокобезопасны, но из-за этого медленнее. Поэтому по умолчанию берут StringBuilder, а StringBuffer только там, где с одной строкой работают несколько потоков.
- Кратко
- Класс String в Java
- Класс StringBuffer в Java
- Класс StringBuilder в Java
- String vs StringBuffer vs StringBuilder
- Вопросы и ответы
- Читайте также
Кратко
Stringнеизменяемый иfinal: изменить существующий объект нельзя, каждая операция вродеtoUpperCase()илиtrim()возвращает новый объект, а старый превращается в мусор.- За неизменность
Stringплатит мусором, но получает взамен кэшированный хэшкод, безопасность в многопоточности и пул строк, где одинаковые литералы хранятся в единственном экземпляре. StringBufferиStringBuilderизменяемые:append,insert,deleteиreverseправят содержимое на месте, не создавая новых объектов.- Набор методов у них одинаковый, разница ровно одна: методы
StringBufferсинхронизированы, уStringBuilderсинхронизации нет. - Синхронизация стоит производительности, поэтому
StringBuilderбыстрее, и в однопоточном коде выбирают именно его. StringBufferнужен только тогда, когда одну и ту же строку меняют несколько потоков одновременно.

Класс String
Этот класс представляет собой последовательность символов. Все определенные в программе строковые литералы, вроде "This is String", это экземпляры класса String. У String есть две фундаментальные особенности:- это immutable (неизменный) класс
- это final класс
final) и экземпляры класса нельзя изменить после создания (immutable). Что такое неизменяемость и зачем она нужна, разобрано в статье про final, константы и immutable.
Это дает классу String несколько важных преимуществ:
Благодаря неизменности, хэшкод экземпляра класса String кэшируется. Его не нужно вычислять каждый раз, потому что значения полей объекта никогда не изменятся после его создания. Это дает высокую производительность при использовании данного класса в качестве ключа для
HashMap.Класс String можно использовать в многопоточной среде без дополнительной синхронизации.
Еще одна особенность класса String: для него перегружен оператор "
+" в Java. Поэтому конкатенация (сложение) строк выполняется довольно просто:
public static void main(String[] args) {
String command = "Follow" + " " + "the" + " " + "white" + " " + "rabbit";
System.out.println(command); // Follow the white rabbit
}
До Java 9 компилятор превращал конкатенацию в цепочку вызовов append у StringBuilder либо StringBuffer (об этих классах поговорим чуть позже). С Java 9 он вместо этого вставляет invokedynamic, а способ склейки выбирается уже во время выполнения, в StringConcatFactory. Практический вывод от этого не меняется: каждая конкатенация все равно создает новый объект String.
Если мы будем складывать экземпляры класса String с экземплярами других классов, последние будут приводиться к строковому представлению:
public static void main(String[] args) {
Boolean b = Boolean.TRUE;
String result = "b is " + b;
System.out.println(result); //b is true
}
Это еще одно интересное свойство класса String: объекты любых классов можно привести к строковому представлению, используя метод toString(), определенный в классе Object и наследуемый всеми остальными классами.
Часто метод toString() у объекта вызывается неявно. Например когда мы выводим что-то на экран или складываем String с объектом другого класса.
У класса String есть еще одна особенность. Все строковые литералы, определенные в Java коде, вроде "asdf", при первой загрузке класса кэшируются и добавляются в так называемый пул строк.
Если мы запустим следующий код:
String a = "Wake up, Neo";
String b = "Wake up, Neo";
System.out.println(a == b);
Мы увидим в консоли true, потому что переменные a и b в действительности будут ссылаться на один и тот же экземпляр класса String, добавленный в пул строк при первой загрузке класса. То есть, разные экземпляры класса с одинаковым значением не создаются, и память экономится.
Недостатки String
Нетрудно догадаться, что класс String нужен в первую очередь для работы со строками. Но в некоторых случаях перечисленные выше особенности класса String могут превратиться из достоинств в недостатки. После создания строк в коде Java-программы с ними часто совершается множество операций:- перевод строк в разные регистры;
- извлечение подстрок;
- конкатенация;
- и т.д.
public static void main(String[] args) {
String s = " Wake up, Neo! ";
s = s.toUpperCase();
s = s.trim();
System.out.println("\"" + s + "\"");
}
С первого взгляда кажется, что мы всего-то перевели строку "Wake up, Neo!" в верхний регистр, удалили из данной строки лишние пробелы и обернули в кавычки.
На самом деле, в силу неизменности класса String, в результате каждой операции создаются новые экземпляры строк, а старые отбрасываются, порождая большое количество мусора.
Как же избежать нерационального использования памяти?
Класс StringBuffer
Чтобы справиться с созданием временного мусора из-за модификаций объекта String, можно использовать класс StringBuffer. Этоmutable класс, т.е. изменяемый. Объект класса StringBuffer может содержать в себе определенный набор символов, длину и значение которого можно изменить через вызов определенных методов.
Посмотрим, как работает данный класс.
Для создания нового объекта используется один из его конструкторов, например:
- StringBuffer() создаст пустой (не содержащий символов) объект
- StringBuffer(String str) создаст объект на основе переменной str (содержащий все символы str в той же последовательности)
StringBuffer sb = new StringBuffer();
StringBuffer sb2 = new StringBuffer("Not empty");
Конкатенация строк через StringBuffer в Java выполняется с помощью метода append.
Вообще метод append в классе StringBuffer перегружен таким образом, что может принимать в себя практически любой тип данных:
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
sb.append(Integer.valueOf(2));
sb.append("; ");
sb.append(false);
sb.append("; ");
sb.append(Arrays.asList(1,2,3));
sb.append("; ");
System.out.println(sb); // 2; false; [1, 2, 3];
}
Метод append возвращает объект, на котором был вызван (как и многие другие методы), что позволяет вызывать его “цепочкой”. Пример выше можно написать так:
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
sb.append(Integer.valueOf(2))
.append("; ")
.append(false)
.append("; ")
.append(Arrays.asList(1,2,3))
.append("; ");
System.out.println(sb); // 2; false; [1, 2, 3];
}
Для работы со строками у класса StringBuffer есть ряд методов. Перечислим основные:
delete(int start, int end): удаляет подстроку символов начиная с позицииstart, заканчиваяenddeleteCharAt(int index): удаляет символ в позицииindexinsert(int offset, String str): вставляет строкуstrв позициюoffset. Методinsertтакже перегружен и может принимать различные аргументыreplace(int start, int end, String str): заменит все символы начиная с позицииstartдо позицииendнаstrreverse(): меняет порядок всех символов на противоположныйsubstring(int start): вернет подстроку, начиная с позицииstartsubstring(int start, int end): вернет подстроку, начиная с позицииstartдо позицииend
public static void main(String[] args) {
String numbers = "0123456789";
StringBuffer sb = new StringBuffer(numbers);
System.out.println(sb.substring(3)); // 3456789
System.out.println(sb.substring(4, 8)); // 4567
System.out.println(sb.replace(3, 5, "ABCDE")); // 012ABCDE56789
sb = new StringBuffer(numbers);
System.out.println(sb.reverse()); // 9876543210
sb.reverse(); // Вернем изначальный порядок
sb = new StringBuffer(numbers);
System.out.println(sb.delete(5, 9)); // 012349
System.out.println(sb.deleteCharAt(1)); // 02349
System.out.println(sb.insert(1, "One")); // 0One2349
}
Преимущества StringBuffer
Как уже сказано, StringBuffer это изменяемый класс, поэтому при работе с ним не возникает такого же количества мусора в памяти, как со String. Поэтому если над строками проводится много модификаций, лучше использовать
StringBuffer.StringBuffer это потокобезопасный класс. Его методы синхронизированы, а экземпляры могут быть использованы несколькими потоками одновременно. Важная оговорка: синхронизирован каждый отдельный вызов, но не их последовательность. Если два потока одновременно выполняют
if (sb.length() > 0) sb.deleteCharAt(0);, каждый вызов сам по себе атомарен, а вместе они дают гонку, и такую цепочку все равно придется оборачивать в свойsynchronized.
Недостатки StringBuffer
С одной стороны, потокобезопасность это преимущество класса, а с другой стороны недостаток. Синхронизированные методы работают медленнее несинхронизированных. И здесь в игру вступает StringBuilder. Давайте разберемся, что это за класс StringBuilder в Java, какие методы есть и в чем его особенности.Класс StringBuilder
StringBuilder в Java это класс, представляющий последовательность символов. Он очень похож на StringBuffer во всем, кроме потокобезопасности. StringBuilder предоставляет API, аналогичный API StringBuffer’a. Продемонстрируем это на уже знакомом примере, заменив объявление переменных со StringBuffer’а на StringBuilder:
public static void main(String[] args) {
String numbers = "0123456789";
StringBuilder sb = new StringBuilder(numbers);
System.out.println(sb.substring(3)); //3456789
System.out.println(sb.substring(4, 8)); //4567
System.out.println(sb.replace(3, 5, "ABCDE")); //012ABCDE56789
sb = new StringBuilder(numbers);
System.out.println(sb.reverse()); //9876543210
sb.reverse(); // Вернем изначальный порядок
sb = new StringBuilder(numbers);
System.out.println(sb.delete(5, 9)); //012349
System.out.println(sb.deleteCharAt(1)); //02349
System.out.println(sb.insert(1, "One")); //0One2349
}
Разница лишь в том, что StringBuffer потокобезопасен, и все его методы синхронизированы, а StringBuilder нет. Это единственная особенность.
StringBuilder в Java работает быстрее StringBuffer’а благодаря несинхронизированности методов.
Поэтому в большинстве случаев, кроме многопоточной среды, лучше использовать для программы на Java StringBuilder.
Резюмируем все в сравнительной таблице трех классов:
String vs StringBuffer vs StringBuilder
| String | StringBuffer | StringBuilder | |
|---|---|---|---|
| Изменяемость | Immutable (нет) |
mutable (да) |
mutable (да) |
| Расширяемость | final (нет) |
final (нет) |
final (нет) |
| Потокобезопасность | Да, за счет неизменяемости | Да, за счет синхронизации | Нет |
| Когда использовать | При работе со строками, которые редко будут модифицироваться | При работе со строками, которые часто будут модифицироваться в многопоточной среде | При работе со строками, которые часто будут модифицироваться, в однопоточной среде |
- Внутреннее устройство String, метод substring
- StringBuilder, StringBuffer
- Задачи по StringBuilder, StringBuffer
Вопросы и ответы
В чем разница между String, StringBuffer и StringBuilder?
String неизменяемый: любая операция над строкой создает новый объект, а старый становится мусором. StringBuffer и StringBuilder изменяемые, они правят свое содержимое на месте. Между собой эти двое отличаются ровно одним: методы StringBuffer синхронизированы, поэтому с одним объектом могут работать несколько потоков, а у StringBuilder синхронизации нет, зато он быстрее.Почему String сделали неизменяемым?
Неизменяемость дает сразу несколько выгод. Хэшкод строки вычисляется один раз и кэшируется, поэтомуString быстро работает ключом в HashMap. Один и тот же литерал можно хранить в единственном экземпляре в пуле строк и экономить память. И объект не нужно защищать от одновременного доступа из разных потоков: изменить его все равно нельзя.Что такое пул строк?
Это область памяти, в которой JVM хранит по одному экземпляру каждого строкового литерала. Когда в коде дважды встречается один и тот же литерал, обе переменные получают ссылку на один объект, поэтому сравнение через== для них дает true. Строки, созданные явно через new String(...), в пул не попадают и остаются разными объектами.Что выбрать: StringBuilder или StringBuffer?
По умолчаниюStringBuilder. Он делает то же самое, но без накладных расходов на синхронизацию, поэтому работает быстрее. StringBuffer имеет смысл только тогда, когда один и тот же объект действительно меняют несколько потоков одновременно, а такое встречается редко: чаще строку собирают внутри одного метода, то есть в одном потоке.Почему сравнение двух одинаковых литералов дает true, а сравнение с new String дает false?
Потому что литералы берутся из пула строк: обе переменные ссылаются на один и тот же объект, а== сравнивает именно ссылки, и они совпали. Выражение new String("текст") всегда создает отдельный объект в куче, поэтому ссылки будут разными и == вернет false. Содержимое строк нужно сравнивать методом equals().Обязательно ли собирать строку через StringBuilder в цикле?
Да, если итераций много. Каждая операцияs += ... создает новую строку и копирует в нее все, что уже накоплено, поэтому время растет квадратично от числа итераций. StringBuilder дописывает символы в уже существующий буфер, и та же задача решается за линейное время. Для одного-двух сложений разницы нет, обычная конкатенация читается лучше.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ