Питання 23 · Розділ 12

Чому String реалізує Comparable та CharSequence?

Клас String реалізує два важливих інтерфейси, і кожен дає йому свої можливості:

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Клас String реалізує два важливих інтерфейси, і кожен дає йому свої можливості:

Comparable<String> — дозволяє порівнювати рядки і сортувати їх:

List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
Collections.sort(names); // Працює завдяки Comparable!
// ["Alice", "Bob", "Charlie"]

CharSequence — означає, що String є «послідовністю символів», як і StringBuilder:

// Метод приймає будь-який CharSequence
void process(CharSequence text) { ... }

process("Hello");                  // String — працює
process(new StringBuilder("Hi"));  // StringBuilder — теж працює!

Проста аналогія:

  • Comparable — це «вміння порівнювати» (як терези, які показують, що важче)
  • CharSequence — це «я текст» (як документ, який можна прочитати по літерах)

🟡 Middle Level

Інтерфейс Comparable<String>

Метод compareTo(String anotherString) порівнює рядки лексикографічно (за Unicode-значенням символів):

"abc".compareTo("def");   // від'ємне (abc < def)
"xyz".compareTo("abc");   // додатне (xyz > abc)
"abc".compareTo("abc");   // 0 (рівні)

Де використовується:

  • Collections.sort() і Arrays.sort() — сортування списків рядків
  • TreeMap<String, V> і TreeSet<String> — зберігання у відсортованому вигляді
  • String як ключ в HashMap (Java 8+) — при колізіях бакет перетворюється на дерево, і Comparable допомагає балансуванню

Інтерфейс CharSequence

CharSequence — це інтерфейс з чотирма методами:

  • length() — довжина послідовності
  • charAt(int index) — символ за індексом
  • subSequence(int start, int end) — підрядок
  • toString() — рядкове представлення

Реалізації: String, StringBuilder, StringBuffer, CharBuffer, Segment (Java 20+)

Де використовується:

  • Pattern.matcher(CharSequence) — regex працює з будь-яким CharSequence
  • String.join(CharSequence delimiter, CharSequence... elements)
  • Більшість текстових API приймають CharSequence, а не String

Таблиця типових помилок

Помилка Наслідки Рішення
compareTo(null) NullPointerException Завжди перевіряйте на null перед порівнянням
Думати, що compareTo = equals Неправильна логіка compareTo повертає int (порядок), equals — boolean (рівність)
Використовувати compareTo для locale-sensitive порівняння Неправильне сортування (ё vs є, і vs и) Використовуйте Collator для української/російської
StringBuilder в TreeSet Не компілюється — StringBuilder не реалізує Comparable Конвертуйте в String або використовуйте Comparator

Порівняння: Comparable vs CharSequence

Аспект Comparable<String> CharSequence
Призначення Порівняння і сортування Абстракція «текстової послідовності»
Методи compareTo(String) length(), charAt(), subSequence(), toString()
Де потрібен TreeMap, TreeSet, Collections.sort() Regex, text processing, string builders
Тип повернення int (negative/zero/positive) Різні (int, char, CharSequence, String)
Mutability requirement Immutable (зміна порушить порядок) Будь-яка реалізація

Коли НЕ використовувати

  • compareTo для locale-sensitive сортування — використовуйте Collator.getInstance(locale)
  • CharSequence для зберігання — це інтерфейс, не клас зберігання; для mutable тексту — StringBuilder
  • Mutable CharSequence в TreeMap/TreeSet — зміна ключа порушить порядок дерева

🔴 Senior Level

Internal Implementation

Comparable<String>compareTo:

public int compareTo(String anotherString) {
    byte v1[] = this.value;
    byte v2[] = anotherString.value;
    byte coder = coder();
    if (coder == anotherString.coder()) {
        return isLatin1()
            ? StringLatin1.compareTo(v1, v2)
            : StringUTF16.compareTo(v1, v2);
    }
    return compareToUTF16(v1, v2);
}

Порівняння посимвольне за Unicode code point. Якщо один рядок — префікс іншого, коротший вважається «меншим». JIT компілятор інлайнить StringLatin1.compareTo в SIMD-оптимізований код.

CharSequence contract:

public interface CharSequence {
    int length();
    char charAt(int index);
    CharSequence subSequence(int start, int end);
    public String toString();
}

Контракт: charAt(i) повинен повертати той самий символ, що й toString().charAt(i). subSequence(start, end) повинен бути еквівалентний toString().substring(start, end).

Архітектурні Trade-offs

