Java8函数式编程

TeXt 是一款 100% 兼容 GitHub Pages 的 Jekyll 主题,你可以通过 Fork、下载或主题等方式安装。

在这篇文章中,你将学到如何安装设置主题,通过本地预览进行开发以及编译发布

第 1 章 简介

1.1 为什么需要再次修改Java

  • 多核CPU并发编程,涉及锁的编程算法不但容易出错,而且耗费时间; java.util.concurrent 包和很多第三方类库 目前对并发抽象化,还未能达到很好的成果。

  • 面对大型数据集合,Java还欠缺高效的并行操作。

  • 为了编写这类处理批量数据的并行类库,需要在语言层面上修改现有的 Java:增加 Lambda 表达式。

1.2 什么是函数式编程

每个人对函数式编程的理解不尽相同。但其核心是:在思考问题时,使用不可变值和函数,函数对一个值进行处理,映射成另一个值。

第 2 章 Lambda表达式

Java8最大的变化是引入了 Lambda 表达式— 一种紧凑的、传递行为的方式。

2.1 第一个Lambda表达式

使用匿名内部类将行为和按钮单击进行关联

   button.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent event) {
          System.out.println("button clicked");
      }
   });

使用 Lambda 表达式将行为和按钮单击进行关联

   button.addActionListener(event -> System.out.println("button clicked"));

2.2 如何辨别Lambda表达式

Lambda表达式的语法:

   (parameters) -> expression

或者

   (parameters) -> { statements; }

Lambda 表达式的不同形式

   // 无参,返回类型为void
   Runnable noArguments = () -> System.out.println("Hello World");

   // 一个参数,返回类型为void
   ActionListener oneArgument = event -> System.out.println("button clicked");

   // 无参,表达式的主体是代码块(可以返回或抛出异常来退出)
   Runnable multiStatement = () -> { 􏰒
      System.out.print("Hello");
      System.out.println(" World");
   };

   // 接收2个参数,计算两个数字相加的结果
   BinaryOperator<Long> add = (x, y) -> x + y;

   // 显式声明参数类型,接收2个Long型整数,计算两个数字相加的结果
   BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;

目标类型是指 Lambda 表达式所在上下文环境的类型。比如,将 Lambda 表达式赋值给一个局部变量, 或传递给一个方法作为参数,局部变量或方法参数的类型就是 Lambda 表达式的目标类型。

2.3 引用值,而不是变量

匿名内部类中使用 final 局部变量

   final String name = getUserName();
   button.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent event) {
          System.out.println("hi " + name);
      }
   });

Lambda 表达式中引用既成事实上的 final 变量

   String name = getUserName();
   button.addActionListener(event -> System.out.println("hi " + name));

未使用既成事实上的 final 变量,导致无法通过编译

   String name = getUserName();
   name = formatUserName(name);
   button.addActionListener(event -> System.out.println("hi " + name));

Lambda 表达式中引用的局部变量必须是 final 或既成事实上的 final 变量。 i

2.4 函数接口

函数接口是只有一个抽象方法的接口,用作 Lambda 表达式的类型。

ActionListener 接口:接受 ActionEvent 类型的参数,返回空

   public interface ActionListener extends EventListener {
      public void actionPerformed(ActionEvent event);
   }

Java中重要的函数接口

接口 参数 返回类型 示例
Predicate<T> T boolean 这张唱片已经发行了吗
Consumer<T> T void 输出一个值
Function<T,R> T R 获得 Artist 对象的名字
Supplier<T> None T 工厂方法
UnaryOperator<T> T T 逻辑非(!)
BinaryOperator<T> (T, T) T 求两个数的乘积(*)

2.5 类型推断

使用菱形操作符,根据变量类型做推断

   Map<String, Integer> oldWordCounts = new HashMap<String, Integer>(); 􏰁
   Map<String, Integer> diamondWordCounts = new HashMap<>();

