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

文章来源声明: 原文作者:金銀銅鐵; 来源站点:掘金; 原文链接:https://juejin.cn/post/7689656350306517038; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

文章循序渐进,适合正在学习 Java 8 函数式编程与重构的开发者,能帮助理解从策略模式到 Lambda、Predicate、Stream 的思维跃迁,并用于实际集合筛选场景。

背景 --

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

正文

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

<span>public</span> <span>record</span> <span>Book</span><span>(String name, String author)</span> {
}

我们也可以用一个普通的类来表示书,具体的代码如下 ⬇️ 但用记录类比较简洁,所以在本文中,我们采用 上面 这个版本的 Book\text{Book}

<span>public</span> <span>class</span> <span>Book</span> {
    <span>private</span> String name;
    <span>private</span> String author;

    <span>public</span> String <span>getName</span><span>()</span> {
        <span>return</span> name;
    }

    <span>public</span> <span>void</span> <span>setName</span><span>(String name)</span> {
        <span>this</span>.name = name;
    }

    <span>public</span> String <span>getAuthor</span><span>()</span> {
        <span>return</span> author;
    }

    <span>public</span> <span>void</span> <span>setAuthor</span><span>(String author)</span> {
        <span>this</span>.author = author;
    }
}

1. 小试牛刀:筛选金庸的书籍

如果需要从书的列表中筛选出金庸的书,我们可以写一个这样的方法 ⬇️

<span>public</span> List<Book> <span>filterBooks</span><span>(List<Book> books)</span> {
    List<Book> result = <span>new</span> <span>ArrayList</span><>();
    <span>for</span> (Book book : books) {
        <span>if</span> (<span>"金庸"</span>.equals(book.author())) {
            result.add(book);
        }
    }
    <span>return</span> result;
}

2. 把作者作为参数

在第 1\text{1} 步里,我们在方法中硬编码了作者(即,金庸),这样的代码难以复用,所以我们可以把指定的作者作为方法的参数 ⬇️

<span>public</span> List<Book> <span>filterBooksByAuthor</span><span>(List<Book> books, String author)</span> {
    List<Book> result = <span>new</span> <span>ArrayList</span><>();
    <span>for</span> (Book book : books) {
        <span>if</span> (author.equals(book.author())) {
            result.add(book);
        }
    }
    <span>return</span> result;
}

而调用方将指定的作者作为参数 ⬇️

<span>// 变量名中最好不要使用非 ASCII 字符,以下两行为了让结果易于理解,姑且使用了汉字</span>
List<Book> 金庸的书 = filterBooksByAuthor(books, <span>"金庸"</span>); 
List<Book> 曹雪芹的书 = filterBooksByAuthor(books, <span>"曹雪芹"</span>);

如果我们需要对书名进行筛选,可以写一个类似的方法 ⬇️

<span>public</span> List<Book> <span>filterBooksByPrefix</span><span>(List<Book> books, String prefix)</span> {
    List<Book> result = <span>new</span> <span>ArrayList</span><>();
    <span>for</span> (Book book : books) {
        <span>if</span> (book.name.startsWith(prefix)) {
            result.add(book);
        }
    }
    <span>return</span> result;
}

调用方通过提供指定的 prefix\text{prefix} 参数,就可以对书进行筛选 ⬇️

List<Book> result1 = filterBooksByPrefix(books, <span>"连"</span>); 
List<Book> result2 = filterBooksByPrefix(books, <span>"倚天"</span>);

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

刚才提到的 filterBooksByAuthor\text{filterBooksByAuthor} 方法以及 filterBooksByPrefix\text{filterBooksByPrefix} 方法的逻辑是类似的,它们都会对 books\text{books} 参数进行筛选操作,然后返回筛选结果。有没有办法将这两个方法合并呢?我们先看一个笨办法,可以用一个 flag\text{flag} 参数来表示对哪个属性进行筛选 ⬇️ (我是感觉这种写法很蹩脚,不过书中有类似的内容,所以就借鉴了一下)

<span>public</span> List<Book> <span>filterBooks</span><span>(List<Book> books, String author, String prefix, <span>boolean</span> flag)</span> {
    List<Book> result = <span>new</span> <span>ArrayList</span><>();
    <span>for</span> (Book book : books) {
        <span>if</span> (flag && author.equals(book.author) ||
                (!flag && book.name.startsWith(prefix))) {
            result.add(book);
        }
    }
    <span>return</span> result;
}

我们可以这样调用它 ⬇️

<span>// 变量名中最好不要使用非 ASCII 字符,以下两行为了让结果易于理解,姑且使用了汉字</span>
List<Book> 金庸的书 = filterBooks(books, <span>"金庸"</span>, <span>null</span>, <span>true</span>); 
List<Book> 倚天 = filterBooks(books, <span>null</span>, <span>"倚天"</span>, <span>false</span>);

