Java 编程问题:九、函数式编程——深入研究5

简介: Java 编程问题:九、函数式编程——深入研究

197 组合函数、谓词和比较器

组合(或链接)函数、谓词和比较器允许我们编写应该统一应用的复合标准。

组合谓词

假设我们有以下Melon类和MelonList

public class Melon {
  private final String type;
  private final int weight;
  // constructors, getters, setters, equals(),
  // hashCode(), toString() omitted for brevity
}
List<Melon> melons = Arrays.asList(new Melon("Gac", 2000),
  new Melon("Horned", 1600), new Melon("Apollo", 3000),
  new Melon("Gac", 3000), new Melon("Hemi", 1600));

Predicate接口提供了三种方法,它们接受一个Predicate,并使用它来获得一个丰富的Predicate。这些方法是and()or()negate()

例如,假设我们要过滤重量超过 2000 克的西瓜。为此,我们可以写一个Predicate,如下所示:

Predicate<Melon> p2000 = m -> m.getWeight() > 2000;

现在,让我们假设我们想要丰富这个Predicate,只过滤符合p2000的瓜,并且是GacApollo类型的瓜。为此,我们可以使用and()or()方法,如下所示:

Predicate<Melon> p2000GacApollo 
  = p2000.and(m -> m.getType().equals("Gac"))
    .or(m -> m.getType().equals("Apollo"));

这从左到右被解释为a && (b || c),其中我们有以下内容:

  • am -> m.getWeight() > 2000
  • bm -> m.getType().equals("Gac")
  • cm -> m.getType().equals("Apollo")

显然,我们可以以同样的方式添加更多的标准。

我们把这个Predicate传给filter()

// Apollo(3000g), Gac(3000g)
List<Melon> result = melons.stream()
  .filter(p2000GacApollo)
  .collect(Collectors.toList());

现在,假设我们的问题要求我们得到上述复合谓词的否定。将这个谓词重写为!a && !b && !c或任何其他对应的表达式是很麻烦的。更好的解决方案是调用negate()方法,如下所示:

Predicate<Melon> restOf = p2000GacApollo.negate();

我们把它传给filter()

// Gac(2000g), Horned(1600g), Hemi(1600g)
List<Melon> result = melons.stream()
  .filter(restOf)
  .collect(Collectors.toList());

从 JDK11 开始,我们可以否定作为参数传递给not()方法的Predicate。例如,让我们使用not()过滤所有重量小于(或等于)2000 克的西瓜:

Predicate<Melon> pNot2000 = Predicate.not(m -> m.getWeight() > 2000);
// Gac(2000g), Horned(1600g), Hemi(1600g)
List<Melon> result = melons.stream()
  .filter(pNot2000)
  .collect(Collectors.toList());

组合比较器

让我们考虑上一节中相同的Melon类和MelonList

现在,让我们使用Comparator.comparing()按重量对Melon中的List进行排序:

Comparator<Melon> byWeight = Comparator.comparing(Melon::getWeight);
// Horned(1600g), Hemi(1600g), Gac(2000g), Apollo(3000g), Gac(3000g)
List<Melon> sortedMelons = melons.stream()
  .sorted(byWeight)
  .collect(Collectors.toList());

我们也可以按类型对列表进行排序:

Comparator<Melon> byType = Comparator.comparing(Melon::getType);
// Apollo(3000g), Gac(2000g), Gac(3000g), Hemi(1600g), Horned(1600g)
List<Melon> sortedMelons = melons.stream()
  .sorted(byType)
  .collect(Collectors.toList());

要反转排序顺序,只需调用reversed()

Comparator<Melon> byWeight 
  = Comparator.comparing(Melon::getWeight).reversed();

到目前为止,一切都很好!

现在,假设我们想按重量和类型对列表进行排序。换言之,当两个瓜的重量相同时(例如,Horned (1600g)Hemi(1600g),它们应该按类型分类(例如,Hemi(1600g)Horned(1600g))。朴素的方法如下所示:

// Apollo(3000g), Gac(2000g), Gac(3000g), Hemi(1600g), Horned(1600g)
List<Melon> sortedMelons = melons.stream()
  .sorted(byWeight)
  .sorted(byType)
  .collect(Collectors.toList());

显然,结果不是我们所期望的。这是因为比较器没有应用于同一个列表。byWeight比较器应用于原始列表,而byType比较器应用于byWeight的输出。基本上,byType取消了byWeight的影响。

解决方案来自Comparator.thenComparing()方法。此方法允许我们链接比较器:

Comparator<Melon> byWeightAndType 
  = Comparator.comparing(Melon::getWeight)
    .thenComparing(Melon::getType);
// Hemi(1600g), Horned(1600g), Gac(2000g), Apollo(3000g), Gac(3000g)
List<Melon> sortedMelons = melons.stream()
  .sorted(byWeightAndType)
  .collect(Collectors.toList());

这种口味的thenComparing()Function为参数。此Function用于提取Comparable排序键。返回的Comparator只有在前面的Comparator找到两个相等的对象时才应用。

另一种口味的thenComparing()得到了Comparator

Comparator<Melon> byWeightAndType = Comparator.comparing(Melon::getWeight)
  .thenComparing(Comparator.comparing(Melon::getType));

最后,我们来考虑一下Melon的以下List

List<Melon> melons = Arrays.asList(new Melon("Gac", 2000),
  new Melon("Horned", 1600), new Melon("Apollo", 3000),
  new Melon("Gac", 3000), new Melon("hemi", 1600));

我们故意在最后一个Melon上加了一个错误。它的类型这次是小写的。如果我们使用byWeightAndType比较器,则输出如下:

Horned(1600g), hemi(1600g), ...

作为一个字典顺序比较器,byWeightAndType将把Horned放在hemi之前。因此,以不区分大小写的方式按类型排序将非常有用。这个问题的优雅解决方案将依赖于另一种风格的thenComparing(),它允许我们传递一个FunctionComparator作为参数。传递的Function提取Comparable排序键,给定的Comparator用于比较该排序键:

Comparator<Melon> byWeightAndType = Comparator.comparing(Melon::getWeight)
  .thenComparing(Melon::getType, String.CASE_INSENSITIVE_ORDER);

这一次,结果如下(我们回到正轨):

hemi(1600g), Horned(1600g),...

对于intlongdouble,我们有comparingInt()comparingLong()comparingDouble()thenComparingInt()thenComparingLong()thenComparingDouble()comparing()thenComparing()方法有相同的味道。

组合函数

通过Function接口表示的 Lambda 表达式可以通过Function.andThen()Function.compose()方法组合。

andThen(Function<? super R,? extends V> after)返回一个组合的Function,它执行以下操作:

  • 将此函数应用于其输入
  • after函数应用于结果

我们来看一个例子:

Function<Double, Double> f = x -> x * 2;
Function<Double, Double> g = x -> Math.pow(x, 2);
Function<Double, Double> gf = f.andThen(g);
double resultgf = gf.apply(4d); // 64.0

在本例中,将f函数应用于其输入(4)。f的应用结果为 8(f(4) = 4 * 2。此结果是第二个函数g的输入。g申请结果为 64(g(8) = Math.pow(8, 2)。下图描述了四个输入的流程—1234

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qDIqxVzf-1657285412205)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/bb13bcce-901b-4a0c-9af1-9136d137794f.png)]

所以,这就像g(f(x))。相反的f(g(x))可以用Function.compose()来塑造。返回的合成函数将函数之前的应用于其输入,然后将此函数应用于结果:

double resultfg = fg.apply(4d); // 32.0

在本例中,g函数应用于其输入(4)。应用g的结果是 16(g(4) = Math.pow(4, 2)。这个结果是第二个函数f的输入。应用f的结果为 32(f(16) = 16 * 2)。下图描述了四个输入的流程–1234

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z8htdA9A-1657285412205)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/299fb4da-3ba6-408e-bf3c-3bf925db688b.png)]

基于同样的原则,我们可以通过组合addIntroduction()addBody()addConclusion()方法来开发编辑文章的应用。请看一下与本书捆绑在一起的代码,看看这本书的实现。

我们也可以编写其他管道,只需将其与合成过程结合起来。

198 默认方法

默认方法被添加到 Java8 中。它们的主要目标是为接口提供支持,以便它们能够超越抽象契约(仅包含抽象方法)而发展。对于编写库并希望以兼容的方式发展 API 的人来说,这个工具非常有用。通过默认方法,可以在不中断现有实现的情况下丰富接口。

接口直接实现默认方法,并通过default关键字进行识别。

例如,以下接口定义了一个抽象方法area(),默认方法称为perimeter()

public interface Polygon {
  public double area();
  default double perimeter(double...segments) {
    return Arrays.stream(segments)
      .sum();
  }
}

因为所有公共多边形(例如,正方形)的周长都是边的总和,所以我们可以在这里实现它。另一方面,面积公式因多边形而异,因此默认实现将不太有用。

现在,我们定义一个实现PolygonSquare类。其目标是通过周长表示正方形的面积:

public class Square implements Polygon {
  private final double edge;
  public Square(double edge) {
    this.edge = edge;
  }
  @Override
  public double area() {
    return Math.pow(perimeter(edge, edge, edge, edge) / 4, 2);
  }
}

其他多边形(例如矩形和三角形)可以实现Polygon,并基于通过默认实现计算的周长来表示面积。

但是,在某些情况下,我们可能需要覆盖默认方法的默认实现。例如,Square类可以覆盖perimeter()方法,如下所示:

@Override
public double perimeter(double...segments) {
  return segments[0] * 4;
}

我们可以称之为:

@Override
public double area() {
  return Math.pow(perimeter(edge) / 4, 2);
}

总结

我们的任务完成了!本章介绍无限流、空安全流和默认方法。一系列问题涵盖了分组、分区和收集器,包括 JDK12 teeing()收集器和编写自定义收集器。此外,takeWhile()dropWhile()、组合函数、谓词和比较器、Lambda 的测试和调试,以及其他一些很酷的话题。

从本章下载应用以查看结果和其他详细信息。

相关文章
|
3天前
|
存储 SQL 安全
Java 安全性编程:基本概念与实战指南
【4月更文挑战第27天】在当今的软件开发领域,安全性编程是一个至关重要的方面。Java,作为广泛使用的编程语言之一,提供了多种机制来保护应用免受常见的安全威胁。本博客将探讨 Java 安全性编程的基本概念,并通过实际示例来展示如何实现这些安全措施。
10 3
|
1天前
|
Java
Java中的条件语句结构在编程中的应用
Java中的条件语句结构在编程中的应用
4 0
|
1天前
|
安全 Java
Java修饰符在编程中的应用研究
Java修饰符在编程中的应用研究
6 0
|
1天前
|
Java 关系型数据库 MySQL
【JDBC编程】基于MySql的Java应用程序中访问数据库与交互数据的技术
【JDBC编程】基于MySql的Java应用程序中访问数据库与交互数据的技术
|
3天前
|
Java 开发者 UED
Java 异步和事件驱动编程:探索响应式模式
【4月更文挑战第27天】在现代软件开发中,异步和事件驱动编程是提高应用性能和响应性的关键策略。Java 提供了多种机制来支持这些编程模式,使开发者能够构建高效、可扩展的应用程序。
14 4
|
3天前
|
设计模式 Java
Java 设计模式:混合、装饰器与组合的编程实践
【4月更文挑战第27天】在面向对象编程中,混合(Mixins)、装饰器(Decorators)和组合(Composition)是三种强大的设计模式,用于增强和扩展类的功能。
9 1
|
3天前
|
Java
Java 事件驱动编程:概念、优势与实战示例
【4月更文挑战第27天】事件驱动编程是一种编程范式,其中程序的执行流程由外部事件的发生而触发或驱动。
9 0
|
3天前
|
Java Shell API
Java 模块化编程:概念、优势与实战指南
【4月更文挑战第27天】Java 模块化编程是 Java 9 中引入的一项重大特性,通过 Java Platform Module System (JPMS) 实现。模块化旨在解决 Java 应用的封装性、可维护性和性能问题
10 0
|
3天前
|
安全 Java
【JAVA】Java并发编程中的锁升级机制
【JAVA】Java并发编程中的锁升级机制
|
4天前
|
缓存 Java
Java并发编程:深入理解线程池
【4月更文挑战第26天】在Java中,线程池是一种重要的并发工具,它可以有效地管理和控制线程的执行。本文将深入探讨线程池的工作原理,以及如何使用Java的Executor框架来创建和管理线程池。我们将看到线程池如何提高性能,减少资源消耗,并提供更好的线程管理。