Статьи

Лучшие посты 2013 года: 10 тонких рекомендаций при кодировании Java

Это список из 10 лучших практик, которые более тонки, чем ваше среднее   правило Josh Bloch Effective Java . Хотя список Джоша Блоха очень прост для изучения и касается повседневных ситуаций, этот список содержит менее распространенные ситуации, связанные с разработкой API / SPI,   которые, тем не менее  , могут оказать большое влияние.

Я сталкивался с этими вещами при написании и поддержке  jOOQвнутреннего DSL-  моделирования SQL в Java. Будучи внутренним DSL, jOOQ по максимуму бросает вызов компиляторам и дженерикам Java,  сочетая дженерики, переменные и перегрузки  таким образом, что Джош Блох, вероятно, не рекомендовал бы для «среднего API».

Позвольте мне поделиться с вами 10 тонкими рекомендациями при кодировании Java:

1. Помните деструкторы C ++

Помните  деструкторы C ++ ? Нет? Тогда вам может повезти, поскольку вам никогда не приходилось отлаживать код, оставляя утечки памяти из-за того, что выделенная память не была освобождена после удаления объекта. Спасибо Sun / Oracle за реализацию сборки мусора!

Но, тем не менее, деструкторы имеют интересную черту для них. Часто имеет смысл освобождать память в  обратном  порядке распределения. Помните об этом и в Java, когда вы работаете с семантикой, похожей на деструктор:

  • При использовании аннотаций @Before и @After JUnit
  • При выделении освобождая ресурсы JDBC
  • При вызове супер методов

Существуют и другие варианты использования. Вот конкретный пример, показывающий, как вы могли бы реализовать некоторый SPI прослушивателя событий:

@Override
public void beforeEvent(EventContext e) {
  super.beforeEvent(e);
  // Super code before my code
}

@Override
public void afterEvent(EventContext e) {
  // Super code after my code
  super.afterEvent(e);
}

Еще один хороший пример , показывающий , почему это может быть важным является  пресловутый Dining проблема ФилософыБольше информации о столовых философах можно увидеть в этом удивительном сообщении:

http://adit.io/posts/2013-05-11-The-Dining-Philosophers-Problem-With-Ron-Swanson.html 

Правило : всякий раз, когда вы реализуете логику, используя семантику «до / после», «выделить / освободить», «взять / вернуть», подумайте о том, должна ли операция «после / освободить / вернуть» выполнять вещи в обратном порядке.

2. Не доверяйте своим ранним оценкам развития SPI

Предоставление SPI своим пользователям — это простой способ позволить им внедрить пользовательское поведение в вашу библиотеку / код. Однако помните, что ваше суждение об эволюции SPI может заставить вас думать, что вам  (не) понадобится этот дополнительный параметр . Правда, никакой функциональности не  следует добавлять рано . Но как только вы опубликуете свой SPI и решите использовать семантическое управление версиями , вы действительно пожалеете, что добавили в SPI глупый метод с одним аргументом, когда поняли, что в некоторых случаях вам может понадобиться другой аргумент:

interface EventListener {
  // Bad
  void message(String message);
}

Что если вам также понадобится идентификатор сообщения и источник сообщения? Эволюция API не позволит вам легко добавить этот параметр в приведенный выше тип. Конечно, с Java 8 вы можете добавить метод защитника, чтобы «защитить» вас от плохого раннего решения о дизайне:

interface EventListener {
  // Bad
  default void message(String message) {
    message(message, null, null);
  }
  // Better?
  void message(
    String message,
    Integer id,
    MessageSource source
  );
}

Обратите внимание, к сожалению, метод защитника  не может быть окончательным .

Но гораздо лучше, чем загрязнять ваш SPI десятками методов, использовать объект контекста (или объект аргумента)  только для этой цели.

interface MessageContext {
  String message();
  Integer id();
  MessageSource source();
}

interface EventListener {
  // Awesome!
  void message(MessageContext context);
}

Вы можете развить API-интерфейс MessageContext гораздо проще, чем EventListener SPI, так как его будет реализовано меньше пользователей.

Правило : всякий раз, когда вы указываете SPI, рассмотрите возможность использования объектов контекста / параметров вместо написания методов с фиксированным количеством параметров.

Примечание . Часто полезно также передавать результаты через специальный тип MessageResult, который можно создать с помощью API-интерфейса разработчика. Это добавит еще большую гибкость эволюции SPI к вашему SPI.

3. Избегайте возврата анонимных, локальных или внутренних классов

У программистов Swing, вероятно, есть пара сочетаний клавиш для генерации кода для их сотен анонимных классов. Во многих случаях создавать их приятно, так как вы можете локально придерживаться интерфейса, не думая о «полном» жизненном цикле подтипа SPI.

