Pull to refresh

Comments 68

это мой оригинальный текст
Мне всегда было интересно хоть кто-то нашел применение StringBuffer. Не очень понятно в каком случае он может пригодиться. Попеременно дописывать какие-то строки и потом их куда-то выводить. Получается в такой строке порядок должен быть не очень важен. Могу подумать только о логировании и чем-то подобном, но для этого есть масса других средств.
Конечно, именно StringBuffer. StringBuilder-то один из самых часто используемых классов в стандартной библиотеке.
У меня только одно объяснение: по историческим причинам и в силу того, что java дает обратную совместимость

 * @since       1.5
 */
public final class StringBuilder


 * @since   JDK1.0
 */
 public final class StringBuffer
String second = new String(habr); // аналогично second = habr;

Разве аналогично?
1. String second = new String(habr) — создается новый объект second с тем же содержимым, что и в объекте habr. При этом second и habr ссылаются на разные объекты.
2. String second = habr — копируется ссылка на объект habr, то есть second и habr ссылаются на один объект.
Верно?
Опечатался, спасибо. Все верно отметили.
Хотелось бы дополнить и сказать, что все ссылки на string объекты хранится в String Pool и перед созданием строки с помощью литерала проверяется нет ли эквивалентной строки в пуле, если нет — добавляется, иначе просто получаем ссылку на уже готовый объект. В случаи с new, новый объект создаться в любом случаи, независимо от пула.
Вам спасибо за статью! Давайте продолжение.

(Чисто языковой комментарий: слово «что», конечно, заменяет слово «который», но не без потерь: оно не передает род заменяемого слова, поэтому текст воспринимается хуже. Пример:
«если длина строки, что хранится...» — к чему здесь относится «что»: к длине или к строке? Конкретно в этом случае подойдет причастие:
«длина строки, хранящаяся...» или
«длина строки, хранящейся...»)
А теперь запустите тот же тест с -XX:BiasedLockingStartupDelay=0:
StringBuffer: 5590ms.
StringBuilder: 5493ms.

И чего в итоге померили?

Также изменения коснулись hashCode (новый алгоритм hashing)
Оппа! Как такое может быть, если алгоритм хеширования для String строго прописан в спецификации?
А при добавлении третьей строчки у меня вообще весело получается:
    test(new StringBuffer("")); // StringBuffer: 5020ms.
    test(new StringBuilder("")); // StringBuilder: 14041ms.
    test(new StringBuilder("")); // StringBuilder: 10716ms.
Учитывая цикл 1e9 итераций и что StringBuilder/StringBuffer расширяются — может произойти GC, наверняка может случится и JIT — словом, что именно происходило в фоне — неизвестно, так, что можно гадать на кофейной гуще, что же именно мерялось.
Алгоритм действительно не изменился (где-то прочитал, но не проверял тогда), спасибо.
Вдобавок к вышеперечисленному, хочу добавить, что одним из важных аспектов, почему String объявлен как final — это безопасность. Например, когда загружаем класс, то имя класса передается в виде строки. Если бы строки были не финальные, то вредоносный код мог бы изменить имя класса и, таким образом, загрузить неправильный класс. Например, вместо java.io.FileReader был бы загружен com.malicious.FileRemover. Также можно было бы сломать контракт equals/hashCode.

Для обхода всех этих проблем, возможно, был бы введен класс java.lang.SafeString, который бы помог предотвратить эти проблемы, но попутно создал бы новых (например, конвертацию из SafeString в String и обратно, замусоривание API). Но так как это один из базовых классов, то было принято решение сделать класс String финальным.
В принципе не плохая статья, но ни слова о String pool, злом методе intern и злом методе substring(до версии 1.7.0_06)
Это ни в коем случае не камень в ваш огород, просто логическое продолжение замечания, учитывая то, что этого доклада в комментариях еще не было
Метод substring напротив был добрым до 1.7.0_06, если им правильно пользоваться =)
Ну вообще да, смотря что называть «добротой» трату памяти или трату времени)
При правильном использовании substring мог даже сэкономить память: можно было на один char[]-массив повесить тыщу строк, снижая оверхед на заголовки каждого массива. К примеру, вы загружаете из базы readonly-словарь, который всё равно будете держать всегда целиком в памяти. Можно было конкатенировать все строки и разбить по substring.
Маленькое уточнение по поводу оператора "+". Как с казано в документации к Oracle JDK 6, 7 и 8, реализация этого оператора сделана с использованием StringBuilder:

