Вступление
Один из наиболее часто используемых классов в Java - это класс String
Он представляет собой строку (массив) символов и, следовательно,
содержит текстовые данные, такие как «Hello World!». Помимо String
,
есть еще два других класса, которые используются для аналогичных целей,
хотя и не так часто, - StringBuilder
и StringBuffer
.
Каждый существует по своей причине, и, не зная о преимуществах других классов, многие начинающие программисты используют только строки, что приводит к снижению производительности и плохой масштабируемости.
Нить
Инициализировать строку так же просто, как:
String string = "Hello World!";
Это нетипично, как и во всех других случаях, мы бы создали экземпляр
объекта, используя new
, тогда как здесь у нас есть «сокращенная»
версия.
Есть несколько способов создать экземпляр String:
// Most common, short way
String str1 = "Hello World";
// Using the `new` keyword and passing text to the constructor
String str2 = new String("Hello World");
// Initializing an array of characters and assigning them to a String
char[] charArray = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd'};
String str3 = new String(charArray);
Давайте посмотрим на исходный код класса и сделаем несколько наблюдений:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/**
* Initializes a newly created {@code String} object so that it represents
* an empty character sequence. Note that use of this constructor is
* unnecessary since Strings are immutable.
*/
public String() {
this.value = new char[0];
}
/**
* Allocates a new {@code String} so that it represents the sequence of
* characters currently contained in the character array argument. The
* contents of the character array are copied; subsequent modification of
* the character array does not affect the newly created string.
*
* @param value
* The initial value of the string
*/
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
...
}
Сначала мы можем наблюдать, как сохраняется сам текст - в массиве char
При этом для нас логично иметь возможность формировать String из массива
символов.
Здесь действительно важно отметить тот факт, что String
определен как
final
. Это означает, что String
неизменяем .
Что это значит?
String str1 = "Hello World!";
str1.substring(1,4).concat("abc").toLowerCase().trim().replace('a', 'b');
System.out.println(str1);
Выход:
Hello World!
Поскольку String
является final, ни один из этих методов не изменил
его. Они просто вернули измененное состояние, которое мы нигде не
использовали и не назначали. Каждый раз, когда вызывается метод String,
создается новая String, состояние изменяется и возвращается.
Снова посмотрим на исходный код:
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
Исходная str
никогда не изменяется. Его значение копируется, и к нему
добавляется текст, который мы объединяем, после чего возвращается
String
Если бы мы сделали что-то вроде этого:
String str1 = "Hello World!";
String str2 = str1.substring(1,4).concat("abc").toLowerCase().trim().replace('a', 'b');
System.out.println(str2);
Затем нас встретит вывод:
ellbbc
Теперь давайте посмотрим на эти две строки:
String str1 = "qwerty";
String str2 = "qwerty";
Когда мы создаем экземпляр String
подобный этой, значение, в данном
случае qwerty
, сохраняется в Java Heap Memory, которая используется
для распределения динамической памяти для всех Java-объектов.
Хотя в этом примере у нас есть две разные ссылочные переменные, обе они
относятся только к одной области памяти в Java Heap Memory. Хотя может
показаться, что существует два разных объекта String, на самом деле есть
только один - str2
никогда не создается как объект, а скорее
назначается объекту в памяти, который соответствует str1
.
Это происходит из-за того, как Java была оптимизирована для работы со строками. Каждый раз, когда вы хотите создать экземпляр объекта String, подобного этому, значение, которое вы хотите добавить в память кучи, сравнивается с ранее добавленными значениями. Если равное значение уже существует, объект не инициализируется, и значение присваивается ссылочной переменной.
Эти значения сохраняются в так называемом пуле строк , который содержит все буквальные значения String. Однако есть способ обойти это
- используя ключевое слово
new
Давайте посмотрим на другой пример:
String str1 = "qwerty";
String str2 = "qwerty";
String str3 = new String("qwerty");
System.out.println(str1 == str2);
System.out.println(str1 == str3);
System.out.println(str1.equals(str2));
System.out.println(str1.equals(str3));
Выход:
true
false
true
true
Это логично, поскольку str1
и str2
указывают на один и тот же объект
в памяти. str3
явно как new
поэтому для него создается новый объект,
даже если строковый литерал уже существует в пуле. Метод equals()
сравнивает их значения, а не объекты, на которые они указывают, поэтому
он возвращает true
для всех этих строк.
Важно отметить, что substring()
и concat()
возвращают новый String
и сохраняют его в пуле String.
Это очень небольшой фрагмент кода, но если мы рассмотрим несколько
больших проектов, использующих сотни String
и тысячи операций, таких
как substring()
или concat()
, это может вызвать серьезные утечки
памяти и задержки во времени. Именно поэтому мы хотим использовать
StringBuffer
или StringBuilder
.
StringBuffer и StringBuilder
Изменчивость
StringBuffer
и StringBuilder
основном содержат то же значение, что и
String
- последовательность символов. И StringBuffer
и
StringBuilder
также являются изменяемыми, что означает, что после
присвоения им значения это значение обрабатывается как атрибут объекта
StringBuffer
или StringBuilder
.
Независимо от того, сколько раз мы изменяем их значение, в результате
не будет создан новый объект String
, StringBuffer
или
StringBuilder
Такой подход намного эффективнее по времени и требует
меньше ресурсов.
StringBuilder против StringBuffer
Эти два класса почти идентичны друг другу - они используют методы с одинаковыми именами, которые возвращают одинаковые результаты. Хотя между ними есть два основных различия:
-
Безопасность потоков : методы
StringBuffer
синхронизируются , что означает, что только один поток может вызывать методыStringBuffer
одновременно. С другой стороны,StringBuilder
не синхронизируются, поэтому несколько потоков могут вызывать методы вStringBuilder
без блокировки.Итак, мы пришли к выводу, что
StringBuffer
является потокобезопасным классом, аStringBuffer
- нет.Это то, о чем вам следует беспокоиться? Может быть. Если вы работаете над приложением, которое использует несколько потоков, работа с
StringBuilder
может быть потенциально опасной. -
Скорость :
StringBuffer
на самом деле в два-три раза медленнее, чемStringBuilder
. Причиной этого являетсяStringBuffer
- разрешение на выполнение только одного потока на объекте за раз приводит к гораздо более медленному выполнению кода.
Методы
И StringBuffer
и StringBuilder
имеют одинаковые методы (помимо
synchronized
метода в StringBuilder
). Давайте рассмотрим некоторые
из наиболее распространенных:
append()
insert()
replace()
delete()
reverse()
Как видите, название каждого метода в значительной степени описывает то, что он делает. Вот простая демонстрация:
StringBuffer sb1 = new StringBuffer("Buffer no 1");
System.out.println(sb1);
sb1.append(" - and this is appended!");
System.out.println(sb1);
sb1.insert(11, ", this is inserted");
System.out.println(sb1);
sb1.replace(7, 9, "Number");
System.out.println(sb1);
sb1.delete(7, 14);
System.out.println(sb1);
sb1.reverse();
System.out.println(sb1);
Выход:
Buffer no 1
Buffer no 1 - and this is appended!
Buffer no 1, this is inserted - and this is appended!
Buffer Number 1, this is inserted - and this is appended!
Buffer 1, this is inserted - and this is appended!
!dedneppa si siht dna - detresni si siht ,1 reffuB
Строка против StringBuilder против StringBuffer
Нить StringBuffer StringBuilder
Мутабельный Нет да да Поточно-безопасный да да Нет Эффективное время Нет Нет да Эффективная память Нет да да
Примечание . Как видно из таблицы выше, String
менее эффективен по
времени и памяти, но это не значит, что мы никогда не должны
использовать его снова.
Фактически, String
может быть очень удобным в использовании, потому
что он может быть написан быстро, и если вы когда-нибудь разработаете
приложение, которое хранит строки, которые не будут обрабатываться /
изменяться позже, использовать String
абсолютно нормально.
Пример кода
Чтобы показать, насколько эффективны String
, StringBuffer
и
StringBuilder
, мы собираемся провести тест производительности:
String concatString = "concatString";
StringBuffer appendBuffer = new StringBuffer("appendBuffer");
StringBuilder appendBuilder = new StringBuilder("appendBuilder");
long timerStarted;
timerStarted = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
concatString += " another string";
}
System.out.println("Time needed for 50000 String concatenations: " + (System.currentTimeMillis() - timerStarted) + "ms");
timerStarted = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
appendBuffer.append(" another string");
}
System.out.println("Time needed for 50000 StringBuffer appends: " + (System.currentTimeMillis() - timerStarted) + "ms");
timerStarted = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
appendBuilder.append(" another string");
}
System.out.println("Time needed for 50000 StringBuilder appends: " + (System.currentTimeMillis() - timerStarted) + "ms");
Выход:
Time needed for 50000 String concatenations: 18108ms
Time needed for 50000 StringBuffer appends: 7ms
Time needed for 50000 StringBuilder appends: 3ms
Этот вывод может отличаться в зависимости от вашей виртуальной машины
Java. Итак, из этого тестового теста мы видим, что StringBuilder
является самым быстрым в обработке строк. Далее идет StringBuffer
,
который в два-три раза медленнее, чем StringBuilder
. И, наконец, у
нас есть String
который на сегодняшний день является самым медленным в
обработке строк.
Использование StringBuilder
привело к времени в ~ 6000 раз быстрее,
чем у обычного String
. То, что потребуется StringBuilder
для
объединения за 1 секунду, займет у String
1,6 часа (если бы мы могли
объединить столько).
Заключение
Мы видели производительность String
, StringBuffer
и StringBuilder
а также их плюсы и минусы. Теперь возникает последний вопрос:
Кто из них победитель?
Что ж, идеальный ответ на этот вопрос - «Это зависит от обстоятельств».
Мы знаем, что String
легко набирать, легко использовать и
потокобезопасно. С другой стороны, они неизменяемы (что означает большее
потребление памяти) и очень медленны при манипуляциях со строками.
StringBuffer
являются изменяемыми, эффективными с точки зрения памяти
и потокобезопасными. Их недостаток - скорость по сравнению с гораздо
более быстрыми StringBuilder
s.
Что касается StringBuilder
s, они также изменяемы и эффективны с точки
зрения памяти, они являются самыми быстрыми в обработке строк, но, к
сожалению, они не являются потокобезопасными.
Если вы примете во внимание эти факты, вы всегда сделаете правильный выбор!