Содержание
Примечание редактора : в 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"); } }
В зависимости от выбранной вами схемы ведения журнала вы можете использовать одну из следующих аннотаций:
-
@Log4J
— использует каркас Log4j -
@Log4J2
— использует каркас Log4j2 -
@Slf4J
— использует фреймворк SLF4J -
@CommonsLog
— использует@CommonsLog
Apache Commons -
@JBossLog
— использует JBoss Logging -
@Log
— использует стандартный Java@Log
журнала -
@XSlf4j
— использует расширенный регистратор SLF4J
Ленивые добытчики
Еще одна интересная функция 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 .