Вступление
Что вы знаете о обработке строк в Java? Как много этих знаний и насколько они углублены и актуальны? Давайте попробуем вместе со мной разобрать все вопросы, связанные с этой важной, фундаментальной и часто используемой частью языка. Наш маленький гайд будет разбит на две публикации:
Реализация строк на Java представлена тремя основными классами: String, StringBuffer, StringBuilder. Давайте поговорим о них.
String
Строка — объект, что представляет последовательность символов. Для создания и манипулирования строками Java платформа предоставляет общедоступный финальный (не может иметь подклассов) класс java.lang.String. Данный класс является неизменяемым (immutable) — созданный объект класса String не может быть изменен. Можно подумать что методы имеют право изменять этот объект, но это неверно. Методы могут только создавать и возвращать новые строки, в которых хранится результат операции. Неизменяемость строк предоставляет ряд возможностей:
- использование строк в многопоточных средах (String является потокобезопасным (thread-safe) )
- использование String Pool (это коллекция ссылок на String объекты, используется для оптимизации памяти)
- использование строк в качестве ключей в HashMap (ключ рекомендуется делать неизменяемым)
Создание
Мы можем создать объект класса String несколькими способами:
1. Используя строковые литералы:
String habr = "habrahabr";
Строковый литерал — последовательность символов заключенных в двойные кавычки. Важно понимать, что всегда когда вы используете строковой литерал компилятор создает объект со значением этого литерала:
System.out.print("habrahabr"); // создали объект и вывели его значение
2. С помощью конструкторов:
String habr = "habrahabr";
char[] habrAsArrayOfChars = {'h', 'a', 'b', 'r', 'a', 'h', 'a', 'b', 'r'};
byte[] habrAsArrayOfBytes = {104, 97, 98, 114, 97, 104, 97, 98, 114};
String first = new String();
String second = new String(habr);
Если копия строки не требуется явно, использование этих конструкторов нежелательно и в них нет необходимости, так как строки являются неизменными. Постоянное строительство новых объектов таким способом может привести к снижению производительности. Их лучше заменить на аналогичные инициализации с помощью строковых литералов.
String third = new String(habrAsArrayOfChars); // "habrahabr"
String fourth = new String(habrAsArrayOfChars, 0, 4); // "habr"
Конструкторы могут формировать объект строки с помощью массива символов. Происходит копирование массива, для этого используются статические методы copyOf и copyOfRange (копирование всего массива и его части (если указаны 2-й и 3-й параметр конструктора) соответственно) класса Arrays, которые в свою очередь используют платформо-зависимую реализацию System.arraycopy.
String fifth = new String(habrAsArrayOfBytes, Charset.forName("UTF-16BE")); // кодировка нам явно не подходит "?????"
Можно также создать объект строки с помощью массива байтов. Дополнительно можно передать параметр класса Charset, что будет отвечать за кодировку. Происходит декодирование массива с помощью указанной кодировки (если не указано — используется Charset.defaultCharset(), который зависит от кодировки операционной системы) и, далее, полученный массив символов копируется в значение объекта.
String sixth = new String(new StringBuffer(habr));
String seventh = new String(new StringBuilder(habr));
Ну и наконец-то конструкторы использующие объекты StringBuffer и StringBuilder, их значения (getValue()) и длину (length()) для создания объекта строки. С этими классами мы познакомимся чуть позже.
Приведены примеры наиболее часто используемых конструкторов класса String, на самом деле их пятнадцать (два из которых помечены как deprecated).
Длина
Важной частью каждой строки есть ее длина. Узнать ее можно обратившись к объекту String с помощью метода доступа (accessor method) length(), который возвращает количество символов в строке, например:
public static void main(String[] args) {
String habr = "habrahabr";
// получить длину строки
int length = habr.length();
// теперь можно узнать есть ли символ символ 'h' в "habrahabr"
char searchChar = 'h';
boolean isFound = false;
for (int i = 0; i < length; ++i) {
if (habr.charAt(i) == searchChar) {
isFound = true;
break; // первое вхождение
}
}
System.out.println(message(isFound)); // Your char had been found!
// ой, забыл, есть же метод indexOf
System.out.println(message(habr.indexOf(searchChar) != -1)); // Your char had been found!
}
private static String message(boolean b) {
return "Your char had" + (b ? " " : "n't ") + "been found!";
}
Конкатенация
Конкатенация — операция объединения строк, что возвращает новую строку, что есть результатом объединения второй строки с окончанием первой. Операция для объекта String может быть выполнена двумя способами:
1. Метод concat
String javaHub = "habrhabr".concat(".ru").concat("/hub").concat("/java");
System.out.println(javaHub); // получим "habrhabr.ru/hub/java"
// перепишем наш метод используя concat
private static String message(boolean b) {
return "Your char had".concat(b ? " " : "n't ").concat("been found!");
}
Важно понимать, что метод concat не изменяет строку, а лишь создает новую как результат слияния текущей и переданной в качестве параметра. Да, метод возвращает новый объект String, поэтому возможны такие длинные «цепочки».
2. Перегруженные операторы "+" и "+="
String habr = "habra" + "habr"; // "habrahabr"
habr += ".ru"; // "habrahabr.ru"
Это одни с немногих перегруженных операторов в Java — язык не позволяет перегружать операции для объектов пользовательских классов. Оператор "+" не использует метод concat, тут используется следующий механизм:
String habra = "habra";
String habr = "habr";
// все просто и красиво
String habrahabr = habra + habr;
// а на самом деле
String habrahabr = new StringBuilder()).append(habra).append(habr).toString(); // может быть использован StringBuffer
Используйте метод concat, если слияние нужно провести только один раз, для остальных случаев рекомендовано использовать или оператор "+" или StringBuffer / StringBuilder. Также стоит отметить, что получить NPE (NullPointerException), если один с операндов равен null, невозможно с помощью оператора "+" или "+=", чего не скажешь о методе concat, например:
String string = null;
string += " habrahabr"; // null преобразуется в "null", в результате "null habrahabr"
string = null;
string.concat("s"); // логично что NullPointerException
Форматирование
Класс String предоставляет возможность создания форматированных строк. За это отвечает статический метод format, например:
String formatString = "We are printing double variable (%f), string ('%s') and integer variable (%d).";
System.out.println(String.format(formatString, 2.3, "habr", 10));
// We are printing double variable (2.300000), string ('habr') and integer variable (10).
Методы
Благодаря множеству методов предоставляется возможность манипулирования строкой и ее символами. Описывать их здесь нет смысла, потому что Oracle имеет хорошие статьи о манипулировании и сравнении строк. Также у вас под рукой всегда есть их документация. Хотелось отметить новый статический метод join, который появился в Java 8. Теперь мы можем удобно объединять несколько строк в одну используя разделитель (был добавлен класс java.lang.StringJoiner, что за него отвечает), например:
String hello = "Hello";
String habr = "habrahabr";
String delimiter = ", ";
System.out.println(String.join(delimiter, hello, habr));
// или так
System.out.println(String.join(delimiter, new ArrayList<CharSequence>(Arrays.asList(hello, habr))));
// в обоих случаях "Hello, habrahabr"
Это не единственное изменение класса в Java 8. Oracle сообщает о улучшении производительности в конструкторе String(byte[], *) и методе getBytes().
Преобразование
1. Число в строку
int integerVariable = 10;
String first = integerVariable + ""; // конкатенация с пустой строкой
String second = String.valueOf(integerVariable); // вызов статического метода valueOf класса String
String third = Integer.toString(integerVariable); // вызов метода toString класса-обертки
2. Строку в число
String string = "10";
int first = Integer.parseInt(string);
/*
получаем примитивный тип (primitive type)
используя метод parseXхх нужного класса-обертки,
где Xxx - имя примитива с заглавной буквы (например parseInt)
*/
int second = Integer.valueOf(string); // получаем объект wrapper класса и автоматически распаковываем
StringBuffer
Строки являются неизменными, поэтому частая их модификация приводит к созданию новых объектов, что в свою очередь расходует драгоценную память. Для решения этой проблемы был создан класс java.lang.StringBuffer, который позволяет более эффективно работать над модификацией строки. Класс является mutable, то есть изменяемым — используйте его, если Вы хотите изменять содержимое строки. StringBuffer может быть использован в многопоточных средах, так как все необходимые методы являются синхронизированными.
Создание
Существует четыре способа создания объекта класса StringBuffer. Каждый объект имеет свою вместимость (capacity), что отвечает за длину внутреннего буфера. Если длина строки, что хранится в внутреннем буфере, не превышает размер этого буфера (capacity), то нет необходимости выделять новый массив буфера. Если же буфер переполняется — он автоматически становиться больше.
StringBuffer firstBuffer = new StringBuffer(); // capacity = 16
StringBuffer secondBuffer = new StringBuffer("habrahabr"); // capacity = str.length() + 16
StringBuffer thirdBuffer = new StringBuffer(secondBuffer); // параметр - любой класс, что реализует CharSequence
StringBuffer fourthBuffer = new StringBuffer(50); // передаем capacity
Модификация
В большинстве случаев мы используем StringBuffer для многократного выполнения операций добавления (append), вставки (insert) и удаления (delete) подстрок. Тут все очень просто, например:
String domain = ".ru";
// создадим буфер с помощью String объекта
StringBuffer buffer = new StringBuffer("habrahabr"); // "habrahabr"
// вставим домен в конец
buffer.append(domain); // "habrahabr.ru"
// удалим домен
buffer.delete(buffer.length() - domain.length(), buffer.length()); // "habrahabr"
// вставим домен в конец на этот раз используя insert
buffer.insert(buffer.length(), domain); // "habrahabr.ru"
Все остальные методы для работы с StringBuffer можно посмотреть в документации.
StringBuilder
StringBuilder — класс, что представляет изменяемую последовательность символов. Класс был введен в Java 5 и имеет полностью идентичный API с StringBuffer. Единственное отличие — StringBuilder не синхронизирован. Это означает, что его использование в многопоточных средах есть нежелательным. Следовательно, если вы работаете с многопоточностью, Вам идеально подходит StringBuffer, иначе используйте StringBuilder, который работает намного быстрее в большинстве реализаций. Напишем небольшой тест для сравнения скорости работы этих двух классов:
public class Test {
public static void main(String[] args) {
try {
test(new StringBuffer("")); // StringBuffer: 35117ms.
test(new StringBuilder("")); // StringBuilder: 3358ms.
} catch (java.io.IOException e) {
System.err.println(e.getMessage());
}
}
private static void test(Appendable obj) throws java.io.IOException {
// узнаем текущее время до теста
long before = System.currentTimeMillis();
for (int i = 0; i++ < 1e9; ) {
obj.append("");
}
// узнаем текущее время после теста
long after = System.currentTimeMillis();
// выводим результат
System.out.println(obj.getClass().getSimpleName() + ": " + (after - before) + "ms.");
}
}
Спасибо за внимание. Надеюсь статья поможет узнать что-то новое и натолкнет на удаление всех пробелов в этих вопросах. Все дополнения, уточнения и критика приветствуются.
Комментарии (68)
hmpd
20.06.2015 23:25+7String second = new String(habr); // аналогично second = habr;
Разве аналогично?
1. String second = new String(habr) — создается новый объект second с тем же содержимым, что и в объекте habr. При этом second и habr ссылаются на разные объекты.
2. String second = habr — копируется ссылка на объект habr, то есть second и habr ссылаются на один объект.
Верно?tobilko Автор
20.06.2015 23:48Опечатался, спасибо. Все верно отметили.
Хотелось бы дополнить и сказать, что все ссылки на string объекты хранится в String Pool и перед созданием строки с помощью литерала проверяется нет ли эквивалентной строки в пуле, если нет — добавляется, иначе просто получаем ссылку на уже готовый объект. В случаи с new, новый объект создаться в любом случаи, независимо от пула.hmpd
20.06.2015 23:53Вам спасибо за статью! Давайте продолжение.
(Чисто языковой комментарий: слово «что», конечно, заменяет слово «который», но не без потерь: оно не передает род заменяемого слова, поэтому текст воспринимается хуже. Пример:
«если длина строки, что хранится...» — к чему здесь относится «что»: к длине или к строке? Конкретно в этом случае подойдет причастие:
«длина строки, хранящаяся...» или
«длина строки, хранящейся...»)
apangin
20.06.2015 23:29+6А теперь запустите тот же тест с
-XX:BiasedLockingStartupDelay=0
:
StringBuffer: 5590ms. StringBuilder: 5493ms.
И чего в итоге померили?
Также изменения коснулись hashCode (новый алгоритм hashing)
Оппа! Как такое может быть, если алгоритм хеширования дляString
строго прописан в спецификации?apangin
20.06.2015 23:55+3А при добавлении третьей строчки у меня вообще весело получается:
test(new StringBuffer("")); // StringBuffer: 5020ms. test(new StringBuilder("")); // StringBuilder: 14041ms. test(new StringBuilder("")); // StringBuilder: 10716ms.
vladimir_dolzhenko
21.06.2015 19:42Учитывая цикл 1e9 итераций и что StringBuilder/StringBuffer расширяются — может произойти GC, наверняка может случится и JIT — словом, что именно происходило в фоне — неизвестно, так, что можно гадать на кофейной гуще, что же именно мерялось.
tobilko Автор
20.06.2015 23:59Алгоритм действительно не изменился (где-то прочитал, но не проверял тогда), спасибо.
uthark
21.06.2015 06:16+1Вдобавок к вышеперечисленному, хочу добавить, что одним из важных аспектов, почему String объявлен как final — это безопасность. Например, когда загружаем класс, то имя класса передается в виде строки. Если бы строки были не финальные, то вредоносный код мог бы изменить имя класса и, таким образом, загрузить неправильный класс. Например, вместо java.io.FileReader был бы загружен com.malicious.FileRemover. Также можно было бы сломать контракт equals/hashCode.
Для обхода всех этих проблем, возможно, был бы введен класс java.lang.SafeString, который бы помог предотвратить эти проблемы, но попутно создал бы новых (например, конвертацию из SafeString в String и обратно, замусоривание API). Но так как это один из базовых классов, то было принято решение сделать класс String финальным.
gurinderu
21.06.2015 11:04+3В принципе не плохая статья, но ни слова о String pool, злом методе intern и злом методе substring(до версии 1.7.0_06)
vladimir_dolzhenko
21.06.2015 19:13+3Просто оставлю это здесь
gurinderu
21.06.2015 23:24-1А я это видел и итак знаю)
vladimir_dolzhenko
21.06.2015 23:27Это ни в коем случае не камень в ваш огород, просто логическое продолжение замечания, учитывая то, что этого доклада в комментариях еще не было
ASuprun
21.06.2015 11:15+1Маленькое уточнение по поводу оператора "+". Как с казано в документации к 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 и там был детально описан процесс конкатенации (ссылка).23derevo
21.06.2015 15:05Как сказано в документации к Oracle JDK 6, 7 и 8
это не документация к Oracle JDK. Это javadoc, то есть, документация к стандарту Java SE (6, 7, 8).
tobilko Автор
21.06.2015 15:11String habr = new StringBuilder(String.valueOf("habra")).append("habr").toString();
это уточнения было в статьиvladimir_dolzhenko
21.06.2015 19:09+3Объясните — откуда взялось это убожество 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 не может (по крайней мере пока) вычислить значение на стадии компиляции и уже будет честно загружать класс и его константу.tobilko Автор
21.06.2015 23:01String habrahabr = new StringBuilder()).append(habra).append(habr).toString();
вы правы, исправил
23derevo
21.06.2015 23:01поддерживаю. Проверить очень легко: скомпирировать конкатенацию, а потом декомпилировать.
Даже в байткод руками лазать не надо :)vladimir_dolzhenko
21.06.2015 23:05+2скомпирировать… декомпилировать.
Настоящим пацанам нужна только консолька, vi, javac да javap для этого
vladimir_dolzhenko
21.06.2015 11:18+6tobilko Автор
21.06.2015 13:47-2предложите свой вариант
23derevo
21.06.2015 15:08+6Прежде, чем писать любые перфомансные тесты и юзать всякие System.currentTimeMillis(), мы с vladimir_dolzhenko настоятельно рекомендуем Вам ознакомиться вот с этой лекцией:
tobilko Автор
21.06.2015 15:21+1спасибо, посмотрю, перепишу тест :)
vladimir_dolzhenko
21.06.2015 16:20+3Всецело согласен с Лешей 23derevo, но стоит добавить, что одной лекцией не стоит ограничится, чтобы понять как же измерять и пользоваться jmh, но вектор правильный.
Стоит обратить внимание, что в java есть такая вещь как Escape Analysys, которая делает много волшебных вещей, н-р, стирание блокировок, если экземпляр никуда не утекает.
Т.е в однопоточном случае разница между StringBuilder и StringBuffer будет в пределах погрешности измерений (после того, как JIT свернет). Это следует учитывать, когда будете писать jmh-тест.
apangin
21.06.2015 22:46+2Используйте метод concat, если слияние нужно провести только один раз, для остальных случаев рекомендовано использовать или оператор "+" или StringBuffer / StringBuilder.
Никогда не используйтеconcat
. Обычный"+"
илиStringBuilder
всегда будет эффективней, даже всего для двух строк. Просто потому, что JVM оптимизирует"+"
специальным образом (это JVM intrinsic), аconcat
— нет (это обычный Java метод).vladimir_dolzhenko
21.06.2015 22:49Можно подробнее про JVM intrinsic используемый в +?
Как бы байткод всегда показывает цепочку new StringBuilder().append()...toString() и не знал, что intrinsic может действовать над цепочкойapangin
22.06.2015 00:57+5Именно так. HotSpot JVM распознаёт паттерн
new StringBuilder().append()...toString()
(или то же самое соStringBuffer
), и компилирует это как одно целое. Регулируется опцией-XX:+OptimizeStringConcat
, по умолчанию включено.23derevo
14.07.2015 12:57+1vladimir_dolzhenko, apangin, lany —
зацените шипилёвский JEP в тему: openjdk.java.net/jeps/8085796
23derevo
21.06.2015 23:16в теории ведь никто не мешает concat реализовать как JVM intrinsic?
vladimir_dolzhenko
21.06.2015 23:42Представляешь как было бы… фантастично-феерично иметь pluggable intrinsic?
23derevo
21.06.2015 23:57а чего там представлять? Есть такая штука — OpenJDK. Она некоторыми и используется именно так, как ты хочешь: Taobao JVM (слайды 34-37)
vladimir_dolzhenko
22.06.2015 00:06Все клева конечно, только доклад 2011.
Почему-то на ум приходит академическая разработка RedHat под названием Shenandoah — вызов Zing VM и С4 — но только больше о них ничего не слышно.23derevo
22.06.2015 00:13погоди, тут другой случай :)
Как я понял — это просто кастомизированная OpenJDK (classlib, jvm), работающая внутри Taobao. Они делают определенные изменения и оптимизации, заточенные под их конкретные юзкейсы (ворклоады, железо, сценарии и т.п.). И как я понимаю, именно поэтому они не всегда коммитят назад в OpenJDK — их изменения не всем подойдут.vladimir_dolzhenko
22.06.2015 00:50+1Я вообще о том, что ежели продукт пошел — то должны быть упоминания и развитие, кроме данного доклада.
Но все равно было бы варенье-с-маслом видеть pluggable intrinsic в java-по-умолчанию23derevo
22.06.2015 02:23+3Ну вот смотри, у нас в Одноклассниках тоже есть свои сборки OpenJDK патченные — и classlib и JVM. Мы об этом упоминаем периодически, но чтобы прямо кричать об этом на каждом углу…
Многие, кстати, патчат. Например, тот же гугл. Посмотри доклады Jeremy Manson с JVMLS 2013 и JVMLS 2014. И тоже об этом мало в интернете есть. Потому что это больше не инфоповод и не рокит саенс.
leventov
Это перевод? Отметьте, откуда
tobilko Автор
это мой оригинальный текст
pleha
Мне всегда было интересно хоть кто-то нашел применение StringBuffer. Не очень понятно в каком случае он может пригодиться. Попеременно дописывать какие-то строки и потом их куда-то выводить. Получается в такой строке порядок должен быть не очень важен. Могу подумать только о логировании и чем-то подобном, но для этого есть масса других средств.
vladimir_dolzhenko
именно StringBuffer? или в целом подход StringBuilder/StringBuffer?
leventov
Конечно, именно StringBuffer. StringBuilder-то один из самых часто используемых классов в стандартной библиотеке.
vladimir_dolzhenko
У меня только одно объяснение: по историческим причинам и в силу того, что java дает обратную совместимость