使用菱形操作符,根据方法签名做推断

   useHashmap(new HashMap<>());

   private void useHashmap(Map<String, String> values);

程序依然要经过类型检查来保证运行的安全性,但不用再显示声明类型罢了。这就是所谓的类型推断。

2.6 要点回顾

  • Lambda 表达式是一个匿名方法,将行为像数据一样进行传递

  • Lambda 表达式的常见结构:BinaryOperator add = (x, y) → x + y。

  • 函数接口指仅具有单个抽象方法的接口,用来表示Lambda表达式的类型。

第 3 章 流

3.1 从外部迭代到内部迭代

使用 for 循环计算来自伦敦的艺术家人数

   int count = 0;
   for (Artist artist : allArtists) {
      if (artist.isFrom("London")) {
          count++;
      }
   }

使用迭代器计算来自伦敦的艺术家人数

   int count = 0;
   Iterator<Artist> iterator = allArtists.iterator();
   while (iterator.hasNext()) {
      Artist artist = iterator.next();
      if (artist.isFrom("London")) {
          count++;
      }
   }

外部迭代存在的问题:

  • 每次迭代集合类时,都需要写很多样板代码

  • for 循环的样板代码模糊了代码的本意,程序员必须阅读整个循环体才能理解

  • 本质来讲是一种串行化操作

使用内部迭代计算来自伦敦的艺术家人数

   long count = allArtists.stream()
          .filter(artist -> artist.isFrom("London"))
          .count();

上面例子可被分解为两步更简单的操作:

  • 找出所有来自伦敦的艺术家;
  • 计算他们的人数。

每种操作都对应 Stream 接口的一个方法

Stream 是用函数式编程方式在集合类上进行复杂操作的工具。

3.2 实现机制

上面例子中,整个过程被分解为两种简单的操作:过滤和计数。 两种操作是否意味着需要两次循环?事实上,类库设计精妙, 只需对艺术家列表迭代一次。

只过滤,不计数

   allArtists.stream()
          .filter(artist -> artist.isFrom("London"));

filter只刻画出了stream,但没有产生新的集合。 像filter这样只描述stream,最终不产生新集合的方法叫作惰性求值方法; 而像count这样最终会从stream产生值的方法叫作及早求值方法

判断一个操作是惰性求值还是及早求值:只需看它的返回值

  • 返回值是stream,那么是惰性求值
  • 返回值是另一个值或为空,那么是及早求值 为什么要区分惰性求值和及早求值:

例如:如果要找出大于10的第一数字,那么并不需要和所有元素去做比较,只要找出第一个匹配的元素就够了。 这也意味着可以在集合类上级联多种操作,但迭代只需一次。

3.3 常用的流操作

3.3.1 collect(toList())

collect(toList()) 方法由 Stream 里的值生成一个列表,是一个及早求值操作。

3.3.2 map

如果有一个函数可以将一种类型的值转换成另外一种类型,map操作就可以使用该函数,将一个流中的值 转换成一个新的流。

3.3.3 filter

遍历数据并检查其中的元素时,可以尝试使用stream中提供的新方法filter。

3.3.4 flatMap

flatMap方法可以用stream替换值,然后将多个stream练成一个stream。

包含多个列表的 Stream

   List<Integer> together = Stream.of(asList(1, 2), asList(3, 4))
          .flatMap(numbers -> numbers.stream())
          .collect(toList());

调用 stream 方法,将每个列表转换成 Stream 对象,其余部分由 flatMap 方法处理。

flatMap 方法的相关函数接口和 map 方法一样,都是 Function 接口,只是方法的返回值限定为 stream 类型罢了。

3.3.5 max和min

Stream 上常用的操作之一是求最大值和最小值。

查找 Stream 中的最大或最小元素,首先要考虑的是用什么作为排序的指标。

3.3.6 通用模式

