С 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
|
@Overridepublic 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
|
@Overridepublic 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() в качестве метода для просмотра общей информации об экземпляре в представлении, которое может быть изменено, и предоставить конкретные методы для определенных фрагментов данных в представлениях, которые с гораздо меньшей вероятностью изменяются и являются более простыми. программный доступ и принятие решений на основе большой строки, которая потенциально требует синтаксического анализа.
| Ссылка: | О достоинствах избегания синтаксического анализа или логики на основе toString () Результат от нашего партнера JCG Дастина Маркса в блоге Inspired by Actual Events . |