Java中Lambda表达式并行与组合行为

从串行到并行

串行指一个步骤一个步骤地处理,也就是通常情况下,代码一行一行地执行。

如果将我们常用的迭代器式的循环展开的话,就是串行执行了循环体内所定义的操作:

sum += arr.get(0);
sum += arr.get(1);
sum += arr.get(2);
//...

在书的一开始,就提到Java需要支持集合的并行计算(而Lambda为这个需求提供了可能)。

这些功能将全部被实现于库代码中,对于我们使用者,实现并行的复杂性被大大降低(最低程度上只需要调用相关方法)。

另外,关于并发与并行这两个概念,其实是不同的,如果不明白的话请自行了解,在此只引用一句非常流行的话:

一个是关于代码结构,一个是关于代码执行。

如果我们想将一个计算任务均匀地分配给CPU的四个内核,我们会给每个核分配一个用于计算的线程,每个线程上进行整个任务的子任务。

书上有一段非常形象的伪代码:

if the task list contains more than N/4 elements {
 leftTask = task.getLeftHalf()
 rightTask = task.getRightHalf()
 doInparallel {
 leftResult = leftTask.solve()
 rightResult = rightTask.solve()
 }
 result = combine(leftResult, rightResult)
} else {
 result = task.solveSequentially()
}

代码中,将每四个任务元素分为一组,用四个内核对其进行并行处理,然后每两组进行一次结果的合并,最终得到整个任务队列的最终结果。

从整体处理流程上看,先将任务队列递归地进行分组,并行处理每一组,然后将结果递归地进行合并(合并通过管道终止操作实现)。

Java8之前,开发者们使用一种针对集合的fork/join框架来实现该模式。

然而现在,想对代码进行性能优化,就是一件非常容易的事了。

还记得我们上一节中所得出的最终代码:

long validContactCounter = contactList.stream()
 .map(s -> new Contact().setName(s))
 .filter(Contact::call)
 .count();

稍加改动:

long validContactCounter = contactList.parallelStream()
 .map(s -> new Contact().setName(s))
 .filter(Contact::call)
 .count();

注意stream()变为parallelStream()

同时下图将展示如何根据四个核对上述任务进行分解处理,最终合并结果并终止管道。

注意递归分解的目的是使子任务们足够小来串行执行。

组合行为

Java写手应该知道,Java中并不存在纯粹的“函数”,只存在“方法”。也就是说,Java中的函数必须依赖于某一个类,或者作为类的某种行为存在。

而在其他语言中,存在纯函数,以CoffeeScript的语法,声明一个函数:

eat = (x) ->
 alert("#{x} has been eatten!")

这种写法与Lambda表达式的语法非常相近,也就是说,相比于匿名内部类,Lambda表达式看上去更像是一种函数表达式。

对于函数,一个核心操作便是组合。如果要求一元二次函数的其中一个解sqrt(sqr(b) - 4 * a * c),便是对多个子函数进行了组合。

对于面向对象,我们通过解耦的方式来分解它,同样,我们也希望以此种方式分解一个函数行为。

首先,沿用上两节中使用的例子,对Contact类稍作修改,将name属性分拆为名和姓:

private String firstName;
private String lastName;

假设我们现在想要对联系人们进行排序,创建自定义排序的Java标准方式是创建一个Comparator:

public interface Comparator<T> {
 int compare(T o1, T o2);
 //...
}

我们想通过比较名的首字母来为联系人排序:

Comparator<Contact> byFirstName = new Comparator<Contact>() {
 @Override
 public int compare(Contact o1, Contact o2) {
 return Character.compare(o1.getFirstName().charAt(0), o2.getFirstName().charAt(0));
 }
};

Lambda写法:

Comparator<Contact> byFirstNameLambdaForm = (o1, o2) ->
 Character.compare(o1.getFirstName().charAt(0), o2.getFirstName().charAt(0));

写完这段代码后,IDEA立即提醒我代码可以替换为Comparator.comparingInt(...),不过这是后话,暂且不表。

在上面的代码中,我们发现了组合行为,即Comparator<Contact>的compare(...)方法里面还套用了o.getFirstName()与Character.compare(...)这两个方法(为了简洁,这里暂不考虑charAt(...)),在java.util.function中,我们找到了这种函数的原型:

public interface Function<T, R> {
 R apply(T t);
 //...
}

接收一个T类型的参数,返回一个R类型的结果。

现在我们将“比较名的首字母”这个比较键的提取行为抽成一个函数对象的实例:

Function<Contact, Character> keyExtractor = o -> o.getFirstName().charAt(0);

再将“比较首字母”这个具体的比较行为抽出来:

Comparator<Character> keyComparator = (c1, c2) -> Character.compare(c1, c2);

有了keyExtractor和keyComparator,我们再来重新装配一下Comparator:

Comparator<Contact> byFirstNameAdvanced = (o1, o2) ->
 keyComparator.compare(keyExtractor.apply(o1), keyExtractor.apply(o2));

