谨慎对待封装组件或工具类
更好的封装应该满足以下几点中的至少2点:
- 简单易懂
- 性能更好
- 可读性更好
- 拓展性更好
- 不易出bug
- 降低心智负担
最近接触了一些过度封装的组件,很多封装在笔者看来是没有必要的。
下面我们分析一些代码:
1. CollectionUtils#isNotEmpty
// v1
list != null && !list.isEmpty()
// v2
CollectionUtils.isNotEmpty(list)
// v3, import static,可读性更好
isNotEmpty(list)
-
良好的代码规范应该是:集合类不使用 null 表示空,所以理想情况下应该直接调用 isEmpty()。
-
isNotEmpty 和 isEmpty 表示的逻辑是相反的,就算封装方法,只需要提供一个 isEmpty 即可。
-
实现中的代码不受我们自己控制,需要进行防御性编程。isEmpty 如果后续处理使用list,那边我们应该使用的是CollectionUtils#emptyIfNull,所以不应出现isEmpty。
-
进一步说 CollectionUtils#emptyIfNull 实际上的实现是 JDK9+: Objects#requrieNonNullElse 或者guava中的实现 MoreObjects#firstNonNull,所以CollectionUtils#isEmpty 实际上并不需要。
// 防御性编程实现版本
List<Foo> list = firstNonNull(untrustedList, emptyList());
if (!list.isEmpty()) {
// 后续处理
}
2. Sets.newHashSet
Guava 提供了便利方法能在一行代码中创建 + 初始化 HashSet,其实这种便利方法很没有必要。Java 提供了初始化的方法,虽然可读性一般,但是作为标准库的实现,其已被大多数人所接受。
// v1
Set<String> set = Sets.newHashSet("foo", "bar");
// v2
Set<String> set = new HashSet<>(Arrays.asList("foo", "bar"));
// v3 不可变实现
public static final Set<String> set = Set.of("foo", "bar");
// v4 guava 不可变类实现
public static final ImmutableSet<String> set = ImmutableSet.of("foo", "bar");
- V1 实现使用了类库,和V2实现相比,没有什么特别的提升。不要为了少写一些代码而创建一个看似方便的函数。
- v3,v4 是不可变实现,很多时候我们使用的集合是不可变的。相比于HashSet,ImmutableSet的性能更好。两种实现都是可以接受的,甚至v4更好,其能明确告知使用者不要修改。
3. 流式编程相关实现
所有和流式编程相关实现的重写几乎都没有必要,因为 stream 作为标准实现已经足够好,可读性、性能都很不错。
以下是一个错误示例,某个自定义的工具类:
/**
* 返回被删除的元素,便于记log
* 性能敏感的场景考虑直接生成新的list
*/
public static <T> List<T> removeIf(Collection<T> collection, Predicate<T> predicate) {
if (CollectionUtilsEngine.isEmpty(collection)) {
return new ArrayList<>();
}
List<T> removedItemList = new ArrayList<>();
for (T item : collection) {
if (predicate.test(item)) {
removedItemList.add(item);
}
}
// speed up
collection.removeAll(new HashSet<>(removedItemList));
return removedItemList;
}
这段代码看似没有问题,实则问题重重。
-
改变了 Collection#removeIf(Predicate<? super E> filter) => boolean 方法签名,如果要实现这样的功能,可以起一个新名字
-
性能问题,使用removeAll 可能实现并不如 forEach + remove 高效,一方面removeAll 多进行了一次遍历,另一方面 AbstractSet 的具体实现算法性能问题。

-
这个方法的实质是把集合根据 predicate 进行拆分,stream 提供了相关方法:
var userParts = users.stream().collect(partitionBy(User::isSpecial));
var specialUsers = userParts.get(true);
var normalUsers = userParts.get(false);
不要为了少些两行代码去创建工具方法,因为并没有使问题简化多少。
- 方法内部实际上执行了副作用,即对集合进行删除元素操作,对于不了解的用户来说,可能使用错误,特别是这里代码既有副作用,又有返回值,令人迷惑。能不使用副作用时就不要使用副作用。
4. 封装调用形式
笔者见到过这样的回调实现:
MyFuture#submmit(Runnable task, MyCallBack callback) => CompletableFuture<Void>
这个方法封装了 CompletableFuture 的 onComplete 方法,如果用户知道 CompletableFuture,实际上直接调用 onComplete 方法就可以。
还有一种常见的 CompletableFuture 使用方法,多线程获取结果,然后等待所有结果,返回 List。很多人封装了如下方法:submitTasks(List<Callable> tasks) => List
实际上,直接使用 ExecutorService#invokeAll 方法即可。
5. 封装老旧实现
比如 SimpleDateFormat + ThreadLocal 实现线程安全格式化,实际上在能使用 Java8 +版本时直接使用DateTimeFormatter 即可。
6. Splitter
Guava 的 Splitter 接口设计值得每一个人学习:
@Test
public void whenCreateListFromString_thenCreated() {
String input = "apple - banana - orange";
List<String> result = Splitter.on("-").trimResults()
.splitToList(input);
assertThat(result, contains("apple", "banana", "orange"));
}
Splitter 不可变,其实现可读性极强,每次调用都是对于分隔模式的显示说明(比如 trimResults)。其避免了String#split 难以使用的问题。
对于使用者来说,只需要知道这个类名即可使用,不需要理解其底层实现,学习成本很低。
其不是静态类,很多工具类库的实现应该学习其设计成非静态类背后的思想。
7. Flogger
Flogger 是谷歌对于日志记录方式的封装,其是一种 fluent 语法:
// 来自官方文档
for (Thing t : things) {
int status = process(t);
logger.atFinest().log("processed %s, status=%x", t, status);
}
其相比传统的log.info来说,性能更好,可读性强,便于拓展。感兴趣的读者可以参考官方文档,其对于日志性能进行了详细的分析,并提出了使用 Flogger 进行日志记录。
8. AssertJ
assertThat(cf1)
.succeedsWithin(Duration.ofSeconds(1))
.isEqualTo(cf2.get());
AssertJ 经常用于单元测试, 提供了丰富的断言方法,使得测试代码更加简洁和可读。通过使用 AssertJ,可以更容易地编写和维护测试代码,提高代码的质量和可靠性。