Сравнение строк в 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 throwsequalsIgnoreCase выполняет посимвольное сравнение после 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 сравнивает с любым CharSequence — String, StringBuilder, StringBuffer или CharBuffer. Это правильный метод, когда нужно узнать, равно ли текущее содержимое построителя известной строке без промежуточного вызова toString():
StringBuilder sb = new StringBuilder("hello");
"hello".contentEquals(sb); // true — no toString allocationequals здесь не поможет, потому что по контракту 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 видимыми бок о бок.
Три сортировки делают различия наглядными. 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.