到了这一步,我们牺牲了简洁性,但获得了相应的灵活性,也就是说,如果我们改变比较键为姓而非名,只需改动keyExtractor为:

Function<Contact, Character> keyExtractor = o -> o.getLastName().charAt(0);

值得庆幸的是,库的设计者考虑到了这一自然比较的需求的普遍性,因此为Comparator接口提供了静态方法comparing(...),只需传入比较键的提取规则,就能针对该键生成相应的Comparator,是不是非常神奇:

Comparator<Contact> compareByFirstName = Comparator.comparing(keyExtractor);

即使我们想改变比较的规则,比如比较联系人姓与名的长度,也只需做些许改动:

Comparator<Contact> compareByNameLength = Comparator.comparing(p -> (p.getFirstName() + p.getLastName()).length());

这是一个重大的改进,它将我们所关注的焦点真正集中在了比较的规则上面,而不是大量地构建所必须的胶水代码。

comparing(...)通过接收一个简单的行为,进而基于这个行为构造出更加复杂的行为。

赞!

然而更赞的是,对于流和管道,我们所需要的改动甚至更少:

contacts.stream()
 .sorted(compareByNameLength)
 .forEach(c -> System.out.println(c.getFirstName() + " " + c.getLastName()));

小结

本章的代码:

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;
public class Bar {
 public static void main(String[] args) {
//    long validContactCounter = contactList.parallelStream()
//    .map(s -> new Contact().setFirstName(s))
//    .filter(Contact::call)
//    .count();
  List<Contact> contacts = new ArrayList<Contact>() {{
   add(new Contact().setFirstName("Foo").setLastName("Jack"));
   add(new Contact().setFirstName("Bar").setLastName("Ma"));
   add(new Contact().setFirstName("Olala").setLastName("Awesome"));
  }};
  Comparator<Contact> byFirstName = new Comparator<Contact>() {
   @Override
   public int compare(Contact o1, Contact o2) {
    return Character.compare(o1.getFirstName().charAt(0), o2.getFirstName().charAt(0));
   }
  };
  //--- Using Lambda form ---//
  Comparator<Contact> byFirstNameLambdaForm = (o1, o2) ->
    Character.compare(o1.getFirstName().charAt(0), o2.getFirstName().charAt(0));
  Function<Contact, Character> keyExtractor = o -> o.getFirstName().charAt(0);
  Comparator<Character> keyComparator = (c1, c2) ->
    Character.compare(c1, c2);
  Comparator<Contact> byFirstNameAdvanced = (o1, o2) ->
    keyComparator.compare(keyExtractor.apply(o1), keyExtractor.apply(o2));
  Comparator<Contact> compareByFirstName = Comparator.comparing(keyExtractor);
  Comparator<Contact> compareByNameLength = Comparator.comparing(p -> (p.getFirstName() + p.getLastName()).length());
  contacts.stream()
    .sorted(compareByNameLength)
    .forEach(c -> System.out.println(c.getFirstName() + " " + c.getLastName()));
 }
}

以及运行结果:

Bar Ma
Foo Jack
Olala Awesome

以上所述是小编给大家介绍的Java中Lambda表达式并行与组合行为,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!

(0)

