[Перевод] Java Challengers #2: Сравнение строк

У нас как всегда много опаздывающих к началу курса, так что только вчера провели второе занятие среди нового потока «Разработчик Java». Но это так, мелочи жизни, а пока что мы продолжаем публикацию серии статей Java Challengers, перевод которых подготовили для вас.

В Java класс String инкапсулирует массив char (прим. переводчика — с java 9 это уже массив byte, см. Компактные строки в Java 9). Говоря по простому, String — это массив символов, используемый для составления слов, предложений или других конструкций.

Инкапсуляция — это одна из самых мощных концепций объектно — ориентированного программирования. Благодаря инкапсуляции вам не нужно знать как работает класс String. Вам достаточно знать методы его интерфейса.

Когда вы смотрите на класс String в Java, вы можете увидеть как инкапсулирован массив char:

public String(char value[]) {
  this(value, 0, value.length, null);
}

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

Первая статья в серии Java Challengers была про перегрузку методов, которая широко используется в классе String. Перегрузка может сделать ваши классы действительно гибкими:

public String(String original) {}
public String(char value[], int offset, int count) {}
public String(int[] codePoints, int offset, int count) {}
public String(byte bytes[], int offset, int length, String charsetName) {}
// И так далее ...

Вместо того, чтобы пытаться понять, как работает класс String, эта статья поможет вам понять что он делает и как использовать его в вашем коде.


Что такое пул строк (String pool)

Класс String, возможно, наиболее часто используемый класс в Java. Если новый объект создавать в динамической памяти (memory heap) каждый раз, когда мы используем String, то мы потратим впустую много памяти. Пул строк (String pool) решает эту проблему, сохраняя только один объект для каждого значения строки.

strings-in-the-string-pool

Строки в пуле строк

Хотя мы создали несколько переменных String со значениями Duke и Juggy, но в динамической памяти (куче) создаётся и храниться только два объекта. Для доказательства посмотрите следующий пример кода. (Напомним, что в Java оператор »==» используется для сравнения двух объектов и определения того один и тот же это объект или нет.)

String juggy = "Juggy";
String anotherJuggy = "Juggy";
System.out.println(juggy == anotherJuggy);

Этот код вернет true, потому что две переменные String указывают на один и тот же объект в пуле строк. Их значения одинаковые.


Исключение — оператор new

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

String duke = new String("duke");
String anotherDuke = new String("duke");

System.out.println(duke == anotherDuke);

На основе предыдущего примера можно подумать, что этот код вернёт true, но это не так. Добавление оператора new приводит к созданию нового объекта String в памяти. Таким образом, JVM создаст два разных объекта.


Native — методы

Native-методы в Java — это методы, которые будут компилироваться с использованием языка C, обычно с целью управления памятью и оптимизации производительности.


Пулы строк и метод intern()

Для хранения строк в пуле используется способ, называемый «интернирование строк» (String interning).

Вот, что Javadoc говорит нам о методе intern():

 /**
 * Возвращает каноническое представление для строкового объекта.
 *
 * Пул строк (первоначально пустой) управляется классом {@code String}.
 *
 * Когда вызывается метод intern, если пул уже содержит строку, 
 * равную этому объекту {@code String}, определяемому через
 * метод  {@link #equals(Object)}, тогда возвращается строка из пула. 
 * Иначе, этот объект {@code String} добавляется к 
 * пулу и возвращается ссылка на этот объект {@code String}. 
 *
 * Из этого следует, что для любых двух строк {@code s} и {@code t},
 * {@code s.intern() == t.intern()} будет {@code true}
 * тогда и только тогда, когда {@code s.equals(t)} равно {@code true}.
 * 
 * Все литеральные строки и строковые константы интернируются. 
 * Строковые литералы определяются в разделе 3.10.5 The Java™ Language Specification.
 * 
 * @returns строка, которая имеет то же самое содержание как эта строка, 
 *          но, гарантируется, что она будет из пула уникальных строк.
 *
 * @jls 3.10.5 String Literals
 */ public native String intern();

Метод intern() используется для хранения строк в пуле строк. Во-первых, он проверяет, существует ли уже созданная строка в пуле. Если нет, то создает новую строку в пуле. Логика пула строк основана на паттерне Flyweight.

Теперь, обратите внимание, что происходит, когда мы используем new для создания двух строк:

String duke = new String("duke");
String duke2 = new String("duke");
System.out.println(duke == duke2); // Здесь результат будет false
System.out.println(duke.intern() == duke2.intern()); // Здесь результат будет true

В отличие от предыдущего примера с ключевым словом new, в данном случае сравнение вернёт true. Это потому, что использование метода intern() гарантирует, что строка будет в пуле.


Метод equals в классе String

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

Взгляните:

