Статьи

Вне POJOs — еще десять способов уменьшить Boilerplate с Lombok

Примечание редактора : в SitePoint мы всегда ищем авторов, которые знают свое дело. Если вы опытный Java-разработчик и хотели бы поделиться своими знаниями, почему бы не написать для нас ?

Lombok — отличная библиотека, и ее главное преимущество состоит в том, как она очищает определения POJO . Но это не ограничивается этим вариантом использования! В этой статье я покажу вам шесть стабильных и четыре экспериментальных функции Lombok, которые могут сделать ваш Java-код еще чище. Они охватывают множество различных тем: от регистрации до средств доступа и от нулевой безопасности до служебных классов. Но у всех них есть одна общая черта: сокращение шаблонов для облегчения чтения и выразительности кода.

Логирование Аннотации

Сколько раз вы копировали определение логгера из одного класса в другой и забыли изменить имя класса?

public class LogTest { private static final Logger log = LoggerFactory.getLogger(NotTheLogTest.class); public void foo() { log.info("Info message"); } } 

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

 @Slf4j public class LogTest { public void foo() { log.info("Info message"); } } 

В зависимости от выбранной вами схемы ведения журнала вы можете использовать одну из следующих аннотаций:

Ленивые добытчики

Еще одна интересная функция Lombok — поддержка отложенной инициализации. Ленивая инициализация — это метод оптимизации, который используется для задержки дорогостоящей инициализации поля. Для правильной реализации вам необходимо защитить свою инициализацию от условий гонки, что приводит к тому, что код будет сложным для понимания и легко испорченным. С Lombok вы можете просто аннотировать поле с помощью @Getter(lazy = true) .

