Автор оригинала: Guest Contributor.
Вступление
Одним из наиболее часто используемых классов в Java является класс String
. Он представляет собой строку (массив) символов и, следовательно, содержит текстовые данные, такие как “Привет, мир!”. Помимо класса String
, есть два других класса, используемых для аналогичных целей, хотя и не так часто – StringBuilder
и StringBuffer
.
Каждый из них существует по своей собственной причине, и, не зная о преимуществах других классов, многие начинающие программисты используют только строки, что приводит к снижению производительности и плохой масштабируемости.
Строка
Инициализация строки так же проста, как:
String string = "Hello World!";
Это нетипично, как и во всех других случаях, мы создаем экземпляр объекта с помощью ключевого слова new
, тогда как здесь у нас есть версия “ярлыка”.
Существует несколько способов создания экземпляров строк:
// 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, 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 str1 = "Hello World!"; str1.substring(1,4).concat("abc").toLowerCase().trim().replace('a', 'b'); System.out.println(str1);
Выход:
Hello World!
Поскольку 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); }
Оригинал история
никогда не меняется. Его значение копируется, и к нему добавляется текст, который мы объединяем, после чего возвращается новая Строка
.
Если бы мы сделали что-то подобное:
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 , которая используется для динамического выделения памяти для всех объектов Java.
Хотя в этом примере у нас есть две разные ссылочные переменные, обе они ссылаются только на одно место памяти в кучной памяти Java. Хотя может показаться, что существуют два разных строковых объекта, на самом деле существует только один – str2
никогда не создается как объект, а скорее присваивается объекту в памяти, который соответствует str1
.
Это происходит из-за того, как Java была оптимизирована для строк. Каждый раз, когда вы хотите создать экземпляр строкового объекта, подобного этому, значение, которое вы хотите добавить в память кучи, сравнивается с ранее добавленными значениями. Если равное значение уже существует, объект не инициализируется, и значение присваивается ссылочной переменной.
Git Essentials
Ознакомьтесь с этим практическим руководством по изучению Git, содержащим лучшие практики и принятые в отрасли стандарты. Прекратите гуглить команды Git и на самом деле изучите это!
Эти значения сохраняются в так называемом Строковом пуле , который содержит все буквальные строковые значения. Однако есть способ обойти это – используя ключевое слово 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
создается явно как новый
, поэтому для него создается новый объект, даже если строковый литерал уже существует в пуле. Метод equals()
сравнивает их значения, а не объекты, на которые они указывают, поэтому он возвращает true
для всех этих строк.
Важно отметить, что методы substring()
и concat()
возвращают новый объект String
и сохраняют его в пуле строк.
Это очень маленький фрагмент кода, но если мы рассмотрим некоторые крупные проекты , использующие сотни строковых
переменных и тысячи операций, таких как подстрока()
или конкат ()
, это может привести к серьезным утечкам памяти и временным задержкам. Именно поэтому мы хотим использовать StringBuffer
или StringBuilder
.
Строковый буферизатор и строковый конструктор
Изменчивость
Объекты StringBuffer
и StringBuilder
в основном содержат то же значение, что и объект String
– последовательность символов. Оба StringBuffer
и StringBuilder
также являются изменяемыми, что означает, что как только мы присваиваем им значение, это значение обрабатывается как атрибут объекта StringBuffer
или StringBuilder
.
Независимо от того, сколько раз мы изменяем их значение, в результате новый String
, StringBuffer
или StringBuilder
объект не будет создан. Такой подход гораздо более эффективен по времени и менее ресурсоемок.
StringBuilder против StringBuffer
Эти два класса почти идентичны друг другу – они используют методы с одинаковыми именами, которые возвращают одинаковые результаты. Хотя между ними есть два основных различия:
Потокобезопасность :
StringBuffer
методы синхронизированы , что означает, что только один поток может вызывать методы экземпляраStringBuffer
одновременно. С другой стороны, методыStringBuilder
не синхронизированы, поэтому несколько потоков могут вызывать методы в классеStringBuilder
без блокировки.Таким образом, мы пришли к выводу, что
StringBuffer
является потокобезопасным классом, в то время какStringBuffer
им не является.Это то, о чем тебе стоит беспокоиться? Может быть. Если вы работаете над приложением, которое использует несколько потоков, работа с
StringBuilder
может быть потенциально опасной .Скорость :
StringBuffer
на самом деле в два-три раза медленнее, чемStringBuilder
. Причиной этого являетсяStringBuffer
синхронизация – одновременное выполнение только 1 потока для объекта приводит к гораздо более медленному выполнению кода.
Методы
Оба StringBuffer
и StringBuilder
имеют одинаковые методы (кроме синхронизированного
объявления метода в классе StringBuilder
). Давайте рассмотрим некоторые из наиболее распространенных:
добавить()
вставить()
заменить()
удалить()
реверс()
Как вы можете видеть, имя каждого метода в значительной степени описывает то, что он делает. Вот простая демонстрация:
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
Строка против строкостроителя против строкового буфера
Нет | Изменчивый | Да | Да |
Да | Потокобезопасный | Да | Нет |
Нет | Экономия Времени | Нет | Да |
Нет | Эффективная память | Да | Да |
Примечание : Как мы видим из приведенной выше таблицы, Строка
менее эффективна как по времени, так и по памяти, но это не значит, что мы никогда не должны использовать ее снова.
На самом деле, 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
. И, наконец, у нас есть Строка
, которая на сегодняшний день является самой медленной в обработке строк.
Использование StringBuilder
привело к времени ~в 6000 раз быстрее, чем обычные String
. То, что потребуется StringBuilder
для объединения за 1 секунду, займет String
1,6 часа (если бы мы могли объединить столько).
Вывод
Мы видели производительность String
s, StringBuffer
s и StringBuilder
s, а также их плюсы и минусы. Теперь возникает последний вопрос:
Кто из них победитель?
Ну, идеальный ответ на этот вопрос – “Это зависит”. Мы знаем, что String
s просты в вводе, удобны в использовании и потокобезопасны. С другой стороны, они неизменяемы (что означает большее потребление памяти) и очень медленны при обработке строк.
Строковые буферы
являются изменяемыми, эффективными с точки зрения памяти и потокобезопасными. Их падение-это скорость по сравнению с гораздо более быстрым StringBuilder
s.
Что касается StringBuilder
s, они также изменчивы и эффективны в использовании памяти, они самые быстрые в обработке строк, но, к сожалению, они небезопасны для потоков.
Если вы примете во внимание эти факты, вы всегда сделаете правильный выбор!