Guava使用笔记

Guava 是 Google 出品的一款基础类库,它提供了一系列工具类和容器类,用以改善和弥补 Java 原生类库的不足。相对于 Java 原生类库来说,Guava 更方便易用,更不容易出错。当然,不容易出错也是相对的,这里总结了我在使用 Guava 过程中碰到的容易误用的地方(后续也会继续补充)。

Optional

为什么最先提到的是 Optional 这个类,因为这个类比较有意思,大家平时要么不用,要么就会用错,所以这里先拿出来讲一下。

为什么要用 Optional 这个类?

对于返回对象的方法来说,极有可能会因为某种 case 而需要返回 null,对于调用者来说,往往会因为忽略返回值的检查,而在运行时得到 NullPointerException,注意,这里是运行时,这也就意味着可能系统几年都不会崩溃但突然某一天就挂掉了。因此,我们的原则是以编译期异常替代运行时异常,能通过静态检查查出错误的,就不要通过单元/集成测试来查出。

因此,我们引入 Optional 类,来向调用者表明:这个方法可能会返回空结果,所以,我们可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Optional<Integer> query() {
...
return Optional.fromNullable(dao.query());
}
...
public void foo() {
Optional<Integer> resultOpt = query();
if (!resultOpt.isPresent()) {
throw new IllegalStateException();
}
int result = resultOpt.get();
...
}

使用 Optional 表明了返回值需要做 Null-Check,并且在调用的地方都做了检查,一切都很完美,哈?

但这其实是对 Optional 最大的误用:这样写并不比下面这种写法来的高明。

1
2
3
4
5
6
7
8
9
10
11
12
public Integer query() {
...
return dao.query();
}
...
public void foo() {
Integer result = query();
if (result == null) {
throw new IllegalStateException();
}
...
}

说实话,这也是平时和同事交流时他们不用 Optional 最主要的原因。

那么,Optional 应该如何使用呢?这里给出上述例子的正确使用写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Optional<Integer> query() {
...
return Optional.fromNullable(dao.query());
}
...
public void foo() {
Optional<Integer> resultOpt = query();
int result = resultOpt.or(new Supplier<Integer>() {
@Override
public Integer get() {
throw new IllegalStateException();
}
});
...
}

看起来好像复杂了一些(个人认为应该提供类似 Optional.orThrow 方法),但其实这里的 or(Supplier) 方法并不仅仅用于在 Optional.absent() 时抛出异常,它还可以实现诸如:

1
2
3
4
5
6
7
Optional<Integer> opt = queryFromCache();
int result = opt.or(new Supplier<Integer>() {
@Override
public Integer get() {
return queryFromDb();
}
});

当然,Optional 还有其他方便易用的方法,例如:

  • or(T) —— 提供默认值
  • transform(Function<T, V>) —— 若 isPresent() 返回 true,则调用入参方法来转换返回值,否则返回 absent()

甚至,对同一个 Optional,可以在不同的代码块使用不同的默认值,执行不同的 transform

总之,Optional 提供了一种机制,用于一句话实现『若存在,则xxx,否则xxx』的机制,而不必使用经典的『if…then…』语句块。

ImmutableXXX

不可变容器是 Guava 对 Java 容器的一个有力补充,但当 ImmutableXXX 碰到方法返回值时,就会出现设计上的不正确。

1
2
3
4
5
6
public Set<Type> supportedTypes() {
return ImmutableSet.of(
Type.TYPE1,
Type.TYPE2
);
}

这段代码看似很符合『面向接口编程』的理论,但如果调用者编写了如下的逻辑,就要命了:

1
2
3
4
5
6
Set<Type> types = supportedTypes();
if (!types.isEmpty()) {
types.add(Type.TYPE3);
} else {
types = ImmutableSet.of(Type.TYPE3);
}

supportedTypes 方法并没有告知外部调用者,我会返回一个不可变的容器,甚至很有可能,被调用函数某些条件下会返回 HashSet,而在另外一些条件下会返回 ImmutableSet。假如这个 supportedTypes 方法是一个远程方法,实现细节并不会暴露给调用者,那么问题会更加糟糕。