使用 for 循环查找最短曲目

   List<Track> tracks = asList(new Track("Bakai", 524),
          new Track("Violets for Your Furs", 378),
          new Track("Time Was", 451));

   Track shortestTrack = tracks.get(0);
   for (Track track : tracks) {
      if (track.getLength() < shortestTrack.getLength()) {
          shortestTrack = track;
      }
   }

reduce 模式

   Object accumulator = initialValue;
   for (Object element : collection) {
      accumulator = combine(accumulator, element);
   }
  • 赋给 accumulator 一个初始值:initialValue
  • 调用 combine 函数,拿 accumulator 和集合中的每个元素做运算,并将运算结果赋给 accumulator
  • 最后 accumulator 的值就是想要的结果

3.3.7 reduce

reduce 操作可以实现从一组值中生成一个值。

使用 reduce 求和

   int count = Stream.of(1, 2, 3)
          .reduce(0, (acc, element) -> acc + element);

展开 reduce 操作

   BinaryOperator<Integer> accumulator = (acc, element) -> acc + element;
     int count = accumulator.apply(
             accumulator.apply(
                     accumulator.apply(0, 1),
                     2), 3);

reduce过程的中间值

元素 acc 结果
N/A N/A 0
1 0 1
2 1 3
3 3 6

使用命令式编程方式求和

   intacc = 0;
   for (Integer element : asList(1, 2, 3)) {
      acc = acc + element;
   }
   assertEquals(6, acc);

3.6 高阶函数

高价函数是指接受另外一个函数作为参数,或返回一个函数的函数。

3.7 正确使用Lambda表达式

本章介绍的概念能够帮助用户写出更简单的代码,因为这些概念描述了数据上的操作,明确了要达成什么转化, 而不是说明如何转化

明确要达成什么转化,而不是说明如何转化的另外一层含义在于写出的函数没有副作用。 这点很重要,这样只通过函数的返回值就能充分理解函数的全部作用。

没有副作用的函数不会改变程序或外界的状态。

3.8 要点回顾

  • 内部迭代将更多控制权交给了集合类

  • 和Iterator类似,Stream是一种内部迭代方式

  • 将Lambda表达式和Stream上的方法结合起来,可以完成很多常见的集合操作

第 4 章 类库

4.1 在代码中使用Lambda表达式

从调用 Lambda 表达式的代码的角度来看,它和调用一个普通接口方法没什么区别。

使用 isDebugEnabled 方法降低日志性能开销

   Logger logger = new Logger();
   if (logger.isDebugEnabled()) {
      logger.debug("Look at this: " + expensiveOperation());
   }

使用 Lambda 表达式简化日志代码

   Logger logger = new Logger();
   logger.debug(() -> "Look at this: " + expensiveOperation());

启用 Lambda 表达式实现的日志记录器

   public void debug (Supplier<String> message) {
      if (isDebugEnabled()) {
          debug(message.get());
      }
   }

调用get()方法,相当于调用传入的 Lambda 表达式。

4.2 基本类型

整型在内存中占用 4 字节,整型对象却要占用 16 字节。

将基本类型转换为装箱类型,称为装箱,反之则称为拆箱,两者都需要额外的计算开销。 对于需要大量数值运算的算法来说,装箱和拆箱的计算开销,以及装箱类型占用的额外内存,会明显减缓程序的运行速度。

为了减少这些性能开销,stream 类的某些方法对基本类型和装箱类型做了区分。 对基本类型做特殊处理的方法在命名上明确的规范。

  • 如果方法返回类型为基本类型,则在基本类型前加 To,如 ToLongFunction

  • 如果参数是基本类型,则不加前缀只需类型名即可,如 LongFunction

  • 如果高价函数使用基本类型,则在操作后加后缀 To 再加基本类型,如 mapToLong

使用 summaryStatistics 方法统计曲目长度

   public static void printTrackLengthStatistics (Album album){
      IntSummaryStatistics trackLengthStats
              = album.getTracks()
              .mapToInt(track -> track.getLength())
              .summaryStatistics();
      System.out.printf("Max: %d, Min: %d, Ave: %f, Sum: %d",
              trackLengthStats.getMax(),
              trackLengthStats.getMin(),
              trackLengthStats.getAverage(),
              trackLengthStats.getSum());
   }

