背景
我在看《Java 实战(第2版)》(Modern Java in Action, 2nd Edition)一书时,学到了些知识。本文取材于该书第二章:通过行为参数化传递代码。本文没有照抄原书的例子,但本文的结构参考了书中的内容。
正文
假设我们需要对一些书进行筛选操作,我们用如下的记录类来表示书。
java
public record Book(String name, String author) {
}
我们也可以用一个普通的类来表示书,具体的代码如下 ⬇️ 但用记录类比较简洁,所以在本文中,我们采用 上面 这个版本的 Book
java
public class Book {
private String name;
private String author;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
}
1. 小试牛刀:筛选金庸的书籍
如果需要从书的列表中筛选出金庸的书,我们可以写一个这样的方法 ⬇️
java
public List<Book> filterBooks(List<Book> books) {
List<Book> result = new ArrayList<>();
for (Book book : books) {
if ("金庸".equals(book.author())) {
result.add(book);
}
}
return result;
}
2. 把作者作为参数
在第 1 步里,我们在方法中硬编码了作者(即,金庸),这样的代码难以复用,所以我们可以把指定的作者作为方法的参数 ⬇️
java
public List<Book> filterBooksByAuthor(List<Book> books, String author) {
List<Book> result = new ArrayList<>();
for (Book book : books) {
if (author.equals(book.author())) {
result.add(book);
}
}
return result;
}
而调用方将指定的作者作为参数 ⬇️
java
// 变量名中最好不要使用非 ASCII 字符,以下两行为了让结果易于理解,姑且使用了汉字
List<Book> 金庸的书 = filterBooksByAuthor(books, "金庸");
List<Book> 曹雪芹的书 = filterBooksByAuthor(books, "曹雪芹");
如果我们需要对书名进行筛选,可以写一个类似的方法 ⬇️
java
public List<Book> filterBooksByPrefix(List<Book> books, String prefix) {
List<Book> result = new ArrayList<>();
for (Book book : books) {
if (book.name.startsWith(prefix)) {
result.add(book);
}
}
return result;
}
调用方通过提供指定的 prefix 参数,就可以对书进行筛选 ⬇️
java
List<Book> result1 = filterBooksByPrefix(books, "连");
List<Book> result2 = filterBooksByPrefix(books, "倚天");
3. 对作者进行筛选或者对书名进行筛选
刚才提到的 filterBooksByAuthor 方法以及 filterBooksByPrefix 方法的逻辑是类似的,它们都会对 books 参数进行筛选操作,然后返回筛选结果。有没有办法将这两个方法合并呢?我们先看一个笨办法,可以用一个 flag 参数来表示对哪个属性进行筛选 ⬇️ (我是感觉这种写法很蹩脚,不过书中有类似的内容,所以就借鉴了一下)
java
public List<Book> filterBooks(List<Book> books, String author, String prefix, boolean flag) {
List<Book> result = new ArrayList<>();
for (Book book : books) {
if (flag && author.equals(book.author) ||
(!flag && book.name.startsWith(prefix))) {
result.add(book);
}
}
return result;
}
我们可以这样调用它 ⬇️
java
// 变量名中最好不要使用非 ASCII 字符,以下两行为了让结果易于理解,姑且使用了汉字
List<Book> 金庸的书 = filterBooks(books, "金庸", null, true);
List<Book> 倚天 = filterBooks(books, null, "倚天", false);
4. 根据抽象条件筛选
第 3 小节的写法难以扩展而且可读性差,我们可以将筛选行为参数化。首先定义一个接口 ⬇️
java
public interface BookPredicate {
boolean test(Book book);
}
然后我们就可以用不同的实现类来代表不同的筛选条件了 ⬇️ (为了简洁起见,这里用了记录类,用普通类也没问题)
java
record BookAuthorPredicate(String author) implements BookPredicate {
@Override
public boolean test(Book book) {
return author.equals(book.author());
}
}
record BookPrefixPredicate(String prefix) implements BookPredicate {
@Override
public boolean test(Book book) {
return book.name().startsWith(prefix);
}
}
相关类图如下 ⬇️

