Как сравнивать строки в Java

Использование оператора == для сравнения строк в Java неправильно и может привести к неожиданным результатам в том смысле, что иногда при сравнении одинаковых строк с помощью == мы будем получать true, а иногда false.
Для корректного и предсказуемого сравнения строк в Java необходимо пользоваться методом equals(), который может быть вызван на любой переменной, литерале или ссылке типа String (и вообще на любом объекте, так как наследуется из класса Oject):
"Hello".equals(new String("Hello"));//true

Оператор ==


Оператор == сравнивает значения своих операндов. Применительно к примитивным типам данных это означает сравнение непосредственных значений (чисел, значений true/false) содержащихся в переменных или литералах:
double d = 1.0;if (d == 1.0) {}
В данном примере сравнивается значение переменной d и литерала 1.0. Разумеется, эти два значения равны и мы войдём внутрь if'а.
Это же справедливо и для ссылочных типов. Однако, загвоздка в том, что значением ссылочного типа является ссылка, то есть адрес в памяти машины, а уже по этому адресу располагается объект. Создадим две ссылочные переменные:
String str1 = new String("Hello");String str2 = new String("Hello");
Хотя мы и будем оперировать объектом "Hello" с помощью переменной str1, но значение этой переменной не "Hello". Значение этой переменной может быть таким: 4b67cf4d. То есть это какой-то адрес в памяти (на самом деле, это хэшкод объекта, но для простоты будем считать его адресом в памяти), в котором располагается конкретная строка.
str1 -> 4b67cf4d -> "Hello"
Таким образом значения переменных str1 и str2 разные. Значение str1, например, равно 4b67cf4d, а str2 -- 7ea987ac. Соответственно значение этих ссылочных переменных разные. А тот факт, что по этим разным адресам лежат одинаковые строки, для оператора == никакого значения не имеет, так как он не умеет ходить по ссылкам.
Оператор ==, применённый к двум ссылочным типам вернёт true только в том случае, если переменные будут ссылаться на один и тот же объект, то есть их значением будет один и тот же адрес памяти машины.

Метод equals()


Метод equals() имеется у класса Object и, как следствие, наследуется всеми остальными объектами.
По умолчанию он выглядит так:
public boolean equals(Object obj) { return (this == obj);}
То есть делает то же самое, что и оператор ==, а именно сравнивает значения переменных. Здесь эти переменные всегда будут ссылочного типа, а значит equals() сравнит значения адресов, безотносительно содержания конкретных объектов.
Этот метод создан для того, чтобы все классы-потомки его переопределяли. В переопределённой версии метода предполагается сравнивать фактическое состояние непосредственных объектов, на которые указывают ссылки, а не просто адреса.
Класс String, разумеется, переопределяет этот метод и в переопределённой версии сравнивает непосредственно строки, расположенные по адресам на которые ссылаются переменные (и/или строковые литералы).
Таким образом метод equals() вернёт true, если фактическое содержание строк будет одинаковым с точки зрения содержащихся в них символов, безотносительно того, одинаковы или различны ссылки на эти объекты:
String str1 = new String("Hello");String str2 = new String("Hello");System.out.println(str1.equals(str2));
Последнее выражение выведет true. Хотя совершенно очевидно, что есть два разных строковых объекта, содержащих символы 'H'+'e'+'l'+'l'+'o'. С точки зрения метода equals() это объекты "эквивалентны" друг другу, а значит нужно вернуть true.
Во избежание проблемы с NullPointerException рекомендуется вызывать метод equals() на объекте, про который мы можем быть уверены, что он не null. Часто таким объектом является литерал, поэтому инструкции вида
boolean isHello = "hello".equals(str);
являются обычным делом, хотя интуитивно хочется сравнивать переменную со строкой: str.equals("hello"), а не наоборот. Но первый вариант является null-безопасным.

Проблема разного поведения == в разных ситуациях