4.3 重载解析

总而言之,Lambda 表达式作为参数时,其类型由它的目标类型推导得出,推导过程遵循 如下规则:

  • 如果只有一个可能的目标类型,由相应函数接口里的参数类型推导得出

  • 如果有多个可能的目标类型,由最具体的类型推导得出

  • 如果有多个可能的目标类型且最具体的类型不明确,则需认为指定类型

4.4 @FunctionalInterface

每个用作函数接口的接口都应该添加 @FunctionalInterface 这个注释。

@FunctionalInterface 该注释会强制 javac 检查一个接口是否符合函数接口的标准。如果该注释添加给一个枚举 类型、类或另一个注释,或者接口包含不止一个抽象方法,javac 就会报错

4.6 默认方法

Collection 接口中增加了新的 stream 方法,如何能让 MyCustomList 类在不知道该方法的 情况下通过编译?Java 8通过如下方法解决该问题:Collection接口告诉它所有的子类: “如果你没有实现 stream 方法,就使用我的吧。”接口中这样的方法叫作默认方法,在任何 接口中,无论函数接口还是非函数接口,都可以使用该方法。

默认方法示例:forEach 实现方式

   default void forEach(Consumer<? super T> action) {
     for (Tt:
          this) {
         action.accept(t);
     }
   }

默认方法是指接口中定义的包含方法体的方法,方法名有default关键字做前缀

4.7 多重继承

接口允许多重继承,因此有可能碰到两个接口包含签名相同的默认方法的情况。

三定律:

  • 类胜于接口。如果在继承链中有方法体或抽象的方法声明,那么就可以忽略接口中定义的方法

  • 子类胜于父类。如果一个接口继承了另一个接口,且两个接口都定义了一个默认方法,那么子类中定义的方法胜出

  • 没有规则三。如果上面两天规则不适用,子类要么需要实现该方法,要么将该方法声明为抽象方法

4.9 接口的静态方法

Stream 是个接口, Stream.of是接口的静态方法。这也是Java 8中添加的一个新的语言特性,旨在帮助编写 类库的开发人员,但对于日常应用程序的开发人员也同样适用。

4.10 Optional

reduce 方法有两种形式,一种如前面出现的需要有一个初始值,另一种变式则不需要有初始值。

没有初始值的情况下,reduce 的第一步使用 Stream 中的前两个元素。有时,reduce 操作不存在有意义的初始值,这样做就是有意义的,此时,reduce 方法返回一个 Optional 对象。

使用 Optional 对象有两个目的:

  • Optional 对象鼓励程序员适时检查 变量是否为空,以避免代码缺陷

  • 它将一个类的 API 中可能为空的值文档化,这比 阅读实现代码要简单很多

使用工厂方法 of,可以从某个值创建出一个 Optional 对象。Optional 对象相当于值的容器,而该值可以通过 get 方法提取。

创建某个值的 Optional 对象

   Optional<String> a = Optional.of("a");
   assertEquals("a", a.get());

创建一个空的 Optional 对象,并检查其是否有值

   Optional emptyOptional = Optional.empty();

   // 将一个空值转换成 Optional 对象
   Optional alsoEmpty = Optional.ofNullable(null);

   // isPresent 方法表示一个 Optional 对象里是否有值
   assertFalse(emptyOptional.isPresent());

使用 Optional 对象的方式之一是在调用 get() 方法前,先使用 isPresent 检查 Optional 对象是否有值。

使用 orElse 和 orElseGet 方法

   // orElse 方法,当 Optional 对象为空时,该方法提供了一个 备选值
   assertEquals("b", emptyOptional.orElse("b"));

   // 如果计算备选值在计算上太过繁琐,即可使用 orElseGet 方法。该方法接受一个 Supplier 对象,
   // 只有在 Optional 对象真正为空时才会调用
   assertEquals("c", emptyOptional.orElseGet(() -> "c"));

