Lombok指南

Lombok为Java提供了不少很甜的语法糖,设计也很合理,很多都符合《Effective Java》所描述的最佳实践。因此结合官方文档和自己的一些实践给大家详细的介绍一下。

使用方法

在Settings->Plugins搜索并启用Lombok plugin

img

然后再pom.xml加入

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.14</version>
</dependency>

完成之后就可以开始使用了,大部分都是采用注解形式来进行一些定义或是功能的增强。

val

可以代表任意类型

局部变量,不可用于实例变量

这个类型会从初始化的表达式中推测出来,有一种动态语言的感觉,但其实并不是

这个val是final,必须有个初始赋值的表达式,而且不可再赋值

举例:

val i="string";
val list=new ArrayList<String>();
s.ensureCapacity(23);
System.out.println(i);

等价于:

final String i="asd";
final ArrayList<String> s=new ArrayList<>();
s.ensureCapacity(23);
System.out.println(i);

@NonNull

不能为空,如果为空会直接抛出NullPointerException

在源码中声明了可以在成员变量,方法,参数,本地变量中使用,但是实际测试只有参数和成员变量中可以正常工作

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE})
@Retention(RetentionPolicy.CLASS)
@Documented
public @interface NonNull {
}

举例:

public class User {
    @NonNull
   private String name;

    public void setName(String name) {
        this.name = name;
    }
}
@Test
public void main() throws Exception{
    User user=new User();
    user.setName(null);
}

@Test
public void main() throws Exception{
    display(null);
}

private void display(@NonNull String source){
    System.out.println(source);
}

等价于:

private void display(String source){
    if(source == null) {
        throw new NullPointerException("source");
    }
        
    System.out.println(source);   
}

下面并不会报错

@Test
public void main() throws Exception{
    @NonNull Integer i=null;
    i=null;
    System.out.println("success");
}

@NonNull
private void display(String source){
    System.out.println(source);
}

@Cleanup

只可用于本地变量

只可用于可释放的资源,确切的说是实现了Closeable的类,否则在会出现编译错误,但是IDEA并不会报错

声明了此注解的变量会在结束时自动释放资源

原理是在定义此变量的后面的全部语言外面加上try{}finally(x.close);

举例:

@Test
public void main() throws Exception{
    @Cleanup InputStream is=new FileInputStream(new File("D:	estaa.txt"));
    System.out.println();
}

将上述代码生成的class反编译

@Test
public void main() throws Exception {
    FileInputStream is = new FileInputStream(new File("D:	estaa.txt"));

    try {
        System.out.println();
    } finally {
        if(Collections.singletonList(is).get(0) != null) {
            is.close();
        }

    }

}

这种写法还等价于JDK1.7中的

@Test
public void main() throws Exception{
    try(InputStream is=new FileInputStream(new File("D:	estaa.txt"))){
        System.out.println();
    }  
}

不过感觉这么用注解更舒服了一点

@Getter and @Setter

用于成员变量和类

可以直接生产成员变量的set和get方法

如:

public class User {
    @Getter
   @Setter
   private Integer id;
    
    @Setter
    @Getter
   private String name;
}

现在就可以直接使用

@Test
public void main() throws Exception{
    User user=new User();
    user.setId(1);
    System.out.println(user.getId());
}

也可以直接在类上声明,那么类的所有变量就都生成了get,set方法

@Getter
@Setter
public class User {
    private Integer id;
    private String name;
}

可以指定访问级别PUBLIC, PROTECTED, PACKAGE, and PRIVATE,默认是PUBLIC

@Setter(AccessLevel.PROTECTED)

@ToString

可以用于类

覆盖默认的toString()方法,将呈现类的基本信息

@Test
public void main() throws Exception{
    User user=new User();
    user.setId(1);
    user.setName("bb");
    System.out.println(user.toString());
}

//输出:User(id=1, name=Asens)

选项:

includeFieldNames:如果为false , 不显示变量名

@ToString(includeFieldNames=false)
//输出:User(1, Asens)

exclude:不显示哪些字段

@ToString(exclude={"id"})
//输出:User(name=Asens)

