[Java] 通过行为参数化传递代码

背景

我在看《Java 实战(第2版)》(Modern Java in Action, 2nd Edition)一书时,学到了些知识。本文取材于该书第二章:通过行为参数化传递代码。本文没有照抄原书的例子,但本文的结构参考了书中的内容。

正文

假设我们需要对一些书进行筛选操作,我们用如下的记录类来表示书。

java 复制代码
public record Book(String name, String author) {
}

我们也可以用一个普通的类来表示书,具体的代码如下 ⬇️ 但用记录类比较简洁,所以在本文中,我们采用 上面 这个版本的 Book\text{Book} 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\text{1} 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\text{prefix} prefix 参数,就可以对书进行筛选 ⬇️

java 复制代码
List<Book> result1 = filterBooksByPrefix(books, "连"); 
List<Book> result2 = filterBooksByPrefix(books, "倚天");

3. 对作者进行筛选或者对书名进行筛选

刚才提到的 filterBooksByAuthor\text{filterBooksByAuthor} filterBooksByAuthor 方法以及 filterBooksByPrefix\text{filterBooksByPrefix} filterBooksByPrefix 方法的逻辑是类似的,它们都会对 books\text{books} books 参数进行筛选操作,然后返回筛选结果。有没有办法将这两个方法合并呢?我们先看一个笨办法,可以用一个 flag\text{flag} 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. 根据抽象条件筛选

第 33 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\text{filterBooks} 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\text{4} 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\text{5} 5 步的基础上,我们可以更进一步,使用 Lambda\text{Lambda} Lambda 表达式 ⬇️

java 复制代码
// 变量名中最好不要使用非 ASCII 字符,以下两行为了让结果易于理解,姑且使用了汉字
List<Book> 金庸的书 = filterBooks(books, book -> "金庸".equals(book.author()));
List<Book> 倚天 = filterBooks(books, book -> book.name().startsWith("倚天"));

7. 将 List 类型抽象化

如果再抽象一层,我们可以把 BookPredicate\text{BookPredicate} BookPredicate 变为一个通用的函数式接口 ⬇️

java 复制代码
public interface Predidate<T> {
  boolean test(T t);
}

这已经很接近 java.util.function.Predicate\text{java.util.function.Predicate} java.util.function.Predicate 了。

相应地, filterBooks\text{filterBooks} 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\text{java.util.stream.Stream} java.util.stream.Stream 接口中定义的 filter\text{filter} filter 方法,作用是类似的 (但是它的返回值是 Stream<T> 类型的)

如果利用 java.util.function.Predicate\text{java.util.function.Predicate} java.util.function.Predicate 和 java.util.stream.Stream\text{java.util.stream.Stream} 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\text{UML} UML 类图是如何绘制的?

我是借助了 IntelliJ IDEA (Community Edition)\text{IntelliJ IDEA (Community Edition)} IntelliJ IDEA (Community Edition) 中的 PlantUML\text{PlantUML} 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
相关推荐
欧西1 小时前
AI全栈安全Agent平台(九):自动化测试用例生成引擎实战
后端
独泪了无痕1 小时前
Hutool之RandomUtil:随机数生成的终极利器
java·后端
imDwAaY1 小时前
6篇文章讲清楚Git:Git 远程协作:fetch、pull、push 到底有什么区别?(3/6)
git·后端
浪遏1 小时前
D2C 系统架构复盘:从设计稿到代码,我们踩过的坑和最终方案
前端·javascript·后端
明月_清风1 小时前
数据处理完放在哪里?一文搞懂数据仓库与数据湖
大数据·后端·数据分析
据说幸运很容易1 小时前
TestNG 分组接入现有框架:实现、踩坑与解法
后端·架构
IT_陈寒1 小时前
我的JavaScript代码为啥在forEach里没按预期执行?
前端·人工智能·后端
136096757231 小时前
1Panel 部署 ThinkPHP8 踩坑实录
后端
用户788477316341 小时前
源码交付清单:做完一个项目,你到底该从开发商手里拿到什么
后端