Comparable — навіщо String реалізує Comparable:

  1. Natural ordering: Рядки мають природний лексикографічний порядок — фундаментальна властивість тексту
  2. Tree-based collections: TreeMap/TreeSet вимагають Comparable або Comparator
  3. HashMap tree bins (Java 8+): При колізіях hashCode бакет HashMap перетворюється на червоно-чорне дерево. Comparable використовується для балансування:
    // Всередині HashMap.TreeNode
    Comparable<?> k = (key instanceof Comparable) ? (Comparable<?>)key : null;
    // Якщо ключі Comparable — дерево балансується за compareTo
    // Інакше — tie-breaking через System.identityHashCode
    

    Якщо hashCode() двох різних рядків збігається (колізія), HashMap будує збалансоване дерево замість списку, і compareTo() використовується для впорядкування вузлів дерева.

CharSequence — навіщо String реалізує CharSequence:

  1. Абстракція: Єдиний інтерфейс для всіх «текстових» типів — поліморфізм без конвертації
  2. Економія: Не потрібно конвертувати StringBuilderString для передачі в API
  3. Regex integration: Pattern.matcher(CharSequence) — працює напряму з StringBuilder, без алокації нового рядка

Edge Cases (мінімум 3)

1. Locale-sensitive comparison:

"abc".compareTo("ABC"); // Від'ємне (Unicode: 'a'(97) > 'A'(65))
// Це НЕ те, що очікує користувач для сортування імен!

// Для locale-aware порівняння використовуйте Collator:
Collator collator = Collator.getInstance(Locale.UKRAINE);
collator.compare("аб", "аа"); // Українське сортування: аб > аа
collator.compare("є", "е");   // Український порядок: є після е

compareTo порівнює за Unicode code point, а не за правилами мови.

2. StringBuilder НЕ реалізує Comparable:

StringBuilder sb1 = new StringBuilder("abc");
StringBuilder sb2 = new StringBuilder("abc");
// sb1.compareTo(sb2) — НЕ ІСНУЄ!
// Причина: mutable об'єкти не повинні бути в TreeSet/TreeMap
// — зміна ключа порушить порядок дерева, і get() не знайде елемент

3. Surrogate pairs (UTF-16) і compareTo:

// Emoji = 2 char (surrogate pair), 1 code point
String emoji = "\uD83D\uDE00"; // "😀"
emoji.length();        // 2 (char count / code units)
emoji.codePointCount(0, emoji.length()); // 1 (code point count)

// compareTo працює за code unit (char), не за code point
// Це може дати неочікуваний порядок для рядків з емодзі
String s1 = "a\uD83D\uDE00"; // "a😀"
String s2 = "ab";
s1.compareTo(s2); // Залежить від surrogate value, не від «візуального» символу

4. CharSequence і memory aliasing:

// StringBuilder.subSequence() повертає новий String (копію!)
StringBuilder sb = new StringBuilder("Hello World");
CharSequence sub = sb.subSequence(0, 5); // "Hello" — новий String
// Це НЕ zero-copy! Алокується новий об'єкт
// Якщо потрібен zero-copy — використовуйте ByteBuffer.asCharBuffer()

5. HashMap tree bin і Comparable:

// Java 8+: при колізіях HashMap використовує compareTo для tree bin
String k1 = "FB"; // hashCode = 2236
String k2 = "Ea"; // hashCode = 2236 (колізія!)
// HashMap.TreeNode використовує compareTo для балансування дерева
// Без Comparable HashMap використовує tieBreakOrder() (identityHashCode + class name).
// Дерево НЕ деградує — все ще O(log n), але ordering стає arbitrary і non-reproducible.

Продуктивність

Операція Час Примітка
compareTo (Latin-1, 10 chars) ~3ns SIMD оптимізація
compareTo (UTF-16, 10 chars) ~5ns  
compareTo (diff at first char) ~1ns Early exit
compareTo (one is prefix) ~2ns Довжина вирішує
compareTo vs Comparator.comparing() ~3ns vs ~8ns Instance method краще інлайниться
Collator.compare() ~50–200ns Locale-aware, значно дорожче

Thread Safety

  • String.compareTo()thread-safe (String незмінний)
  • CharSequenceНЕ guaranteed thread-safe; залежить від реалізації
    • String — thread-safe
    • StringBuilder — NOT thread-safe
    • StringBuffer — thread-safe (synchronized)
  • При використанні CharSequence як параметра методу — сторона, що викликає, відповідає за синхронізацію

Production War Story