of:只显示哪些字段

@ToString(of={"id"})
//输出:User(id=1)

@EqualsAndHashCode

只用于类

覆盖默认的equals和hashCode

方法和参数都与@ToString类似

除了一般的equals和hashCode实现外可以使用of或exclude,只对或是不对哪些字段实现比较会计算hashCode

@NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor

均是只适用于类

@NoArgsConstructor

会生成一个无参的构造函数,一个类是有默认的无参的构造函数的,但是如果创建了有参数的构造函数,那么无参的函数就没有了,这个注解相当于添加了一个无参的构造函数,而且添加了一些新的功能

如果有finall的字段的话会出现编译错误,除非使用@NoArgsConstructor(force = true)那么所有的final字段会被定义为0,false,null等

@RequiredArgsConstructor

使用该注解会对final和@NonNull字段生成对应参数的构造函数

如我设定了name不能为null

@Getter
@Setter
@RequiredArgsConstructor
public class User {
    private Integer id;
    @NonNull
    private String name;
}

那么在调用的时候就生成了只有name的构造函数

img

如果我将id同样设置为@NonNull

那么在提示的时候就成了

img

@AllArgsConstructor

顾名思义,生成了全参数的构造函数,会配合@NonNull

@Getter
@Setter
@ToString
@AllArgsConstructor
public class User {
    private Integer id;
    @NonNull
    private String name;
}

反编译生成的代码

@ConstructorProperties({"id", "name"})
public User(Integer id, @NonNull String name) {
    if(name == null) {
        throw new NullPointerException("name");
    } else {
        this.id = id;
        this.name = name;
    }
}

三者都有一个参数staticName ,使用这个参数可以将构造函数变成静态工厂方法

此时User不再可new,只能使用静态工厂的方法

同样可以使用access 控制类的访问范围

@AllArgsConstructor(staticName = "of")

User user=User.of(1,"Asens");

@AllArgsConstructor(access = AccessLevel.PROTECTED)

@Data

@ToString

@EqualsAndHashCode

@Getter

@Setter

@RequiredArgsConstructor

的集合

可以使用staticConstructor来实现上述staticName 的功能

@Data(staticConstructor="of")

@Value

只用于类

@Value用于生成不可变类,与@Data相似,@Value也是一系列注解的合集

分别包括

@ToString

@EqualsAndHashCode

@AllArgsConstructor

@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)

@Getter

区别是不再提供@Setter,每个成员变量都是final和private的,实例化需要提供所有参数用于初始化

@Builder

如果你熟悉建造者模式,那么你一定知道建造者模式有多麻烦

建造者的模式一般是这样(减少了成员变量,只有一个成员变量一般不用此模式)

public class User {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public User(Builder builder) {
        this.name= builder.name;
    }

    public static Builder builder(){
        return new Builder();
    }

    public static class Builder{
        private String name;

        public Builder name(String name) {
            this.name= name;
            return this;
        }

        public User build(){
            return new User(this);
        }
    }
}

使用了@Builder之后,只需要这样

@Builder
@Getter
@Setter
public class User {
    private String name;
}

二者调用方法相同

@Test
public void main() throws Exception{
    User user=User.builder().name("Asens").build();
    System.out.println(user.getName());
}

@SneakyThrows

如果方法里有受检异常使用的这个注解可以不必在方法中声明throws对应的异常

比如读取一个文件,FileNotFoundException是个受检异常,需要try,catch或是throws抛出

private void read() throws FileNotFoundException {
    File file=new File("D:	estaa.txt");
    InputStream is = new FileInputStream(file);
    //ignore
}

然后用了@SneakyThrows就可以直接无视受检异常

但是在实际运行过程中和抛出并没有什么区别

@SneakyThrows
private void read() {
    File file=new File("D:	estaa.txt");
    InputStream is = new FileInputStream(file);
}

这个是反编译的代码

private void read() {
    try {
        File $ex = new File("D:	estaa.txt");
        new FileInputStream($ex);
    } catch (Throwable var3) {
        throw var3;
    }
}