4.11 要点回顾

  • 使用为基本类型定制的Lambda表达式和Stream,如IntStream可以显著提升系统性能

  • 默认方法是指接口中定义的包含方法体的方法,方法名有default关键字做前缀

  • 在一个值可能为空的建模情况下,使用Optional对象能替代使用null值

第 5 章 高级集合类和收集器

5.1 方法引用

有时候,当我们想要实现一个函数式接口的那个抽象方法,但是已经有类实现了我们想要的功能, 这个时候我们就可以用方法引用来直接使用现有类的功能去实现。

方法引用共分为四类:

1.类名::静态方法名

2.对象::实例方法名

3.类名::实例方法名

4.类名::new

5.2 元素顺序

直观上看,流是有序的,因为流中的元素都是按顺序处理的。这种顺序称为出现顺序。 出现顺序的定义依赖于数据源和对流的操作。

在一个有序集合中创建一个流时,流中的元素就按出现顺序排列。

顺序测试永远通过

   List<Integer> numbers = asList(1, 2, 3, 4);
   List<Integer> sameOrder = numbers.stream()
          .collect(toList());
   assertEquals(numbers, sameOrder);

如果集合本身就是无序的,由此生成的流也是无序的。

顺序测试不能保证每次通过

   Set<Integer> numbers = new HashSet<>(asList(4, 3, 2, 1));
   List<Integer> sameOrder = numbers.stream()
          .collect(toList());

   // 该断言有时会失败
   assertEquals(numbers, sameOrder);

5.3 使用收集器

5.3.1 转换成其他集合

有一些收集器可以生成其他集合。比如前面已经见过的 toList,生成了 java.util.List 类 的实例

5.3.2 转换成值

还可以利用收集器让流生成一个值。maxBy 和 minBy 允许用户按某种特定的顺序生成一个 值。

   public Optional<Artist> biggestGroup (Stream < Artist > artists) {
      Function<Artist, Long> getCount = artist -> artist.getMembers().count();
      return artists.collect(maxBy(comparing(getCount)));
   }

5.3.3 数据分块

另外一个常用的流操作是将其分解成两个集合。

将艺术家组成的流分成乐队和独唱歌手两部分

   public Map<Boolean, List<Artist>> bandsAndSolo (Stream < Artist > artists) {
      return artists.collect(partitioningBy(artist -> artist.isSolo()));
   }

5.3.4 数据分组

数据分组是一种更自然的分割数据操作,与将数据分成 ture 和 false 两部分不同,可以使用任意值对数据分组

使用主唱对专辑分组

   public Map<Artist, List<Album>> albumsByArtist (Stream < Album > albums) {
      return albums.collect(groupingBy(album -> album.getMainMusician()));
   }

5.3.5 字符串

很多时候,收集流中的数据都是为了在最后生成一个字符串。

使用 for 循环格式化艺术家姓名

   StringBuilder builder = new StringBuilder("[");
   for (Artist artist : artists) {
      if (builder.length() > 1) builder.append(", ");
      String name = artist.getName();
      builder.append(name);
   }
   builder.append("]");
   String result = builder.toString();

使用流和收集器格式化艺术家姓名

   String result =
          artists.stream()
                  .map(Artist::getName)
                  .collect(Collectors.joining(", ", "[", "]"));

5.3.6 组合收集器

计算每个艺术家专辑数的简单方式

   Map<Artist, List<Album>> albumsByArtist
          = albums.collect(groupingBy(album -> album.getMainMusician()));
   Map<Artist, Integer> numberOfAlbums = new HashMap<>();
   for (Entry<Artist, List<Album>> entry : albumsByArtist.entrySet()) {
      numberOfAlbums.put(entry.getKey(), entry.getValue().size());
   }