Java для своих программ поддерживает так называемый пул строк. Так, строковые объекты могут быть записаны непосредственно в текст программы с помощью строковых литералов в двойных кавычках:
String str1 = "Hello";
В данном примере "Hello" -- это литерал объекта строки. Подобные литералы помещаются в пул строк ещё на этапе компиляции программы и во время выполнения Java-машина не создаёт новые строковые объекты, а пользуется этим пулом. Например:
String str1 = "Hello";String str2 = "Hello";
В данном случае обе переменные будут ссылаться на один и тот же объект в памяти.
При компиляции программы в первой строчке компилятор обнаружит строковый литерал "Hello" и поместит его в пул строк. На второй строчке компилятор снова обнаружит литерал "Hello", но, поскольку в пуле такая строка уже есть, то компилятор просто заставит обе переменные ссылаться на один и тот же объект в пуле. Таким образом:
String str1 = "Hello";String str2 = "Hello";System.out.println(str1 == str2);
выведет true, так как фактически str1 и str2 ссылаются на один и тот же адрес в памяти (в пуле строк).
Однако, компиляция строковых литералов может оказаться более сложным процессом, чем видится на первый взгляд, в силу различных дополнительных оптимизаций, в том числе обработки констант времени компиляции. Например:
String str1 = "Hello";String str2 = "He" + "llo";System.out.println(str1 == str2);
Сравнение str1 == str2 вернёт true, что мы увидим на консоли, и вот почему. Может показаться, что раз в программе использовано три строковых литерала: "Hello", "He" и "llo", то все три будут добавлены в пул строк. А значение переменной str2 будет вычислено во время выполнения. В результате суммирования строк появится новый объект, которого ни в каком пуле быть не может, а значит переменные должны ссылаться на разные объекты: str1 на "Hello" в пуле строк, а str2 на новосозданный "Hello" где-то в другом месте.
Но эта штука работает по-другому. Компилятор видит выражение String str2 = "He" + "llo"; следующим образом: "He" + "llo" могут быть "суммированы" уже на этапе компиляции, что и происходит. В результате в "сумме" получается строка "Hello", которая, как оказывается, уже есть в пуле. Таким образом можно заставить переменную str2 просто ссылаться на "Hello" в пуле и на работу программы это никак не повлияет, а вычислительные ресурсы сэкономить позволит.
Также важно понимать, что в данном случае == по-прежнему не сравнивает строки. Сравниваются только адреса ссылок.
Кроме того, в классе String есть метод intern(), который позволяет добавлять в пул строку, созданную во время выполнения программы. Таким образом, следующее выражение вернёт true:
new String("test").intern() == "test"
Часть выражение, расположенная слева от == в итоге будет также указывать на строку "test" в пуле, так как к вновь созданному объекту применён метод intern(). Разумеется литерал "test" также указывает на тот же адрес в пуле и == вернёт true.

Скорость работы equals()


Метод String.equals() работает очень оптимально. - Сначала ссылки проверяются через ==. И если ссылки указывают на один и тот же строковый объект, то, безусловно, сразу возвращается true.
- Затем проверяется длина строк. Если строки оказываются разной длины, то сразу возвращается false;
- Затем строки сравниваются символ за символом до первого расхождения. При первом же различии возвращается false. Но если строки совпадут полностью, то вернётся true.
Первичная проверка с помощью == в совокупности с пулом строк делают работу со строками в Java довольно непроблемным с точки зрения производительности делом. Тем не менее, при больших объёмах данных бывает всякое.
Если новых строк образуется много и сравнения происходят часто и это становится проблемой, то, стоит попробовать пользоваться методом intern().

Другие способы сравнения строк


Метод equalsIgnoreCase() позволяет сравнить строки без относительно регистра содержащихся в строках символов. Следующий код выведет true:
System.out.println("HELLO".equalsIgnoreCase("hello"));
Метод contentEquals() принимает в качестве параметра CharSequence или StringBuffer и позволяет сравнивать строки с объектами этих типов без лишних преобразований к String:
System.out.println("hello".contentEquals("hello"));
Метод compareTo() вернёт 0, если строки окажутся одинаковыми. Тем не менее, это не тот способ, которым стоит пользоваться для этой цели.