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

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

相关文章
|
1月前
|
Java 程序员
Java编程中的异常处理:从基础到高级
在Java的世界中,异常处理是代码健壮性的守护神。本文将带你从异常的基本概念出发,逐步深入到高级用法,探索如何优雅地处理程序中的错误和异常情况。通过实际案例,我们将一起学习如何编写更可靠、更易于维护的Java代码。准备好了吗?让我们一起踏上这段旅程,解锁Java异常处理的秘密!
|
1月前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
1月前
|
算法 Java 调度
java并发编程中Monitor里的waitSet和EntryList都是做什么的
在Java并发编程中,Monitor内部包含两个重要队列:等待集(Wait Set)和入口列表(Entry List)。Wait Set用于线程的条件等待和协作,线程调用`wait()`后进入此集合,通过`notify()`或`notifyAll()`唤醒。Entry List则管理锁的竞争,未能获取锁的线程在此排队,等待锁释放后重新竞争。理解两者区别有助于设计高效的多线程程序。 - **Wait Set**:线程调用`wait()`后进入,等待条件满足被唤醒,需重新竞争锁。 - **Entry List**:多个线程竞争锁时,未获锁的线程在此排队,等待锁释放后获取锁继续执行。
67 12
|
1月前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
169 2
|
1月前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
1月前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
66 3
|
1月前
|
存储 Java 数据挖掘
Java 8 新特性之 Stream API:函数式编程风格的数据处理范式
Java 8 引入的 Stream API 提供了一种新的数据处理方式,支持函数式编程风格,能够高效、简洁地处理集合数据,实现过滤、映射、聚合等操作。
84 6
|
1月前
|
开发框架 安全 Java
Java 反射机制:动态编程的强大利器
Java反射机制允许程序在运行时检查类、接口、字段和方法的信息,并能操作对象。它提供了一种动态编程的方式,使得代码更加灵活,能够适应未知的或变化的需求,是开发框架和库的重要工具。
71 4
|
2月前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
78 1
|
18天前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
80 17

热门文章

最新文章