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

🔥 🚀 Важно для всех, кто работает с Java! 🔥
На JavaRocks ты найдешь уникальные туториалы, практические задачи и редкие книги, которых не найти в свободном доступе. Присоединяйся к нашему Telegram-каналу JavaRocks — стань частью профессионального сообщества!

В этой статье речь пойдет о важной и довольно интересной теме — о сравнении объектов в Java, на примере строк и метода equals. Вы можете выбрать удобный для вас формат — посмотреть видеоурок на английском языке или прочитать текстовую версию ниже.

Итак, в каких случаях объект A будет равен объекту B?

Давайте попробуем написать пример:

public class Car {

   String model;
   int maxSpeed;

   public static void main(String[] args) {

       Car car1 = new Car();
       car1.model = "Ferrari";
       car1.maxSpeed = 300;

       Car car2 = new Car();
       car2.model = "Ferrari";
       car2.maxSpeed = 300;

       System.out.println(car1 == car2);
   }
}
Run Code

Вывод в консоль:

false

Стоп. Почему же эти две машины не равны? Мы присвоили им одинаковые значения полей, но результат сравнения все равно false.

Все очень просто: оператор == сравнивает ссылки на объекты, а не их содержимое. Два объекта могут иметь даже 500 полей с одинаковыми значениями, но при их сравнении все равно получим false, потому что ссылки car1 и car2 указывают на разные участки памяти — то есть, на два разных объекта.

Представьте, что вы сравниваете людей. Конечно, где-то в мире точно есть человек с таким же именем, цветом глаз, возрастом, ростом, цветом волос и т. д. Это делает вас похожими во многих отношениях, но вы все равно не близнецы — и уж точно вы не один и тот же человек.

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

Оператор == использует примерно ту же логику, когда мы применяем его для сравнения двух объектов. Он проверяет, один и тот же это объект в памяти или нет.

Но что, если нужно, чтобы программа использовала другую логику?

Например, предположим, что программа занимается анализом ДНК. Она сравнивает генетический код двух людей и должна определить, являются ли они близнецами.

public class Man {

   int geneticCode;

   public static void main(String[] args) {

       Man man1 = new Man();
       man1.geneticCode = 1111222233;

       Man man2 = new Man();
       man2.geneticCode = 1111222233;

       System.out.println(man1 == man2);
   }
}
Run Code

Вывод в консоль:

false 

Мы получаем тот же самый результат (ведь мы почти ничего не изменили), но теперь эта логика нас не устраивает. В конце концов, в реальной жизни анализ ДНК должен давать нам 100-процентную гарантию, что перед нами близнецы. Но наша программа и оператор == говорят об обратном. Как изменить это поведение и научить программу возвращать правильный результат при совпадении ДНК?

В Java для таких случаев есть специальный метод — equals().

Как и метод toString(), equals() принадлежит классу Object — базовому классу в Java, от которого происходят все остальные классы.

Но сам по себе метод equals() не меняет поведение нашей программы:

public class Man {

   String geneticCode;

   public static void main(String[] args) {

       Man man1 = new Man();
       man1.geneticCode = "111122223333";

       Man man2 = new Man();
       man2.geneticCode = "111122223333";

       System.out.println(man1.equals(man2));
   }
}
Run Code

Консольный вывод:

false

Тот же результат, что и с ==. Тогда зачем вообще нужен этот метод?

Все просто. сейчас мы используем реализацию equals() из класса Object по умолчанию.
Если заглянуть в код, то увидим следующее:

public boolean equals(Object obj) {
   return (this == obj);
}
Run Code

Вот почему поведение программы не изменилось — поведение equals() по умолчанию такое же, как у ==, то есть сравниваются ссылки, а не содержимое объектов.

Но фишка этого метода в том, что его можно переопределить — написать свой собственный метод equals(), придав ему нужное нам поведение!

В настоящее время нам не нравится, что man1.equals(man2) фактически работает также, как и man1 == man2.

Вот что мы сделаем в этой ситуации:

public class Man {