The Java language provides special support for the string concatenation operator ( + ), and for conversion of other objects to strings. String concatenation is implemented through the StringBuilder(or StringBuffer) class and its append method. String conversions are implemented through the method toString, defined by Object and inherited by all classes in Java.
(ссылка на v8)

Что также отражено в спецификации:
An implementation may choose to perform conversion and concatenation in one step to avoid creating and then discarding an intermediate String object. To increase the performance of repeated string concatenation, a Java compiler may use the StringBuffer class or a similar technique to reduce the number of intermediate String objects that are created by evaluation of an expression.

For primitive types, an implementation may also optimize away the creation of a wrapper object by converting directly from a primitive type to a string.
(ссылка)

Эта тема также поднималась на StackOverflow и там был детально описан процесс конкатенации (ссылка).
Как сказано в документации к Oracle JDK 6, 7 и 8


это не документация к Oracle JDK. Это javadoc, то есть, документация к стандарту Java SE (6, 7, 8).
String habr = new StringBuilder(String.valueOf("habra")).append("habr").toString();

это уточнения было в статьи
Объясните — откуда взялось это убожество String.valueOf?

public String s(String v, String f){
    return v + f;
}


и его bytecode
 public java.lang.String s(java.lang.String, java.lang.String);
    descriptor: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: new           #2                  // class java/lang/StringBuilder
         3: dup
         4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
         7: aload_1
         8: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        11: aload_2
        12: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: invokevirtual #5                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        18: areturn
      LineNumberTable:
        line 3: 0


т.е + это синтетический сахар, а на деле

public String s(String v, String f){
    return new StringBuilder().append(v).append(f).toString();
}

и никаких String.valueOf — не вводите людей в заблуждение.

И вы уверены, что в следующем методе используется конкатенация?
public String q(){
return "ha" + "br";
}


Данный строковый литерал может быть вычислен уже на стадии компиляции и javac подставляет уже результат
Constant pool:
   #2 = String             #20            // habr

 public java.lang.String q();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: ldc           #2                  // String habr
         2: areturn
      LineNumberTable:
        line 3: 0


Что может привести к неожиданным результатам:
К примеру, вы используете константу из сторонней библиотеки, которая скажем возвращает ее версию в виде строки, н-р «v1». Если вы обновите эту библиотеку (которая возвращает «v2») без пересборки класса, который ее использует — результат будет по прежнему «v1» ибо именно это значение было подставлено на стадии компиляции.

Hack, который позволяет обходить данные грабли — использование метода intern — как например тут. По сути строковый литерал уже находится в пуле строк и никакого выигрыша/проигрыша нет, кроме того, что теперь javac не может (по крайней мере пока) вычислить значение на стадии компиляции и уже будет честно загружать класс и его константу.
String habrahabr = new StringBuilder()).append(habra).append(habr).toString();


вы правы, исправил
поддерживаю. Проверить очень легко: скомпирировать конкатенацию, а потом декомпилировать.
Даже в байткод руками лазать не надо :)
скомпирировать… декомпилировать.


Настоящим пацанам нужна только консолька, vi, javac да javap для этого
предложите свой вариант
Прежде, чем писать любые перфомансные тесты и юзать всякие System.currentTimeMillis(), мы с vladimir_dolzhenko настоятельно рекомендуем Вам ознакомиться вот с этой лекцией:
спасибо, посмотрю, перепишу тест :)
Всецело согласен с Лешей 23derevo, но стоит добавить, что одной лекцией не стоит ограничится, чтобы понять как же измерять и пользоваться jmh, но вектор правильный.

