JavaRush /Java блог /Random UA /Рядки в Java (class java.lang.String)
Viacheslav
3 рівень

Рядки в Java (class java.lang.String)

Стаття з групи Random UA

Вступ

Шлях програміста – складний та тривалий процес. І здебільшого починається він із програми, яка виводить 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, а це: і Згадуємо Tutorial від Oracle chars()« codePoints() 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 ми побачабо. Тепер незрозуміло, що за кодові пункти такі. Поняття 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). Вказано його у шістнадцятковому форматі. Якщо перевести в десяткове число, то отримаємо 128 123. Це більше, ніж дозволяє 16 біт (тобто більше ніж 65 535). Скопіюємо його:
Рядки в 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 цікаво прочитати наступний пост: http://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 дуже корисний інструмент. Рядки є незабутні, тобто. незмінними. А складати хочеться. Тому нам на допомогу дано 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
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