   int dnaCode;

   public boolean equals(Man man) {
       return this.dnaCode ==  man.dnaCode;

   }

   public static void main(String[] args) {

       Man man1 = new Man();
       man1.dnaCode = 1111222233;

       Man man2 = new Man();
       man2.dnaCode = 1111222233;

       System.out.println(man1.equals(man2));

   }
}
Run Code

Консольный вывод:

true

Теперь результат совершенно другой! Написав собственный метод equals() и использовав его вместо стандартного, мы добились правильного поведения. Теперь, если у двух людей одинаковая ДНК, программа сообщает: “DNA analysis has proven they are twins” (“анализ ДНК подтвердил, что они близнецы”) и возвращает true!

Переопределив метод equals() в классах, можно легко настроить любую необходимую логику сравнения объектов.

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

Почему мы рассматриваем сравнения строк отдельно от других объектов? Дело в том, что строки — это особая тема в программировании.

Во-первых, если посмотреть на все когда-либо написанные на Java программы, то окажется, что около 25 % объектов в них — это строки. Поэтому эта тема такая важная.

Во-вторых, процесс сравнения строк сильно отличается от сравнения других объектов.

Рассмотрим простой пример:

public class Main {

   public static void main(String[] args) {

       String s1 = "CodeGym is the best website for learning Java!";
       String s2 = new String("CodeGym is the best website for learning Java!");
       System.out.println(s1 == s2);
   }
}
Run Code

Консольный вывод:

false

Но почему false? Ведь строки абсолютно одинаковые, слово в слово :/

Вероятно, вы уже догадались, почему: потому что оператор == сравнивает ссылки, а не содержимое! Очевидно, что у s1 и s2 разные адреса в памяти. Теперь давайте поменяем наш пример:

public class Main {

   public static void main(String[] args) {

       String s1 = "CodeGym is the best website for learning Java!";
       String s2 = "CodeGym is the best website for learning Java!";
       System.out.println(s1 == s2);
   }
}
Run Code

Теперь у нас снова две переменные, но результат противоположный.

Консольный вывод:

true 

Запутались окончательно? Давайте разбираться, что происходит.

Оператор == действительно сравнивает адреса в памяти. Это всегда так, и в этом нет никаких сомнений. Это значит, что если s1 == s2 возвращает true, то эти две строки имеют одинаковый адрес. И это действительно так!

Пришло время познакомиться со специальной областью памяти для строк: string pool (пул строк).

string pool

Пул строк — это область памяти, в которой хранятся все строковые значения, созданные в программе.

Зачем он нужен? Как мы уже говорили, строки составляют значительную часть всех объектов. Любая большая программа создает множество строк. Пул строк был создан для экономии памяти: строки помещаются в специальную область, и если такая же строка уже существует, то используется существующая — нет необходимости каждый раз выделять дополнительную память.

Каждый раз, когда вы пишете String = "...", программа проверяет, есть ли идентичная строка в пуле строк. Если есть — новая строка создаваться не будет, а переменная просто будет ссылаться на уже существующую.

Поэтому, когда мы написали

String s1 = "CodeGym is the best website for learning Java!";

String s2 = "CodeGym is the best website for learning Java!";
Run Code

s2 указывает на то же место, что и s1. Первый оператор создает новый объект в пуле строк, а второй оператор просто обращается к той же области памяти, что и s1.

Можно создать еще 500 одинаковых строк — результат не изменится.

Но если все это правда, то почему не сработал предыдущий пример?

public class Main {

   public static void main(String[] args) {

       String s1 = "CodeGym is the best website for learning Java!";
       String s2 = new String("CodeGym is the best website for learning Java!");
       System.out.println(s1 == s2);
   }
}
Run Code

Думаю, ваша интуиция уже подсказывает вам ответ =) Попробуйте предположить, прежде чем читать дальше.

Видно, что эти две строки были объявлены по-разному. Одна с помощью оператора new, а другая без него. Именно в этом кроется разгадка. Когда объект создается через new, под него всегда выделяется новая область памяти. Такая строка не попадает в пул строк, даже если ее текст полностью совпадает с уже существующей строкой.

