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 的测试和调试,以及其他一些很酷的话题。

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

相关文章
|
10月前
|
IDE Java 编译器
java编程最基础学习
Java入门需掌握:环境搭建、基础语法、面向对象、数组集合与异常处理。通过实践编写简单程序,逐步深入学习,打牢编程基础。
473 1
|
10月前
|
Java
如何在Java中进行多线程编程
Java多线程编程常用方式包括:继承Thread类、实现Runnable接口、Callable接口(可返回结果)及使用线程池。推荐线程池以提升性能,避免频繁创建线程。结合同步与通信机制,可有效管理并发任务。
341 6
|
10月前
|
安全 前端开发 Java
从反射到方法句柄:深入探索Java动态编程的终极解决方案
从反射到方法句柄,Java 动态编程不断演进。方法句柄以强类型、低开销、易优化的特性,解决反射性能差、类型弱、安全性低等问题,结合 `invokedynamic` 成为支撑 Lambda 与动态语言的终极方案。
371 0
|
11月前
|
Java 开发者
Java 函数式编程全解析:静态方法引用、实例方法引用、特定类型方法引用与构造器引用实战教程
本文介绍Java 8函数式编程中的四种方法引用:静态、实例、特定类型及构造器引用,通过简洁示例演示其用法,帮助开发者提升代码可读性与简洁性。
|
11月前
|
SQL Java 数据库
2025 年 Java 从零基础小白到编程高手的详细学习路线攻略
2025年Java学习路线涵盖基础语法、面向对象、数据库、JavaWeb、Spring全家桶、分布式、云原生与高并发技术,结合实战项目与源码分析,助力零基础学员系统掌握Java开发技能,从入门到精通,全面提升竞争力,顺利进阶编程高手。
1457 2
|
11月前
|
Java 大数据 API
Java Stream API:现代集合处理与函数式编程
Java Stream API:现代集合处理与函数式编程
448 100
|
11月前
|
Java 开发者
Java并发编程:CountDownLatch实战解析
Java并发编程:CountDownLatch实战解析
645 100
|
11月前
|
NoSQL Java 关系型数据库
超全 Java 学习路线,帮你系统掌握编程的超详细 Java 学习路线
本文为超全Java学习路线,涵盖基础语法、面向对象编程、数据结构与算法、多线程、JVM原理、主流框架(如Spring Boot)、数据库(MySQL、Redis)及项目实战等内容,助力从零基础到企业级开发高手的进阶之路。
706 1
|
10月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
441 1
|
10月前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
411 1