Статьи

О достоинствах избегания синтаксического анализа или логики на основе результата toString ()

С Java или любым другим языком программирования, который я использовал в значительной степени, я обнаружил, что иногда в этом языке можно что-то сделать, но обычно этого делать не следует. Часто такие злоупотребления языком кажутся безвредными и, возможно, полезными, когда разработчик впервые их использует, но позже тот же разработчик или другой разработчик сталкивается с сопутствующими проблемами, которые обходятся дорого, чтобы их преодолеть или изменить. Примером этого и темой этого сообщения в блоге является использование результатов вызова toString() в Java для логического выбора или анализа содержимого.

В 2010 году я написал в Java toString () Замечания , которые я обычно предпочитаю, когда методы toString() явно доступны для классов и когда они содержат соответствующее открытое состояние объекта этого класса. Я до сих пор чувствую себя так. Тем не менее, я ожидаю, что реализация toString() будет достаточной для того, чтобы человек мог прочитать содержимое объекта через зарегистрированный оператор или отладчик, а не для анализа кода или скрипта. Использование String возвращаемого методом toString() для любого типа условной или логической обработки, слишком хрупко. Аналогично, анализ возвращаемой String toString() для получения сведений о состоянии экземпляра также является хрупким. Я предупреждал о (даже непреднамеренно) требовании от разработчиков разбора результатов toString() в ранее упомянутом сообщении в блоге .

Разработчики могут по toString() выбору изменить сгенерированную строку toString() по ряду причин, включая добавление к выводу существующих полей, которые, возможно, не были представлены ранее, добавление дополнительных данных к уже существующим полям, добавление текста для вновь добавленных полей. удаление представления полей, отсутствующих в классе, или изменение формата по эстетическим соображениям. Разработчики могут также изменить орфографические и грамматические проблемы сгенерированной String toString() . Если предоставленная toString() String просто используется людьми, анализирующими состояние объекта в сообщениях журнала, эти изменения вряд ли будут проблемой, если они не удаляют информацию о веществе. Однако, если код зависит от всей String или анализирует String для определенных полей, он может быть легко нарушен этими типами изменений.

Для иллюстрации рассмотрим следующую начальную версию класса Movie :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package dustin.examples.strings;
 
/**
 * Motion Picture, Version 1.
 */
public class Movie
{
   private String movieTitle;
 
   public Movie(final String newMovieTitle)
   {
      this.movieTitle = newMovieTitle;
   }
 
   public String getMovieTitle()
   {
      return this.movieTitle;
   }
 
   @Override
   public String toString()
   {
      return this.movieTitle;
   }
}

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