public boolean equals(Object anObject) {
  if (this == anObject) {
    return true;
  }

  if (anObject instanceof String) {
    String aString = (String)anObject;
    if (coder() == aString.coder()) {
       return isLatin1() ? StringLatin1.equals(value, aString.value)
          : StringUTF16.equals(value, aString.value);
    }
  }

  return false;
}

Как вы видите, значение класса String сравнивается через equals(), а не через ссылку на объект. Не имеет значения, если ссылки на объекты разные; будут сравниваться состояния.


Наиболее распространенные методы String

Есть ещё одна вещь, которую вам нужно знать, прежде чем решить задачку на сравнение строк.

Рассмотрим наиболее распространённые методы класса String:

// Удаляет пробелы в начале и в конце строки
trim() 
// Получает подстроку по индексам
substring(int beginIndex, int endIndex)
// Возвращает длину строки
length() 
//  Заменяет строку, можно использовать регулярное выражение
replaceAll(String regex, String replacement)
// Проверяет, есть ли указанная последовательность CharSequence  в строке
contains(CharSequences) 


Решите задачку на сравнение строк

Давайте проверим, что вы узнали о классе String, решив небольшую задачку.

В этой задаче вы сравните несколько строк, используя изученные концепции. Глядя на код ниже, можете ли вы определить значение каждой переменной result?

public class ComparisonStringChallenge {
  public static void main(String... doYourBest) {
    String result = "";
    result += " powerfulCode ".trim() == "powerfulCode" ? "0" : "1";

    result += "flexibleCode" == "flexibleCode" ? "2" : "3";

    result += new String("doYourBest") 
        == new String("doYourBest") ? "4" : "5";

    result += new String("noBugsProject")
        .equals("noBugsProject") ? "6" : "7";

    result += new String("breakYourLimits").intern()
        == new String("breakYourLimits").intern() ? "8" : "9";

    System.out.println(result);
  }
}

Какой будет вывод?


  • A: 02468
  • B: 12469
  • C: 12579
  • D: 12568

Правильный ответ приведён в конце статьи.


Что сейчас произошло? Понимание поведения String

В первой строке мы видим:

result += " powerfulCode ".trim() == "powerfulCode" ? "0" : "1";

В этом случае результат false, потому что, когда метод trim() удаляет пробелы он создаёт новый String с помощью оператора new.

Далее мы видим:

result += "flexibleCode" == "flexibleCode" ? "2" : "3";

Здесь нет никакой тайны, строки одинаковы в пуле строк. Это сравнение возвращает true.

Затем, мы имеем:

result += new String("doYourBest") 
    == new String("doYourBest") ? "4" : "5";

Использование new приводит к созданию двух новых строк и не важно равны их значения или нет. В этом случае сравнение будет false даже если значения одинаковые.

Далее:

result += new String("noBugsProject")
    .equals("noBugsProject") ? "6" : "7";

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

В этом случае, не имеет значение разные объекты или нет, поскольку сравнивается значение. Результат true.

Окончательно, мы имеем:

result += new String("breakYourLimits").intern()
    == new String("breakYourLimits").intern() ? "8" : "9";

Как вы видели ранее, метод intern() помещает строку в пул строк. Обе строки указывают на один и тот же объект, поэтому в этом случае true.


Распространенные ошибки со строками

Бывает трудно определить, указывают ли две строки на один и тот же объект или нет, особенно когда строки содержат одно и то же значение. Полезно помнить, что использование new всегда приводит к созданию нового объекта в памяти, даже если значения строк одинаковые.

Использование методов класса String для сравнения ссылок на объекты также может быть сложным. Особенность в том, что если метод изменяет что-то в строке, то будут разные ссылки на объекты.

Несколько примеров, которые помогут прояснить:

System.out.println("duke".trim() == "duke".trim());

Это сравнение будет истинным, потому что метод trim() не создает новую строку.

System.out.println(" duke".trim() == "duke".trim()); 

В этом случае первый метод trim() генерирует новую строку, так как метод будет выполнять свою работу и поэтому ссылки будут разные.

Наконец, когда trim() выполнит свою работу, он создает новую строку:

// Реализация метода trim в классе String
new String(Arrays.copyOfRange(val, index, index + len), LATIN1);


Что нужно помнить о строках


  • Строки не изменяемые, поэтому состояние строки изменить нельзя.


  • Для экономии памяти JVM хранит строки в пуле строк. При создании новой строки JVM проверяет ее значение и указывает на существующий объект. Если в пуле нет строки с этим значением, то JVM создаёт новую строку.


  • Оператор »==» сравнивает ссылки на объект. Метод equals() сравнивает значения строк. То же правило будет применяться ко всем объектам.


  • При использовании оператора new будет создана новая строка в пуле строк, даже если есть строка с тем же значением.



Ответ

Ответ на эту задачу — D. Вывод будет 12568.

Продолжение следует…

Habrahabr.ru прочитано 16230 раз