Вступление

Путь программиста – сложный и долгий процесс. И в большинстве случаев начинается он с программы, которая выводит Hello World на экран. Java не исключение (см. Lesson: The "Hello World!" Application). Как мы видим, вывод сообщения осуществляется при помощи System.out.println("Hello World!"); Если посмотреть в Java API, то метод System.out.println принимает входным параметром String. Про этот тип данных и пойдёт речь.

String как последовательность символов

Собственно, String в переводе с английского – строка. Так и есть, тип String представляет текстовую строку. А чем же является текстовая строка? Текстовая строка - это какая-то упорядоченная последовательность символов, которые идут друг за другом. Символ – char. Последовательность – sequence. Так что да, абсолютно правильно, String является реализацией java.lang.CharSequence. А если заглянуть внутрь самого класса String, то внутри него ничто иное как массив char’ов: private final char value[]; У java.lang.CharSequence довольно простой контракт:
Строки в Java (class java.lang.String) - 1
У нас есть метод получения количества элементов, получения конкретного элемента и получения набора элементов + непосредственно сам метод toString, который вернёт this) Интереснее разобраться в методах, которые пришли к нам в Java 8, а это: chars() и codePoints() Вспоминаем по Tutorial от Oracle «Primitive Data Types», что char - это single 16-bit Unicode character.То есть по сути char это просто тип размером в половину типа int (32 бита), который представляет числа от 0 до 65535 (см. decimal значения в ASCII Table). То есть при желании мы можем char представить в виде int. И в Java 8 этим воспользовались. Начиная с 8 версии Java у нас появляется IntStream - стрим для работы с примитивными int'ами. Поэтому в charSequence есть возможность получить IntStream, представляющий или char’ы или codePoint’ы. Прежде чем перейдём к ним, увидим пример, чтобы показать всё удобство этого подхода. Воспользуемся Tutorialspoint online java compiler’ом и выполним код:

public static void main(String []args){
        String line = "aaabccdddc";
        System.out.println( line.chars().distinct().count() );
}
Теперь таким незамысловатым способом можно получить кол-во уникальных символов.

CodePoints

Итак, про chars мы увидели. Теперь непонятно, что за code points такие. Понятие codePoint появилось потому, что когда Java появилась, то хватало 16 бит (половина int) чтобы закодировть символ. Поэтому char в java представлен в UTF-16 формате ("Unicode 88" specification). Позже повяился Unicode 2.0, концепция которого заключалась в представлении символа в виде сурогатной пары (2 чаров). Это позволило расширить диапазон возможных значений до значения int. Подробнее см. на stackoverflow: "Comparing a char to a code-point?". Про UTF-16 так же указано и в JavaDoc к Character . Там же, в JavaDoc, сказано, что: In this representation, supplementary characters are represented as a pair of char values, the first from the high-surrogates range, (\uD800-\uDBFF), the second from the low-surrogates range (\uDC00-\uDFFF). На стандартных алфавитах довольно трудно (а может даже нельзя) воспроизвести это. Но символы буквами и цифрами не заканчиваются. В японии придумали такую сложную для кодировок штуку, как emoji - язык идеограмм и смайликов. Есть про это интересная статья на википедии: «Эмодзи». Найдём пример emoji, например такой: «Emoji Ghost». Как мы видим, там даже указан тот самый codePoint (значение = U+1F47B). Указан он в шестнадцатеричном формате. Если перевести в десятичное число, то получим 128123. Это больше, чем позволяет 16 бит (т.е. больше чем 65535). Скопируем его:
Строки в Java (class java.lang.String) - 2
К сожалению, платформа JavaRush не поддерживает такие символы в тексте. Поэтому, в пример ниже нужно будет в String вставить значение. Поэтому, теперь нам будет понятен простой тест:

public static void main(String []args){
	    String emojiString = "Вставте сюда эмоджи через ctrl+v";
	    //На один emojiString приходится 2 чара (т.к. не влезает в 16 бит)
	    System.out.println(emojiString.codePoints().count()); //1
	    System.out.println(emojiString.chars().count()); //2
}
Как видно, в данном случае 1 codePoint идёт за 2 char’а. Вот такая вот магия.

Character

Как мы увидели выше, String’и в Java состоят из char. Примитивный тип позволяет хранить значение, а вот обёртка java.lang.Character над примитивным типом позволяет сделать много полезного с этим символом. Например, мы можем перевести строку в верхний регистр:

public static void main(String[] args) {
    String line = "организация объединённых наций";
    char[] chars = line.toCharArray();
    for (int i = 0; i < chars.length; i++) {
        if (i == 0 || chars[i - 1] == ' ') {
            chars[i] = Character.toUpperCase(chars[i]);
        }
    }
    System.out.println(new String(chars));
}
Ну и разные интересности: isAlphabetic(), isLetter(), isSpaceChar(), isDigit(), isUpperCase(), isMirrored() (например, скобки. '(' имеет зеркальное отражение ')').

String Pool

Строки в Java неизменяемы, то есть константны. В том числе об этом указано в самом JavaDoc класса java.lang.String. Второе и тоже очень важное – строки могут задаваться литералами:

