Рубрики
Без рубрики

String vs StringBuilder vs StringBuffer в Java

Автор оригинала: 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, они также изменчивы и эффективны в использовании памяти, они самые быстрые в обработке строк, но, к сожалению, они небезопасны для потоков.

Если вы примете во внимание эти факты, вы всегда сделаете правильный выбор!