В следующем листинге кода содержится неудачное решение (строки 22-23) основывать логику на методе toString() класса Movie .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
 * This is a contrived class filled with some ill-advised use
 * of the {@link Movie#toString()} method.
 */
public class FavoriteMoviesFilter
{
   private final static List<Movie> someFavoriteMovies;
 
   static
   {
      final ArrayList<Movie> tempMovies = new ArrayList<>();
      tempMovies.add(new Movie("Rear Window"));
      tempMovies.add(new Movie("Pink Panther"));
      tempMovies.add(new Movie("Ocean's Eleven"));
      tempMovies.add(new Movie("Ghostbusters"));
      tempMovies.add(new Movie("Taken"));
      someFavoriteMovies = Collections.unmodifiableList(tempMovies);
   }
 
   public static boolean isMovieFavorite(final String candidateMovieTitle)
   {
      return someFavoriteMovies.stream().anyMatch(
         movie -> movie.toString().equals(candidateMovieTitle));
   }
}

Этот код может работать некоторое время, несмотря на некоторые проблемы, связанные с ним, когда несколько фильмов имеют одинаковый заголовок . Однако даже до того, как столкнуться с этими проблемами, риск использования toString() в проверке равенства может быть реализован, если разработчик решит, что он или она хочет изменить формат представления Movie.toString() на тот, который показан в следующем листинг кода

1
2
3
4
5
@Override
public String toString()
{
   return "Movie: " + this.movieTitle;
}

Возможно, возвращаемое значение Movie.toString() было изменено, чтобы было яснее, что предоставляемая String связана с экземпляром класса Movie . Независимо от причины изменения ранее перечисленный код, который использует равенство в названии фильма, теперь не работает. Этот код необходимо изменить, чтобы использовать в contains вместо « equals как показано в следующем листинге кода.

1
2
3
4
5
public static boolean isMovieFavorite(final String candidateMovieTitle)
{
   return someFavoriteMovies.stream().anyMatch(
      movie -> movie.toString().contains(candidateMovieTitle));
}

Когда станет понятно, что классу Movie требуется больше информации, чтобы сделать фильмы дифференцируемыми, разработчик может добавить год выпуска к классу фильмов. Новый класс Movie показан ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package dustin.examples.strings;
 
/**
 * Motion Picture, Version 2.
 */
public class Movie
{
   private String movieTitle;
 
   private int releaseYear;
 
   public Movie(final String newMovieTitle, final int newReleaseYear)
   {
      this.movieTitle = newMovieTitle;
      this.releaseYear = newReleaseYear;
   }
 
   public String getMovieTitle()
   {
      return this.movieTitle;
   }
 
   public int getReleaseYear()
   {
      return this.releaseYear;
   }
 
   @Override
   public String toString()
   {
      return "Movie: " + this.movieTitle;
   }
}

Добавление года выпуска помогает различать фильмы с одинаковым названием. Это также помогает отличать римейки от оригиналов. Однако код, который использовал класс Movie для поиска избранных, по-прежнему будет показывать все фильмы с одинаковым названием независимо от года выпуска фильмов. Другими словами, версия Ocean’s Eleven 1960 года ( рейтинг 6.6 для IMDB в настоящее время ) будет считаться фаворитом наряду с версией Ocean Eleven 2001 года ( рейтинг 7.8 для IMDB в настоящее время ), хотя я очень предпочитаю более новую версию. Точно так же версия « Заднего окна», созданная для телевидения 1988 года ( рейтинг 5.6 в настоящее время находится на IMDB ), будет возвращена в качестве фаворита вместе с версией « Заднего окна» 1954 года (режиссер Альфред Хичкок , в главных ролях Джеймс Стюарт и Грейс Келли , с рейтингом 8.5). в настоящее время в IMDB ) хотя я предпочитаю более старую версию.

Я думаю, что реализация toString() как правило, должна включать в себя все общедоступные детали объекта. Однако, даже если метод toString() в Movie расширен и включает год выпуска, клиентский код по-прежнему не будет различаться в зависимости от года, поскольку он выполняет только contain заголовка фильма.

1
2
3
4
5
@Override
public String toString()
{
   return "Movie: " + this.movieTitle + " (" + this.releaseYear + ")";
}

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

1
2
3
4
5
6
7
8
public static boolean isMovieFavorite(
   final String candidateMovieTitle,
   final int candidateReleaseYear)
{
   return someFavoriteMovies.stream().anyMatch(
      movie ->   movie.toString().contains(candidateMovieTitle)
              && movie.getReleaseYear() == candidateReleaseYear);
}

Мне трудно представить себе случаи, когда было бы полезно проанализировать метод toString() или основать условие или другую логику на результатах метода toString() . Примерно в любом примере, о котором я думаю, есть лучший способ. В моем примере выше было бы лучше добавить методы equals()hashCode() ) в Movie а затем использовать проверки на равенство для экземпляров Movie вместо использования отдельных атрибутов. Если отдельные атрибуты нужно сравнивать (например, в случаях, когда равенство объектов не требуется, и только одно или два поля должны быть равны), то можно использовать соответствующие методы getXXX .

Как разработчик, если я хочу, чтобы пользователям моих классов (которые часто заканчиваются в том числе и я) не нужно было анализировать результаты toString() или зависеть от определенного результата, я должен убедиться, что мои классы предоставляют любую полезную информацию из toString() доступны из других легкодоступных и более дружественных к программированию источников, таких как методы «get» и методы равенства и сравнения. Если разработчик не хочет предоставлять некоторые данные через общедоступный API, то, скорее всего, разработчик, вероятно, на самом деле не хочет представлять их в возвращенном результате toString() . Джошуа Блох в « Эффективной Java» формулирует это в выделенном жирным шрифтом тексте: «… обеспечивает программный доступ ко всей информации, содержащейся в значении, возвращаемом toString() ».

