注意: 开始学习Lombok前至少需要保证完成JavaSE课程中的注解部分,本课程采用的版本为Java17。
我们发现,在以往编写项目时,尤其是在类进行类内部成员字段封装时,需要编写大量的get/set方法,这不仅使得我们类定义中充满了get和set方法,同时如果字段名称发生改变,又要挨个进行修改,甚至当字段变得很多时,构造方法的编写会非常麻烦:
public class Account {
private int id;
private String name;
private int age;
private String gender;
private String password;
private String description;
...
}
依次编写类中所有字段的Getter和Setter还有构造方法简直是场灾难,后期字段名字变了甚至还得一个一个修改!
只不过这种问题在Java17之后得到的一定程度的解决,我们可以使用记录类型来快速得到一个自带构造方法、Getter以及重写ToString等方法的类:
public record Account(int id, String name, int age, String gender, String password, String description) {
}
只不过,虽然记录类型不需要我们额外动手编写一部分代码了,但是它依然不够灵活,同时最关键的Setter也并未生成,所以说它依然没有大规模使用。
那么有没有一种更加完美的方案来处理这种问题呢?通过使用Lombok(小辣椒)就可以做到,它就是专门用于简化 Java 编程中的样板代码的,它通过注解的方式,能够自动生成常见的代码,比如构造函数、getter 和 setter 方法、toString 方法、equals 和 hashCode 方法等,从而使开发者能够专注于业务逻辑,而不必重复编写冗长的代码。官网地址:https://projectlombok.org
使用Lombok后,你的代码就会变成这样:
@Getter
@Setter
@AllArgsConstructor
public class Student {
private Integer sid;
private String name;
private String sex;
}
使用Lombok提供的注解,即可一步到位,直接生成对应的Getter和Setter方法,包括构造方法、toString等都可以全包。
首先我们需要导入Lombok的jar依赖,和jdbc依赖是一样的,放在项目目录下直接导入就行了。可以在这里进行下载:https://projectlombok.org/download,如果你已经学完了Maven,那么也可以直接使用Maven导入:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
<scope>provided</scope>
</dependency>
然后我们要安装一下Lombok插件,由于IDEA终极版默认都安装了Lombok的插件,因此直接导入依赖后就可以使用了。
现在我们在需要测试的实体类上添加@Data
注解试试看:
import lombok.Data;
@Data
public class Account {
private int id;
private String name;
private int age;
private String gender;
private String password;
private String description;
}
接着测试一下是否可以直接使用,@Data
会为我们的类自动生成Getter和Setter方法,我们可以直接调用:
public static void main(String[] args) {
Account account = new Account();
account.setId(10);
}
如果运行后出现要求启用Lombok注解处理,请务必开启,否则会出现错误:
如果在启用注解处理后依然在运行时存在找不到符号问题,建议重启IDEA或是重启电脑后再试。
那么Lombok是如何做到一个注解就包揽了代码生成工作的呢?这里又要说到我们Java的编译过程,它可以分成三个阶段:
Lombok会在上述的第二阶段,执行*lombok.core.AnnotationProcessor*,它所做的工作就是我们上面所说的,修改语法树,并将注解对应需要生成的内容全部添加到类文件中,这样,我们即使没有在源代码中编写的内容,也会存在于生成出来的class文件中。
我们接着来为大家介绍Lombok提供的主要注解。
我们还是从类属性相关注解开始介绍,首先是@Getter
,它用于自动生成Getter方法,定义如下:
@Target({ElementType.FIELD, ElementType.TYPE}) //此注解可以添加在字段或是类型上
@Retention(RetentionPolicy.SOURCE)
public @interface Getter {
AccessLevel value() default AccessLevel.PUBLIC; //自动生成的Getter的访问权限级别
AnyAnnotation[] onMethod() default {}; //用于添加额外的注解
boolean lazy() default false; //懒加载功能
...
}
它最简单的用法,就是直接添加到类上或是字段上:
@Getter //添加到类上时,将为类中所有字段添加Getter方法
public class Account {
private int id;
@Getter //当添加到字段上时,仅对此字段生效
private String name;
private int age;
}
假设我们这里将@Getter
编写在类上,那么生成得到的代码为:
import lombok.Generated;
public class Account {
private int id;
private String name;
private int age;
public Account() {}
@Generated
public int getId() { //自动为所有字段生成了对应的Getter方法
return this.id;
}
...省略
}
是不是感觉非常方便?而且使用起来也很灵活。注意它存在一定的命名规则,如果该字段名为foo
,则将其直接按照字段名称命名为getFoo
,但是注意,如果字段的类型为boolean
,则会命名isFoo
,这是比较特殊的地方。
我们接着来看Getter注解的其他属性,首先是访问权限,默认情况下为public,但是有时候可能我们只希望生成一个private的get方法,此时我们可以对其进行修改:
这里我们尝试将其更改为:
@Getter(AccessLevel.PRIVATE) //为所有字段生成private的Getter方法
public class Account {
private int id;
@Getter(AccessLevel.NONE) //不为name生成Getter方法,字段上的注解优先级更高
private String name;
private int age;
}
得到的结果就是:
public class Account {
private int id;
private String name;
private int age;
public Account() {
}
private int getId() { //得到的就是private的Getter方法
return this.id;
}
...
}
我们接着来看它的onMethod
属性,这个属性用于添加一些额外的注解到生成的方法上,比如我们要为Getter方法添加一个额外的@Deprecated
表示它不推荐使用,那么:
@Getter
public class Account {
private int id;
@Getter(onMethod_ = { @Deprecated })
private String name;
private int age;
}
此时得到的代码为:
public class Account {
...
/** @deprecated */
@Deprecated //由Lombok额外添加的注解
public String getName() {
return this.name;
}
}
最后我们再来看看它的lazy
属性,这是用于控制懒加载
懒加载就是在一开始的时候此字段没有值,当我们需要的时候再将值添加到此处。
只不过它有一些要求,我们的字段必须是private且final的:
public class Account {
@Getter(lazy = true)
private final String name = "你干嘛";
}
生成的代码如下:
public class Account {
//这里会自动将我们的字段修改为AtomicReference原子类型,以防止多线程环境下出现的问题
private final AtomicReference<Object> name = new AtomicReference();
...
//当我们调用getName才会去初始化字段的值,为了保证初始化只进行一次,整个过程与懒汉式单例模式一致
public String getName() {
Object $value = this.name.get();
if ($value == null) { //判断值是否为null,如果是则需要进行懒初始化
synchronized(this.name) { //对我们的字段加锁,保证同时只能进一个
$value = this.name.get();
if ($value == null) { //再次进行一次判断,因为有可能其他线程后进入
String actualValue = "你干嘛";
$value = "你干嘛" == null ? this.name : "你干嘛";
this.name.set($value);
}
}
}
//返回得到的结果
return (String)($value == this.name ? null : $value);
}
}
有关原子类相关知识点,可以在JUC篇视频教程中进行学习,有关单例模式相关知识点,可以在Java设计模式篇视频教程中学习,这里不再赘述。我们作为使用者来说,只需要知道懒加载其实就是将字段的值延迟赋值给它了。比如下面这种场景就很适合:
public class Account {
@Getter(lazy = true)
private final String name = initValue();
private String initValue() {
System.out.println("我不希望在对象创建时就执行");
return "666";
}
}
至此,有关@Getter
注解相关的内容我们就介绍完毕了,我们接着来看@Setter
注解,它与@Getter
非常相似,用于生成字段对应的Setter方法:
public class Account {
@Setter
private String name;
}
得到结果为:
public class Account {
private String name;
...
public void setName(String name) { //自动生成一个setter方法
this.name = name;
}
}
可以看到它同样会根据字段名称来生成Setter方法,其他参数与@Getter
用法一致,这里就不重复介绍了。唯一一个不一样的参数为onParam
,它可以在形参上的额外添加的自定义注解。
最后需要注意的是,如果我们手动编写了对应字段的Getter或是Setter方法(按照上述命名规则进行判断)那么Lombok提供的注解将不会生效,也不会覆盖我们自己编写的方法:
public class Account {
@Setter
private String name;
public void setName(int name) { //即使上面添加Setter注解,这里也不会被覆盖,但是仅限于同名不同参的情况
System.out.println("我是自定义的");
this.name = name;
}
}
如果出现同名不同参数的情况导致误判,我们也可以使用@Tolerate
注解使Lombok忽略它的存在,继续生成。
Lombok也可以为我们自动生成对应的构造方法,它提供了三个用于处理构造方法的注解,我们来依次认识一下它们。首先是最简单的@AllArgsConstructor
,它用于为类中所有字段生成一个构造方法:
@AllArgsConstructor
public class Account {
private int id;
private String name;
private int age;
}
它只能添加到类上,之后生成:
public class Account {
private int id;
private String name;
private int age;
public Account(int id, String name, int age) { //自动生成一个携带所有参数的构造方法
this.id = id;
this.name = name;
this.age = age;
}
}
我们接着来看它的一些属性:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface AllArgsConstructor {
//用于生成一个静态构造方法
String staticName() default "";
//用于在构造方法上添加额外的注解
AnyAnnotation[] onConstructor() default {};
//设置构造方法的访问权限级别
AccessLevel access() default lombok.AccessLevel.PUBLIC;
...
}
其中onConstructor
和access
和我们上一节介绍的内容差不多,这里就不再多说了,我们直接来看它的staticName
属性,它主要用于生成静态构造方法,现在很多类都包含一些静态构造方法,比如:
List<String> strings = List.of("A", "B", "C", "D");
这里的List.of()
其实就是一种静态构造方法,通常用于快速构造对应的类对象,我们也可以像这样去编写,只需要将staticName
设置一个名字即可:
@AllArgsConstructor(staticName = "with")
public class Account {
...
}
此时得到结果为:
public class Account {
...
//强制生成一个带全部参数的private方法,不可修改
private Account(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public static Account with(int id, String name, int age) {
return new Account(id, name, age);
}
}
我们在使用时,需要调用此静态构造方法来创建对象:
Account account = Account.with(1, "小明", 18);
官方说这种方式非常适合用作泛型的类型推断,简化代码,比如 MapEntry.of("foo", 5)
而不是更长的new MapEntry<String, Integer>("foo", 5)
现在有了全参构造,但是此时我们又需要一个无参构造怎么办呢,Lombok早就为我们准备好了,我们只需要再添加一个@NoArgsConstructor
注解即可:
@NoArgsConstructor
@AllArgsConstructor
public class Account {
...
}
这样我们就可以得到一个既有全参构造又有无参构造的类:
public class Account {
...
public Account() {}
public Account(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
}
但是注意,由于这里会生成一个无参构造,当我们使用@NoArgsConstructor
时类中不允许存在final类型的字段,否则会出现错误:
只不过,@NoArgsConstructor
有一个force
属性,它可以在创建无参构造时,为final类型的字段给一个默认值,这样就可以同时存在了:
@NoArgsConstructor(force = true) //强制开启
@AllArgsConstructor
public class Account {
private final int id; //字段必须初始化
private String name;
private int age;
}
得到的结果为:
public class Account {
...
public Account() {
this.id = 0; //强行生成一个无参构造,但是这里也会为属性设置一个默认值,不然编译会报错
}
...
}
我们来看最后一个构造相关的注解,@RequiredArgsConstructor
用于生成那些需要初始化的参数的构造方法,也就是说类中哪些字段为final,它就只针对这些字段生成对应的构造方法,比如:
@RequiredArgsConstructor
public class Account {
private final int id;
private String name;
private final int age;
}
生成的结果为:
public class Account {
...
public Account(int id, int age) { //只为fianl字段id和age生成了对应的构造方法
this.id = id;
this.age = age;
}
}
有关构造函数相关内容我们就介绍到这里。
我们也可以使用Lombok为类生成toString、equals以及hashCode等方法,我们首先来看最简单的toString方法生成,只需要在类上添加一个@ToString
注解即可:
@ToString
@AllArgsConstructor
public class Account {
private int id;
private String name;
private int age;
}
public static void main(String[] args) {
Account account = new Account(1, "小明", 18);
System.out.println(account); //尝试直接打印
}
这样就可以直接得到一个格式化好的字符串:
我们来看看生成出来的内容:
public class Account {
...
public String toString() { //自动为每个字段都生成打印
return "Account(id=" + this.id + ", name=" + this.name + ", age=" + this.age + ")";
}
...
}
是不是感觉非常方便?一个注解就搞定了这么繁杂的代码。我们来看看它有哪些参数:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface ToString {
//是否在打印的内容中带上对应字段的名字
boolean includeFieldNames() default true;
//用于排除不需要打印的字段(这种用法很快会被移除,不建议使用)
String[] exclude() default {};
//和上面相反,设置哪些字段需要打印,默认打印所有(这种用法很快会被移除,不建议使用)
String[] of() default {};
//不仅为当前类中所有字段生成,同时还调用父类toString进行拼接
boolean callSuper() default false;
//默认情况下生成的toString会尽可能使用get方法获取字段值,我们也可以手段关闭这个功能
boolean doNotUseGetters() default false;
//开启后将只为字段或get方法上添加了@ToString.Includ注解的内容生成toString方法,白名单模式
boolean onlyExplicitlyIncluded() default false;
/**
* 用于排除toString中的字段
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface Exclude {}
/**
* 用于手动包含toString中的字段
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface Include {
//配置字段打印顺序的优先级
int rank() default 0;
//配置一个自定义的字段名称进行打印
String name() default "";
}
}
我们首先来看@ToString
对于get方法的特殊机制,它会尽可能使用我们自定义的get方法获取字段的值,命名规则判定和之前一样:
@ToString
@AllArgsConstructor
public class Account {
...
public String getName() {
return name + "同学"; //编写了一个自定义的getName方法
}
}
打印得到的结果为:
有时候可能我们希望的是直接打印字段原本的值,所以为了避免这种情况,我们可以手动为@ToString
配置doNotUseGetters属性:
@ToString(doNotUseGetters = true)
@AllArgsConstructor
public class Account {
这样生成的代码中就不会采用getter方法了。
我们接着来看字段的包含和排除,首先当我们在类上添加@ToString
后,默认会为所有字段生成对应的ToString操作,但是如果我们需要排除某些字段,我们可以使用@ToString.Exclude
注解,将其添加到对应的字段上时,就不会打印了:
@ToString
@AllArgsConstructor
public class Account {
@ToString.Exclude
private int id;
private String name;
private int age;
}
这种黑名单模式虽然用起来很方便,但是白名单模式就会很麻烦,也就是我们需要指定哪些字段打印,哪些字段才打印,考虑到这个问题,Lombok也为我们提供了白名单相关的注解,要开启白名单模式,需要将onlyExplicitlyIncluded设置为真,接着为我们需要打印的字段添加@ToString.Include
注解:
@ToString(onlyExplicitlyIncluded = true)
@AllArgsConstructor
public class Account {
@ToString.Include
private int id;
@ToString.Include
private String name;
private int age;
}
不过值得注意的是,@ToString.Include
不仅可以对字段生效,还可以对方法生效,它可以将某些方法执行后的结果也包含在toString中:
@ToString(onlyExplicitlyIncluded = true)
@AllArgsConstructor
public class Account {
...
@ToString.Include
public String test() {
return "你干嘛";
}
}
我们来看看@ToString.Include
可以设置的一些参数,比如我们可以手动为字段起一个用于打印的名字:
@ToString.Include(name = "编号")
private int id;
@ToString.Include(name = "名字")
private String name;
默认情况下toString打印的字段属性是按照声明顺序进行的,我们也可以手动为其指定顺序:
@ToString.Include(name = "编号")
private int id;
@ToString.Include(name = "名字", rank = 1) //rank越大,越靠前,默认为0
private String name;
Lombok可以为我们自动生成类属性的比较方法以及对应的HashCode计算。我们只需要为类添加@EqualsAndHashCode
注解,即可开启:
@EqualsAndHashCode
@AllArgsConstructor
public class Account {
private int id;
private String name;
private int age;
}
此时生成的类:
public class Account {
...
public boolean equals(Object o) { //自动生成的equals重写方法,包含所有参数的比较
...
}
protected boolean canEqual(Object other) {
return other instanceof Account;
}
public int hashCode() { //自动生成的hashCode重写方法
...
}
...
}
它自动为我们所有的参数生成了对应的比较方法,我们可以来测试一下生成的代码是否可以正常运行:
public static void main(String[] args) {
Account a1 = new Account(1, "小明", 18);
Account a2 = new Account(1, "小明", 18);
System.out.println(a1.equals(a2)); //结果为true
}
@EqualsAndHashCode
注解的参数大部分与我们上节讲解的@ToString
类似,比如exclude
、of
、callSuper
(默认关闭,开启后调用equals比较前先调用父类的equals进行一次比较)、doNotUseGetters
以及生成的方法中需要额外携带的注解onParam
属性等。
它同样可以使用onlyExplicitlyIncluded
属性来开启白名单模式,我们可以使用以下注解来自由控制哪些属性会作为比较的目标,哪些需要排除:
@EqualsAndHashCode.Exclude
- 用于排除不需要参与比较的字段。@EqualsAndHashCode.Include
- 开启白名单模式后,用于标明哪些字段需要参与比较。它与前面讲解的@ToString
一样,我们可以来试试看:
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@AllArgsConstructor
public class Account {
@EqualsAndHashCode.Include
private int id; //此时只对id字段进行比较
private String name;
private int age;
}
public static void main(String[] args) {
Account a1 = new Account(1, "小明", 18);
Account a2 = new Account(1, "小红", 17);
System.out.println(a1.equals(a2)); //由于只比较id字段,因此结果为true
}
和@ToString.Include
一样,我们也可以将其添加到一个方法上,使其结果也参与到比较中。
@EqualsAndHashCode.Include
public int test() {
return 1;
}
不过,@EqualsAndHashCode.Include
有一个replaces
属性,它可以用于将当前方法的结果替代目标字段进行比较,比如我们像这样编写:
@EqualsAndHashCode.Include(replaces = "id") //此时id字段的比较不会直接比较其本身,而是改为调用此方法获取对应结果进行比较
public int test() {
return 1;
}
这样,在生成的equals方法中,需要比较id字段时,会直接比较两个对象调用test
方法的结果。
最后我们来看看它独特的hashCode缓存机制,我们可以通过设置cacheStrategy
属性来控制hashCode结果的缓存,这有助于优化程序的性能,默认情况下缓存为NEVER也就是不启用,我们也可以开启LAZY模式:
@EqualsAndHashCode(cacheStrategy = EqualsAndHashCode.CacheStrategy.LAZY)
@AllArgsConstructor
public class Account {
private int id;
private String name;
private int age;
}
此时,生成的hashCode方法在第一次生成结果后,后续将一直使用同样的结果直接返回,避免二次计算:
public int hashCode() {
if (this.$hashCodeCache != 0) { //判断是否生成过hashCode,直接使用缓存
return this.$hashCodeCache;
} else {
...
this.$hashCodeCache = result; //设置缓存
return result;
}
}
这对于那些属性值不会发生变化的实体类来说,是一个很好的选择,但是如果后续使用中字段值可能会发生变化从而影响HashCode的结果,则不建议使用此功能。
至此,我们已经介绍了@ToString
,@EqualsAndHashCode
,@Getter
和@Setter
以及构造方法相关注解,很多情况下我们可能需要在用作数据传递的实体类上将它们一并用上,我们可以直接使用@Data
注解,它等价于我们在类上添加这些注解:@Getter
@Setter
@RequiredArgsConstructor
@ToString
@EqualsAndHashCode
@Data //一个注解直接把get和set方法、构造方法、toString、equals全包了
public class Account {
private int id;
private String name;
private int age;
}
当然,如果我们希望某个类只作为结果,里面的数据不可进行修改,我们也可以使用@Data
的只读版本@Value
,它等价于添加注解:@Getter
@FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE)
@AllArgsConstructor
@ToString
@EqualsAndHashCode
@Value
public class Account { //类会自动变成final类型
int id; //在`@FieldDefaults`配置下,会自动将类属性变为private和final的状态,这里我们无需手动编写
String name;
int age;
}
只不过在Java17之后,这种类完全可以被记录类型平替,因此使用时IDEA会提示我们直接使用记录类型。