先分组,后计数。上面的代码也是命令式的代码,不能自动适应并行化操作。

使用收集器计算每个艺术家的专辑数

   public Map<Artist, Long> numberOfAlbums(Stream<Album> albums) {
     return albums.collect(groupingBy(album -> album.getMainMusician(),
             counting()));
   }

mapping 允许在收集器的容器上执行类似 map 的操作。但是需要指明使用什么样的集合类 存储结果,比如 toList。

mapping 收集器和 map 方法一样,接受一个 Function 对象作为参数

使用收集器求每个艺术家的专辑名

   public Map<Artist, List<String>> nameOfAlbums(Stream<Album> albums) {
     return albums.collect(groupingBy(Album::getMainMusician,
             mapping(Album::getName, toList())));
   }

这两个例子中我们都用到了第二个收集器,用以收集最终结果的一个子集。这些收集器叫作下游收集器

5.5 要点回顾

  • 方法引用是一种引用方法的轻量级语法,形如:ClassName::methodName。

  • 收集器可用来计算流的最终值,是reduce方法的模拟。

  • Java 8 提供了收集多种容器类型的方式,同时允许用户自定义收集器。

第 6 章 数据并行化

6.1 并行和并发

并发是两个任务共享时间段,并行则是两个任务在同一时间发生,比如运行在多核 CPU 上。 如果一个程序要运行两个任务,并且只有一个 CPU 给它们分配了不同时间片,那么这就是并发,而不是并行。

并行和并发区别

并行化是指为缩短任务执行时间,将一个任务分解成几部分,然后并行执行。这和顺序执行的任务量是一样的, 区别就像用更多的马车拉车,花费的时间自然减少了。实际上,和 顺序执行相比,并行化执行任务时,CPU 承载的工作量更大。

数据并行化是指将数据分块,为每块数据分配单独的处理单元。

当需要在大量数据上执行同样的操作时,数据并行化很管用。它将问题分解为可在多块数 据上求解的形式,然后对每块数据执行运算,最后将各数据块上得到的结果汇总,从而获 得最终答案。

6.2 为什么并行化如此重要

阿姆达尔定律是一个简单规则,预测了搭载多核处理器的机器提升程序速度的理论最大 值。以一段完全串行化的程序为例,如果将其一半改为并行化处理,则不管增加多少处理 器,其理论上的最大速度只是原来的 2 倍。有了大量的处理器后,现在这已经是现实了, 问题的求解时间将完全取决于它可被分解成几个部分。

6.3 并行化流操作

并行化操作流只需改变一个方法调用。

  • Stream 对象,调用它的 parallel 方法就能让其拥有并行操作的能力
  • 从一个集合类创建一个流,调用 parallelStream 就能立即获得一个拥有并行能力的流

串行化计算专辑曲目长度

   public int serialArraySum() {
     return albums.stream()
             .flatMap(Album::getTracks)
             .mapToInt(Track::getLength)
             .sum();
   }

并行化计算专辑曲目长度

   public int serialArraySum() {
     return albums.parallelStream()
             .flatMap(Album::getTracks)
             .mapToInt(Track::getLength)
             .sum();
   }

6.5 限制

为了发挥并行流框架的优势,写代码时必须遵 守一些规则和限制。

调用 reduce 方法,初始值可以为任意值,为了让其在并行化时能工作正常,初值必须为组合函数的恒等值。 拿恒等值和其他值做 reduce 操作时,其他值保持不变。比如,使用 reduce操作求和,组合函数为(acc, element) -> acc + element,则其初值必须为0,因为任何数字加 0,值不变。

reduce 操作的另一个限制是组合操作必须符合结合律。这意味着只要序列的值不变,组合操作的顺序不重要。

加法和乘法满足结合律

   (4+2)+1=4+(2+1)=7

   (4*2)*1=4*(2*1)=8

要避免的是持有锁。流框架会在需要时,自己处理同步操作,因此程序员没有必要为自己 的数据结构加锁。如果你执意为流中要使用的数据结构加锁,比如操作的原始集合,那么有可能是自找麻烦。

在要对流求值时,不能同时处于两种模式,要么是并行的,要么是串行的。如果同时调用了 parallel 和 sequential 方法,最后调用的那个方法起效。

6.6 性能

影响并行流性能的主要因素有 5 个:

  • 数据大小 输入数据的大小会影响并行化处理对性能的提升。将问题分解之后并行化处理,再将结 果合并会带来额外的开销。因此只有数据足够大、每个数据处理管道花费的时间足够多时,并行化处理才有意义。

  • 源数据结构 每个管道的操作都基于一些初始数据源,通常是集合。将不同的数据源分割相对容易, 这里的开销影响了在管道中并行处理数据时到底能带来多少性能上的提升。

  • 装箱 处理基本类型比处理装箱类型要快。

  • 核的数量 极端情况下,只有一个核,因此完全没必要并行化。显然,拥有的核越多,获得潜在性 能提升的幅度就越大。在实践中,核的数量不单指你的机器上有多少核,更是指运行时 你的机器能使用多少核。这也就是说同时运行的其他进程,或者线程关联性(强制线程 在某些核或 CPU 上运行)会影响性能。

  • 单元处理开销 比如数据大小,这是一场并行执行花费时间和分解合并操作开销之间的战争。花在流中 每个元素身上的时间越长,并行操作带来的性能提升越明显。

使用并行流框架,理解如何分解和合并问题是很有帮助的。这让我们能够知悉底层如何工作,但却不必了解框架的细节。

并行求和

   private int addIntegers(List<Integer> values) {
     return values.parallelStream()
             .mapToInt(i -> i)
             .sum();
   }

在底层,并行流还是沿用了 fork/join 框架。fork 递归式地分解问题,然后每段并行执行, 最终由 join 合并结果,返回最后的值。

使用 fork/join 分解合并问题

并行求和例子中,假设并行流将我们的工作分解开,在一个四核的机器上并行执行。

  1. 数据被分成四块。

  2. 计算工作在每个线程里并行执行。这包括将每个 Integer 对象映射为 int 值,然后在每个线程里将 1/4 的数字相加。理想情况下,我们希望在这里花的时间越多越好,因为这里是并行操作的最佳场所。

  3. 然后合并结果。就是 sum 操作,但这也可能是 reduce、collect 或其他终结操作。

根据问题的分解方式,初始的数据源的特性变得尤其重要,它影响了分解的性能。直观上看,能重复将数据结构对半分解的难易程度, 决定了分解操作的快慢。能对半分解同时意 味着待分解的值能够被等量地分解。

  • 性能好 ArrayList、数组或 IntStream.range,这些数据结构支持随机读取,也就是说它们能轻 而易举地被任意分解。

  • 性能一般 HashSet、TreeSet,这些数据结构不易公平地被分解,但是大多数时候分解是可能的。

  • 性能差 有些数据结构难于分解,比如,可能要花 O(N) 的时间复杂度来分解问题。其中包括 LinkedList,对半分解太难了。还有 Streams.iterate 和 BufferedReader.lines,它们 长度未知,因此很难预测该在哪里分解。

在讨论流中单独操作每一块的种类时,可以分成两种不同的操作:无状态的和有状态的。 无状态操作整个过程中不必维护状态,有状态操作则有维护状态所需的开销和限制。

如果能避开有状态,选用无状态操作,就能获得更好的并行性能。无状态操作包括 map、 filter 和 flatMap,有状态操作包括 sorted、distinct 和 limit。

6.7 并行化数组操作

数组上的并行化操作

方法名 操作
parallelPrefix 任意给定一个函数,计算数组的和
parallelSetAll 使用 Lambda 表达式更新数组元素
parallelSort 并行化对数组元素排序

使用 for 循环初始化数组

   public static double[] imperativeInitilize(int size) {
     double[] values = new double[size];
     for (int i = 0; i < values.length; i++) {
         values[i] = i;
     }
     return values;
   }

