1 前言

几天前 Goolge 在 I/O 大会上宣布了 Android 将官方支持 Kotlin, 这意味着 Android开发者可以更好地使用 Kotlin 开发 Android.

我虽不是 Android 开发者,但是也为 Android 开发者多了一个选择而感到高兴,略显意外的是,接下来到处可以看到 “Java已死,Kotlin 当立” 之类的言论。

一群人围在一起诉说被 Java “折磨” 的血泪史,然后为Kotlin 的到来欢欣鼓舞。我学过挺多的语言,也并不是一个 “Java 卫道士”.

但是看到很多人都说 “Java 的语法啰嗦,每次都要编写一大段 “Setter/Getter” 这类的模板代码,还有各种的 Bean; 真的好累” 我就觉得其实很多人都是人云亦云,他们也并没有对 Java 有多少关注。

其实 Java8 发布以后,使用 Java8 的函数式进行编码已经可以减少很多代码了;其次,一个新颖的类库也可以帮 Java 的代码进行瘦身 – Lombok

2 Lombok

2.1 简介

很多开发者都对模板代码嗤之以鼻,但是 Java 中就有很多非常类似且改动很少的样板 代码。

这问题一方面是由于类库的设计决定,另外一方面也是 Java 的自身语言的特性。 而 Lombok 这个项目就是希望通过注解来减少模板代码。

就注解而言,大多是各类框架用于生成代码 (典型的就是 Spring 和 Hibernate 了),而很少直接使用注解生成的代 码。

因为如果想要直接在程序中使用注解生成的代码,就意味着在代码进行编译之前,注解就要进行相应的处理。这似乎是没可能发生的事情: 在编译代码之前使用编译后生成的代码。

但是 Lombok 在 IDE 的配合下就真的做到在开发的时候就插入相应的代码。

2.2 @Getter and @Setter

百闻不如一见,还是直接看例子吧。

对于使用过 Java 的开发者而言,我相信他们最熟悉的肯定是 Java 无处不在的封装以及对应的 Getter/Setter. 现在就来看一下如何 为最常见的模板代码瘦身。

未使用 Lombok 的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private boolean employed = true;
private String name;

public boolean isEmployed() {
    return employed;
}

public void setEmployed(final boolean employed) {
    this.employed = employed;
}

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

并没有上面特点,还是 “旧把式”. 那么现在来看一下使用了 Lombok 的同等作用的代 码:

1
2
@Getter @Setter private boolean employed = true;
@Setter(AccessLevel.PROTECTED) private String name;

除了必要的变量定义以及 @Getter, @Setter 注解以外,没有了其他的东西的。

但是使用 Lombok 的代码比原生的 Java 代码少了很多行,定义的类的属性越多,减少的代码数就越可观。

而 @Getter 和 @Setter 注解的作用就是为一个类的属性生成 getter 和 setter 方法,而这些生成的方法跟我们自己编写的代码是一样的。

2.3 @NonNull

相信每一个使用过 Java 的开发者都不会对空指针这个异常陌生吧,因为 NullPointException 导致了各种 Bug, 以至于它的发明者 Tony Hoare 都自嘲到他创 造了价值十亿的错误 (“Null Reference: The Billion Dollar Mistake”).

因此在Java 的代码中,出于安全性的考虑,对于可能出现空指针的地方,都需要进行空指针 检查,自然无可避免地产生了很多的模板代码。

而 Lombok 引入的 @NonNull 注解可以让需要进行空指针检查的代码 fast-fail; 这样就无需显示添加空指针检查了。

当为类 的属性添加了 @NonNull 注解以后,在对应的 setter 函数,Lombok 也会生成对应的 空指针检查。例子:使用 Lombok 对 Family 类添加注解:

1
2
@Getter @Setter @NonNull
private List<Person> members;

对应的相同作用的原生代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@NonNull
private List<Person> members;

public Family(@NonNull final List<Person> members) {
    if (members == null) throw new java.lang.NullPointerException("members");
    this.members = members;
}

@NonNull
public List<Person> getMembers() {
    return members;
}

public void setMembers(@NonNull final List<Person> members) {
    if (members == null) throw new java.lang.NullPointerException("members");
    this.members = members;
}

不得不感慨,使用 Lombok, 敲击键盘的次数都成指数级下降.

2.4 @EqualsAndHashCode

因为 Java 的 Object 类存在用于比较的 equals() 以及对应的 hashCode() 方法,而很多类都经常需要重写这两个方法来实现比较操作。

比较的操作大多是逐一比较子类的属性,而计算 hash 值的函数也基本是逐一取各个属性的 hash 值,然后与固定值相乘在相加. 这样的操作并不需要复杂算法,完成的都是重复性的 “体力活”.

幸运的是, Lombok 也提供了相应的注解来减少这些模板代码。类级别的 @EqualsAndHashCode 注解可以为指定的属性生成 equals() 方法和 hashCode() 方法。