То есть, если мы напишем следующий код:

public class Main {

   public static void main(String[] args) {

       String s1 = "CodeGym is the best website for learning Java!";
       String s2 = "CodeGym is the best website for learning Java!";
       String s3 = new String("CodeGym is the best website for learning Java!");
   }
}
Run Code

в памяти это будет выглядеть следующим образом:

string pool in java

И каждый раз, когда вы создаете новый объект с помощью new, под него выделяется новая область памяти, даже если текст строки точно такой же!

Похоже, с оператором == мы разобрались. Но что делать с методом equals()?

public class Main {

   public static void main(String[] args) {

       String s1 = "CodeGym is the best website for learning Java!";
       String s2 = new String("CodeGym is the best website for learning Java!");
       System.out.println(s1.equals(s2));
   }
}
Run Code

Консольный вывод:

true

Интересно. Мы уверены, что s1 и s2 ссылаются на разные участки памяти. Но метод equals() по-прежнему утверждает, что они равны. Почему?

Помните, мы говорили, что метод equals() можно переопределить, чтобы сравнивать объекты по нужным критериям?

Именно так и сделано в классе String: метод equals() переопределен, и вместо ссылок он сравнивает последовательность символов в строках. Если текст совпадает — неважно, где эти строки хранятся и как они были созданы — результат будет true.

Кстати, Java позволяет выполнять сравнение строк без учета регистра.

Обычно, если одна из строк содержит все заглавные буквы, то результатом сравнения будет false:

public class Main {

   public static void main(String[] args) {

       String s1 = "CodeGym is the best website for learning Java!";
       String s2 = new String("CODEGYM IS THE BEST WEBSITE FOR LEARNING JAVA!");
       System.out.println(s1.equals(s2));
   }
}
Run Code

Консольный вывод:

false

Для сравнения строк без учета регистра в классе String существует метод equalsIgnoreCase(). Его удобно использовать, если важно сравнить только последовательность определенных символов, а не их регистр. Например, это может пригодиться при сравнении адресов:

public class Main {

   public static void main(String[] args) {

       String address1 = "2311 Broadway Street, San Francisco";
       String address2 = new String("2311 BROADWAY STREET, SAN FRANCISCO");
       System.out.println(address1.equalsIgnoreCase(address2));
   }
}
Run Code

В этом случае очевидно, что речь идет об одном и том же адресе, поэтому логично использовать метод equalsIgnoreCase().

Метод String.intern()

У класса String есть еще один интересный метод — intern();

Метод intern() работает непосредственно с пулом строк. Что происходит, если вызвать его на какой-либо строке?

  • Проверяется, есть ли такая строка в пуле строк.
  • Если есть, то возвращается ссылка на эту строку в пуле.
  • Если нет, он добавляет строку в пул строк и возвращает ссылку на нее.

После того применения метода intern() к строке, созданной через new, можно сравнивать ее с другой строкой из пула с помощью оператора ==.

public class Main {

   public static void main(String[] args) {

       String s1 = "CodeGym is the best website for learning Java!";
       String s2 = new String("CodeGym is the best website for learning Java!");
       System.out.println(s1 == s2.intern());
   }
}
Run Code

Консольный вывод:

true

Раньше, когда мы сравнивали эти строки без intern(), результат был false. Теперь метод intern() проверяет, есть ли строка "CodeGym is the best website for learning Java!" в пуле. Конечно, есть — она создана так:

String s1 = "CodeGym is the best website for learning Java!";

Мы проверяем, указывают ли s1 и s2.intern() на одну и ту же область памяти. И да — указывают 🙂

В общем, запомните и применяйте это важное правило:

ВСЕГДА используйте метод equals() для сравнения строк!
Когда мы сравниваем строки, чаще всего мы хотим сравнить текст, а не ссылки на объекты. Именно это и делает equals().

Перевод статьи «Java Compare Strings».

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх