Java8实战

2018年10月27日

基础知识

java8改进:

  • lambda 表达式
  • stream 流
  • 接口默认方法 (为了把stream()等方法加在 Collection 上,增加了接口默认方法的方式来实现)

第二章:

  • 行为参数化,就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。
  • 行为参数化可让代码更好地适应不断变化的要求,减轻未来的工作量。
  • 传递代码,就是将新行为作为参数传递给方法。但在Java 8之前这实现起来很啰嗦。为接口声明许多只用一次的实体类而造成的啰嗦代码,在Java 8之前可以用匿名类来减少。
  • Java API包含很多可以用不同行为进行参数化的方法,包括排序、线程和GUI处理

常用函数式接口

  • java.util.function.Predicate 包含 negate、and和or 用来创建更加复杂的谓词
  • java.util.function.Consumer
  • java.util.function.Function<T, R> andThen和compose两个默认方法 用于function组合

为避免简单对象的拆箱和装箱造成的性能损耗,jdk提供了 IntPredicate,LongPredicate,IntToDoubleFunction, IntToLongFunction等接口

lambda 对局部变量引用的限制:必须为final或者事实final(即使没有声明为final,但是并没有修改过它的值)的。 第一,实例变量和局部变量背后的实现有一 个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局 部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线 程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它 的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了 这个限制。

对构造器有多个参数的类使用构造函数引用

        static class Color {
            public Color(int a, int b, int c) {
            }
          }

        public interface TriFunction<T, U, V, R>{
                 R apply(T t, U u, V v);
        }

        TriFunction<Integer, Integer, Integer, Color> colorFactory = Color::new;

@FunctionalInterface 这个标注用于表示该接口会设计成 一个函数式接口,不是必需的,但对于为此设计的接口而言,使用它是比较好的做法。它就像是@Override 标注表示方法被重写了。

Comparator.comparing 比较器链

函数式数据处理

流到底是什么呢?简短的定义就是“从支持数据处理操作的源生成的元素序列”

流就 像是一个延迟创建的集合:只有在消费者要求的时候才会计算值(用管理学的话说这就是需求驱 动,甚至是实时制造)。与此相反,集合则是急切创建的(供应商驱动:先把仓库装满,再开始卖,就像那些昙花一现的圣诞新玩意儿一样)。

请注意,流和迭代器类似,流只能遍历一次

流操作:

  • 中间操作,filter,map等
  • 终端操作,forEach,count,collect等方法

使用流:

  • 筛选、切片和匹配 (filter,limit, skip,map)
  • 查找、匹配和归约 (allMatch、anyMatch、noneMatch、findFirst和findAny, reduce)
  • 使用数值范围等数值流  从多个源创建流
  • 无限流

流不仅可以从集合创建,也可从值、数组、文件以及iterate与generate等特定方法创建。

用流收集数据:

  • 用Collectors类创建和使用收集器
  • 将数据流归约为一个值, Collectors.averagingInt,Collectors.counting() ,Collectors.joining()等
  • 汇总:归约的特殊情况 Collectors.joining()等
  • 数据分组和分区 Collectors.groupingBy 支持多级分类, partitioningBy 分区(得到的分组Map的键类型是Boolean)
  • 开发自己的自定义收集器,使用 java.util.stream.Collector 自定义收集器

并行数据处理与性能:

  • 用并行流并行处理数据
  • 并行流的性能分析
  • 分支/合并框架
  • 使用Spliterator分割流, 自定义 java.util.Spliterator 实现 并行流 的 任务拆分逻辑。

高效 Java 8 编程

  • 流提供的peek方法在分析Stream流水线时,能将中间变量的值输出到日志中,是非常有用的工具。

接口默认方法

接口默认方法的菱形继承问题:

多接口相同签名的默认方法调用规则:

  1. 类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优 先级。
  2. 如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择 拥有最具体实现的默认方法的接口,即如果B继承了A,那么B就比A更加具体。
  3. 最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法, 显式地选择使用哪一个默认方法的实现。

情况1(根据规则1打印 hello D):

        interface A {
            default void hello() {
              System.out.println("hello A");
            }
          }

          interface B extends A{
            default void hello() {
              System.out.println("hello B");
            }
          }

          static class D implements A {
            public void hello() {
              System.out.println("hello D");
            }
          }

          static class C extends D implements A,B {

          }

          public static void main(String[] args) {
            new C().hello();
          }

情况2(根据规则2打印 hello B):

      interface A {
          default void hello() {
            System.out.println("hello A");
          }
        }

        interface B extend A {
          default void hello() {
            System.out.println("hello B");
          }
        }

        static class C implements A,B {

        }

        public static void main(String[] args) {
          new C().hello();
        }

情况3 (根据规则3,无法通过编译必须手动指定调用A还是B接口的默认实现):

          interface A {
              default void hello() {
                System.out.println("hello A");
              }
            }

            interface B {
              default void hello() {
                System.out.println("hello B");
              }
            }

            static class C implements A,B {

            }
            public static void main(String[] args) {
              new C().hello();
            }

用Optional取代null

使用map从Optional对象中提取和转换值

    Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
    Optional<String> name = optInsurance.map(Insurance::getName);

使用flatMap转换Optional的值对象

        person.flatMap(Person::getCar)
                             .flatMap(Car::getInsurance)
                             .map(Insurance::getName)
                             .orElse("Unknown");

以不解包的方式组合两个Optional对象

    person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));

CompletableFuture: 组合式异步编程