Но вы не должны использовать анонимные, локальные или внутренние классы слишком часто по простой причине: они сохраняют ссылку на внешний экземпляр. И они будут перетаскивать этот внешний экземпляр туда, куда они направляются, например, в какую-то область за пределами вашего локального класса, если вы не будете осторожны. Это может быть основным источником утечек памяти, так как весь ваш объектный граф неожиданно запутывается тонкими способами.

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

Замечание : в отношении двойных фигурных скобок для создания простого экземпляра объекта была применена некоторая умная практика:

new HashMap<String, String>() {{
  put("1", "a");
  put("2", "b");
}}

Это использует инициализатор экземпляра Java, как  указано в JLS §8.6 . Выглядит хорошо (может быть, немного странно), но это действительно плохая идея. То, что в противном случае было бы полностью независимым экземпляром HashMap, теперь сохраняет ссылку на внешний экземпляр, каким бы он ни был. Кроме того, вы создадите дополнительный класс для управления загрузчиком классов.

4. Начните писать SAMs сейчас!

Ява 8 стучит в дверь. А с  Java 8 приходят лямбды , нравятся они вам или нет. Тем не менее, вашим пользователям API они могут понравиться, и вам лучше убедиться, что они могут использовать их как можно чаще. Таким образом, если ваш API не принимает простые «скалярные» типы , такие как  intlongStringDate, пусть ваш API принимать Sams как можно чаще.

Что такое SAM? SAM — это один абстрактный метод [Type]. Также известный как функциональный интерфейс , вскоре будет аннотирован  аннотацией @FunctionalInterface . Это хорошо согласуется с правилом № 2, где EventListener фактически является SAM. Лучшими SAM являются те, которые имеют один аргумент, поскольку они еще больше упростят написание лямбды. Представьте себе письмо

listeners.add(c -> System.out.println(c.message()));

Вместо

listeners.add(new EventListener() {
  @Override
  public void message(MessageContext c) {
    System.out.println(c.message()));
  }
});

Представьте себе обработку XML через  jOOX , в которой есть несколько SAM:

$(document)
  // Find elements with an ID
  .find(c -> $(c).id() != null)
  // Find their child elements
  .children(c -> $(c).tag().equals("order"))
  // Print all matches
  .each(c -> System.out.println($(c)))

Правило : будьте хороши с вашими потребителями API и пишите SAMs / функциональные интерфейсы уже  сейчас .

Замечания : пару интересных постов в блоге о Java 8 Lambdas и улучшенном API коллекций можно посмотреть здесь:

5. Избегайте возврата null из методов API

Я писал о NULL в Java   один или два раза. Я также написал в блоге о введении Java 8 в  Optional . Это интересные темы как с академической, так и с практической точки зрения.

Несмотря на то, что NULL и NullPointerExceptions, вероятно, некоторое время будут оставаться основной проблемой в Java, вы все равно можете разработать свой API таким образом, чтобы пользователи не столкнулись с какими-либо проблемами. Старайтесь по возможности избегать возврата null из методов API. Ваши потребители API должны иметь возможность связывать методы, когда это применимо:

initialise(someArgument).calculate(data).dispatch();

В приведенном выше фрагменте ни один из методов не должен возвращать значение null. Фактически, использование семантики null (отсутствие значения) должно быть довольно исключительным в целом. В библиотеках, таких как  jQuery  (или  jOOX , его порт Java), пустые значения полностью исключаются, поскольку вы  всегда работаете с итеративными объектами . Соответствуете ли вы чему-либо или нет, не имеет значения для следующего вызова метода.

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

Правило : по возможности избегайте возврата нулей из методов. Используйте null только для «неинициализированной» или «отсутствующей» семантики.

6. Никогда не возвращайте нулевые массивы или списки из методов API

Несмотря на то, что в некоторых случаях возврат значений null из методов в порядке, абсолютно нет смысла возвращать нулевые массивы или нулевые коллекции! Давайте рассмотрим отвратительный  java.io.File.list() метод. Возвращает:

Массив строк с именами файлов и каталогов в каталоге, обозначенном этим абстрактным путем. Массив будет пустым, если каталог пуст. Возвращает ноль, если это абстрактное имя пути не обозначает каталог или если произошла ошибка ввода-вывода.

Следовательно, правильный способ борьбы с этим методом

File directory = // ...

if (directory.isDirectory()) {
  String[] list = directory.list();

  if (list != null) {
    for (String file : list) {
      // ...
    }
  }
}

Была ли эта нулевая проверка действительно необходимой? Большинство операций ввода / вывода создают IOException, но эта возвращает нуль. Ноль не может содержать никаких сообщений об ошибках, указывающих, почему произошла ошибка ввода-вывода. Так что это неправильно тремя способами:

  • Null не помогает в поиске ошибки
  • Null не позволяет отличить ошибки ввода-вывода от экземпляра файла, не являющегося каталогом
  • Все будут забывать о нуле, здесь

