W3docs

Сравнение строк в Java

Сравнивайте строки Java правильно с помощью equals, equalsIgnoreCase, compareTo и узнайте, почему == сравнивает ссылки.

Сравнение двух строк выглядит как самая безобидная операция в языке, но именно здесь новые Java-программисты чаще всего спотыкаются. Причина в том, что == — это очевидный синтаксис для «одно и то же?» — а для строк он отвечает на вопрос, который вам почти никогда не нужен. Эта глава рассматривает API сравнения в том порядке, в котором к нему следует обращаться, и заканчивается правилами упорядочивания — сворачивание регистра, локаль, числовые строки и ловушка полагаться на сортировку JVM по умолчанию.

Почему == неправильно работает со строками

== сравнивает ссылки для объектов. Он спрашивает «указывают ли эти две переменные буквально на один и тот же объект в куче?». Для строк, разделяющих идентичность через пул строк, == случайно возвращает true. Для всего остального — строк, построенных во время выполнения, строк, разобранных из ввода, строк, возвращённых из new String(...) — он возвращает false, даже когда содержимое идентично.

String a = "hello";
String b = "hello";
String c = new String("hello");
String d = "hel" + new String("lo");

a == b;             // true  — both pooled literals
a == c;             // false — c is a fresh object
a == d;             // false — d is built at runtime
a.equals(c);        // true  — contents match
a.equals(d);        // true  — contents match

Правило короткое и абсолютное: для равенства строк используйте equals. Всегда. Независимо от того, откуда пришли строки. == «работает случайно» в ранних тестах с литералами, а затем незаметно перестаёт работать, как только строка проходит через ввод-вывод.

equals и equalsIgnoreCase

equals возвращает true тогда и только тогда, когда две строки содержат одинаковые символы в одинаковом порядке:

"hello".equals("hello");           // true
"hello".equals("Hello");           // false — case-sensitive
"hello".equals(null);              // false — never throws

equalsIgnoreCase выполняет посимвольное сравнение после Unicode-сворачивания регистра:

"hello".equalsIgnoreCase("HELLO");      // true
"hello".equalsIgnoreCase("Hello");      // true
"straße".equalsIgnoreCase("STRASSE");   // false — ß and SS differ in length

Пример с немецкой ß — полезное предупреждение: сворачивание регистра в Unicode — это не биекция (ß переводится в нижний регистр как сама себя, но также соответствует ss в некоторых контрактах). Если пользовательский ввод может содержать не-ASCII текст и вам нужно сопоставление без учёта регистра, предпочтительнее нормализовать обе стороны с помощью String#toLowerCase(Locale) перед сравнением или использовать java.text.Collator для учитывающего локаль сопоставления.

Безопасное сравнение с null

Вызов equals на получателе null выбрасывает NullPointerException. Так что это ошибка:

if (input.equals("admin")) { ... }     // NPE when input is null

Три идиоматичных способа исправить:

"admin".equals(input)                  // literal on the left — handles null safely
Objects.equals(input, "admin")         // null-safe symmetric comparison (java.util.Objects)
Objects.requireNonNull(input).equals("admin")  // fail-fast if null is a bug

"literal".equals(variable) — иногда называемое сравнением Йоды — наиболее распространено в устоявшихся кодовых базах. Objects.equals немного удобнее, когда ни одна из сторон статически не известна.

compareTo и compareToIgnoreCase

Когда нужен порядок (сортировка, проверка диапазона, бинарный поиск), compareTo возвращает int:

  • < 0, если получатель стоит перед аргументом при сортировке
  • 0, если они равны
  • > 0, если получатель стоит после аргумента при сортировке
"apple".compareTo("banana");        // negative
"banana".compareTo("apple");        // positive
"apple".compareTo("apple");         // 0

Сравнение производится по кодовым единицам Unicode (UTF-16), символ за символом, при этом более короткие строки сортируются перед более длинными, когда одна является префиксом другой. Это даёт определённый полный порядок — но не тот порядок, который человек назвал бы «алфавитным»:

"Z".compareTo("a");                 // negative — 'Z' is 0x5A, 'a' is 0x61
"apple".compareTo("Banana");        // positive — uppercase letters come first in ASCII

Для сортировки, видимой человеку, есть два реальных варианта:

  • compareToIgnoreCase — дешёвое решение с уклоном в ASCII, хорошо работающее для английского языка.
  • java.text.Collator — для полноценной сортировки с учётом локали, обрабатывающей ударения, немецкую ß, правильное место для ñ в испанском и соглашение о том, что в некоторых письменах цифры идут после букв.
Collator c = Collator.getInstance(Locale.FRENCH);
List<String> names = new ArrayList<>(List.of("éclair", "Étoile", "anvil"));
names.sort(c);                     // ["anvil", "éclair", "Étoile"]  — proper French sort

Если список будет виден пользователю, используйте Collator. Если это ключ сортировки во внутреннем индексе, compareTo быстрее и стабильнее.

contentEquals и CharSequence

String#contentEquals сравнивает с любым CharSequenceString, StringBuilder, StringBuffer или CharBuffer. Это правильный метод, когда нужно узнать, равно ли текущее содержимое построителя известной строке без промежуточного вызова toString():

StringBuilder sb = new StringBuilder("hello");
"hello".contentEquals(sb);         // true — no toString allocation

equals здесь не поможет, потому что по контракту String#equals возвращает false для любого аргумента, не являющегося String.

equals полностью игнорирует пул

Пул — это внутренняя оптимизация хранения; equals не обращается к нему. Две строки с одинаковыми символами сравниваются как равные независимо от того, находятся ли они по одному адресу:

String pooled = "x";
String fresh  = new String("x");
pooled == fresh;             // false — different objects
pooled.equals(fresh);        // true  — same contents
fresh.hashCode() == pooled.hashCode();  // true — equal strings always hash the same

Последняя строка — причина, по которой String безопасно использовать в качестве ключа HashMap: равные строки всегда имеют одинаковые хэш-коды, а кэш делает поиск дешёвым.

Рабочий пример

Программа проходит через решения по сравнению, которые вы будете принимать в реальном коде: выбор правильного метода для равенства, обработка null, сортировка с правильным компаратором. Вывод делает различия между compareTo и Collator видимыми бок о бок.

java— editable, runs on the server

Три сортировки делают различия наглядными. compareTo даёт [Banana, anvil, apple, Étoile, éclair]: заглавная Banana оказывается первой, потому что B (0x42) стоит перед любой строчной буквой, а Étoile оказывается в конце, потому что É (0xC9) идёт после z. CASE_INSENSITIVE_ORDER даёт [anvil, apple, Banana, éclair, Étoile], складывая регистр так, что список читается в алфавитном порядке. На этом конкретном списке французский Collator даёт тот же порядок — но это единственный, чей результат по конструкции правилен: он складывает регистр и трактует ударения как вторичное отличие, поэтому остаётся корректным на входных данных, где трюк с кодовыми точками ломается (например, сортировка côté относительно cote или правильное размещение ñ в испанском).

Что дальше

Разбиение строки на части — естественная следующая тема. В Java есть два инструмента для этого, и один из них старше, чем хотел бы дизайн языка. Перейдите к Java StringTokenizer.

Практика

Практика
Какой вызов для проверки того, равен ли (возможно `null`) `input` строке 'admin', является **одновременно** правильным и безопасным для `null`?
Какой вызов для проверки того, равен ли (возможно `null`) `input` строке 'admin', является **одновременно** правильным и безопасным для `null`?
Was this page helpful?