Стоит обратить внимание, что в java есть такая вещь как Escape Analysys, которая делает много волшебных вещей, н-р, стирание блокировок, если экземпляр никуда не утекает.

Т.е в однопоточном случае разница между StringBuilder и StringBuffer будет в пределах погрешности измерений (после того, как JIT свернет). Это следует учитывать, когда будете писать jmh-тест.
Используйте метод concat, если слияние нужно провести только один раз, для остальных случаев рекомендовано использовать или оператор "+" или StringBuffer / StringBuilder.
Никогда не используйте concat. Обычный "+" или StringBuilder всегда будет эффективней, даже всего для двух строк. Просто потому, что JVM оптимизирует "+" специальным образом (это JVM intrinsic), а concat — нет (это обычный Java метод).
Можно подробнее про JVM intrinsic  используемый в +?

Как бы байткод всегда показывает цепочку new StringBuilder().append()...toString() и не знал, что intrinsic может действовать над цепочкой
Именно так. HotSpot JVM распознаёт паттерн new StringBuilder().append()...toString() (или то же самое со StringBuffer), и компилирует это как одно целое. Регулируется опцией -XX:+OptimizeStringConcat, по умолчанию включено.
в теории ведь никто не мешает concat реализовать как JVM intrinsic?
Представляешь как было бы… фантастично-феерично иметь pluggable intrinsic?
а чего там представлять? Есть такая штука — OpenJDK. Она некоторыми и используется именно так, как ты хочешь: Taobao JVM (слайды 34-37)
Все клева конечно, только доклад 2011.
Почему-то на ум приходит академическая разработка RedHat под названием Shenandoah — вызов Zing VM и С4 — но только больше о них ничего не слышно.
погоди, тут другой случай :)

Как я понял — это просто кастомизированная OpenJDK (classlib, jvm), работающая внутри Taobao. Они делают определенные изменения и оптимизации, заточенные под их конкретные юзкейсы (ворклоады, железо, сценарии и т.п.). И как я понимаю, именно поэтому они не всегда коммитят назад в OpenJDK — их изменения не всем подойдут.
Я вообще о том, что ежели продукт пошел — то должны быть упоминания и развитие, кроме данного доклада.

Но все равно было бы варенье-с-маслом видеть pluggable intrinsic в java-по-умолчанию
Ну вот смотри, у нас в Одноклассниках тоже есть свои сборки OpenJDK патченные — и classlib и JVM. Мы об этом упоминаем периодически, но чтобы прямо кричать об этом на каждом углу…

Многие, кстати, патчат. Например, тот же гугл. Посмотри доклады Jeremy Manson с JVMLS 2013 и JVMLS 2014. И тоже об этом мало в интернете есть. Потому что это больше не инфоповод и не рокит саенс.
В теории — да. На практике вряд ли его напишут, потому что его мало используют. Практический выход никакой, зато трудозатраты на написание низкоуровневого кода и новое место для багов. Многие куда более полезные интринсики (например, Array.get) и то отсутствуют.
На самом деле дело ещё в том, что по факту внутренняя реализация оператора "+" не специфицирована строго. Спецификация не требует, чтобы это обязательно был StringBuilder. Вон Алексей колбасит новую реализацию на invokedynamic. Если что-то получится, может работать ещё быстрее.

