参考:
https://www.cnblogs.com/death00/p/11722152.html
https://www.cnblogs.com/heyonggang/p/8638374.html
https://blog.csdn.net/baidu_35085676/article/details/89193416
什么Lombok
官方介绍如下:
Project Lombok makes java a spicier language by adding 'handlers' that know how to build and compile simple, boilerplate-free, not-quite-java code.
大致意思是 Lombok 通过增加一些"处理程序",可以让 Java 代码变得简洁、快速。
Lombok能以简单的注解形式来简化java代码,提高开发人员的开发效率。例如开发中经常需要写的javabean,都需要花时间去添加相应的getter/setter,也许还要去写构造器、equals等方法,而且需要维护,当属性多时会出现大量的getter/setter方法,这些显得很冗长也没有太多技术含量,一旦修改属性,就容易出现忘记修改对应方法的失误。
Lombok能通过注解的方式,在编译时自动为属性生成构造器、getter/setter、equals、hashcode、toString方法。出现的神奇就是在源码中没有getter和setter方法,但是在编译生成的字节码文件中有getter和setter方法。这样就省去了手动重建这些代码的麻烦,使代码看起来更简洁些。
Lombok的使用跟引用jar包一样,可以在官网(https://projectlombok.org/download)下载jar包,也可以使用maven添加依赖:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
<scope>provided</scope>
</dependency>
Lombok中常用额注解如下:
注解名称 | 功能 |
---|---|
@Setter | 自动添加类中所有属性相关的 set 方法 |
@Getter | 自动添加类中所有属性相关的 get 方法 |
@Builder | 使得该类可以通过 builder (建造者模式)构建对象 |
@RequiredArgsConstructor | 生成一个该类的构造方法,禁止无参构造 |
@ToString | 重写该类的toString()方法 |
@EqualsAndHashCode | 重写该类的equals()和hashCode()方法 |
@Data | 等价于上面的@Setter、@Getter、@RequiredArgsConstructor、@ToString、@EqualsAndHashCode |
@Getter和@Setter
在实体类中,为了增强数据的安全性和隐蔽性,通常会对数据和与数据有关的方法进行封装;
- 将类中的属性设置为private(私有的),只能本类才能访问,其他类都访问不了,如此就对信息进行了隐藏。
- 对每个属性提供对外的公共方法访问,也就是创建一对赋取值方法(getting方法、setting方法),用于对私有属性的访问。
@Getter和@Setter注解使用:
- 注解在属性上,表示为属性提供getting、setting方法。
- 注解在类上,表示为类的所有属性提供getting、setting方法。
样例:
import lombok.Getter;
import lombok.Setter;
@getter
@setter
public class User {
String name;
String address;
}
@toString
注解在类上。
Lombok会生成一个toString()方法,默认情况下,会输出类名、所有属性(会按照属性定义顺序),用逗号来分割。
@ToString(exclude="id")
public class ToStringExample {
private static final int STATIC_VAR = 10;
private String name;
private Shape shape = new Square(5, 10);
private String[] tags;
private int id;
public String getName() {
return this.getName();
}
@ToString(callSuper=true, includeFieldNames=true)
public static class Square extends Shape {
private final int width, height;
public Square(int width, int height) {
this.width = width;
this.height = height;
}
}
}
- @ToString的includeFieldNames属性默认为true,可以为toString()方法的输出增加一些清晰度(即打印出字段名称)。
- 默认情况下,将打印所有非静态字段。如果要跳过某些字段,可以使用exclude属性。
- 也可精确指定要被toString的字段,首先设置设onlyExplicitlyIncluded属性为true,然后使用include属性标记要包含的字段。
- 通过设置callSuper属性为true,可以将超类实现toString的输出包含到当前输出中。请注意,toString()的默认实现java.lang.Object几乎没有意义,因此除非你主动继承了另一个类,否则你这样做没有意义。
- 可以更改用于标识成员的名称@ToString.Include(name = “some other name”),name相当于给字段起别名;
- 也可以通过更改成员的打印顺序@ToString.Include(rank = -1)。没有等级的成员被认为是等级0,等级数字大的成员首先被打印,等级相同的成员以它们在源文件中出现的顺序打印。
@Builder
@Builder注解在类上。
@Builder使用创建者模式又叫建造者模式。简单来说,就是一步步创建一个对象,它对用户屏蔽了里面构建的细节,但却可以精细地控制对象的构造过程。
@Builder注解的类,可以让你以下面显示的那样调用你的代码,来初始化你的实例对象:
@Builder
public class Card {
private int id;
private String name;
private boolean sex;
}
Card card = Card.builder().id(10).name("dasd").sex(true).build();
@Builder的使用:
- 可以放在类,构造函数或方法上。虽然放在类上和放在构造函数上这两种模式是最常见的用例,但@Builder最容易用放在方法的用例来解释。
@Builder的优点
- 不需些太多的set方法来定义属性内容
- 写法更优雅
@Builder注解对类做了啥?
我们反编译上面定义的Card类,可以看到:
public class Card {
private int id;
private String name;
private boolean sex;
Card(int id, String name, boolean sex) {
this.id = id;
this.name = name;
this.sex = sex;
}
public static Card.CardBuilder builder() {
return new Card.CardBuilder();
}
public static class CardBuilder {
private int id;
private String name;
private boolean sex;
CardBuilder() {
}
public Card.CardBuilder id(int id) {
this.id = id;
return this;
}
public Card.CardBuilder name(String name) {
this.name = name;
return this;
}
public Card.CardBuilder sex(boolean sex) {
this.sex = sex;
return this;
}
public Card build() {
return new Card(this.id, this.name, this.sex);
}
public String toString() {
return "Card.CardBuilder(id=" + this.id + ", name=" + this.name + ", sex=" + this.sex + ")";
}
}
}
- 创建一个名为CardBuilder的内部静态类,并具有和实体类相同的属性。(称为构建器)
- 在构建器中:实体类中的所有属性和未初始化final字段,都会在“构建器”中创建对应属性。
- 在构建器中:创建一个无参的default构造函数。
- 在构建器中:对于实体类中的每个参数,都会对应创建类似于setter的方法,只不过方法名与该参数名相同。返回值是“构建器”本身。
- 在构建器中:创建一个build()方法,调用此方法,就会根据设置的值进行创建“实体类对象”。
- 在构建器中:同时也会生成一个toString()方法。
- 在实体类中:会创建一个builder()方法,它的目的是用来创建构建器。
所以,如下创建实例的过程就可以解释为:
- Card.builder()创建一个“构建器”.
- 通过setter方法(方法名就是属性名),设置“构建器”中属性值。
- 调用“构建器”的build()方法,根据“构建器”创建实体类实例。
Card card = Card.builder().id(10).name("dasd").sex(true).build();
@Builder的缺点
最明显的一点,在生成实体类实例之前,实际上是先创建了一个“构建器”实例,这样很明显额外占用了内存。
@Builder的toBuilder参数
如果使用格式是:
@Builder(toBuilder = true)
则“构造器”中会新生成一个toBuilder方法,允许你将一个实例化好的Card更新字段生成新的Card实例。
public Card.CardBuilder toBuilder() {
return (new Card.CardBuilder()).id(this.id).name(this.name).sex(this.sex);
}
@EqualsAndHashCode
- 此注解会生成equals(Object other) 和 hashCode()方法。
- 它默认使用非静态,非瞬态的属性
- 可通过参数exclude排除一些属性
- 可通过参数of指定仅使用哪些属性
- 它默认仅使用该类中定义的属性且不调用父类的方法
- 可通过callSuper=true解决上一点问题。让其生成的方法中调用父类的方法。
@EqualsAndHashCode(exclude={"id", "shape"})
public class EqualsAndHashCodeExample {
private transient int transientVar = 10;
private String name;
private double score;
private Shape shape = new Square(5, 10);
private String[] tags;
private int id;
public String getName() {
return this.name;
}
@EqualsAndHashCode(callSuper=true)
public static class Square extends Shape {
private final int width, height;
public Square(int width, int height) {
this.width = width;
this.height = height;
}
}
}
四、@Data
注解在类上。提供类所有属性的getting和setting方法,此外还提供了equals、canEqual、hashCode、toString方法。
@Data注解是SpringBoot提供的更广泛注解,其包含了@Getter、@Setter、@ToString、@EqualsAndHashCode等
@Data注解的功能:
- 为类提供读写功能,从而不用写getter、setter方法。
- 为类提供 equals()、hashCode()、toString() 方法。
@Data注解使用:
- 在maven库中添加依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
- 实体类上添加@Data注解即可生效
使用@Data注解自动生成的equals()问题
@Data是包含了@EqualsAndHashCode的功能,那么它究竟是如何重写equals()和hashCode()方法的呢?
我们定义一个类TestA:
@Data
public class TestA {
String oldName;
}
我们将其编译后的 class 文件进行反编译:
public class TestA {
String oldName;
public TestA() {
}
public String getOldName() {
return this.oldName;
}
public void setOldName(String oldName) {
this.oldName = oldName;
}
public boolean equals(Object o) {
// 判断是否是同一个对象
if (o == this) {
return true;
}
// 判断是否是同一个类
else if (!(o instanceof TestA)) {
return false;
} else {
TestA other = (TestA) o;
if (!other.canEqual(this)) {
return false;
} else {
// 比较类中的属性(注意这里,只比较了当前类中的属性)
Object this$oldName = this.getOldName();
Object other$oldName = other.getOldName();
if (this$oldName == null) {
if (other$oldName != null) {
return false;
}
} else if (!this$oldName.equals(other$oldName)) {
return false;
}
return true;
}
}
}
protected boolean canEqual(Object other) {
return other instanceof TestA;
}
public int hashCode() {
int PRIME = true;
int result = 1;
Object $oldName = this.getOldName();
int result = result * 59 + ($oldName == null ? 43 : $oldName.hashCode());
return result;
}
public String toString() {
return "TestA(oldName=" + this.getOldName() + ")";
}
}
针对其equals()方法,当它进行属性比较时,其实只比较了当前类中的属性。如果你不信的话,我们再来创建一个类TestB,它是TestA的子类:
@Data
public class TestB extends TestA {
private String name;
private int age;
}
我们将其编译后的 class 文件进行反编译:
public class TestB extends TestA {
private String name;
private int age;
public TestB() {
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (!(o instanceof TestB)) {
return false;
} else {
TestB other = (TestB)o;
if (!other.canEqual(this)) {
return false;
} else {
// 注意这里,真的是只比较了当前类中的属性,并没有比较父类中的属性
Object this$name = this.getName();
Object other$name = other.getName();
if (this$name == null) {
if (other$name == null) {
return this.getAge() == other.getAge();
}
} else if (this$name.equals(other$name)) {
return this.getAge() == other.getAge();
}
return false;
}
}
}
protected boolean canEqual(Object other) {
return other instanceof TestB;
}
public int hashCode() {
int PRIME = true;
int result = 1;
Object $name = this.getName();
int result = result * 59 + ($name == null ? 43 : $name.hashCode());
result = result * 59 + this.getAge();
return result;
}
public String toString() {
return "TestB(name=" + this.getName() + ", age=" + this.getAge() + ")";
}
}
按照代码的理解,如果两个子类对象,其子类中的属性相同、父类中的属性不同时,利用equals()方法时,依旧会认为这两个对象相同,测试一下:
public static void main(String[] args) {
TestB t1 = new TestB();
TestB t2 = new TestB();
t1.setOldName("123");
t2.setOldName("12345");
String name = "1";
t1.name = name;
t2.name = name;
int age = 1;
t1.age = age;
t2.age = age;
System.out.println(t1.equals(t2));
System.out.println(t2.equals(t1));
System.out.println(t1.hashCode());
System.out.println(t2.hashCode());
System.out.println(t1 == t2);
System.out.println(Objects.equals(t1, t2));
}
结果为:
true
true
6373
6373
false
true
So,使用@Data时候要注意。
比如,有多个类有相同的部分属性,把它们定义到父类中,恰好id(数据库主键)也在父类中,那么就会存在部分对象在比较时,它们并不相等,却因为lombok自动生成的equals(Object other) 和 hashCode()方法判定为相等,从而导致出错。
修复此问题的方法:
- 使用@Getter @Setter @ToString代替@Data,并自定义equals(Object other) 和 hashCode()方法,比如有些类只需要判断主键id是否相等即足矣。
- 或者使用在使用@Data时同时加上@EqualsAndHashCode(callSuper=true)注解。
@NoArgsConstructor, @RequiredArgsConstructor and @AllArgsConstructor
- @NoArgsConstructor
注解在类上,为类提供一个全参的构造方法。
注:
- 当类中有 final 字段没有被初始化时,编译器会报错,此时可用 @NoArgsConstructor(force = true),然后就会为没有初始化的final字段设置默认值 0/false/null。
- 对于具有约束的字段(例如 @NonNull 字段),不会生成检查或分配,因此请注意,正确初始化这些字段之前,这些约束无效。
- @AllArgsConstructor
注解在类上,为类提供一个无参的构造方法。
默认生成的方法是 public 的,如果要修改方法修饰符可以设置 AccessLevel 的值。如:
@AllArgsConstructor(access = AccessLevel.PROTECTED)
3、@RequiredArgsConstructor
注解在类上,会生成构造方法(可能带参数也可能不带参数)。
注意:
- 如果带参数,这参数只能是以 final 修饰的未经初始化的字段或者是以 @NonNull 注解的未经初始化的字段。
- 该注解还可以用 @RequiredArgsConstructor(staticName="methodName") 生成一个指定名称的静态方法,返回一个调用相应的构造方法产生的对象。
如:
// 使用注解
@RequiredArgsConstructor(staticName = "hangge")
public class Shape {
private int x;
@NonNull
private double y;
@NonNull
private String name;
}
// 不使用注解
public class Shape {
private int x;
private double y;
private String name;
public Shape(double y, String name){
this.y = y;
this.name = name;
}
public static Shape hangge(double y, String name){
return new Shape(y, name);
}
}
@NonNull
@NonNull可以标注在方法、字段、参数之上,表示对应的值不可以为空。Lombok会生成一个非空的声明,可用于校验参数,能帮助避免空指针。
import lombok.NonNull;
public class NonNullExample extends Something {
private String name;
public NonNullExample(@NonNull Person person) {
super("Hello");
this.name = person.getName();
}
}
@Cleanup
该注解能帮助我们自动调用close()方法,很大的简化了代码。
import lombok.Cleanup;
public class CleanupExample {
public static void main(String[] args) throws IOException {
@Cleanup InputStream in = new FileInputStream(args[0]);
@Cleanup OutputStream out = new FileOutputStream(args[1]);
byte[] b = new byte[10000];
while (true) {
int r = in.read(b);
if (r == -1) break;
out.write(b, 0, r);
}
}
}
Lombok的优缺点
- 优点:
- 能通过注解的形式自动生成构造器、getter/setter、equals、hashcode、toString等方法,提高了一定的开发效率。
- 让代码变得简洁,不用过多的去关注相应的方法
- 属性做修改时,也简化了维护为这些属性所生成的getter/setter方法等
- 缺点:
- 不支持多种参数构造器的重载
- 虽然省去了手动创建getter/setter方法的麻烦,但大大降低了源代码的可读性和完整性,降低了阅读源代码的舒适度
Lombok虽然有很多优点,但Lombok更类似于一种IDE插件,项目也需要依赖相应的jar包。Lombok依赖jar包是因为编译时要用它的注解,为什么说它又类似插件?因为在使用时,eclipse或IntelliJ IDEA都需要安装相应的插件,在编译器编译时通过操作AST(抽象语法树)改变字节码生成,变向的就是说它在改变java语法。它不像spring的依赖注入或者mybatis的ORM一样是运行时的特性,而是编译时的特性。这里我个人最感觉不爽的地方就是对插件的依赖!因为Lombok只是省去了一些人工生成代码的麻烦,但IDE都有快捷键来协助生成getter/setter等方法,也非常方便。
知乎上有位大神发表过对Lombok的一些看法:
这是一种低级趣味的插件,不建议使用。JAVA发展到今天,各种插件层出不穷,如何甄别各种插件的优劣?能从架构上优化你的设计的,能提高应用程序性能的 ,
实现高度封装可扩展的..., 像lombok这种,像这种插件,已经不仅仅是插件了,改变了你如何编写源码,事实上,少去了代码你写上去又如何?
如果JAVA家族到处充斥这样的东西,那只不过是一坨披着金属颜色的屎,迟早会被其它的语言取代。
虽然话糙但理确实不糙,试想一个项目有非常多类似Lombok这样的插件,个人觉得真的会极大的降低阅读源代码的舒适度。
虽然非常不建议在属性的getter/setter写一些业务代码,但在多年项目的实战中,有时通过给getter/setter加一点点业务代码,能极大的简化某些业务场景的代码。所谓取舍,也许就是这时的舍弃一定的规范,取得极大的方便。
我现在非常坚信一条理念,任何编程语言或插件,都仅仅只是工具而已,即使工具再强大也在于用的人,就如同小米加步枪照样能赢飞机大炮的道理一样。结合具体业务场景和项目实际情况,无需一味追求高大上的技术,适合的才是王道。
Lombok有它的得天独厚的优点,也有它避之不及的缺点,熟知其优缺点,在实战中灵活运用才是王道。