相关推荐

  • Java通过Fork/Join优化并行计算

    本文实例为大家分享了Java通过Fork/Join优化并行计算的具体代码,供大家参考,具体内容如下 Java代码: package Threads; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.RecursiveAction; /** * Created by Frank */ public class RecursiveActionDemo extends RecursiveAction { sta

  • 浅谈Java Fork/Join并行框架

    初步了解Fork/Join框架 Fork/Join 框架是java7中加入的一个并行任务框架,可以将任务分割成足够小的小任务,然后让不同的线程来做这些分割出来的小事情,然后完成之后再进行join,将小任务的结果组装成大任务的结果.下面的图片展示了这种框架的工作模型: 使用Fork/Join并行框架的前提是我们的任务可以拆分成足够小的任务,而且可以根据小任务的结果来组装出大任务的结果,一个最简单的例子是使用Fork/Join框架来求一个数组中的最大/最小值,这个任务就可以拆成很多小任务,大任务就是

  • Java中Lambda表达式并行与组合行为

    从串行到并行 串行指一个步骤一个步骤地处理,也就是通常情况下,代码一行一行地执行. 如果将我们常用的迭代器式的循环展开的话,就是串行执行了循环体内所定义的操作: sum += arr.get(0); sum += arr.get(1); sum += arr.get(2); //... 在书的一开始,就提到Java需要支持集合的并行计算(而Lambda为这个需求提供了可能). 这些功能将全部被实现于库代码中,对于我们使用者,实现并行的复杂性被大大降低(最低程度上只需要调用相关方法). 另外,关于

  • Java中Lambda表达式之Lambda语法与作用域解析

    接上一篇:初探Lambda表达式/Java多核编程[2]并行与组合行为 本节是第二章开篇,前一章已经浅显地将所有新概念点到,书中剩下的部分将对这些概念做一个基础知识的补充与深入探讨实践. 本章将介绍Lambda表达式基础知识. 前言 把上一张书中的结语放到这里作为本章学习内容的开头,以此来概括Lambda表达式的优点: 提升性能.自动的并行化 更棒的API(comparing(...)细粒度的方法将成为标准) 编码风格得到改进.代码简化 反观前面几篇文章中的代码实践,以上三个优点全部得到了验证.

  • java中lambda表达式简单用例

    我对java中lambda表达式的看法是相当纠结的: 一个我这么想:lambda表达式降低了java程序的阅读体验.java程序一直不以表现力出众,正相反使Java流行的一个因素正是它的安全和保守--即使是初学者只要注意些也能写出健壮且容易维护的代码来.lambda表达式对开发人员的要求相对来说高了一层,因此也增加了一些维护难度. 另一个我这么想:作为一个码代码的,有必要学习并接受语言的新特性.如果只是因为它的阅读体验差就放弃它在表现力方面的长处,那么即使是三目表达式也有人觉得理解起来困难呢.语

  • java中lambda表达式的分析与具体用法

    Lamda表达式 λ 希腊字母表中排序第十一位字母,英语名称为Lambda 避免匿名内部类定义过多 其实质属于函数式 编程的概念 (params)->expression[表达式] (params)->statement[语句] (params)->{statements} (a-> System.out.print("i like lambda–>"+a)); new Thread (()->System.out.println("多线程

  • 浅谈Java中Lambda表达式的相关操作

    为什么要使用Lambda? 可以对一个接口进行非常简洁的实现. Lambda对接口的要求? 接口中定义的抽象方法有且只有一个才可以. 传统实现一个接口需要这样做: 方法一: // 实现接口,同时必须重写接口中抽象方法 class Test implements IntrfacefN { @Override public void getUser(int a, int b) { } } // @FunctionalInterface 注解意思:函数式接口,用来做规范,有这个注解,说明此接口有且只有

  • Java中Lambda表达式基础及使用

    目录 一.举例说明 1.无参无返回 1.1 定义一个接口 1.2接口实现类 1.3 测试类 2.有参无返回代码示例 3.有参有返回 二.简单事项 1.省略模式 2.注意事项 三.Lambda表达式和匿名内部类的区别 1.所需类型不同: 2.使用限制不同: 3.实现原理不同: 标准格式: 三要素:形式参数 箭头 代码块 格式:(形式参数)->{代码块} 形式参数:如果多个参数用逗号隔开,无参留空 ->:英文中划线和大于号组成 代码块:具体要做的事 使用前提: 有一个接口 接口中有且仅有一个抽象方

  • Java中Lambda表达式的使用详解

    目录 理解函数式接口以及 Lambda表达式的发展过程 Lambda表达式及语法 一起来看看具体的使用 你需要注意什么 Lambda的实际运用 1.对集合排序 2.遍历集合 3.遍历集合(带条件) 4.代替 Runnable,开启一个线程 理解函数式接口以及 Lambda表达式的发展过程 任何接口,只包含唯一一个抽象方法,就是函数式接口 /** * lambdab表达式的发展 */ public class TestLambda1 { //3.静态内部类 static class Like2 i

  • Java中Lambda表达式用法介绍

    Lambda lambda是一个匿名函数,我们可以把lambda表达式理解为是一段可以传递的代码. lambda简明的地将代码或方法作为参数传递进去执行. "函数式编程"其核心是把函数作为值. 函数式接口 :只有一个 抽象方法的接口 称之为 函数式接口.函数式接口可以使用@FunctionalInterface进行注解. lambda表达式拆分为两部分 左侧:lambda 表达式的参数列表 右侧:lambda 表达式中所需要执行的功能,即lambda体 语法格式一:无参数,无返回值 @

  • Java中lambda表达式的基本运用

    目录 一.实现接口 二.数字排序 三.字符串排序方法 四.对象排序 总结 一.实现接口 调用一个接口中的方法,传统方法:接口类A: package lombda; /** * @author yeqv * @program A2 * @Classname A * @Date 2022/1/25 20:38 * @Email w16638771062@163.com */ public interface A { int po(int i); } 实现接口: package lombda; /**

  • Java中lambda表达式实现aop切面功能

    目录 lambda表达式实现切面功能 定义一个函数式接口 创建两个实现类 客户端 缺点 背景:最近项目中涉及到自定义线程池中子线程获取父线程的traceId,这个数据的传递过程可以用lamdba表达式进行封装实现的.这让我想到spring容器的三级缓存.其中的一个缓存singletonFactories就是存放的lambda表达式的. // 缓存的声明 private final Map<String, ObjectFactory<?>> singletonFactories =

随机推荐