String.concat может быть полезен, если ожидается много пустых строк на входе. Тогда он просто вернёт непустую строку, не создавая новых объектов вообще. Это будет быстрее, чем "+", даже с учётом интринсиков: спецификация обязывает "+" возвращать новый объект (если это не compile-time constant).
Мне, кстати, не очень понятно, почему операторы "+" и "+=" названы «перегруженными». Можно ли считать, что оператор деления "/" «перегружен», потому что он ведёт себя по-разному для целых чисел и для дробных? По-моему, слово «перегружен» здесь неуместно вообще. Обычно его используют для пользовательской перегрузки в тех языках, где она поддерживается (в Java — нет). А так это просто «оператор конкатенации строк».
Оператор "+" «перегружен» для строк, потому что главное его назначение это сложения для числовых примитивных типов данных.
А на каком основании вы полагаете, что одно применение «главнее», чем другое?
литералы числовых типов используются для создания примитивных типов, строковые литералы же используются для создания объектов класса String.

если ошибаюсь, с радостью узнаю истину :)
а как эта ваша реплика связана с моим вопросом? :)
с этой реплики следует, что операции с примитивными типами «главнее» за операции с объектами, имхо
вот-вот :) и «имхо» здесь ключевое слово.
В языке Java некоторые объекты обрабатываются специальным образом на уровне синтаксиса или имеют специальное значение. К примеру, после слова throw может идти только объект класса Throwable или его наследник. Вот и String имеет специальное значение не только как часть библиотеки, но и как часть самого языка.

Кстати, вот вам такой код:

Integer a = 1;
Integer b = 2;
Integer c = a + b;
Integer d = a - b;

Здесь мы применяем плюс и минус к объектам. Значит ли это по вашему, что и тут эти операторы «перегружены»?
тот же пример хотел скинуть, но ты меня опередил :)
нет, здесь операторы не перегружены, потому что операции совершаются над примитивными типами, а не над объектами, в которые «завернуты» эти типы:

        Integer a = Integer.valueOf(1);
        Integer b = Integer.valueOf(2);
        Integer с = Integer.valueOf(a.intValue() + b.intValue());
        Integer d = Integer.valueOf(a.intValue() - b.intValue());
Что-то вы не последовательны — значит как строки складывать — так + перегружен, а как Integer/Long/Double и т.п — так не перегружен.

Откуда же тогда берется эта магия с unboxing?
И да, я понимаю, что объекты некоторых классов имеет «особый статус» и их обработка оговорена в спецификации.
С моей точки зрения многие люди часто путают Java и C++ — как-то generics vs templates или перегрузку операторов с особыми случаями синтаксиса.

В этом смысле и string + string и number + number в каком-то смысле «перегружены», что для данных классов на уровне языка определен расширенный синтаксис (или если угодно синтаксический сахар). В самом деле, в java же невозможно задать некоторый свой класс my.package.Vector(x,y) и определить для него + как операцию сложения векторов на уровне синтаксиса, тогда как в C++ / groovy / др это можно сделать легко — и это неловкое чувство, что some animals are more equal.
lany, vladimir_dolzhenko, 23derevo, спасибо за помощь, разобрался, почитал спецификацию.

Не понимаю людей, которые яро минусуют посты, где обсуждаются вопросы по теме. Я пытаюсь разобраться, докопаться до истины, высказывая свои мысли. Это ни в коем случаи не камень в огород знаний собеседников.
А StringJoiner в Java-8 адски медленный. В девятке разогнали.
Ну ты же наверняка разобрался почему. Ждем статью с подробностями.
А что там описывать? В восьмёрке под капотом обычный StringBuilder. В девятке элементы складывают в массив и подсчитывают суммарную длину, а итоговую строку создают через приватный конструктор (SharedSecrets, всё такое). В итоге массив char[] создаётся ровно один раз вместо того, чтобы его пересоздавать и копировать, когда он кончается (а в конце ещё раз пересоздавать и копировать, потому что его длина почти наверняка не совпадает с длиной результирующей строки). Я сам хотел подобную оптимизацию предложить, но потом догадался в девятке посмотреть. В общем-то на целую статью этот рассказ вроде не тянет.
В любом случае от тебя давно статей не было. Было бы прекрасно увидеть очередной труд.
Sign up to leave a comment.

Articles