原文:Java Coding Problems
协议:CC BY-NC-SA 4.0
贡献者:飞龙
本文来自【ApacheCN Java 谷歌翻译用谷歌翻译。
本章包括 11 个涉及 Java 函数编程问题。我们将从一个旨在提供的问题开始 0 函数接口的完整过程。然后,我们将继续研究 GoF 我们将使用一套设计模式 Java 解释这些模式的函数风格。
本章结束时,您应熟悉函数编程,并准备继续处理一组问题,允许我们深入研究这个主题。您应该能够使用一堆通用的函数设计模式,并知道如何开发代码来使用函数接口。
问题
使用以下问题来测试您的函数编程能力。在使用解决方案和下载示例程序之前,我强烈建议您尝试每个问题:
- 编写函数接口:通过一组有意义的例子来定义编写程序 0 到函数接口的路径。
- :解释什么是 Lambda 表达式。
- :基于 Lambda 编写实现环绕执行模式的程序。
- :基于 Lambda 编写一个实现工厂模式的程序。
- :基于 Lambda 编写实现战略模式的程序。
- :基于 Lambda 编写实现模板方法模式的程序。
- :基于 Lambda 编写实现观察者模式的程序。
- :基于 Lambda 编制实现贷款模式的程序。
- :基于 Lambda 编写实现装饰模式的程序。
- :基于 Lambda 编写实现级联生成器模式的程序。
- :基于 Lambda 编写实现命令模式的程序。
以下部分介绍了上述问题的解决方案。请记住,通常没有正确的方法来解决特定的问题。此外,请记住,这里的解释只包括解决这些问题所需的最有趣和最重要的细节。您可以下载示例解决方案,查看更多详细信息并尝试程序。
166 编写函数接口
在这个解决方案中,我们将强调函数接口的使用和可用性,并与几个替代方案进行比较。我们将研究如何从基本和严格的实现发展到基于函数接口的灵活实现。为此,让我们考虑以下内容Melon
类:
public class Melon {
private final String type; private final int weight; private final String origin; public Melon(String type, int weight, String origin) {
this.type = type; this.weight = weight; this.origin = origin; } // getters, toString(), and so on omitted for brevity }
假设我们有一个客户——我们叫他马克——他想开一家卖甜瓜的公司。我们根据他的描述创建了以前的类别。他的主要目标是有一个库存应用程序来支持他的想法和决策,因此有必要创建一个必须基于业务需求和发展的应用程序。我们将检查每天开发这个应用程序所需的时间。
第 1 天(按瓜类过滤)
有一天,马克让我们根据瓜的类型提供过滤瓜的功能。因此,我们创建了一个名字Filters
工具类,实现了一个static
该方法以瓜列表和要过滤的类型为参数。
方法很简单:
public static List<Melon> filterByType( List<Melon> melons, String type) {
List<Melon> result = new ArrayList<>();
for (Melon melon: melons) {
if (melon != null && type.equalsIgnoreCase(melon.getType())) {
result.add(melon);
}
}
return result;
}
完成!现在,我们可以很容易地按类型过滤西瓜,如下例所示:
List<Melon> bailans = Filters.filterByType(melons, "Bailan");
第 2 天(过滤一定重量的瓜)
虽然马克对结果很满意,但他要求另一个过滤器来获得一定重量的瓜(例如,所有 1200 克的瓜)。我们刚刚对甜瓜类型实现了这样一个过滤器,因此我们可以为一定重量的甜瓜提出一个新的static
方法,如下所示:
public static List<Melon> filterByWeight(
List<Melon> melons, int weight) {
List<Melon> result = new ArrayList<>();
for (Melon melon: melons) {
if (melon != null && melon.getWeight() == weight) {
result.add(melon);
}
}
return result;
}
这与filterByType()
类似,只是它有不同的条件/过滤器。作为开发人员,我们开始明白,如果我们继续这样做,Filters
类最终会有很多方法,这些方法只是重复代码并使用不同的条件。我们非常接近一个样板代码案例。
第 3 天(按类型和重量过滤瓜)
事情变得更糟了。马克现在要求我们添加一个新的过滤器,按类型和重量过滤西瓜,他需要这个很快。然而,最快的实现是最丑陋的。过来看:
public static List<Melon> filterByTypeAndWeight(
List<Melon> melons, String type, int weight) {
List<Melon> result = new ArrayList<>();
for (Melon melon: melons) {
if (melon != null && type.equalsIgnoreCase(melon.getType())
&& melon.getWeight() == weight) {
result.add(melon);
}
}
return result;
}
在我们的情况下,这是不可接受的。如果我们在这里添加一个新的过滤条件,代码将变得很难维护并且容易出错。
第 4 天(将行为作为参数)
会议时间到了!我们不能继续像这样添加更多的过滤器;我们能想到的每一个属性的过滤器最终都会出现在一个巨大的Filters
类中,这个类有大量复杂的方法,其中包含太多的参数和大量的样板代码。
主要的问题是我们在样板代码中有不同的行为。因此,只编写一次样板代码并将行为作为一个参数来推送是很好的。这样,我们就可以将任何选择条件/标准塑造成行为,并根据需要对它们进行处理。代码将变得更加清晰、灵活、易于维护,并且具有更少的参数。
这被称为,如下图所示(左侧显示我们现在拥有的;右侧显示我们想要的):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kLSlKwKL-1657284745693)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/11024e3c-1a23-4e17-a15b-f2e9060caada.png)]
如果我们将每个选择条件/标准看作一种行为,那么将每个行为看作一个接口的实现是非常直观的。基本上,所有这些行为都有一个共同点——选择条件/标准和返回boolean
类型(这被称为)。在接口的上下文中,这是一个可以按如下方式编写的合同:
public interface MelonPredicate {
boolean test(Melon melon);
}
此外,我们可以编写MelonPredicate
的不同实现。例如,过滤Gac
瓜可以这样写:
public class GacMelonPredicate implements MelonPredicate {
@Override
public boolean test(Melon melon) {
return "gac".equalsIgnoreCase(melon.getType());
}
}
或者,过滤所有重量超过 5000 克的西瓜可以写:
public class HugeMelonPredicate implements MelonPredicate {
@Override
public boolean test(Melon melon) {
return melon.getWeight() > 5000;
}
}
这种技术有一个名字——策略设计模式。根据 GoF 的说法,这可以“定义一系列算法,封装每个算法,并使它们可以互换。策略模式允许算法在客户端之间独立变化”。
因此,主要思想是在运行时动态选择算法的行为。MelonPredicate
接口统一了所有用于选择西瓜的算法,每个实现都是一个策略。
目前,我们有策略,但没有任何方法接收到一个MelonPredicate
参数。我们需要一个filterMelons()
方法,如下图所示:
所以,我们需要一个参数和多个行为。让我们看看filterMelons()
的源代码:
public static List<Melon> filterMelons(
List<Melon> melons, MelonPredicate predicate) {
List<Melon> result = new ArrayList<>();
for (Melon melon: melons) {
if (melon != null && predicate.test(melon)) {
result.add(melon);
}
}
return result;
}
这样好多了!我们可以通过以下不同的行为重用此方法(这里,我们传递GacMelonPredicate
和HugeMelonPredicate
:
List<Melon> gacs = Filters.filterMelons(
melons, new GacMelonPredicate());
List<Melon> huge = Filters.filterMelons(
melons, new HugeMelonPredicate());
第 5 天(实现另外 100 个过滤器)
马克要求我们再安装 100 个过滤器。这一次,我们有足够的灵活性和支持来完成这项任务,但是我们仍然需要为每个选择标准编写 100 个实现MelonPredicate
的策略或类。此外,我们必须创建这些策略的实例,并将它们传递给filterMelons()
方法。
这意味着大量的代码和时间。为了保存这两者,我们可以依赖 Java 匿名类。换句话说,同时声明和实例化没有名称的类将导致如下结果:
List<Melon> europeans = Filters.filterMelons(
melons, new MelonPredicate() {
@Override
public boolean test(Melon melon) {
return "europe".equalsIgnoreCase(melon.getOrigin());
}
});
在这方面取得了一些进展,但这并不是很重要,因为我们仍然需要编写大量代码。检查下图中突出显示的代码(此代码对每个实现的行为重复):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VvKKN3tu-1657284745695)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/3b1ba54b-775c-46c8-8948-7c4970e96d1c.png)]
在这里,代码不友好。匿名类看起来很复杂,而且它们看起来有些不完整和奇怪,特别是对新手来说。
第 6 天(匿名类可以写成 Lambda)
新的一天,新的想法!任何智能 IDE 都可以为我们指明前进的道路。例如,NetbeansIDE 将不连续地警告我们,这个匿名类可以作为 Lambda 表达式编写。
如以下屏幕截图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fZAx0nTz-1657284745696)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/016979a0-1499-49a3-ae48-f83e60f74e31.png)]
这个消息非常清楚——这个匿名的内部类创建可以转换成 Lambda 表达式。在这里,手工进行转换,或者让 IDE 为我们做。
结果如下:
List<Melon> europeansLambda = Filters.filterMelons(
melons, m -> "europe".equalsIgnoreCase(m.getOrigin()));
这样好多了!Java8Lambda 表达式这次做得很好。现在,我们可以以更灵活、快速、干净、可读和可维护的方式编写马克的过滤器。
第 7 天(抽象列表类型)
马克第二天带来了一些好消息——他将扩展业务,销售其他水果和瓜类。这很酷,但是我们的谓词只支持Melon
实例。
那么,我们应该如何继续支持其他水果呢?还有多少水果?如果马克决定开始销售另一类产品,如蔬菜,该怎么办?我们不能简单地为它们中的每一个创建谓词。这将带我们回到起点。
显而易见的解决方案是抽象List
类型。我们首先定义一个新接口,这次将其命名为Predicate
(从名称中删除Melon
):
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
接下来,我们覆盖filterMelons()
方法并将其重命名为filter()
:
public static <T> List<T> filter(
List<T> list, Predicate<T> predicate) {
List<T> result = new ArrayList<>();
for (T t: list) {
if (t != null && predicate.test(t)) {
result.add(t);
}
}
return result;
}
现在,我们可以为Melon
编写过滤器:
List<Melon> watermelons = Filters.filter(
melons, (Melon m) -> "Watermelon".equalsIgnoreCase(m.getType()));
我们也可以对数字做同样的处理:
List<Integer> numbers = Arrays.asList(1, 13, 15, 2, 67);
List<Integer> smallThan10 = Filters
.filter(numbers, (Integer i) -> i < 10);
退后一步,看看我们的起点和现在。由于 Java8 函数式接口和 Lambda 表达式,这种差异是巨大的。你注意到Predicate
接口上的@FunctionalInterface
注解了吗?好吧,这是一个信息注释类型,用于标记函数式接口。如果标记的接口不起作用,则发生错误是很有用的。
从概念上讲,函数式接口只有一个抽象方法。此外,我们定义的Predicate
接口已经作为java.util.function.Predicate
接口存在于 Java8 中。java.util.function
包包含 40 多个这样的接口。因此,在定义一个新的包之前,最好检查这个包的内容。大多数情况下,六个标准的内置函数式接口就可以完成这项工作。具体如下:
Predicate<T>
Consumer<T>
Supplier<T>
Function<T, R>
UnaryOperator<T>
BinaryOperator<T>
函数式接口和 Lambda 表达式是一个很好的团队。Lambda 表达式支持直接内联实现函数式接口的抽象方法。基本上,整个表达式被视为函数式接口的具体实现的实例,如以下代码所示:
Predicate<Melon> predicate = (Melon m)
-> "Watermelon".equalsIgnoreCase(m.getType());
167 Lambda 简述
剖析 Lambda 表达式将显示三个主要部分,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3SkumBq3-1657284745697)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/7239e25b-eb5c-4b10-b5ed-978a5d2312e9.png)]
以下是 Lambda 表达式每个部分的说明:
- 在箭头的左侧,我们有 Lambda 主体中使用的参数。这些是
FilenameFilter.accept(File folder, String fileName)
方法的参数。 - 在箭头的右侧,我们有 Lambda 主体,在本例中,它检查找到文件的文件夹是否可以读取,以及文件名是否以
.pdf
后缀结尾。 - 箭头只是 Lambda 参数和主体的分隔符。
此 Lambda 的匿名类版本如下所示:
FilenameFilter filter = new FilenameFilter() {
@Override
public boolean accept(File folder, String fileName) {
return folder.canRead() && fileName.endsWith(".pdf");
}
};
现在,如果我们看 Lambda 和它的匿名版本,那么我们可以得出结论,Lambda 表达式是一个简明的匿名函数,可以作为参数传递给方法或保存在变量中。我们可以得出结论,Lambda 表达式可以根据下图中所示的四个单词来描述:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GzCNamau-1657284745698)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/5f6de777-d9b2-4335-a7b3-36e5af1eb30d.png)]
Lambda 支持行为参数化,这是一个很大的优点(查看前面的问题以获得对此的详细解释)。最后,请记住 Lambda 只能在函数式接口的上下文中使用。
168 实现环绕执行模式
环绕执行模式试图消除围绕特定任务的样板代码。例如,为了打开和关闭文件,特定于文件的任务需要被代码包围。
主要地,环绕执行模式在暗示在资源的开-关生命周期内发生的任务的场景中很有用。例如,假设我们有一个Scanner
,我们的第一个任务是从文件中读取一个double
值:
try (Scanner scanner = new Scanner(
Path.of("doubles.txt"), StandardCharsets.UTF_8)) {
if (scanner.hasNextDouble()) {
double value = scanner.nextDouble();
}
}
稍后,另一项任务包括打印所有double
值:
try (Scanner scanner = new Scanner(
Path.of("doubles.txt"), StandardCharsets.UTF_8)) {
while (scanner.hasNextDouble()) {
System.out.println(scanner.nextDouble());
}
}
下图突出了围绕这两项任务的样板代码:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VTncnQyP-1657284745699)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/9c5cc105-bad6-40a4-9334-0ea4aac81b72.png)]
为了避免这个样板代码,环绕执行模式依赖于行为参数化(在“编写函数式接口”一节中进一步详细说明)。实现这一点所需的步骤如下:
- 第一步是定义一个与
Scanner -> double
签名匹配的函数式接口,该接口可能抛出一个IOException
:
@FunctionalInterface
public interface ScannerDoubleFunction {
double readDouble(Scanner scanner) throws IOException;
}
声明函数式接口只是解决方案的一半。
- 到目前为止,我们可以编写一个
Scanner -> double
类型的 Lambda,但是我们需要一个接收并执行它的方法。为此,让我们考虑一下Doubles
工具类中的以下方法:
public static double read(ScannerDoubleFunction snf)
throws IOException {
try (Scanner scanner = new Scanner(
Path.of("doubles.txt"), StandardCharsets.UTF_8)) {
return snf.readDouble(scanner);
}
}
传递给read()
方法的 Lambda 在这个方法的主体中执行。当我们传递 Lambda 时,我们提供了一个称为直接内联的abstract
方法的实现。主要是作为函数式接口ScannerDoubleFunction
的一个实例,因此我们可以调用readDouble()
方法来获得期望的结果。
- 现在,我们可以简单地将任务作为 Lambda 传递并重用
read()
方法。例如,我们的任务可以包装在两个static
方法中,如图所示(这种做法是为了获得干净的代码并避免大 Lambda):
private static double getFirst(Scanner scanner) {
if (scanner.hasNextDouble()) {
return scanner.nextDouble();
}
return Double.NaN;
}
private static double sumAll(Scanner scanner) {
double sum = 0.0d;
while (scanner.hasNextDouble()) {
sum += scanner.nextDouble();
}
return sum;
}
- 以这两个任务为例,我们还可以编写其他任务。让我们把它们传递给
read()
方法:
double singleDouble
= Doubles.read((Scanner sc) -> getFirst(sc));
double sumAllDoubles
= Doubles.read((Scanner sc) -> sumAll(sc));
环绕执行模式对于消除特定于打开和关闭资源(I/O 操作)的样板代码非常有用。
169 实现工厂模式
简而言之,工厂模式允许我们创建多种对象,而无需向调用者公开实例化过程。通过这种方式,我们可以隐藏创建对象的复杂和/或敏感过程,并向调用者公开直观且易于使用的对象工厂
在经典实现中,工厂模式依赖于实习生switch()
,如下例所示:
public static Fruit newInstance(Class<?> clazz) {
switch (clazz.getSimpleName()) {
case "Gac":
return new Gac();
case "Hemi":
return new Hemi();
case "Cantaloupe":
return new Cantaloupe();
default:
throw new IllegalArgumentException(
"Invalid clazz argument: " + clazz);
}
}
这里,Gac
、Hemi
、Cantaloupe
实现相同的Fruit
接口,并有空构造器。如果该方法生活在名为MelonFactory
的实用类中,则可以调用如下:
Gac gac = (Gac) MelonFactory.newInstance(Gac.class);
但是,Java8 函数样式允许我们使用方法引用技术引用构造器。这意味着我们可以定义一个Supplier<Fruit>
来引用Gac
空构造器,如下所示:
Supplier<Fruit> gac = Gac::new;
那么Hemi
、Cantaloupe
等呢?好吧,我们可以简单地把它们都放在一个Map
中(注意这里没有实例化甜瓜类型;它们只是懒惰的方法引用):
private static final Map<String, Supplier<Fruit>> MELONS
= Map.of("Gac", Gac::new, "Hemi", Hemi::new,
"Cantaloupe", Cantaloupe::new);
此外,我们可以覆盖newInstance()
方法来使用这个映射:
public static Fruit newInstance(Class<?> clazz) { Supplier<Fruit> supplier = MELONS.get(clazz.getSimpleName()); if (supplier == null) { throw new IllegalArgumentException( "Invalid clazz argument: " + clazz); } return supplier.get