О том, как замечательно project lombok сам генерирует геттеры/сеттеры и конструкторы, я уже писал. Но у класса Object есть ещё методы и их тоже можно переопределять автоматически.
equals() и hashCode()
Эти методы глубоко пересечены, поэтому генерируются вдвоём, аннотацией @EqualsAndHashCode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | /** * Sample user entity */ @FieldDefaults(level = AccessLevel.PRIVATE) @AllArgsConstructor @ToString @EqualsAndHashCode(exclude = {"value"}) public class User { /** * Id. */ @Getter Long id; /** * User name. */ @Getter @NonNull String name; /** * Some entity, that you can't set. */ @Getter @Setter @NonFinal BigDecimal value; } |
Параметр exclude указывает, какие поля необходимо исключить из сгенерированных функций. По умолчанию так же исключаются все static и transient поля класса. Можно и наоборот, указать список полей, требующих включения, перечислив их в параметре of.
1 2 3 4 5 6 7 8 9 10 11 | public class UserTest { @Test public void testEquals() { User testedObject = new User(1L, "TEST", BigDecimal.ONE); User oppositeObject = new User(1L, "TEST", BigDecimal.TEN); assertEquals(oppositeObject, testedObject); System.out.println(testedObject.toString()); System.out.println(oppositeObject.toString()); } } |
Аннотация @EqualsAndHashCode имеет ещё два параметра: первый, doNotUseGetters, указывает, как методы equals() и hashCode() должны получать данные, обращаясь напрямую к полям класса или вызывая соответствующие геттеры. Второй параметр, callSuper, гораздо интереснее — он указывает, надо ли при генерации методов equals() и hashCode() вызывать в них соответствующие методы базового класса.
Кроме того, @EqualsAndHashCode генерирует метод canEqual(), проверяющий, можно ли вообще сравнивать эти объекты. Потребность в canEqual() возникает из-за наследования классов: предположим у нас есть класс Point и класс ColoredPoint extends Point. Очевидно, что если мы сравним экземпляр ColoredPoint с экземпляром Point, они будут не эквивалентны. Но. Если мы сравним наоборот, экземпляр Point с ColoredPoint, то они внезапно станут эквивалентны, потому что Point сравнивает только те данные, о которых знает.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class Point { int x; int y; //some manually written equals method } class ColoredPoint extend Point{ int color; //some manually written equals method } class EqualsTest { @Test public void equalityFail() { ColoredPoint cp = new ColoredPoint(1,1,1); Point p = new Point(1,1); assertFalse(cp.equals(p)); assertTrue(p.equals(cp)); } } |
canEqual() проверяет, совпадает ли тип объектов и корректно ли их сравнивать с учётом их положения в иерархии наследования.
toString()
Но вернёмся к lombok. В тесте класса User выводится результат toString():
1 2 | User(id=1, name=TEST, value=1) User(id=1, name=TEST, value=10) |
Такой красивый toString() генерирует аннотация @ToString, которая принимает те же самые параметры что и @EqualsAndHashCode и которые, очевидно, имеют точно такую же смысловую нагрузку. В поведении @ToString есть одно отличие — transient поля по умолчанию включены в вывод.
@Data
Но всё таки, даже с учётом аннотаций от project lombok, код класса User выглядит перегруженным. На простой класс с тремя полями у нас 10(!) аннотаций. Может быть, можно с этим что-то сделать? Аннотация @Data заменяет (почти) все вышеперечисленные аннотации! Она добавляет @Getter/@Setter ко всем полям, добавляет @EqualsAndHashCode и @ToString и создаёт конструктор для final и @NonNull полей. Поведение по умолчанию всех вышеперечисленных аннотаций сохраняется: геттеры/сеттеры будут публичными, static поля будут исключены из equals(), hashCode() и toString(), а transient поля будут исключены из equals() и hashCode(). Аннотации можно передать параметр of, чтобы сгенерировать статический метод для создания класса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | /** * Sample account entity. */ @Data(staticConstructor = "create") public class Account { /** * Id. */ @NonNull Long id; /** * Account owner. */ @NonNull User owner; /** * Account's value. */ BigDecimal amount; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | public class AccountTest { @Test public void testEquals() { User user = new User(1L, "TEST", BigDecimal.ONE); Account testedObject = Account.create(1L, user); Account opposieObject = Account.create(1L, user); assertEquals(testedObject, opposieObject); System.out.println(testedObject.toString()); System.out.println(opposieObject.toString()); } @Test public void testNotEquals() { User user = new User(1L, "TEST", BigDecimal.ONE); Account testedObject = Account.create(1L, user); Account opposieObject = Account.create(1L, user); testedObject.setAmount(BigDecimal.ONE); opposieObject.setAmount(BigDecimal.TEN); assertNotEquals(testedObject, opposieObject); System.out.println(testedObject.toString()); System.out.println(opposieObject.toString()); } } |
@Value и @Wither
@Value это @Data для неизменяемых (immutable) классов. Вместо того, чтобы художественно расставлять руками final, просто пишем @Value и получаем класс, в котором все поля private final, для них сгенерированны геттеры, equals(), hashCode(), toString() и конструктор, который тоже можно заменить статическим методом.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | /** * Sample immutable entity. */ @Value public class Group { /** * Id. */ Long id; /** * User name. */ @Wither String name; } |
Сеттеров такому классу, конечно же, не полагается. Но зато у него есть @Wither, который как сеттер, только для неизменяемых классов. Для переменных с аннотацией @Wither будет сгенерирован ммм виззер??? который вернёт копию объекта, с новым значением переменной:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public class GroupTest { @Test public void testEquals() { Group testedObject = new Group(1L, "TEST"); Group oppositeObject = new Group(1L, "TEST"); assertEquals(testedObject, oppositeObject); System.out.println(testedObject.toString()); System.out.println(oppositeObject.toString()); } @Test public void testNotEquals() { Group testedObject = new Group(1L, "TEST"); Group oppositeObject = testedObject.withName("NEW"); assertNotEquals(testedObject, oppositeObject); assertFalse(testedObject == oppositeObject); System.out.println(testedObject.toString()); System.out.println(oppositeObject.toString()); } } |
Код примера, за исключением Point и ColoredPoint, доступен на github.