这里的根本问题是:调用者并不知道被调用函数的具体实现,而基于 Set 接口的基本假设,认定其 add 等容器修改方法可以被合法调用。Java 接口的方法并没有规定其实现是否允许调用者调用其方法,也并不会保证其实现的时间复杂度等,它只是告诉调用者,你可以通过我规定的方法,和具体的实现类进行『通信』,至于通信效果,呵呵,我不保证。

我一直纠结这种 case 下,方法的返回值应该定义成什么样,直到我认真看了 Guava 的文档:

For field types and method return types, you should generally use the immutable type (such as ImmutableList) instead of the general collection interface type (such as List). This communicates to your callers all of the semantic guarantees listed above, which is almost always very useful information.

简言之,如果你要返回 ImmutableXXX,请告诉调用者,我要返回不可变容器,如果某些条件下需要返回不可变容器,某些条件下返回正常容器,还是都返回正常的容器吧。

所以,使用任何东西之前,要先好好看看说明书。

Lists.transform

这里提到 transform 方法,并不是要强调其惰性求值能在某些情况下节省 CPU 时间,而是要谈谈其提供的惰性求值在某些条件下可能带来的误用问题。

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
public List<User> queryAndTransform() {
List<User> users = Lists.transform(ImmutableList.of(1, 2, 3), new Function<Integer, String>() {
@Override
public User apply(Integer id) {
return new User(id, "User-" + id);
}
});
...
// #1
for (User user : users) {
user.setName("User#" + user.getId());
}
return users;
}
...
List<User> users = queryAndTransform();
// #2
for (User user : users) {
System.out.println(user.getId() + " => " + user.getName());
}
// prints:
// 1 => User-1
// 2 => User-2
// 3 => User-3

是不是很奇怪,并没有打印出:

1
2
3
// 1 => User#1
// 2 => User#2
// 3 => User#3

原因也很简单,因为这里的 transform 方法使用了惰性求值,因此,在 #1 位置遍历 users 列表时,会调用传入 transform 方法的 Function 对象,得到一个 List<User> 列表对象,在 #2 位置再次遍历 users 列表时,会重新求值(也就是调用传入的 Function 对象),得到一个新的 List<User> 列表对象,和 #1 处的列表对象完全不是同一个,因此,打印出来的数据也就和预期不符合了。

所谓惰性求值,指的是在对容器内元素进行遍历或者解引用时,才会调用求值函数进行求值,这样做的好处是:

  • 不使用容器时,或者不使用容器内某些元素时,不需要调用求值函数的逻辑,对于求值函数开销比较大时(例如需要 IO 或者计算),可以有效节省资源
  • 每次遍历得到新的元素,如果求值函数是可重入的,那么遍历就是线程安全的

但缺点也很明显,高阶函数(即前面的 transform 或其他语言里的 map)返回的容器并不可修改,或者说修改了对于下一次遍历并不会生效。

Preconditions

这个类大家用的也比较多,用于实现如下这种 Pattern:

1
2
3
4
public void foo(User user) {
Preconditions.checkNotNull(user, "参数不能为null");
...
}

这个不多说,没毛病,当然,也有如下方式使用的:

1
2
3
4
5
public void foo(int userId) {
User user = fetchFromRemotePeer(userId);
Preconditions.checkNotNull(user, "用户不存在");
...
}

相对于:

1
2
3
4
5
6
7
public void foo(int userId) {
User user = fetchFromRemotePeer(userId);
if (user == null) {
throw new NullPointerException("用户不存在");
}
...
}

来说,来的更加简练更加直观,但其实这是对 Preconditions 类的一种最大的误用,我们来看一下这个类的方法签名:

1
2
3
4
5
6
public static <T> T checkNotNull(T reference) {
if (reference == null) {
throw new NullPointerException();
}
return reference;
}

这货居然有返回值?而且还返回了入参?What the hell?

所以,正确的写法应该是:

1
2
3
4
public void foo(int userId) {
User user = Preconditions.checkNotNull(fetchFromRemotePeer(userId), "用户不存在");
...
}

这种写法会更加简练,当然,我个人觉得,其提供一个类似:

1
public static <T> T checkNotNull(T reference, UnaryFunction<T> unary)

之类的方法会不会更好,毕竟某些时候还是希望能在出现 null 的时候,能够既抛出异常,又能记录日志,还能打上监控。