默认情况下,所有非静态或者没被标注成 transient 的属性都会被 equals()hashCode() 方 法包含在内。当然,你也可以使用 exclude 声明不需要被包含的属性。

例子:使用了 @EqualAndHashCode 注解的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@EqualsAndHashCode(callSuper=true,exclude={"address","city","state","zip"})
public class Person extends SentientBeing {
    enum Gender { Male, Female }

    @NonNull private String name;
    @NonNull private Gender gender;

    private String ssn;
    private String address;
    private String city;
    private String state;
    private String zip;
}

对应的相同作用的原生代码:

 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class Person extends SentientBeing {

    enum Gender {
	/*public static final*/ Male /* = new Gender() */,
	/*public static final*/ Female /* = new Gender() */;
    }
    @NonNull
    private String name;
    @NonNull
    private Gender gender;
    private String ssn;
    private String address;
    private String city;
    private String state;
    private String zip;

    @java.lang.Override
	public boolean equals(final java.lang.Object o) {
	if (o == this) return true;
	if (o == null) return false;
	if (o.getClass() != this.getClass()) return false;
	if (!super.equals(o)) return false;
	final Person other = (Person)o;
	if (this.name == null ?
	    other.name != null : !this.name.equals(other.name)) return false;
	if (this.gender == null ?
	    other.gender != null : !this.gender.equals(other.gender)) return false;
	if (this.ssn == null ?
	    other.ssn != null : !this.ssn.equals(other.ssn)) return false;
	return true;
    }

    @java.lang.Override
	public int hashCode() {
	final int PRIME = 31;
	int result = 1;
	result = result * PRIME + super.hashCode();
	result = result * PRIME + (this.name == null ?
				   0 : this.name.hashCode());
	result = result * PRIME + (this.gender == null ?
				   0 : this.gender.hashCode());
	result = result * PRIME + (this.ssn == null ?
				   0 : this.ssn.hashCode());
	return result;
    }
}

模板代码和使用了 Lombok 的代码简洁程度而言,差距越来越大了

2.5 @Data

下面我就来介绍一下在我项目中使用最频繁的注解 @Data.

使用 @Data 相当于同时在类级别使用 @EqualAndHashCode 注解以及我未曾提及的 @ToString 注解 (这个应该可以从注解名字猜出注解的作用), 以及为每一个类的属性添加上 @Setter@Getter 注解。

在一个类使用 @Data, Lombok 还会为该类生成构造函数。

例子:使用 了 @Data 注解的函数:

1
2
3
4
5
6
@Data(staticConstructor="of")
public class Company {
    private final Person founder;
    private String name;
    private List<Person> employees;
}

同等作用的原生 Java 代码:

 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class Company {
    private final Person founder;
    private String name;
    private List<Person> employees;

    private Company(final Person founder) {
	this.founder = founder;
    }

    public static Company of(final Person founder) {
	return new Company(founder);
    }

    public Person getFounder() {
	return founder;
    }

    public String getName() {
	return name;
    }

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

    public List<Person> getEmployees() {
	return employees;
    }

    public void setEmployees(final List<Person> employees) {
	this.employees = employees;
    }

    @java.lang.Override
	public boolean equals(final java.lang.Object o) {
	if (o == this) {
	    return true;
	}
	if (o == null) {
	    return false;
	}
	if (o.getClass() != this.getClass()) {
	    return false;
	}
	final Company other = (Company) o;
	if (this.founder == null ?
	    other.founder != null : !this.founder.equals(other.founder)) {
	    return false;
	}
	if (this.name == null ?
	    other.name != null : !this.name.equals(other.name)) {
	    return false;
	}
	if (this.employees == null ?
	    other.employees != null : !this.employees.equals(other.employees)) {
	    return false;
	}
	return true;
    }

    @java.lang.Override
	public int hashCode() {
	final int PRIME = 31;
	int result = 1;
	result = result * PRIME + (this.founder == null ?
				   0 : this.founder.hashCode());
	result = result * PRIME + (this.name == null ?
				   0 : this.name.hashCode());
	result = result * PRIME + (this.employees == null ?
				   0 : this.employees.hashCode());
	return result;
    }

    @java.lang.Override
	public java.lang.String toString() {
	return "Company(founder=" + founder + ", name=" + name + "," +
	    " employees=" + employees + ")";
    }
}

差别更加显而易见了

3 小结

我在日常的开发中,使用的开发语言主要是 Java, 我也学习过其他的语言,所以 Java 和其他语言相比的优缺点也了然于心。

Java 绝佳的工程性,优秀的 OOP 范式,以及大量的类库,框架 (例如 Spring “全家桶”), 以及 JIT 带来的接近 C++ 的性能,但是 Java 语法实在啰嗦,需要编写很多的模板代码,以至于经常出现将小项目写成中项目,中项目写成大项目的烦恼,更被戏称为 “搬砖”.

现在看来,Lombok 为 Java 减少的模板代码实在算是造福 Java 开发者,让开发者在获得 Java 优势的时候,还可以尽量少地打字,可谓是来的及时。


4 参考