提供一种便捷的多线程执行任务方式,默认使用 ForkJoinPool#commonPool() 的线程池执行任务(若 ForkJoinPool.getCommonPoolParallelism() <= 1 则每个任务都会new一个Thread执行任务), 也可指定自定义线程池

新的日期和时间API

  • LocalDate 、 LocalTime 、 Instant 、 Duration(一段时间,秒和毫秒级别) 以及 Period(一段时间,只支持 年/月/日)

使用TemporalField读取LocalDate的值

int year = date.get(ChronoField.YEAR);

单词:

  • Temporal:时间的;世俗的
  • Chrono:年代,慢性的,长期的

LocalDateTime ,是LocalDate和LocalTime的合体,但是不带有时区信息

Instant的设计初衷是为了便于机器使用。它包含的是由秒及纳秒所构成的数字。所以,它 无法处理那些我们非常容易理解的时间单位。比如下面这段语句:

        int day = Instant.now().get(ChronoField.DAY_OF_MONTH);
      它会抛出下面这样的异常:
    java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: DayOfMonth

使用TemporalAdjuster,进行一些更加 复杂的操作,比如,将日期调整到下个周日、下个工作日,或者是本月的最后一天。这时,你可 2 以使用重载版本的with方法,向其传递一个提供了更多定制化选择的TemporalAdjuster对象, 更加灵活地处理日期.

    import static java.time.temporal.TemporalAdjusters.*;
    LocalDate date1 = LocalDate.of(2014, 3, 18);
    LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY));
    LocalDate date3 = date2.with(lastDayOfMonth());

使用 DateTimeFormatter 进行格式化。如果你还需要更加细粒度的控制,DateTimeFormatterBuilder类还提供了更复杂 的格式器

时区的处理是新版日期和时间API新增 加的重要功能,使用新版日期和时间API时区的处理被极大地简化了。新的java.time.ZoneId 类是老版java.util.TimeZone的替代品。

第四部分 ,超越 Java 8

要成为真正的函数式程序还有一个附加条件,不过它在最初 时不太为大家所重视。要被称为函数式,函数或者方法不应该抛出任何异常。如果不使用异常,你该如何对除法这样的函数进行建模呢?答案是请使用 Optional类型。 了解了如何进行函数式的思考;以构造无副作用方法的思想指导你的程序设计 能帮助你编写更具维护性的代码

函数式编程的技巧

  • 一等成员、高阶方法、科里化以及局部应用
  • 持久化数据结构
  • 生成Java Stream时的延迟计算和延迟列表
  • 模式匹配以及如何在Java中应用
  • 引用透明性和缓存

函数式编程的世界里,如果函数,比如Comparator.comparing,能满足下面任一要求就 可以被称为高阶函数(higher-order function):

  • 接受至少一个函数作为参数
  • 返回的结果是一个函数

科里化的理论定义 科里化是一种将具备2个参数(比如,x和y)的函数f转化为使用一个参数的函数g,并 且这个函数的返回值也是一个函数,它会作为新函数的一个参数。后者的返回值和初始函数的 返回值相同,即f(x,y) = (g(x))(y)。当然,我们可以由此推出:你可以将一个使用了6个参数的函数科里化成一个接受第2、4、 6号参数,并返回一个接受5号参数的函数,这个函数又返回一个接受剩下的第1号和第3号参数 的函数。一个函数使用所有参数仅有部分被传递时,通常我们说这个函数是部分应用的(partially applied)。

科里化的概念最早由俄国数学家Moses Schönfinkel引入,而后由著名的数理逻辑学家哈斯格尔·科里(Haskell Curry)丰富和发展,科里化由此得名。它表示一种将一个带有n元组参数的函数转换成n个一元函数链的方法。

我们应该注意的第一件事是,函数式方法不允许修改任何全局数据结构或者任何作为参数 传入的参数。为什么呢?因为一旦对这些数据进行修改,两次相同的调用就很可能产生不同的 结构——这违背了引用透明性原则,我们也就无法将方法简单地看作由参数到结果的映射。

  • 破坏式更新和函数式更新,不推荐在函数中修改传入参数或全局变量的数据,这会导致非常多的问题,推荐在需要修改时使用对象的副本。

Stream的延迟操作,即只在终端方法被调用时,才会执行filter,map等方法。

一旦并发和可变状态的对象揉到 一起,它们引起的复杂度要远超我们的想象

 一等函数是可以作为参数传递,可以作为结果返回,同时还能存储在数据结构中的函数。  高阶函数接受至少一个或者多个函数作为输入参数,或者返回另一个函数的函数。Java 15  科里化是一种帮助你模块化函数和重用代码的技术。 16 中典型的高阶函数包括comparing、andThen和compose。 如果n的值为0,直接返回 “什么也不做”的标识符

  • 持久化数据结构在其被修改之前会对自身前一个版本的内容进行备份。因此,使用该技 术能避免不必要的防御式复制。
  • Java语言中的Stream不是自定义的。
  • 延迟列表是Java语言中让Stream更具表现力的一个特性。延迟列表让你可以通过辅助方法(supplier)即时地创建列表中的元素,辅助方法能帮忙创建更多的数据结构。
  • 模式匹配是一种函数式的特性,它能帮助你解包数据类型。它可以看成Java语言中switch语句的一种泛化。
  • 遵守“引用透明性”原则的函数,其计算结构可以进行缓存。
  • 结合器是一种函数式的思想,它指的是将两个或多个函数或者数据结构进行合并