这个反编译的代码并不能通过编译[脑补笑着哭表情]

究其原因就是在运行期间不管方法是不是声明了throws,都会抛出异常

但是在编译的时候只要抛出了受检异常就必须声明

所以这个才能实现

但是谨慎使用,怎么良好的使用异常是一门学问,推荐看一下《Effective Java》对应的章节

@Synchronized

顾名思义,加锁,仅在方法级别可用

示例:

@Test
public void mm() throws FileNotFoundException {
    aa();
}

@Synchronized
private void aa(){
    System.out.println();
}

反编译之后

private final Object $lock = new Object[0];

public TestS() {
}

@Test
public void mm() throws FileNotFoundException {
    this.aa();
}

private void aa() {
    Object var1 = this.$lock;
    synchronized(this.$lock) {
        System.out.println();
    }
}

这个锁并没用放在方法上而是把方法内部的全部锁住了,感觉和直接在方法上加synchronized也没有什么太大区别,效率稍微高一点

@Getter(lazy=true)

延迟加载

可用于类和成员变量

对于某些操作可以不必在初始化的时候加载而是等到了需要再去加载,此时就需要进行延迟加载

对于字段,需要final

我们定义User,我们希望当getName的时候在赋予name的值

@Setter
@Getter
public class User {
    private Integer id;

    @Getter(lazy=true)
    private final String name=makeName();

    private String makeName() {
        System.out.println("make name");
        return "aa";
    }
}

调用

@Test
public void mm() throws FileNotFoundException {
    User user=new User();
    System.out.println("before user get name");
    user.getName();
    System.out.println("after user get name");
}

输出:
before user get name
make name
after user get name

当user在getName的时候才去执行makeName

作为对比,去掉lazy

@Setter
@Getter
public class User {
    private Integer id;
    private final String name=makeName();

    private String makeName() {
        System.out.println("make name");
        return "aa";
    }
}

再次调用,显然

@Test
public void mm() throws FileNotFoundException {
    User user=new User();
    System.out.println("before user get name");
    user.getName();
    System.out.println("after user get name");
}
输出:
make name
before user get name
after user get name

实现的原理大概和这个有点像

public class User {
    private Integer id;
    private String name=null;

    private String makeName() {
        System.out.println("make name");
        return "aa";
    }

    public String getName() {
        if(name!=null) return name;
        return name=makeName();
    }
}

但是这个实在有点简陋,既不能实现name的final,也不是保存线程安全问题,两个线程同时访问,妥妥的调用两次makeName

为了解决上述问题,lombok是是这么解决的

反编译之后

public class User {
    //省略其他代码
    
    private final AtomicReference<Object> name = new AtomicReference();

    public String getName() {
        Object value = this.name.get();
        if (value == null) {
            AtomicReference var2 = this.name;
            synchronized(this.name) {
                value = this.name.get();
                if (value == null) {
                    String actualValue = this.makeName();
                    value = actualValue == null ? this.name : actualValue;
                    this.name.set(value);
                }
            }
        }

        return (String)((String)(value == this.name ? null : value));
    }
}

首先变量成为了一个容器,容器本身不改变,但是可以进行读写,并且AtomicReference的读写本身就是原子性的,并且在调用makeName和赋值的时候使用synchronized保证了线程安全。

@Log

那个log咋写来着?

为了彰显自家产品的与众不同,各种log的写法也是各具特色,每次建一个新类都要去别的类里面复制Log出来,然后改类名,一样的同学请举手…

lombok总结了各种log的写法

img

在实际的使用中完全做到了无缝衔接,与注释调到的功能完全等价,简洁而优雅

@Controller
@Slf4j
public class SampleController {
    //private static Logger log= LogManager.getLogger(SampleController.class);

    @RequestMapping("/")
    public String home(ModelMap model) {
        log.info("aa");
        return "main";
    }
}

Lombok还有一系列实现性质的功能,这些功能可能不是那么优雅,没有很完整的测试,未来有可能删除或是大改的功能

有兴趣的可以看一下

https://projectlombok.org/features/experimental/all


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!