Сценарій 1: Сортування 1M рядків у TreeMap (user directory, search index):

  • Comparable.compareTo(): ~3ns × 1M × log₂(1M) ≈ 60ms total
  • Custom Comparator: ~8ns (virtual call overhead) ≈ 160ms
  • Різниця: compareTo як instance method краще інлайниться JIT-компілятором
  • Для locale-sensitive сортування (українські імена): compareTo сортує неправильно («Євген» після «Євген» замість перед). Fix: TreeMap з custom Comparator на базі Collator.getInstance(Locale.UKRAINE).

Сценарій 2: Regex matching на StringBuilder (log parser):

// ПОГАНО — зайва конвертація і алокація
StringBuilder sb = ...; // 10KB
Matcher m = Pattern.compile("\\d+").matcher(sb.toString()); // +10KB алокація

// ГАРАЗД — CharSequence напряму, zero-copy
Matcher m = Pattern.compile("\\d+").matcher(sb);

Для 100K log lines/sec: економія 1GB/sec алокацій.

Сценарій 3: HashMap колізії у продакшені (Java 8+):

  • Зловмисник спеціально підбирає рядки з однаковим hashCode → HashMap деградує до O(n) → DoS
  • Java 8+: бакет перетворюється на дерево, compareTo забезпечує O(log n)
  • Без Comparable ordering = non-deterministic. Уразливість до DoS через hash collisions існує незалежно від Comparable — tree bins mitigated з O(n) до O(log n).

Monitoring

# JFR — HashMap tree bin events
java -XX:StartFlightRecording=filename=recording.jfr ...
# В JFR: HashMap — Tree bin conversion events

# JMH — бенчмарк compareTo vs Comparator
@Benchmark
public int compareTo() { return s1.compareTo(s2); }

@Benchmark
public int comparator() { return comparator.compare(s1, s2); }

# GC профілювання — алокації від toString()
java -XX:+PrintGC -Xlog:gc*:file=gc.log ...

Best Practices для Highload

  • Використовуйте CharSequence як тип параметрів — максимальна гнучкість, zero-copy при роботі з StringBuilder/ByteBuffer
  • compareTo швидший за custom Comparator (краще inlining, менше virtual call overhead)
  • Для locale-sensitive сортування: Collator, не compareTocompareTo не враховує правила мови
  • Для case-insensitive: String.CASE_INSENSITIVE_ORDER (pre-compiled Comparator, singleton)
  • Уникайте mutable об’єктів в TreeMap/TreeSet — зміна ключа порушить порядок
  • Для regex: передавайте CharSequence напряму в Pattern.matcher(), уникайте .toString()
  • Для HashMap security: Comparable на String захищає від hash-collision DoS (tree bin balancing)
  • Для ultra-low-latency: compareTo кращий за Comparator на 2–3ns на виклик; при 10M викликів = 30ms економії

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • Comparable<String> — метод compareTo(), лексикографічне порівняння за Unicode code point
  • CharSequence — інтерфейс з 4 методами: length(), charAt(), subSequence(), toString()
  • compareTo використовується в TreeMap, TreeSet, Collections.sort(), і tree bins HashMap (Java 8+)
  • CharSequence дозволяє передавати String, StringBuilder, StringBuffer в один метод — поліморфізм
  • compareTo НЕ враховує locale — для українського/російського сортування використовуйте Collator
  • StringBuilder НЕ реалізує Comparable — mutable об’єкти не повинні бути в TreeSet

Часті уточнюючі запитання:

  • Навіщо String реалізує Comparable? — Для природного лексикографічного порядку і використання в TreeMap/TreeSet. В Java 8+ — для балансування tree bins в HashMap при колізіях hashCode.
  • Чому compareTo != equals?compareTo повертає int (порядок), equals — boolean (рівність). Контракт: compareTo() == 0 повинно відповідати equals() == true.
  • Чому StringBuilder не реалізує Comparable? — Mutable об’єкти не повинні бути ключами в TreeMap/TreeSet — зміна ключа порушить порядок дерева.
  • Як правильно сортувати українські імена?Collator.getInstance(Locale.UKRAINE).compare(), не compareTocompareTo сортує за Unicode code point.

Червоні прапорці (НЕ говорити):

  • ❌ “compareTo враховує мову” — сортує за Unicode code point, не за правилами мови
  • ❌ “StringBuilder можна використовувати в TreeSet” — не реалізує Comparable
  • ❌ “compareTo і equals — одне і те саме” — різні типи повернень і семантика
  • ❌ “CharSequence — це клас зберігання” — це інтерфейс, для зберігання використовуйте String або StringBuilder

Пов’язані теми:

  • [[4. Чому String є незмінним]]
  • [[5. Коли використовувати StringBuilder vs StringBuffer]]
  • [[10. В чому різниця між == та equals() для String]]