Обробка рядків в Java. Частина I: String, StringBuffer, StringBuilder

Вступ
Що ви знаєте про обробці рядків в Java? Як багато цих знань і наскільки вони поглиблено та актуальні? Давайте спробуємо разом зі мною розібрати всі питання, пов'язані з цією важливою, фундаментальної і часто використовуваною частиною мови. Наш маленький гайд буде розбитий на п'ять публікацій, а саме:

  1. String, StringBuffer, StringBuilder (реалізація рядків)
  2. Pattern, Matcher (регулярні вирази)
  3. i18n (інтернаціоналізація)
  4. Кодування символів (Unicode, UTF-8)
  5. Locale, ResourceBundle (локалізація)
Реалізація рядків на 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();
Second String = new String(habr);

Якщо копія рядка не потрібно явно, використання цих конструкторів небажано і в них немає необхідності, так як рядки є незмінними. Постійне будівництво нових об'єктів таким способом може призвести до зниження продуктивності. Їх краще замінити на аналогічні ініціалізації з допомогою строкових літералів.

third String = 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(), де за замовчуванням вказана UTF-8) і, далі, отриманий масив символів копіюється значення об'єкта.

sixth String = new String(new StringBuffer(habr));
String set = 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 ? " " : ";t ") + "been found!";
}

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

Конкатенація — операція об'єднання рядків, що повертає новий рядок, що є результатом об'єднання другий рядки з закінченням першої. Операція для об'єкта String може бути виконана двома способами:

1. Метод concat
String javaHub = "habrhabr".concat(".ua").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 ? " " : ";t ").concat("been found!");
}

Важливо розуміти, що метод concat не змінює рядок, а лише створює нову як результат злиття поточної та переданої в якості параметра. Так, метод повертає новий об'єкт String, тому можливі такі довгі ланцюжки.

2. Перевантажені оператори "+" і "+="
String habr = "habra" + "habr"; // "habrahabr"
habr += ".ua"; // "habrahabr.ua"

Це одні з небагатьох перевантажених операторів в Java — мова не дозволяє перевантажувати операції для об'єктів користувальницьких класів. Оператор "+" не використовує метод concat, тут використовується наступний механізм:

// все просто і красиво
String habr = "habra" + "habr";
// а насправді
String habr = new StringBuilder(String.valueOf("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 = "Привіт";
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 + ""; // конкатенація з порожнім рядком
Second String = String.valueOf(integerVariable); // виклик статичного методу valueOf класу String
Third String = 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 = ".ua";
// створимо буфер з допомогою String об'єкта
StringBuffer buffer = new StringBuffer("habrahabr"); // "habrahabr"
// вставимо домен в кінець
buffer.append(domain); // "habrahabr.ua"
// видалимо домен
buffer.delete(buffer.length() - domain.length(), buffer.length()); // "habrahabr"
// вставимо домен в кінець на цей раз використовуючи insert
buffer.insert(buffer.length(), domain); // "habrahabr.ua"

Всі інші методи для роботи з 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.");
}
}

Дякую за увагу. Сподіваюся, ця стаття допоможе вам дізнатися щось нове і наштовхне на видалення всіх прогалин у цих питаннях. Всі доповнення, уточнення та критика вітаються.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.