使用并行化数组操作初始化数组

   public static double[] parallelInitialize(int size) {
     double[] values = new double[size];
     Arrays.parallelSetAll(values, i -> i);
     return values;
   }

6.8 要点回顾

  • 数据并行化是把工作拆分,同时在多核CPU上执行的方式。

  • 如果使用流编写代码,可通过调用 parallel 或者 parallelStream 方法实现数据并行化操作。

  • 影响性能的五要素是:数据大小、源数据结构、值是否装箱、可用的CPU核数量,以及处理每个元素所花的时间。

第 7 章 测试、调式和重构

  • 重构遗留代码时考虑如何使用Lambda表达式,有一些通用的模式。

  • 如果想要对复杂一点的Lambda表达式编写单元测试,将其抽取成一个常规的方法。

  • peek方法能记录中间值,在调试时非常有用。

第 8 章 设计和架构的原则

软件开发最重要的设计工具不是什么技术,而是一颗在设计原则方面训练有素的头脑。

  • Lambda 表达式能让很多现有设计模式更简单、可读性更强,尤其是命令者模式。

  • 在Java8中,创建领域专用语言有更多的灵活性。

  • 在Java8中,有应用SOLID原则的新机会。

第 9 章 使用Lambda表达式编写并发程序

本章内容涉及到 Vert.x 和 RxJava 框架。

  • 阻塞 I/O
  • 非阻塞 I/O 也叫 异步 I/O

这种方式要求你的项目里包含所有的主题文件,你可以通过以下途径来安装:

  1. 从 GitHub 克隆 jekyll-TeXt-theme 项目:

    $ git clone git@github.com:kitian616/jekyll-TeXt-theme.git
    
  2. 下载主题压缩包并解压到你的项目目录中:

    下载 TeXt 主题

  3. 如果你打算在 GitHub Pages 上搭建你的网站,你可以直接 fork jekyll-TeXt-theme 到你的仓库,然后将其重命名为 USERNAME.github.io — 这里的 USERNAME 是你的 GitHub 用户名。

    并行和并发区别

    Rename

主题方式

  1. 若要安装一套主题,请先将该主题添加到您站点的 Gemfile 文件中:

    gem "jekyll-text-theme"
    
  2. 向站点的 _config.yml 中加入下列代码来启用主题:

    theme: jekyll-text-theme
    
  3. 最后,执行命令行 bundle install 来安装主题和其他的相关依赖。

    $ bundle install
    

设置

这里仅针对主题方式的安装,普通方式安装请跳过。

Jekyll 主题含有主题默认的布局文件、包含文件和样式表, 但是有些目录(例如assets, _layouts, _includes 以及 _sass 目录)需要手动添加到项目目录中,这样的好处在于将主题的文件和站点的内容和配置隔离开来,方便主题的升级。

├── 404.html
├── Gemfile
├── _config.yml
├── _data
│   └── locale.yml
├── _posts
│   └── ...
├── about.md
├── archive.html
└── index.html

你可以参考主题源码的 /test 目录, 这是一个使用主题的示例。

本地预览

Jekyll 集成了一个开发用的服务器,可以让你使用浏览器在本地进行预览。

通过 bundle exec jekyll serve 命令启动开发服务器,然后你就可以访问 http://localhost:4000/ 预览你的网站了。

编译和发布

如果你打算把网站搭建在 GitHub Pages 上,那你所需要做的就是将项目的源码上传到 USERNAME.github.io 源码仓库的 master 分支,GitHub 会自动的编译,几分钟后你就可以通过 https://USERNAME.github.io 访问到你的网站了。

如果你的网站是搭建在其他服务器上的,那么你就需要来自己编译了。首先运行命令 JEKYLL_ENV=production bundle exec jekyll build 编译你的网站,然后将编译的文件(位于 _site 目录)更新到你的服务器上。