В контексте коллекций понятие «отсутствие» лучше всего реализовать с помощью пустых массивов или коллекций. Наличие «отсутствующего» массива или коллекции вряд ли когда-либо пригодится, за исключением повторов, для ленивой инициализации.

Правило : массивы или коллекции никогда не должны быть нулевыми.

7. Избегайте состояния, будьте функциональны

Что хорошо в HTTP, так это тот факт, что он не имеет состояния. Все соответствующие состояния передаются в каждом запросе и в каждом ответе. Это важно для именования REST:  Передача представительского состояния . Это замечательно, когда это делается на Java. Думайте об этом с точки зрения правила № 2, когда методы получают объекты параметров с состоянием. Все может быть намного проще, если в таких объектах передается состояние, а не манипулируется извне. Взять, к примеру, JDBC. Следующий пример извлекает курсор из хранимой процедуры:

CallableStatement s =
  connection.prepareCall("{ ? = ... }");

// Verbose manipulation of statement state:
s.registerOutParameter(1, cursor);
s.setString(2, "abc");
s.execute();
ResultSet rs = s.getObject(1);

// Verbose manipulation of result set state:
rs.next();
rs.next();

Это то, что делает JDBC таким неуклюжим API для работы. Каждый объект невероятно полон состояния и им трудно манипулировать. Конкретно, есть две основные проблемы:

  • Очень сложно правильно работать с API с отслеживанием состояния в многопоточных средах
  • Очень трудно сделать доступными ресурсы в глобальном масштабе, так как состояние не задокументировано
Государство как коробка конфет

Театральный плакат для Форрест Гамп, Copyright © 1994  Paramount Pictures . Все права защищены. Считается, что вышеуказанное использование выполняет то, что известно как  добросовестное использование

Правило : реализуйте больше функционального стиля. Передайте состояние через аргументы метода. Манипулировать меньшим состоянием объекта.

8. Короткое замыкание равно ()

Это низко висящий фрукт. В больших графах объектов вы можете значительно повысить производительность, если equals() сначала все методы ваших объектов  сравнительно дешево сравнивают для идентификации:

@Override
public boolean equals(Object other) {
  if (this == other) return true;
  // Rest of equality logic...
}

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

@Override
public boolean equals(Object other) {
  if (this == other) return true;
  if (other == null) return false;
  // Rest of equality logic...
}

Правило : короткое замыкание всех ваших методов equals () для повышения производительности.

9. Попробуйте сделать методы финальными по умолчанию

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

  • Если вам  действительно  нужно переопределить метод (вы на самом деле?), Вы можете удалить окончательное ключевое слово
  • Вы никогда не будете случайно отменять любой метод больше

Это особенно относится к статическим методам, где «переопределение» (фактически, теневое копирование) вряд ли когда-либо имеет смысл. Недавно я натолкнулся на очень плохой пример отслеживания статических методов в  Apache Tika . Рассматривать:

TikaInputStream расширяет TaggedInputStream и скрывает его статический метод get () с совершенно другой реализацией.

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

Правило : если вы полностью контролируете свой API, попробуйте сделать как можно больше методов финальными по умолчанию.

10. Избегайте метода (T…) подписи

Нет ничего плохого в случайном методе varargs «accept-all», который принимает  Object... аргумент:

void acceptAll(Object... all);

Написание такого метода приносит небольшое чувство JavaScript в экосистему Java. Конечно, вы, вероятно, хотите ограничить фактический тип чем-то более ограниченным в реальной ситуации, например  String.... И поскольку вы не хотите ограничивать слишком много, вы можете подумать, что это хорошая идея заменить Object на общий T:

void acceptAll(T... all);

Но это не так. T всегда может быть выведен на объект. На самом деле, вы можете просто не использовать дженерики с вышеуказанными методами. Что еще более важно, вы можете подумать, что можете перегружать вышеуказанный метод, но не можете:

void acceptAll(T... all);
void acceptAll(String message, T... all);

Это выглядит так, как будто вы можете дополнительно передать сообщение String методу. Но что происходит с этим вызовом здесь?

acceptAll("Message", 123, "abc");

Компилятор будет считать  <? extends Serializable & Comparable<?>> для  T, что делает вызов неоднозначен!

Таким образом, всякий раз, когда у вас есть подпись «принять все» (даже если она является общей), вы никогда больше не сможете безопасно ее перегрузить. Потребителям API может просто повезти, если «случайно» выберет компилятор «правильный» наиболее конкретный метод. Но они также могут быть обмануты в использовании метода accept-all или вообще не могут вызывать какой-либо метод.

Правило : по возможности избегайте подписей «принять все». И если вы не можете, никогда не перегружайте такой метод.

Вывод

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

Оставайтесь с нами для более 10 лучших списков на эту тему!