String literalString = "Hello, World!";
String literalString = "Hello, World!";
То есть любая строка в кавычках, как указано выше, на самом деле объект. И тут напрашивается вопрос – если мы так часто используем строки и они часто могут быть одинаковыми (например, текст «Ошибка» или «Выполнено успешно»), нельзя ли как-нибудь сделать так, чтобы строки не создавались каждый раз? Кстати, у нас ведь ещё есть Map’ы, где ключом может быть строка. Тогда нам точно нельзя, чтобы одинаковые строки были разными объектами, иначе мы потом не сможем из Map достать объект. Разработчики Java подумали подумали и придумали String Pool. Это такое место, где хранятся строки, можно назвать это кэшем строк. Попадают туда «сами» не все строки, а только строки, указанные в коде литералом. В пул можно внести строку и самому, но об этом чуть позже. Итак, в памяти у нас есть где-то этот кэш. Справедливый вопрос: а где этот пул находится? Ответ на него можно найти на stackoverflow: «Where does Java's String constant pool live, the heap or the stack?». Расположен он в Heap памяти, в особой runtime constant pool области. Runtime constant pool выделяется при создании класса или интерфейса виртуальной машиной из method area - особой области в Heap, доступ к которой есть у всех потоков внутри виртуальной машины Java. Что же даёт нам String pool? У этого есть несколько преимуществ:
  • Не будут создаваться однотипные объекты
  • Сравнение по ссылке быстрее, чем посимвольное сравнение через equals
Но что, если нам хочется внести созданный объект в этот кэш? Тогда, у нас есть особый метод: String.intern Данный метод добавляет строку в String Pool. Стоит заметить, что это не просто какой-то кэш в виде массива (как для Integer’ов). Метод intern указан как «native». Это значит, что сам метод реализован на другом языке (в основном c++). В случае с базовыми методами Java к ним могут применяться ещё различные оптимизации на уровне JVM. В общем, тут будет происходить магия. Про intern интересно прочитать следующий пост: https://habr.com/post/79913/#comment_2345814 И вроде идея хороша. Но как это скажется на нас? А ведь действительно, скажется)

public static void main(String[] args) {
    String test = "literal";
    String test2 = new String("literal");
    System.out.println(test == test2);
}
Как видите, строки одинаковые, но результат будет false. А всё потому, что == сравнивает не по значению, а по ссылке. А вот так работает:

public static void main(String[] args) {
    String test = "literal";
    String test2 = new String("literal").intern();
    System.out.println(test == test2);
}
Только заметим, что new String всё равно мы сделаем. То есть intern нам вернёт String из кэша, а вот изначальный String, по которому мы искали в кэше, будет выброшен на очистку, т.к. никто больше про него не знает. На лицо лишнее потребление ресурсов =( Поэтому, сравнивать строки нужно всегда через equals, чтобы уйти по возможности от внезапных и трудно определяемых ошибок.

public static void main(String[] args) {
    String test = "literal";
    String test2 = new String("literal").intern();
    System.out.println(test.equals(test2));
}
Equals выполняет посимвольное сравнивание строк.

Конкатенация

Как мы помним, строки можно складывать. И как мы помним строки у нас неизменяемы. Так как же тогда это работает? Всё верно, создаётся новая строка, которая состоит из символов складываемых объектов. Существует миллион версий о том, как работает конкатенация через плюс. Кто-то считает что будет каждый раз новый объект, кто-то считает что будет ещё что-то. Но прав может быть кто-то один. И этот кто-то – компилятор javac. Воспользуемся сервисом онлайн компилятора и выполним:

public class HelloWorld {

    public static void main(String[] args) {
        String helloMessage = "Hello, ";
        String target = "World";
        System.out.println(helloMessage + target);
    }

}
Теперь сохраним это как zip архив, извлечём в каталог и выполним: javap –c HelloWorld И тут мы всё узнаем:
Строки в Java (class java.lang.String) - 3
В цикле, конечно, лучше делать конкатенацию через StringBuilder самим. И не потому что какая-то магия, а чтобы StringBuilder создавался до цикла, а в самом цикле происходил только append. Кстати, тут есть ещё одна интересность. Есть отличная статья: «Обработка строк в Java. Часть I: String, StringBuffer, StringBuilder». Много полезного в комментариях. Например, указано, что при конкатенации вида new StringBuilder().append()...toString() действует intrinsic оптимизация, регулируемая опцией -XX:+OptimizeStringConcat, которая по умолчанию включена. intrinsic - переводится как "внутренний". Такие вещи JVM обрабатывает особенным образом, обрабатывая их как Native, только без дополнительных затрат на JNI. Подробнее: "Intrinsic Methods in HotSpot VM".

StringBuilder и StringBuffer

Как мы выше видели, StringBuilder очень полезный инструмент. Строки являются immutable, т.е. неизменяемыми. А складывать хочется. Поэтому, нам в помощь даны 2 класса: StringBuilder и StringBuffer. Основное отличие между ними в том, что StringBuffer появился в JDK1.0, в то время как StringBuilder пришёл в java 1.5 как не синхронизированная версия StringBuffer, чтобы снять повышенные затраты на ненужную синхронизацию методов. Оба эти классы являются реализацией абстрактного класса AbstractStringBuilder - A mutable sequence of characters. Внутри хранится массив чаров, который расширяется по правилу: value.length * 2 + 2. По умолчанию размер (capacity) у StringBuilder'а равен 16.

Comparable

Строки являются comparable, т.е. реализуют метод compareTo. Выполняется это при помощи посимвольного сравнения. Интересно, что из двух строк выбирается минимальная длинна и по ней выполняется цикл. Поэтому, compareTo вернёт или разницу между int значениями первых несовпавших символов в пределе наименьшей из длинн строк, либо вернёт разницу между длиннами строк, если в пределах минимальной длинны строки все символы совпадают. Такое сравнение называется «лексикографическим».

Работа со строками Java

String имеет множество полезных методов:
Строки в Java (class java.lang.String) - 4
На работу со строками сущесвует множество задач. Например, на Coding Bat. Так же есть курс на coursera: "Algorithms on Strings".

Заключение

Даже небольшой обзор данного класса занимает внушительное место. А это ещё не всё. Настоятельно рекомендую к просмотру доклад с JPoint 2015 года: Алексей Шипилёв — Катехизис java.lang.String
#Viacheslav