我们对 filterBooks 方法也进行调整 ⬇️
java
public List<Book> filterBooks(List<Book> books, BookPredicate predicate) {
List<Book> result = new ArrayList<>();
for (Book book : books) {
if (predicate.test(book)) {
result.add(book);
}
}
return result;
}
调用方的代码可以这样写 ⬇️
java
// 变量名中最好不要使用非 ASCII 字符,以下两行为了让结果易于理解,姑且使用了汉字
List<Book> 金庸的书 = filterBooks(books, new BookAuthorPredicate("金庸"));
List<Book> 倚天 = filterBooks(books, new BookPrefixPredicate("倚天"));
5. 使用匿名类
在第 4 小节中,我们为不同的筛选条件定义了不同的记录类,但是这些记录类只用到了一次,是否有更简洁的写法呢?我们可以用匿名类来取代这些记录类 ⬇️
java
// 变量名中最好不要使用非 ASCII 字符,以下几行为了让结果易于理解,姑且使用了汉字
List<Book> 金庸的书 = filterBooks(books, new BookPredicate() {
@Override
public boolean test(Book book) {
return "金庸".equals(book.author());
}
});
List<Book> 倚天 = filterBooks(books, new BookPredicate() {
@Override
public boolean test(Book book) {
return book.name().startsWith("倚天");
}
});
6. 使用 Lambda 表达式
在第 5 步的基础上,我们可以更进一步,使用 Lambda 表达式 ⬇️
java
// 变量名中最好不要使用非 ASCII 字符,以下两行为了让结果易于理解,姑且使用了汉字
List<Book> 金庸的书 = filterBooks(books, book -> "金庸".equals(book.author()));
List<Book> 倚天 = filterBooks(books, book -> book.name().startsWith("倚天"));
7. 将 List 类型抽象化
如果再抽象一层,我们可以把 BookPredicate 变为一个通用的函数式接口 ⬇️
java
public interface Predidate<T> {
boolean test(T t);
}
这已经很接近 java.util.function.Predicate 了。
相应地, filterBooks 方法也可以变得更通用 ⬇️
java
public <T> List<T> filter(List<T> list, Predicate<T> predicate) {
List<T> result = new ArrayList<>();
for (T t : list) {
if (predicate.test(t)) {
result.add(t);
}
}
return result;
}
java.util.stream.Stream 接口中定义的 filter 方法,作用是类似的 (但是它的返回值是 Stream<T> 类型的)

如果利用 java.util.function.Predicate 和 java.util.stream.Stream 的话,调用方的代码可以简化为 ⬇️
java
// 变量名中最好不要使用非 ASCII 字符,以下两行为了让结果易于理解,姑且使用了汉字
List<Book> 金庸的书 = books.stream().filter(book -> "金庸".equals(book.author())).toList();
List<Book> 倚天 = books.stream().filter(book -> book.name().startsWith("倚天")).toList();
参考资料
- 《Java 实战(第2版)》(Modern Java in Action, 2nd Edition)
其他
文中的 UML 类图是如何绘制的?
我是借助了 IntelliJ IDEA (Community Edition) 中的 PlantUML 插件绘制的,具体的代码如下 ⬇️
puml
@startuml
caption \n\n
' caption 中的内容是为了防止掘金平台的水印遮盖图中的文字
+record Book {
- String author
- String name
+ Book(String author, String name)
+ String author()
+ String name()
}
+interface BookPredicate {
+ boolean test(Book book)
}
~record BookAuthorPredicate implements BookPredicate {
- String author
~BookAuthorPredicate(String author)
+ boolean test(Book book)
+ String author()
}
~record BookPrefixPredicate implements BookPredicate {
- String prefix
~BookPrefixPredicate(String prefix)
+ boolean test(Book book)
+ String prefix()
}
@enduml