В Эффективной Java Bloch также включает обсуждение того, должен ли метод toString() иметь объявленный формат представления String он предоставляет. Он указывает, что это представление, если оно рекламируется, с тех пор должно быть таким же, если это широко используемый класс, чтобы избежать типов перерывов во время выполнения, которые я продемонстрировал в этом посте. Он также советует, что, если формат не гарантируется, что он останется прежним, Javadoc также содержит заявление на этот счет. В целом, поскольку Javadoc и другие комментарии часто игнорируются больше, чем хотелось бы, и из-за «постоянного» характера рекламируемого представления toString() я предпочитаю не полагаться на toString() для предоставления определенного формата, необходимого клиентам , но вместо этого предоставьте конкретный для этой цели метод, который клиенты могут вызывать. Это оставляет мне возможность изменять мой toString() мере изменения класса.

Пример из JDK иллюстрирует мой предпочтительный подход, а также иллюстрирует опасность назначения определенного формата ранней версии toString() . Представление BigStecimal toString () было изменено в JDK 1.4.2 и Java SE 5, как описано в разделе « Несовместимость в J2SE 5.0 (начиная с 1.4.2) »: «Метод toString() J2SE 5.0 BigDecimal ведет себя иначе, чем в предыдущем версии «. Javadoc для версии BigDecimal.toString() версии 1.4.2 просто говорит в обзоре метода: «Возвращает строковое представление этого BigDecimal. Используется преобразование цифр в символ, предоставляемое Character.forDigit (int, int). Ведущий знак минус используется для обозначения знака, а число цифр справа от десятичной точки используется для обозначения масштаба. (Это представление совместимо с конструктором (String).) »Та же самая обзорная документация по методу для BigDecimal.toString () в Java SE 5 и более поздних версиях является гораздо более подробной. Это такое длинное описание, что я не буду здесь его показывать.

Когда BigDecimal.toString() был изменен в Java SE 5 , были представлены другие методы для представления различных представлений String : toEngineeringString () и toPlainString () . Недавно представленный метод toPlainString() предоставляет то, что BigDecimal toString() предоставляет через JDK 1.4.2. Я предпочитаю предоставлять методы, которые предоставляют конкретные представления и форматы String, потому что эти методы могут иметь специфику формата, описанного в их именах, а также комментарии и изменения Javadoc, и дополнения к классу не так сильно влияют на эти методы, как они влияют общий метод toString() .

Есть несколько простых классов, которые могут соответствовать случаю, когда первоначально реализованный метод toString() будет исправлен раз и навсегда и никогда не изменится. Это могут быть кандидаты для анализа возвращенной строки или логики на основе String , но даже в этих случаях я предпочитаю предоставить альтернативный метод с объявленным и гарантированным форматом и оставить представлению toString() некоторую гибкость для изменений. Нет ничего особенного в том, чтобы иметь дополнительный метод, потому что, хотя они возвращают одно и то же, дополнительный метод может быть просто однострочным методом, вызывающим toString . Затем, если toString() действительно изменяется, реализация вызывающего метода может быть изменена в соответствии с тем, что ранее предоставлял toString() и любые пользователи этого дополнительного метода не увидят никаких изменений.

Синтаксический анализ результата toString() или основание логики на результате вызова toString() , скорее всего, будет выполняться, когда этот конкретный подход воспринимается клиентом как самый простой способ доступа к определенным данным. Обеспечение доступности этих данных с помощью других, общедоступных методов должно быть предпочтительным, и разработчики классов и API могут помочь, гарантируя, что любые даже потенциально полезные данные, которые будут в String, предоставляемые toString() , также будут доступны в определенном альтернативном программно-доступном метод. Короче говоря, я предпочитаю оставить toString() в качестве метода для просмотра общей информации об экземпляре в представлении, которое может быть изменено, и предоставить конкретные методы для определенных фрагментов данных в представлениях, которые с гораздо меньшей вероятностью изменяются и являются более простыми. программный доступ и принятие решений на основе большой строки, которая потенциально требует синтаксического анализа.