О том, как замечательно 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.