在本文中,我们将重点介绍功能接口的用途和可用性,以及几种替代方案。我们将研究如何将代码从基本的刚性实现演变为基于功能接口的灵活实现。为此,让我们考虑以下 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 }
假设我们有一个客户-我们称他为Mark-他想开一家卖瓜子的生意。我们根据他的描述塑造了上一课。他的主要目标是拥有一个能够维持其想法和决定的清单应用程序,因此需要创建一个必须根据业务需求和发展而增长的应用程序。在以下各节中,我们将每天研究开发此应用程序所需的时间。
Day 1 (Filtering Melons by Their Type) 有一天,马克要求我们提供一种功能,可按类型将甜瓜过滤掉。结果,我们创建了一个名为的实用程序类, 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");
Day 2 (Filtering Melons of a Certain Weight) 当Mark对结果感到满意时,他要求另一个过滤器来获得一定重量的瓜(例如,所有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 该类将最终得到许多方法,这些方法只需重复代码并使用不同的条件即可。我们在这里非常接近样板代码案例。
filterByType()
Filters
Day 3 (Filtering Melons by Type and Weight) 事情变得越来越糟。马克现在要求我们添加一个新过滤器,该过滤器可以按类型和重量过滤瓜类,而他很快就需要这样做。但是,最快的实现是最丑陋的。一探究竟:
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; }
在我们看来,这是不能接受的。如果在此处添加新的过滤条件,则代码将变得难以维护并且容易出错。
Day 4 (Pushing the Behavior as a Parameter) 开会时间!我们不能继续添加更多这样的过滤器;我们可以想到的每个属性的过滤都会以巨大的Filters类结束,该类具有大型,复杂的方法,具有太多的参数和大量的样板代码。
主要问题是我们在样板代码中具有不同的行为。因此,最好只编写样板代码 一次并将行为作为参数推送。这样,我们可以将任何选择条件/标准定性为行为,并根据需要进行调整。该代码将变得更加清晰,灵活,易于维护并且具有更少的参数。
这称为行为参数化,在下图中进行了说明(左侧显示了我们现在拥有的;右侧显示了我们想要的):
如果我们将每个选择条件/标准视为一种行为,那么将每种行为视为接口的实现是非常直观的。基本上,所有这些行为都有一个共同点-选择条件/条件和boolean 类型的返回 (这称为谓词)。在接口的上下文中,这是一个合同,可以编写如下:
public interface MelonPredicate { boolean test(Melon melon); }
此外,我们可以编写的不同实现 MelonPredicate。例如,过滤 Gac瓜可以这样写:
MelonPredicate
Gac
public class GacMelonPredicate implements MelonPredicate { @Override public boolean test(Melon melon) { return "gac".equalsIgnoreCase(melon.getType()); } }
另外,可以过滤掉所有重于5,000g的瓜:
public class HugeMelonPredicate implements MelonPredicate { @Override public boolean test(Melon melon) { return melon.getWeight() > 5000; } }
该技术有一个名称-策略设计模式。根据GoF的说法,这可以“定义一系列算法,封装每个算法,并使它们可互换。策略模式使算法因客户端而异。 ”
因此,主要思想是在运行时动态选择算法的行为。该 MelonPredicate 接口统一了所有专用于选择甜瓜的算法,并且每种实现都是一种策略。
目前,我们有策略,但是没有任何方法可以接收 MelonPredicate 参数。我们需要一个 filterMelons() 方法,如下图所示:
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):
GacMelonPredicate
HugeMelonPredicate
List<Melon> gacs = Filters.filterMelons( melons, new GacMelonPredicate()); List<Melon> huge = Filters.filterMelons( melons, new HugeMelonPredicate());
Day 5 (Implementing Another 100 Filters) Mark 要求我们再实施100个过滤器。这次,我们有足够的灵活性和支持来完成此任务,但是我们仍然需要编写100个策略或类来实现 MelonPredicate 每个选择标准。此外,我们必须创建这些策略的实例并将其传递给 filterMelons() 方法。
这意味着大量的代码和时间。为了保存两者,我们可以依靠Java匿名类。换句话说,具有没有名称被同时声明和实例化的类将导致如下所示:
List<Melon> europeans = Filters.filterMelons(melons, new MelonPredicate() { @Override public boolean test(Melon melon) { return "europe".equalsIgnoreCase(melon.getOrigin()); } });
这里取得了一些进展,但这并不是很重要,因为我们仍然需要编写很多代码。在下图中检查突出显示的代码(此代码针对每个已实现的行为重复执行):
在这里,代码不友好。匿名类似乎很复杂,并且它们看起来不完整和怪异,特别是对于新手而言。
Day 6 (Anonymous Classes Can Be Written as Lambdas) 新的一天,新的想法!任何智能IDE都可以向我们展示前进的道路。例如,NetBeans IDE将离散地警告我们该匿名类可以写为lambda表达式。在下面的屏幕快照中显示了该内容:
信息非常清晰–这种匿名内部类的创建可以转换为lambda表达式。在这里,请手动进行转换,或者让IDE为我们完成转换。
结果将如下所示:
List<Melon> europeansLambda = Filters.filterMelons( melons, m -> "europe".equalsIgnoreCase(m.getOrigin()));
这样好多了!这次Java 8 Lambda表达式做得很好。现在,我们可以以更加灵活,快速,干净,可读和可维护的方式编写Mark的过滤器。
Day 7 (Abstracting the List Type) 第二天,马克提出了一个好消息–他将扩展业务并出售其他水果和瓜类。这很酷,但是我们的谓词仅支持 Melon 实例。
那么,我们又该如何继续支持其他成果呢?还有多少其他水果?如果Mark决定开始销售另一类产品,例如蔬菜,该怎么办?我们不能简单地为它们中的每一个创建一个谓词。这将使我们重新开始。
显而易见的解决方案是抽象 List 类型。我们首先定义一个新接口,然后 Predicate 将Melon 其命名(从名称中删除 ):
@FunctionalInterface public interface Predicate<T> { boolean test(T t); }
接下来,我们重写 filterMelons()方法并将其重命名为 filter():
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以下内容编写过滤器 :
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);
退后一步,看看我们从哪里开始以及现在在哪里。归功于Java 8功能接口和lambda表达式,两者之间的差异非常大。您是否注意到 界面@FunctionalInterface上的注释 Predicate ?好吧,这是一种信息丰富的注释类型,用于标记功能接口。如果标记的接口不起作用,则发生错误很有用。
@FunctionalInterface
Predicate
从概念上讲,功能接口仅具有一种抽象方法。而且,Predicate我们定义的 接口已经在Java 8中作为java.util.function.Predicate 接口存在 。该 java.util.function 软件包包含40多个此类接口。因此,在定义一个新包之前,建议检查此包的内容。在大多数情况下,六个标准的内置功能接口可以完成任务。这些列出如下:
java.util.function.Predicate
Predicate<T> Consumer<T> Supplier<T> Function<T, R> UnaryOperator<T> BinaryOperator<T>
功能接口和lambda表达式组成了一个强大的团队。Lambda表达式支持直接内联功能接口的抽象方法的实现。基本上,整个表达式被视为功能接口的具体实现的一个实例,如以下代码所示:
Predicate<Melon> predicate = (Melon m) -> "Watermelon".equalsIgnoreCase(m.getType());
Lambdas in a Nutshell 剖析lambda表达式将揭示三个主要部分,如下图所示:
以下是lambda表达式各部分的描述:
在箭头的左侧,我们具有在lambda主体中使用的该lambda的参数。这些是FilenameFilter.accept (File folder, String fileName) 方法的参数 。 在箭头的右侧,我们有lambda主体,在这种情况下,该主体检查是否可以读取在其中找到文件的文件夹以及文件名是否以.pdf 后缀结尾 。 箭头只是lambda的参数和主体的分隔符。 此lambda的匿名类版本如下:
FilenameFilter.accept (File folder, String fileName)
.pdf
FilenameFilter filter = new FilenameFilter() { @Override public boolean accept(File folder, String fileName) { return folder.canRead() && fileName.endsWith(".pdf"); } };
现在,如果我们看一下lambda及其匿名版本,则可以得出结论,lambda表达式是一个简洁的匿名函数,可以将其作为参数传递给方法或保留在变量中。我们可以得出结论,可以根据下图中显示的四个词来描述lambda表达式:
Lambda支持行为参数化,这是一个很大的优势(有关详细说明,请查看前面的问题)。最后,请记住,lambda只能在功能接口的上下文中使用。
原文链接:http://codingdict.com/