4. 根据抽象条件筛选

第 33 小节的写法难以扩展而且可读性差,我们可以将筛选行为参数化。首先定义一个接口 ⬇️

<span>public</span> <span>interface</span> <span>BookPredicate</span> {
  <span>boolean</span> <span>test</span><span>(Book book)</span>;
}

然后我们就可以用不同的实现类来代表不同的筛选条件了 ⬇️ (为了简洁起见,这里用了记录类,用普通类也没问题)

<span>record</span> <span>BookAuthorPredicate</span><span>(String author)</span> <span>implements</span> <span>BookPredicate</span> {

    <span>@Override</span>
    <span>public</span> <span>boolean</span> <span>test</span><span>(Book book)</span> {
        <span>return</span> author.equals(book.author());
    }
}

<span>record</span> <span>BookPrefixPredicate</span><span>(String prefix)</span> <span>implements</span> <span>BookPredicate</span> {

    <span>@Override</span>
    <span>public</span> <span>boolean</span> <span>test</span><span>(Book book)</span> {
        <span>return</span> book.name().startsWith(prefix);
    }
}

相关类图如下 ⬇️

image.png

我们对 filterBooks\text{filterBooks} 方法也进行调整 ⬇️

<span>public</span> List<Book> <span>filterBooks</span><span>(List<Book> books, BookPredicate predicate)</span> {
    List<Book> result = <span>new</span> <span>ArrayList</span><>();
    <span>for</span> (Book book : books) {
        <span>if</span> (predicate.test(book)) {
            result.add(book);
        }
    }
    <span>return</span> result;
}

调用方的代码可以这样写 ⬇️

<span>// 变量名中最好不要使用非 ASCII 字符,以下两行为了让结果易于理解,姑且使用了汉字</span>
List<Book> 金庸的书 = filterBooks(books, <span>new</span> <span>BookAuthorPredicate</span>(<span>"金庸"</span>));
List<Book> 倚天 = filterBooks(books, <span>new</span> <span>BookPrefixPredicate</span>(<span>"倚天"</span>));

5. 使用匿名类

在第 4\text{4} 小节中,我们为不同的筛选条件定义了不同的记录类,但是这些记录类只用到了一次,是否有更简洁的写法呢?我们可以用匿名类来取代这些记录类 ⬇️

<span>// 变量名中最好不要使用非 ASCII 字符,以下几行为了让结果易于理解,姑且使用了汉字</span>
List<Book> 金庸的书 = filterBooks(books, <span>new</span> <span>BookPredicate</span>() {
    <span>@Override</span>
    <span>public</span> <span>boolean</span> <span>test</span><span>(Book book)</span> {
        <span>return</span> <span>"金庸"</span>.equals(book.author());
    }
});
List<Book> 倚天 = filterBooks(books, <span>new</span> <span>BookPredicate</span>() {
    <span>@Override</span>
    <span>public</span> <span>boolean</span> <span>test</span><span>(Book book)</span> {
        <span>return</span> book.name().startsWith(<span>"倚天"</span>);
    }
});

6. 使用 Lambda 表达式

在第 5\text{5} 步的基础上,我们可以更进一步,使用 Lambda\text{Lambda} 表达式 ⬇️

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

7. 将 List 类型抽象化

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

<span>public</span> <span>interface</span> <span>Predidate</span><T> {
  <span>boolean</span> <span>test</span><span>(T t)</span>;
}

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

相应地,filterBooks\text{filterBooks} 方法也可以变得更通用 ⬇️

<span>public</span> <T> List<T> <span>filter</span><span>(List<T> list, Predicate<T> predicate)</span> {
    List<T> result = <span>new</span> <span>ArrayList</span><>();
    <span>for</span> (T t : list) {
        <span>if</span> (predicate.test(t)) {
            result.add(t);
        }
    }
    <span>return</span> result;
}

java.util.stream.Stream\text{java.util.stream.Stream} 接口中定义的 filter\text{filter} 方法,作用是类似的 (但是它的返回值是 Stream<T> 类型的)

image.png

如果利用 java.util.function.Predicate\text{java.util.function.Predicate} 和 java.util.stream.Stream\text{java.util.stream.Stream} 的话,调用方的代码可以简化为 ⬇️

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

参考资料

  • 《Java 实战(第2版)》(Modern Java in Action, 2nd Edition)

其他

文中的 UML\text{UML} 类图是如何绘制的?

我是借助了 IntelliJ IDEA (Community Edition)\text{IntelliJ IDEA (Community Edition)} 中的 PlantUML\text{PlantUML} 插件绘制的,具体的代码如下 ⬇️

@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