Чтобы использовать эту аннотацию, нам нужно определить приватное и конечное поле и присвоить ему результат вызова функции:

 public class DeepThought { @Getter(lazy = true) private final String theAnswer = calculateTheUltimateAnswer(); public DeepThought() { System.out.println("Building DeepThought"); } // This function won't be called during instance creation private String calculateTheUltimateAnswer() { System.out.println("Thinking for 7.5 million years"); return "42"; } } 

Если мы создадим экземпляр этого класса, значение не будет вычислено. Вместо этого theAnswer инициализируется только при первом обращении к нему. Предположим, мы используем класс DeepThought следующим образом:

 DeepThought deepThought = new DeepThought(); System.out.println("DeepThought is ready"); deepThought.getTheAnswer(); 

Тогда мы получим следующий вывод:

 Building DeepThought DeepThought is ready Thinking for 7.5 million years 

Как видите, значение окончательного ответа вычисляется не во время инициализации объекта, а только при первом обращении к его значению.

Безопаснее synchronized

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

Обычная схема предотвращения этого состоит в том, чтобы создать приватное поле специально для блокировок и вместо этого синхронизировать объект lock :

 public class Foo { private final Object lock = new Object(); public void foo() { synchronized(lock) { // ... } } } 

Но в этом случае вы не можете использовать ключевое слово synchronized на уровне метода, и это не делает ваш код более понятным.

Для этого случая Lombok предоставляет аннотацию @Synchronized . Его можно использовать аналогично ключевому слову synchronized , но вместо ссылки this он создает приватное поле и синхронизирует его:

 public class Foo { // synchronized on a generated private field @Synchronized public void foo() { // ... } } 

Если вам нужно синхронизировать разные методы для разных блокировок, вы можете @Synchronized имя объекта блокировки в аннотации @Synchronized но в этом случае вам нужно определить блокировки самостоятельно:

 public class Foo { // lock to synchronize on private Object barLock = new Object(); @Synchronized("barLock") public void bar() { // ... } } 

В этом случае Lombok будет синхронизировать метод bar на объекте barLock .

Нулевые проверки

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

 public class User { private String name; private String surname; public User(String name, String surname) { if (name == null) { throw new NullPointerException("name"); } if (surname == null) { throw new NullPointerException("surname"); } this.name = name; this.surname = surname; } public void setName(String name) { if (name == null) { throw new NullPointerException("name"); } this.name = name; } public void setSurname(String surname) { if (surname == null) { throw new NullPointerException("surname"); } this.surname = surname; } } 

Чтобы сделать это без проблем, вы можете использовать аннотацию Lombok @NotNull . Если вы пометите поле, метод или аргумент конструктора этим параметром, Lombok автоматически сгенерирует для вас код проверки на нуль.

 @Data @Builder public class User { @NonNull private String name; @NonNull private String surname; private int age; public void setNameAndSurname(@NonNull String name, @NonNull String surname) { this.name = name; this.surname = surname; } } 

Если @NonNull применяется к полю, Lombok добавит нулевую проверку как в установщике, так и в конструкторе. Кроме того, вы можете применять @NonNull не только к полям классов, но и к аргументам методов.

В результате каждая строка следующего фрагмента будет вызывать NullPointerException :

 User user = new User("John", null, 23); user.setSurname(null); user.setNameAndSurname(null, "Doe"); 

Тип Вывод с val

Это может быть довольно радикально, но Lombok позволяет вам добавлять Scala-подобную конструкцию к вашему Java-коду. Вместо ввода имени типа при создании новой локальной переменной вы можете просто написать val . Как и в Scala, тип переменной будет вычтен из правой части оператора присваивания, а переменная будет определена как final.

 import lombok.val; val list = new ArrayList<String>(); list.add("foo"); for (val item : list) { System.out.println(item); } 

Если вы считаете, что это очень не похоже на Java, возможно, вы захотите привыкнуть к этому, поскольку вполне возможно, что в будущем у Java будет похожее ключевое слово .

@SneakyThrows

@SneakyThrows делает невозможным немыслимое: позволяет генерировать отмеченные исключения без использования объявления throws . В зависимости от вашего мировоззрения, это либо исправляет худшее дизайнерское решение Java, либо открывает привкус Pandora’s Box в середине вашей базы кода.

@SneakyThrows пригодится, если, например, вам нужно вызвать исключение из метода с очень ограничительным интерфейсом, например Runnable :

 public class SneakyRunnable implements Runnable { @SneakyThrows(InterruptedException.class) public void run() { throw new InterruptedException(); } } 

Этот код компилируется, и если вы выполните метод run он выдаст экземпляр Exception . Нет необходимости RuntimeException его в RuntimeException как в противном случае.

Недостаток этой аннотации в том, что вы не можете поймать проверенное исключение, которое не объявлено. Следующий код не будет компилироваться:

 try { new SneakyRunnable().run(); } catch (InterruptedException ex) { // javac: exception java.lang.InterruptedException // is never thrown in body of corresponding try statement System.out.println(ex); } 

Экспериментальные Особенности

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

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

Методы расширения

Почти в каждом Java-проекте есть так называемые служебные классы — классы, которые содержат только статические методы. Часто их авторы предпочитают, чтобы методы были частью интерфейса, к которому они относятся. Например, методы в классе StringUtils работают со строками, и было бы хорошо, если бы их можно было вызывать непосредственно в экземплярах String .

 public class StringUtils { // would be nice to call "abc".isNullOrEmpty() // instead of StringUtils.isNullOrEmpty("abc") public static boolean isNullOrEmpty(String str) { return str == null || str.isEmpty(); } } 

Lombok имеет аннотацию для этого варианта использования, навеянную методами расширения других языков (например, в Scala ). Он добавляет методы из служебного класса в интерфейс объекта. Поэтому все, что нам нужно сделать, чтобы добавить isNullOrEmpty к интерфейсу String — это передать класс, который его определяет, в аннотацию @ExtensionMethod . Каждый статический метод в этом случае добавляется к классу своего первого аргумента:

 @ExtensionMethod({StringUtils.class}) public class App { public static void main(String[] args) { String s = null; String s2 = "str"; s.isNullOrEmpty(); // returns "true" s2.isNullOrEmpty(); // returns "false"; } } 

Мы также можем использовать эту аннотацию со встроенными служебными классами, такими как Arrays :

 @ExtensionMethod({Arrays.class}) public class App { public static void main(String[] args) { String[] arr = new String[] {"foo", "bar", "baz"}; List<String> list = arr.asList(); } } 

Это будет иметь эффект только внутри аннотированного класса, в этом примере App .

Конструкторы служебных классов

Говоря о служебных классах … Важно помнить о них, что нам нужно сообщить, что этот класс не должен создаваться. Обычный способ сделать это — создать приватный конструктор, который генерирует исключение (в случае, если кто-то использует отражение для его вызова):

 public class StringUtils { private StringUtils() { throw new UnsupportedOperationException( "Utility class should not be instantiated"); } public static boolean isNullOrEmpty(String str) { return str == null || str.isEmpty(); } } 

Этот конструктор отвлекает и громоздок. К счастью, у Lombok есть аннотация, которая генерирует это для нас:

 @UtilityClass class StringUtils { public static boolean isNullOrEmpty(String str) { return str == null || str.isEmpty(); } } 

Теперь мы можем вызывать методы этого служебного класса, как и раньше, и не можем его создать:

 StringUtils.isNullOrEmpty("str"); // returns "false" // javac: StringUtils() has private access in lomboktest.StringUtils new StringUtils(); 

Гибкие @Accessors

Эта функция не работает сама по себе, но используется для настройки того, как аннотации @Getter и @Setter генерируют новые методы. У этого есть три флага, которые настраивают его поведение:

  • chain : заставляет сеттеров возвращать this ссылку вместо void
  • fluent : Создает свободный интерфейс. Это имя всех методов получения и установки вместо getName и setName . Он также устанавливает chain в true, если не указано иное.
  • prefix : некоторые разработчики предпочитают начинать имена полей с префикса типа «f». Эта аннотация позволяет удалить указанный префикс из методов получения и установки, чтобы избежать имен методов, таких как fName или getFName .

Вот пример класса со свободным интерфейсом, где все поля имеют префикс «f»:

 @Accessors(fluent = true, prefix = "f") @Getter @Setter class Person { private String fName; private int fAge; } 

И вот как вы можете использовать это:

 Person person = new Person() .name("John") .age(34); System.out.println(person.name()); 

Поле по умолчанию

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

 public class Person { private final String name; private final int age; // constructor and getters } 

Чтобы помочь с этим, Lombok имеет экспериментальную аннотацию @FieldDefaults , которая определяет модификаторы, которые должны быть добавлены ко всем полям в классе. Следующий пример делает все поля общедоступными и окончательными:

 @FieldDefaults(level = AccessLevel.PUBLIC, makeFinal = true) @AllArgsConstructor public class Person { String name; int age; } 

Как следствие, вы можете получить доступ к name вне пакета:

 Person person = new Person("John", 34); System.out.println(person.name); 

Если у вас есть несколько полей, которые должны быть определены с разными аннотациями, вы можете переопределить их на уровне полей:

 @FieldDefaults(level = AccessLevel.PUBLIC, makeFinal = true) @AllArgsConstructor public class Person { // Make this field package private @PackagePrivate String name; // Make this field non final @NonFinal int age; } 

Выводы

Lombok предлагает широкий спектр инструментов, которые могут спасти вас от тысяч линий шаблонов и сделать ваши приложения более лаконичными и краткими. Основная проблема заключается в том, что некоторые члены вашей команды могут не согласиться с тем, какие функции использовать и когда их использовать. Аннотации типа @Log вряд ли вызовут серьезные разногласия, в то время как val и @SneakyThrows могут иметь много противников. Прежде чем вы начнете использовать Lombok, я бы предложил договориться о том, как вы собираетесь его использовать.

В любом случае имейте в виду, что все аннотации Lombok могут быть легко преобразованы в простой Java-код с помощью команды delombok .

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