14. 函数式编程

空~2022年8月12日
  • java
大约 45 分钟

14. 函数式编程

Lambda 表达式

lambda表达式用于简化匿名内部类的实现,只作用于接口,抽象方法无法使用。

Lambda 表达式是使用最小可能语法编写的函数定义:

  1. Lambda 作用于函数式接口(有且仅有一个抽象方法,但是可以有多个非抽象方法的接口)。
  2. Lambda 表达式产生函数,而不是类。 虽然在 JVM(Java Virtual Machine,Java 虚拟机)上,一切都是类,但是幕后有各种操作执行让 Lambda 看起来像函数 —— 作为程序员,你可以高兴地假装它们“就是函数”。
  3. Lambda 语法尽可能少,这正是为了使 Lambda 易于编写和使用。
interface Description {
  String brief();
}

interface Body {
  String detailed(String head);
}

interface Multi {
  String twoArg(String head, Double d);
}

public class LambdaExpressions {

  static Body bod = h -> h + " No Parens!"; // [1]

  static Body bod2 = (h) -> h + " More details"; // [2]

  static Description desc = () -> "Short info"; // [3]

  static Multi mult = (h, n) -> h + n; // [4]

  static Description moreLines = () -> { // [5]
    System.out.println("moreLines()");
    return "from moreLines()";
  };

  public static void main(String[] args) {
    System.out.println(bod.detailed("Oh!"));
    System.out.println(bod2.detailed("Hi!"));
    System.out.println(desc.brief());
    System.out.println(mult.twoArg("Pi! ", 3.14159));
    System.out.println(moreLines.brief());
  }
}
/*
    Oh! No Parens!
    Hi! More details
    Short info
    Pi! 3.14159
    moreLines()
    from moreLines()
*/

任何 Lambda 表达式的基本语法是:

  1. 参数(parameters),接口中的抽象方法的实参列表。
  2. 接着 ->,lambda 操作符或箭头函数。
  3. -> 之后的内容都是方法体{ statements; }
  • [1] 当只用一个参数,可以不需要括号 ()
  • [2] 正常情况使用括号 () 包裹参数。
  • [3] 如果没有参数,则必须使用括号 () 表示空参数列表。
  • [4] 对于多个参数,将实参列表放在括号 () 中。

到目前为止,所有 Lambda 表达式方法体都是单行。 该表达式的结果自动成为 Lambda 表达式的返回值,在此处使用 return 关键字是非法的。 这是 Lambda 表达式简化相应语法的另一种方式。

[5] 如果在 Lambda 表达式中确实需要多行,则必须将这些行放在花括号中。 在这种情况下,就需要使用 return

Lambda 表达式通常比匿名内部类产生更易读的代码。

递归

递归函数是一个自我调用的函数。可以编写递归的 Lambda 表达式,但需要注意:递归方法必须是实例变量或静态变量,否则会出现编译时错误。 我们将为每个案例创建一个示例。

这两个示例都需要一个接受 int 型参数并生成 int 的接口:

interface IntCall {
  int call(int arg);
}

整数 n 的阶乘将所有小于或等于 n 的正整数相乘。 阶乘函数是一个常见的递归示例:

public class RecursiveFactorial {
  static IntCall fact;
  public static void main(String[] args) {
    fact = n -> n == 0 ? 1 : n * fact.call(n - 1);
    for(int i = 0; i <= 10; i++)
      System.out.println(fact.call(i));
  }
}
/*
1
1
2
6
24
120
720
5040
40320
362880
3628800
*/

这里,fact 是一个静态变量。 注意使用三元 if-else。 递归函数将一直调用自己,直到 i == 0。所有递归函数都有“停止条件”,否则将无限递归并产生异常。

我们可以将 Fibonacci 序列用递归的 Lambda 表达式来实现,这次使用实例变量:

public class RecursiveFibonacci {
  IntCall fib;

  RecursiveFibonacci() {
    fib = n -> n == 0 ? 0 :
               n == 1 ? 1 :
               fib.call(n - 1) + fib.call(n - 2);
  }

  int fibonacci(int n) { return fib.call(n); }

  public static void main(String[] args) {
    RecursiveFibonacci rf = new RecursiveFibonacci();
    for(int i = 0; i <= 10; i++)
      System.out.println(rf.fibonacci(i));
  }
}
/*
0
1
1
2
3
5
8
13
21
34
55
*/

Fibonacci 序列中的最后两个元素求和来产生下一个元素。

方法引用

Java 8 方法引用没有历史包袱。方法引用组成:类名或对象名 :: 方法名称

  1. 使用场景:当需要传递给 lambda 体的操作,已经有满足结构要求的方法了,可以使用方法引用。
  2. lambda 表达式是对匿名内部类的简写,方法引用就是拿其它类的方法来实现自己接口中的抽象方法。
  3. 被引用的方法所在的类不需要实现对应的函数式接口。

简单来说就是可以引用其它类中方法的具体实现来实例化我们需要的函数式接口。

方法结构要求:

R:返回值

T1:参数1, T2:参数2

方法名:无需相同

抽象方法结构:R 方法名(T1) 引用方法结构:R 方法名(T1) 或 R T1.方法名()

抽象方法结构:R 方法名(T1 T2) 引用方法结构:R 方法名(T1, T2) 或 R T1.方法名(T2)

interface Callable<T1> {
    void call(T1 t1);
}

interface Callable2<T1, T2> {
    void call(T1 t1, T2 t2);
}

class Describe {
    void show(String msg) {
        System.out.println(this);
        System.out.println(msg);
    }
    void show2() {
        System.out.println(this);
    }
}

class Describe2 {
    void show3(String msg,String msg2) {
        System.out.println(this);
        System.out.println(msg + msg2);
    }

    void show4(String s) {
        System.out.println(this);
        System.out.println(s);
    }

}

public class MethodReferences {
    @Test
    public void test() {
        Describe d = new Describe();
        Describe2 d2 = new Describe2();
        Callable<String> c1 = d::show;
        Callable<Describe> c2 = Describe::show2;
        Callable2<String, String> c3 = d2::show3;
        Callable2<Describe2, String> c4 = Describe2::show4;
        c1.call("我是c1");
        System.out.println("====================");
        c2.call(d);
        System.out.println("====================");
        c3.call("我是c3","我是c3");
        System.out.println("====================");
        c4.call(d2, "我是c4");
    }
}

当引用方法是 static 或者结构为 T1.方法名([T2])时,方法引用是用类名引用。

static 为正常的静态调用。

结构为 T1.方法名([T2])时(未绑定的方法引用),类名引用的意义是明确调用者的类型。

Runnable 接口

Runnable 接口自 1.0 版以来一直在 Java 中,因此不需要导入。它也符合特殊的单方法接口格式:它的方法 run() 不带参数,也没有返回值。因此,我们可以使用 Lambda 表达式和方法引用作为 Runnable

class Go {
  static void go() {
    System.out.println("Go::go()");
  }
}

public class RunnableMethodReference {
  public static void main(String[] args) {

    new Thread(new Runnable() {
      public void run() {
        System.out.println("Anonymous");
      }
    }).start();

    new Thread(
      () -> System.out.println("lambda")
    ).start();

    new Thread(Go::go).start();
  }
}
/*
Anonymous
lambda
Go::go()
*/

Thread 对象将 Runnable 作为其构造函数参数,并具有会调用 run() 的方法 start()。 注意这里只有匿名内部类才要求显式声明 run() 方法。

未绑定的方法引用

未绑定的方法引用是指没有关联对象的普通(非静态)方法。 使用未绑定的引用时,我们必须先提供对象:

class X {
  String f() { return "X::f()"; }
}

interface MakeString {
  String make();
}

interface TransformX {
  String transform(X x);
}

public class UnboundMethodReference {
  public static void main(String[] args) {
    // MakeString ms = X::f; // [1]
    TransformX sp = X::f;
    X x = new X();
    System.out.println(sp.transform(x)); // [2]
    System.out.println(x.f()); // 同等效果
  }
}
/*
X::f()
X::f()
*/

到目前为止,我们已经见过了方法引用和对应接口的签名(参数类型和返回类型)一致的几个赋值例子。

[1] 中,我们尝试同样的做法,把 Xf() 方法引用赋值给 MakeString

结果即使 make()f() 具有相同的签名,编译也会报“invalid method reference”(无效方法引用)错误。

问题在于,这里其实还需要另一个隐藏参数参与:我们的老朋友 this

你不能在没有 X 对象的前提下调用 f()。 因此,X :: f 表示未绑定的方法引用。

要解决这个问题,我们需要一个 X 对象,因此我们的接口实际上需要一个额外的参数,正如在 TransformX 中看到的那样。 如果将 X :: f 赋值给 TransformX,在 Java 中是允许的。

[2] 的结果有点像脑筋急转弯。我拿到未绑定的方法引用,并且调用它的transform()方法,将一个 X 类的对象传递给它,最后使得 x.f() 以某种方式被调用。Java 知道它必须拿第一个参数,该参数实际就是this 对象,然后对此调用方法。

如果你的方法有更多个参数,就以第一个参数接受 this 的模式来处理。

class This {
  void two(int i, double d) {}
  void three(int i, double d, String s) {}
  void four(int i, double d, String s, char c) {}
}

interface TwoArgs {
  void call2(This athis, int i, double d);
}

interface ThreeArgs {
  void call3(This athis, int i, double d, String s);
}

interface FourArgs {
  void call4(
    This athis, int i, double d, String s, char c);
}

public class MultiUnbound {
  public static void main(String[] args) {
    TwoArgs twoargs = This::two;
    ThreeArgs threeargs = This::three;
    FourArgs fourargs = This::four;
    This athis = new This();
    twoargs.call2(athis, 11, 3.14);
    threeargs.call3(athis, 11, 3.14, "Three");
    fourargs.call4(athis, 11, 3.14, "Four", 'Z');
  }
}

需要指出的是,我将类命名为 This,并将函数式方法的第一个参数命名为 athis,但生产级代码中应该使用其他名字,以防止混淆。

构造函数引用

你还可以捕获构造函数的引用,然后通过引用调用该构造函数。

class Dog {
  String name;
  int age = -1; // For "unknown"
  Dog() { name = "stray"; }
  Dog(String nm) { name = nm; }
  Dog(String nm, int yrs) { name = nm; age = yrs; }
}

interface MakeNoArgs {
  Dog make();
}

interface Make1Arg {
  Dog make(String nm);
}

interface Make2Args {
  Dog make(String nm, int age);
}

public class CtorReference {
  public static void main(String[] args) {
    MakeNoArgs mna = Dog::new; // [1]
    Make1Arg m1a = Dog::new;   // [2]
    Make2Args m2a = Dog::new;  // [3]

    Dog dn = mna.make();
    Dog d1 = m1a.make("Comet");
    Dog d2 = m2a.make("Ralph", 4);
  }
}

Dog 有三个构造函数,函数式接口内的 make() 方法反映了构造函数参数列表( make() 方法名称可以不同)。

注意我们如何对 [1][2][3] 中的每一个使用 Dog :: new。 这三个构造函数只有一个相同名称::: new,但在每种情况下赋值给不同的接口,编译器可以从中知道具体使用哪个构造函数。

编译器知道调用函数式方法(本例中为 make())就相当于调用构造函数。

函数式接口

函数式接口 (Functional Interface) 就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法(默认方法)的接口。

@FunctionalInterface: 注解只是在编译时起到强制规范定义的作用(可省略,省略后不做规范检查)。

函数式接口可以被隐式转换为 lambda 表达式。

Java 8 引入了 java.util.function 包。它包含一组接口,这些接口是 Lambda 表达式和方法引用的目标类型。 每个接口只包含一个抽象方法,称为 函数式方法

Java 内置四大核心接口:

Consumer<T> : 消费型接口,void accept (T t);:表示接受参数并且不返回结果的操作。

Supplier<T> : 供给型接口,T get ();:表示返回类型 T 的值的函数。

Function<T, R> : 函数型接口,R apply (T t);:表示接受类型 T 的参数并返回类型 R 的结果的函数。

Predicate<T> : 断言型接口,boolean test (T t);:表示为指定参数返回 truefalse 的布尔函数。

简单示例:

public class LambdaTest {

    public void filterString(List<String> strings, Predicate<String> predicate) {
        for (String string : strings) {
            System.out.println(predicate.test(string));
        }
    }

    public void queryString(List<String> strings, Function<String, Boolean> function) {
        for (String string : strings) {
            System.out.println(function.apply(string));
        }
    }

    @Test
    public void test() {
        List<String> strings = Arrays.asList("你好", "我好", "大家好");
        // 打印List信息
        Consumer<List<String>> consumer = s -> System.out.println("List:" + s);
        consumer.accept(strings);
        // 返回一个 String
        Supplier<String> stringSupplier = () -> "Supplier<T> : 供给型接口,T get ();";
        System.out.println(stringSupplier.get());
        // 查找字符串中是否包含 “家” 字符
        Function<String, Boolean> function = s -> s.contains("家");
        queryString(strings, function);
        // 查找字符串中是否包含 “你” 字符
        filterString(strings, s -> s.contains("你"));
    }
}

在编写自定义接口时,可以使用 @FunctionalInterface 注解强制执行此“函数式方法”模式:

@FunctionalInterface
interface Functional {
  String goodbye(String arg);
}

interface FunctionalNoAnn {
  String goodbye(String arg);
}

/*
@FunctionalInterface
interface NotFunctional {
  String goodbye(String arg);
  String hello(String arg);
}
产生错误信息:
NotFunctional is not a functional interface
multiple non-overriding abstract methods
found in interface NotFunctional
*/

public class FunctionalAnnotation {
  public String goodbye(String arg) {
    return "Goodbye, " + arg;
  }
  public static void main(String[] args) {
    FunctionalAnnotation fa =
      new FunctionalAnnotation();
    Functional f = fa::goodbye;
    FunctionalNoAnn fna = fa::goodbye;
    // Functional fac = fa; // Incompatible
    Functional fl = a -> "Goodbye, " + a;
    FunctionalNoAnn fnal = a -> "Goodbye, " + a;
  }
}

仔细观察在定义 ffna 时发生了什么。 FunctionalFunctionalNoAnn 声明了是接口,但 FunctionalAnnotation 并没有实现它们,却依旧可以被赋值。这是添加到 Java 8 中的一点小魔法:如果将方法引用或 Lambda 表达式赋值给函数式接口(类型需要匹配),Java 会适配你的赋值到目标接口。 编译器会在后台把方法引用或 Lambda 表达式包装进实现目标接口的类的实例中。

虽然 FunctionalAnnotation 确实符合 Functional 模型,但是 Java 不允许我们像fac定义的那样,将 FunctionalAnnotation 直接赋值给 Functional,因为 FunctionalAnnotation 并没有显式地去实现 Functional 接口。唯一的惊喜是,Java 8 允许我们将函数赋值给接口,这样的语法更加简单漂亮。

java.util.function 包旨在创建一组完整的目标接口,使得我们一般情况下不需再定义自己的接口。主要因为基本类型的存在,导致预定义的接口数量有少许增加。 如果你了解命名模式,顾名思义就能知道特定接口的作用。

以下是基本命名准则:

  1. 如果只处理对象而非基本类型,名称则为 FunctionConsumerPredicate 等。参数类型通过泛型添加。
  2. 如果接收的参数是基本类型,则由名称的第一部分表示,如 LongConsumerDoubleFunctionIntPredicate 等,但返回基本类型的 Supplier 接口例外。
  3. 如果返回值为基本类型,则用 To 表示,如 ToLongFunction <T>IntToLongFunction
  4. 如果返回值类型与参数类型相同,则是一个 Operator :单个参数使用 UnaryOperator,两个参数使用 BinaryOperator
  5. 如果接收参数并返回一个布尔值,则是一个 谓词 (Predicate)。
  6. 如果接收的两个参数类型不同,则名称中有一个 Bi

java.util.function 中常见的目标类型(包括例外情况):

特征函数式方法名示例
无参数; 无返回值Runnable (java.lang) run()Runnable
无参数; 返回类型任意Supplier get() getAs类型()Supplier<T> BooleanSupplier IntSupplier LongSupplier DoubleSupplier
无参数; 返回类型任意Callable (java.util.concurrent) call()Callable<V>
1 参数; 无返回值Consumer accept()Consumer<T> IntConsumer LongConsumer DoubleConsumer
2 参数 ConsumerBiConsumer accept()BiConsumer<T,U>
2 参数 Consumer; 第一个参数是 引用; 第二个参数是 基本类型Obj 类型 Consumer accept()ObjIntConsumer<T> ObjLongConsumer<T> ObjDoubleConsumer<T>
1 参数; 返回类型不同Function apply() To 类型类型 To 类型 applyAs类型()Function<T,R> IntFunction<R> LongFunction<R> DoubleFunction<R> ToIntFunction<T> ToLongFunction<T> ToDoubleFunction<T> IntToLongFunction IntToDoubleFunction LongToIntFunction LongToDoubleFunction DoubleToIntFunction DoubleToLongFunction
1 参数; 返回类型相同UnaryOperator apply()UnaryOperator<T> IntUnaryOperator LongUnaryOperator DoubleUnaryOperator
2 参数,类型相同; 返回类型相同BinaryOperator apply()BinaryOperator<T> IntBinaryOperator LongBinaryOperator DoubleBinaryOperator
2 参数,类型相同; 返回整型Comparator (java.util) compare()Comparator<T>
2 参数; 返回布尔型Predicate test()Predicate<T> BiPredicate<T,U> IntPredicate LongPredicate DoublePredicate
参数基本类型; 返回基本类型类型 To 类型 Function applyAs类型()IntToLongFunction IntToDoubleFunction LongToIntFunction LongToDoubleFunction DoubleToIntFunction DoubleToLongFunction
2 参数; 类型不同Bi 操作 (不同方法名)BiFunction<T,U,R> BiConsumer<T,U> BiPredicate<T,U> ToIntBiFunction<T,U> ToLongBiFunction<T,U> ToDoubleBiFunction<T>

可以看出,在创建 java.util.function 时,设计者们做出了一些选择。

例如,为什么没有 IntComparatorLongComparatorDoubleComparator 呢?

BooleanSupplier 却没有其他表示 Boolean 的接口;有通用的 BiConsumer 却没有用于 intlongdoubleBiConsumers 变体(我理解他们为什么放弃这些接口)。这到底是疏忽还是有人认为其他组合使用得很少呢(他们是如何得出这个结论的)?

你还可以看到基本类型给 Java 添加了多少复杂性。该语言的第一版中就包含了基本类型,原因是考虑效率问题(该问题很快就缓解了)。现在,在语言的生命周期里,我们一直忍受语言设计的糟糕选择所带来的影响。

下面枚举了基于 Lambda 表达式的所有不同 Function 变体的示例:

import java.util.function.*;

class Foo {}

class Bar {
  Foo f;
  Bar(Foo f) { this.f = f; }
}

class IBaz {
  int i;
  IBaz(int i) {
    this.i = i;
  }
}

class LBaz {
  long l;
  LBaz(long l) {
    this.l = l;
  }
}

class DBaz {
  double d;
  DBaz(double d) {
    this.d = d;
  }
}

public class FunctionVariants {
  static Function<Foo,Bar> f1 = f -> new Bar(f);
  static IntFunction<IBaz> f2 = i -> new IBaz(i);
  static LongFunction<LBaz> f3 = l -> new LBaz(l);
  static DoubleFunction<DBaz> f4 = d -> new DBaz(d);
  static ToIntFunction<IBaz> f5 = ib -> ib.i;
  static ToLongFunction<LBaz> f6 = lb -> lb.l;
  static ToDoubleFunction<DBaz> f7 = db -> db.d;
  static IntToLongFunction f8 = i -> i;
  static IntToDoubleFunction f9 = i -> i;
  static LongToIntFunction f10 = l -> (int)l;
  static LongToDoubleFunction f11 = l -> l;
  static DoubleToIntFunction f12 = d -> (int)d;
  static DoubleToLongFunction f13 = d -> (long)d;

  public static void main(String[] args) {
    Bar b = f1.apply(new Foo());
    IBaz ib = f2.apply(11);
    LBaz lb = f3.apply(11);
    DBaz db = f4.apply(11);
    int i = f5.applyAsInt(ib);
    long l = f6.applyAsLong(lb);
    double d = f7.applyAsDouble(db);
    l = f8.applyAsLong(12);
    d = f9.applyAsDouble(12);
    i = f10.applyAsInt(12);
    d = f11.applyAsDouble(12);
    i = f12.applyAsInt(13.0);
    l = f13.applyAsLong(13.0);
  }
}

这些 Lambda 表达式尝试生成适合函数签名的最简代码。 在某些情况下有必要进行强制类型转换,否则编译器会报截断错误。

main()中的每个测试都显示了 Function 接口中不同类型的 apply() 方法。 每个都产生一个与其关联的 Lambda 表达式的调用。

方法引用有自己的小魔法:

import java.util.function.*;

class In1 {}
class In2 {}

public class MethodConversion {
  static void accept(In1 i1, In2 i2) {
    System.out.println("accept()");
  }
  static void someOtherName(In1 i1, In2 i2) {
    System.out.println("someOtherName()");
  }
  public static void main(String[] args) {
    BiConsumer<In1,In2> bic;

    bic = MethodConversion::accept;
    bic.accept(new In1(), new In2());

    bic = MethodConversion::someOtherName;
    // bic.someOtherName(new In1(), new In2()); // Nope
    bic.accept(new In1(), new In2());
  }
}
/*
accept()
someOtherName()
*/

查看 BiConsumer 的文档,你会看到它的函数式方法为 accept() 。 的确,如果我们将方法命名为 accept(),它就可以作为方法引用。 但是我们也可用不同的名称,比如 someOtherName()。只要参数类型、返回类型与 BiConsumeraccept() 相同即可。

因此,在使用函数接口时,名称无关紧要——只要参数类型和返回类型相同。 Java 会将你的方法映射到接口方法。 要调用方法,可以调用接口的函数式方法名(在本例中为 accept()),而不是你的方法名。

现在我们来看看,将方法引用应用于基于类的函数式接口(即那些不包含基本类型的函数式接口)。下面的例子中,我创建了适合函数式方法签名的最简单的方法:

// functional/ClassFunctionals.java

import java.util.*;
import java.util.function.*;

class AA {}
class BB {}
class CC {}

public class ClassFunctionals {
  static AA f1() { return new AA(); }
  static int f2(AA aa1, AA aa2) { return 1; }
  static void f3(AA aa) {}
  static void f4(AA aa, BB bb) {}
  static CC f5(AA aa) { return new CC(); }
  static CC f6(AA aa, BB bb) { return new CC(); }
  static boolean f7(AA aa) { return true; }
  static boolean f8(AA aa, BB bb) { return true; }
  static AA f9(AA aa) { return new AA(); }
  static AA f10(AA aa1, AA aa2) { return new AA(); }
  public static void main(String[] args) {
    Supplier<AA> s = ClassFunctionals::f1;
    s.get();
    Comparator<AA> c = ClassFunctionals::f2;
    c.compare(new AA(), new AA());
    Consumer<AA> cons = ClassFunctionals::f3;
    cons.accept(new AA());
    BiConsumer<AA,BB> bicons = ClassFunctionals::f4;
    bicons.accept(new AA(), new BB());
    Function<AA,CC> f = ClassFunctionals::f5;
    CC cc = f.apply(new AA());
    BiFunction<AA,BB,CC> bif = ClassFunctionals::f6;
    cc = bif.apply(new AA(), new BB());
    Predicate<AA> p = ClassFunctionals::f7;
    boolean result = p.test(new AA());
    BiPredicate<AA,BB> bip = ClassFunctionals::f8;
    result = bip.test(new AA(), new BB());
    UnaryOperator<AA> uo = ClassFunctionals::f9;
    AA aa = uo.apply(new AA());
    BinaryOperator<AA> bo = ClassFunctionals::f10;
    aa = bo.apply(new AA(), new AA());
  }
}

注意,每个方法名称都是随意的(如 f1()f2()等)。正如你刚才看到的,一旦将方法引用赋值给函数接口,我们就可以调用与该接口关联的函数方法。 在此示例中为 get()compare()accept()apply()test()

多参数函数式接口

java.util.functional 中的接口是有限的。比如有 BiFunction,但也仅此而已。 如果需要三参数函数的接口怎么办? 其实这些接口非常简单,很容易查看 Java 库源代码并自行创建。代码示例:

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

简单测试,验证它是否有效:

public class TriFunctionTest {
  static int f(int i, long l, double d) { return 99; }
  public static void main(String[] args) {
    TriFunction<Integer, Long, Double, Integer> tf =
      TriFunctionTest::f;
    tf = (i, l, d) -> 12;
  }
}

这里我们同时测试了方法引用和 Lambda 表达式。

缺少基本类型的函数

让我们重温一下 BiConsumer,看看我们将如何创建各种缺失的预定义组合,涉及 intlongdouble (基本类型):

import java.util.function.*;

public class BiConsumerPermutations {
  static BiConsumer<Integer, Double> bicid = (i, d) ->
    System.out.format("%d, %f%n", i, d);
  static BiConsumer<Double, Integer> bicdi = (d, i) ->
    System.out.format("%d, %f%n", i, d);
  static BiConsumer<Integer, Long> bicil = (i, l) ->
    System.out.format("%d, %d%n", i, l);
  public static void main(String[] args) {
    bicid.accept(47, 11.34);
    bicdi.accept(22.45, 92);
    bicil.accept(1, 11L);
  }
}
/*
47, 11.340000
92, 22.450000
1, 11
*/

这里使用 System.out.format() 来显示。它类似于 System.out.println() 但提供了更多的显示选项。 这里,%f 表示我将 n 作为浮点值给出,%d 表示 n 是一个整数值。 这其中可以包含空格,输入 %n 会换行 — 当然使用传统的 \n 也能换行,但 %n 是自动跨平台的,这是使用 format() 的另一个原因。

上例只是简单使用了合适的包装类型,而装箱和拆箱负责它与基本类型之间的来回转换。 又比如,我们可以将包装类型和Function一起使用,而不去用各种针对基本类型的预定义接口。代码示例:

import java.util.function.*;

public class FunctionWithWrapped {
  public static void main(String[] args) {
    Function<Integer, Double> fid = i -> (double)i;
    IntToDoubleFunction fid2 = i -> i;
  }
}

如果没有强制转换,则会收到错误消息:“Integer cannot be converted to Double”(Integer 无法转换为 Double),而使用 IntToDoubleFunction 就没有此类问题。 IntToDoubleFunction 接口的源代码是这样的:

@FunctionalInterface
public interface IntToDoubleFunction {
  double applyAsDouble(int value);
}

因为我们可以简单地写 Function <Integer,Double> 并产生正常的结果,所以用基本类型(IntToDoubleFunction)的唯一理由是可以避免传递参数和返回结果过程中的自动拆装箱,进而提升性能。

似乎是考虑到使用频率,某些函数类型并没有预定义。

当然,如果因为缺少针对基本类型的函数式接口造成了性能问题,你可以轻松编写自己的接口( 参考 Java 源代码)——尽管这里出现性能瓶颈的可能性不大。

高阶函数

这个名字可能听起来令人生畏,但是:高阶函数open in new window(Higher-order Function)只是一个消费或产生函数的函数。

我们先来看看如何产生一个函数:

import java.util.function.*;

interface
FuncSS extends Function<String, String> {} // [1]

public class ProduceFunction {
  static FuncSS produce() {
    return s -> s.toLowerCase(); // [2]
  }
  public static void main(String[] args) {
    FuncSS f = produce();
    System.out.println(f.apply("YELLING"));
  }
}
/*
yelling
*/

这里,produce() 是高阶函数。

[1] 使用继承,可以轻松地为专用接口创建别名。

[2] 使用 Lambda 表达式,可以轻松地在方法中创建和返回一个函数。

要消费一个函数,消费函数需要在参数列表正确地描述函数类型。代码示例:

import java.util.function.*;

class One {}
class Two {}

public class ConsumeFunction {
  static Two consume(Function<One,Two> onetwo) {
    return onetwo.apply(new One());
  }
  public static void main(String[] args) {
    Two two = consume(one -> new Two());
  }
}

当基于消费函数生成新函数时,事情就变得相当有趣了。代码示例如下:

import java.util.function.*;

class I {
  @Override
  public String toString() { return "I"; }
}

class O {
  @Override
  public String toString() { return "O"; }
}

public class TransformFunction {
  static Function<I,O> transform(Function<I,O> in) {
    return in.andThen(o -> {
      System.out.println(o);
      return o;
    });
  }
  public static void main(String[] args) {
    Function<I,O> f2 = transform(i -> {
      System.out.println(i);
      return new O();
    });
    O o = f2.apply(new I());
  }
}
/*
I
O
*/

在这里,transform() 生成一个与传入的函数具有相同签名的函数,但是你可以生成任何你想要的类型。

这里使用到了 Function 接口中名为 andThen() 的默认方法,该方法专门用于操作函数。 顾名思义,在调用 in 函数之后调用 andThen()(还有个 compose() 方法,它在 in 函数之前应用新函数)。 要附加一个 andThen() 函数,我们只需将该函数作为参数传递。 transform() 产生的是一个新函数,它将 in 的动作与 andThen() 参数的动作结合起来。

闭包

在上一节的 ProduceFunction.java 中,我们从方法中返回 Lambda 函数。 虽然过程简单,但是有些问题必须再回过头来探讨一下。

闭包(Closure)一词总结了这些问题。 它非常重要,利用闭包可以轻松生成函数。

考虑一个更复杂的 Lambda,它使用函数作用域之外的变量。 返回该函数会发生什么? 也就是说,当你调用函数时,它对那些 “外部 ”变量引用了什么? 如果语言不能自动解决,那问题将变得非常棘手。 能够解决这个问题的语言被称作 支持闭包,或者称作 词法定界lexically scoped ,基于词法作用域的)( 也有用术语 变量捕获 variable capture 称呼的)。Java 8 提供了有限但合理的闭包支持,我们将用一些简单的例子来研究它。

首先,下列方法返回一个函数,该函数访问对象字段和方法参数:

import java.util.function.*;

public class Closure1 {
  int i;
  IntSupplier makeFun(int x) {
    return () -> x + i++;
  }
}

但是,仔细考虑一下,i 的这种用法并非是个大难题,因为对象很可能在你调用 makeFun() 之后就存在了——实际上,垃圾收集器几乎肯定会保留以这种方式被绑定到现存函数的对象。当然,如果你对同一个对象多次调用 makeFun() ,你最终会得到多个函数,它们共享 i 的存储空间:

// functional/SharedStorage.java

import java.util.function.*;

public class SharedStorage {
  public static void main(String[] args) {
    Closure1 c1 = new Closure1();
    IntSupplier f1 = c1.makeFun(0);
    IntSupplier f2 = c1.makeFun(0);
    IntSupplier f3 = c1.makeFun(0);
    System.out.println(f1.getAsInt());
    System.out.println(f2.getAsInt());
    System.out.println(f3.getAsInt());
  }
}
/*
0
1
2
*/

每次调用 getAsInt() 都会增加 i,表明存储是共享的。

如果 imakeFun() 的局部变量怎么办? 在正常情况下,当 makeFun() 完成时 i 就消失。 但它仍可以编译:

import java.util.function.*;

public class Closure2 {
  IntSupplier makeFun(int x) {
    int i = 0;
    return () -> x + i;
  }
}

makeFun() 返回的 IntSupplier “关住了” ix,因此即使makeFun()已执行完毕,当你调用返回的函数时ix仍然有效,而不是像正常情况下那样在 makeFun() 执行后 ix就消失了。 但请注意,我没有像 Closure1.java 那样递增 i,因为会产生编译时错误。代码示例:

import java.util.function.*;

public class Closure3 {
  IntSupplier makeFun(int x) {
    int i = 0;
    // x++ 和 i++ 都会报错:
    return () -> x++ + i++;
  }
}

xi 的操作都犯了同样的错误:被 Lambda 表达式引用的局部变量必须是 final 或者是等同 final 效果的。

如果使用 final 修饰 xi,就不能再递增它们的值了。代码示例:

import java.util.function.*;

public class Closure4 {
  IntSupplier makeFun(final int x) {
    final int i = 0;
    return () -> x + i;
  }
}

那么为什么在 Closure2.java 中, xifinal 却可以运行呢?

这就叫做等同 final 效果(Effectively Final)。这个术语是在 Java 8 才开始出现的,表示虽然没有明确地声明变量是 final 的,但是因变量值没被改变过而实际有了 final 同等的效果。 如果局部变量的初始值永远不会改变,那么它实际上就是 final 的。

如果 xi 的值在方法中的其他位置发生改变(但不在返回的函数内部),则编译器仍将视其为错误。每个递增操作则会分别产生错误消息。代码示例:

import java.util.function.*;

public class Closure5 {
  IntSupplier makeFun(int x) {
    int i = 0;
    i++;
    x++;
    return () -> x + i;
  }
}

等同 final 效果意味着可以在变量声明前加上 final 关键字而不用更改任何其余代码。 实际上它就是具备 final 效果的,只是没有明确说明。

在闭包中,在使用 xi 之前,通过将它们赋值给 final 修饰的变量,我们解决了 Closure5.java 中遇到的问题。代码示例:

import java.util.function.*;

public class Closure6 {
  IntSupplier makeFun(int x) {
    int i = 0;
    i++;
    x++;
    final int iFinal = i;
    final int xFinal = x;
    return () -> xFinal + iFinal;
  }
}

上例中 iFinalxFinal 的值在赋值后并没有改变过,因此在这里使用 final 是多余的。

如果改用包装类型会是什么情况呢?我们可以把int类型改为Integer类型研究一下:

import java.util.function.*;

public class Closure7 {
  IntSupplier makeFun(int x) {
    Integer i = 0;
    i = i + 1;
    return () -> x + i;
  }
}

编译器非常聪明地识别到变量 i 的值被更改过。 包装类型可能是被特殊处理了,我们再尝试下 List

import java.util.*;
import java.util.function.*;

public class Closure8 {
  Supplier<List<Integer>> makeFun() {
    final List<Integer> ai = new ArrayList<>();
    ai.add(1);
    return () -> ai;
  }
  public static void main(String[] args) {
    Closure8 c7 = new Closure8();
    List<Integer>
      l1 = c7.makeFun().get(),
      l2 = c7.makeFun().get();
    System.out.println(l1);
    System.out.println(l2);
    l1.add(42);
    l2.add(96);
    System.out.println(l1);
    System.out.println(l2);
  }
}
/*
[1]
[1]
[1, 42]
[1, 96]
*/

可以看到,这次一切正常。我们改变了 List 的内容却没产生编译时错误。通过观察本例的输出结果,我们发现这看起来非常安全。这是因为每次调用 makeFun() 时,其实都会创建并返回一个全新而非共享的 ArrayList。也就是说,每个闭包都有自己独立的 ArrayList,它们之间互不干扰。

注意我已经声明 aifinal 的了。尽管在这个例子中你可以去掉 final 并得到相同的结果(试试吧!)。 应用于对象引用的 final 关键字仅表示不会重新赋值引用。 它并不代表你不能修改对象本身。

我们来看看 Closure7.javaClosure8.java 之间的区别。我们看到:在 Closure7.java 中变量 i 有过重新赋值。 也许这就是触发等同 final 效果错误消息的原因。

import java.util.*;
import java.util.function.*;

public class Closure9 {
  Supplier<List<Integer>> makeFun() {
    List<Integer> ai = new ArrayList<>();
    ai = new ArrayList<>(); // Reassignment
    return () -> ai;
  }
}

上例,重新赋值引用会触发错误消息。如果只修改指向的对象则没问题,只要没有其他人获得对该对象的引用(这意味着你有多个实体可以修改对象,此时事情会变得非常混乱),基本上就是安全的。

让我们回顾一下 Closure1.java。那么现在问题来了:为什么变量 i 被修改编译器却没有报错呢。 它既不是 final 的,也不是等同 final 效果的。

因为 i 是外部类的成员,所以这样做肯定是安全的(除非你正在创建共享可变内存的多个函数)。是的,你可以辩称在这种情况下不会发生变量捕获(Variable Capture)。

但可以肯定的是,Closure3.java 的错误消息是专门针对局部变量的。因此,规则并非只是 “在 Lambda 之外定义的任何变量必须是 final 的或等同 final 效果” 那么简单。

相反,你必须考虑捕获的变量是否是等同 final 效果的。 如果它是对象中的字段(实例变量),那么它有独立的生命周期,不需要任何特殊的捕获以便稍后在调用 Lambda 时存在。(注:结论是——Lambda 可以没有限制地引用 实例变量和静态变量。但局部变量必须显式声明为 final,或事实上是 final 。)

作为闭包的内部类

我们可以使用匿名内部类重写之前的例子:

import java.util.function.*;

public class AnonymousClosure {
  IntSupplier makeFun(int x) {
    int i = 0;
    // 同样规则的应用:
    // i++; // 非等同 final 效果
    // x++; // 同上
    return new IntSupplier() {
      public int getAsInt() { return x + i; }
    };
  }
}

实际上只要有内部类,就会有闭包(Java 8 只是简化了闭包操作)。在 Java 8 之前,变量 xi 必须被明确声明为 final。在 Java 8 中,内部类的规则放宽,包括等同 final 效果

函数组合

函数组合(Function Composition)意为“多个函数组合成新函数”。它通常是函数式编程的基本组成部分。在前面的 TransformFunction.java 类中,就有一个使用 andThen() 的函数组合示例。一些 java.util.function 接口中包含支持函数组合的方法 。

组合方法支持接口
andThen(argument) 执行原操作,再执行参数操作Function BiFunction Consumer BiConsumer IntConsumer LongConsumer DoubleConsumer UnaryOperator IntUnaryOperator LongUnaryOperator DoubleUnaryOperator BinaryOperator
compose(argument) 执行参数操作,再执行原操作Function UnaryOperator IntUnaryOperator LongUnaryOperator DoubleUnaryOperator
and(argument) 原谓词(Predicate)和参数谓词的短路逻辑与Predicate BiPredicate IntPredicate LongPredicate DoublePredicate
or(argument) 原谓词和参数谓词的短路逻辑或Predicate BiPredicate IntPredicate LongPredicate DoublePredicate
negate() 该谓词的逻辑非Predicate BiPredicate IntPredicate LongPredicate DoublePredicate

下例使用了 Function 里的 compose()andThen()。代码示例:

import java.util.function.*;

public class FunctionComposition {
  static Function<String, String>
    f1 = s -> {
      System.out.println(s);
      return s.replace('A', '_');
    },
    f2 = s -> s.substring(3),
    f3 = s -> s.toLowerCase(),
    f4 = f1.compose(f2).andThen(f3);
  public static void main(String[] args) {
    System.out.println(
      f4.apply("GO AFTER ALL AMBULANCES"));
  }
}
/*
AFTER ALL AMBULANCES
_fter _ll _mbul_nces
*/

这里我们重点看正在创建的新函数 f4。它调用 apply() 的方式与常规几乎无异。

f1 获得字符串时,它已经被 f2 剥离了前三个字符。这是因为 compose(f2) 表示 f2 的调用发生在 f1 之前。

下例是谓词(Predicate) 的逻辑运算演示.代码示例:

import java.util.function.*;
import java.util.stream.*;

public class PredicateComposition {
  static Predicate<String>
    p1 = s -> s.contains("bar"),
    p2 = s -> s.length() < 5,
    p3 = s -> s.contains("foo"),
    p4 = p1.negate().and(p2).or(p3);
  public static void main(String[] args) {
    Stream.of("bar", "foobar", "foobaz", "fongopuckey")
      .filter(p4)
      .forEach(System.out::println);
  }
}
/*
foobar
foobaz
*/

p4 获取到了所有谓词(Predicate)并组合成一个更复杂的谓词。解读:如果字符串中不包含 bar 且长度小于 5,或者它包含 foo ,则结果为 true

正因它产生如此清晰的语法,我在主方法中采用了一些小技巧,并借用了下一章的内容。首先,我创建了一个字符串对象的流,然后将每个对象传递给 filter() 操作。 filter() 使用 p4 的谓词来确定对象的去留。最后我们使用 forEach()println 方法引用应用在每个留存的对象上。

从输出结果我们可以看到 p4 的工作流程:任何带有 "foo" 的字符串都得以保留,即使它的长度大于 5。 "fongopuckey" 因长度超出且不包含 foo 而被丢弃。

流式编程

stream 使用的是函数式编程模式,可以被用来对集合或数组进行链状流式的操作

创建流

单列集合: 集合对象.stream()

List<Author> authors = new ArrayList<>();
Stream<Author> stream = authors.stream();

数组: Arrays.stream()或者Stream.of(arr)

int[] arr = {1, 4, 6, 234, 53, 24, 234, 23,};
IntStream intStream = Arrays.stream(arr);
Stream<int[]> stream2 = Stream.of(arr);

双列集合(map): 先转成单列再创建

Map<String, Integer> map = new HashMap<>();
Set<Map.Entry<String, Integer>> entries = map.entrySet();
Stream<Map.Entry<String, Integer>> stream3 = entries.stream();

随机数流

Random 类被一组生成流的方法增强了。代码示例:

import java.util.*;
import java.util.stream.*;
public class RandomGenerators {
    public static <T> void show(Stream<T> stream) {
        stream
        .limit(4)
        .forEach(System.out::println);
        System.out.println("++++++++");
    }
    
    public static void main(String[] args) {
        Random rand = new Random(47);
        show(rand.ints().boxed());
        show(rand.longs().boxed());
        show(rand.doubles().boxed());
        // 控制上限和下限:
        show(rand.ints(10, 20).boxed());
        show(rand.longs(50, 100).boxed());
        show(rand.doubles(20, 30).boxed());
        // 控制流大小:
        show(rand.ints(2).boxed());
        show(rand.longs(2).boxed());
        show(rand.doubles(2).boxed());
        // 控制流的大小和界限
        show(rand.ints(3, 3, 9).boxed());
        show(rand.longs(3, 12, 22).boxed());
        show(rand.doubles(3, 11.5, 12.3).boxed());
    }
}

Random 类只能生成基本类型 intlongdouble 的流。boxed() 流操作将会自动地把基本类型包装成为对应的装箱类型,从而使得 show() 能够接受流。

中间操作

中间操作用于从一个流中获取对象,并将对象作为另一个流从后端输出,以连接到其他操作。

跟踪和调试

peek() 操作的目的是帮助调试。它允许你无修改地查看流中的元素。代码示例:

class Peeking {
    public static void main(String[] args) throws Exception {
        FileToWords.stream("Cheese.dat")
        .skip(21)
        .limit(4)
        .map(w -> w + " ")
        .peek(System.out::print)
        .map(String::toUpperCase)
        .peek(System.out::print)
        .map(String::toLowerCase)
        .forEach(System.out::print);
    }
}

流元素排序

sorted() 预设了一些默认的比较器。它还有另一种形式的实现:传入一个 Comparator 参数。也可以把 Lambda 函数作为参数传递给 sorted()

import java.util.*;
public class SortedComparator {
    public static void main(String[] args) throws Exception {
        FileToWords.stream("Cheese.dat")
        .skip(10)
        .limit(10)
        .sorted(Comparator.reverseOrder())
        .map(w -> w + " ")
        .forEach(System.out::print);
    }
}

移除元素

  • distinct():在 Randoms.java 类中的 distinct() 可用于消除流中的重复元素。相比创建一个 Set 集合来消除重复,该方法的工作量要少得多。
  • filter(Predicate):过滤操作,保留如下元素:若元素传递给过滤函数产生的结果为 true

检测质数:

import java.util.stream.*;
import static java.util.stream.LongStream.*;
public class Prime {
    public static Boolean isPrime(long n) {
        return rangeClosed(2, (long)Math.sqrt(n))
        .noneMatch(i -> n % i == 0);
    }
    public LongStream numbers() {
        return iterate(2, i -> i + 1)
        .filter(Prime::isPrime);
    }
    public static void main(String[] args) {
        new Prime().numbers()
        .limit(10)
        .forEach(n -> System.out.format("%d ", n));
        System.out.println();
        new Prime().numbers()
        .skip(90)
        .limit(10)
        .forEach(n -> System.out.format("%d ", n));
    }
}

rangeClosed() 包含了上限值。如果不能整除,即余数不等于 0,则 noneMatch() 操作返回 true,如果出现任何等于 0 的结果则返回 falsenoneMatch() 操作一旦有失败就会退出。

应用函数到元素

  • map(Function):将函数操作应用在输入流的元素中,并将返回值传递到输出流中。
  • mapToInt(ToIntFunction):操作同上,但结果是 IntStream
  • mapToLong(ToLongFunction):操作同上,但结果是 LongStream
  • mapToDouble(ToDoubleFunction):操作同上,但结果是 DoubleStream
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
class FunctionMap {
    static String[] elements = { "12", "", "23", "45" };
    static Stream<String>
    testStream() {
        return Arrays.stream(elements);
    }
    static void test(String descr, Function<String, String> func) {
        System.out.println(" ---( " + descr + " )---");
        testStream()
        .map(func)
        .forEach(System.out::println);
    }
    public static void main(String[] args) {
        test("add brackets", s -> "[" + s + "]");
        test("Increment", s -> {
            try {
                return Integer.parseInt(s) + 1 + "";
            }
            catch(NumberFormatException e) {
                return s;
            }
        }
        );
        test("Replace", s -> s.replace("2", "9"));
        test("Take last digit", s -> s.length() > 0 ?
        s.charAt(s.length() - 1) + "" : s);
    }
}

Integer.parseInt() 尝试将一个字符串转化为整数。如果字符串不能被转化成为整数就会抛出 NumberFormatException 异常,此时我们就回过头来把原始字符串放到输出流中。

产生和接收类型完全不同的类型

// Different input and output types (不同的输入输出类型)
import java.util.*;
import java.util.stream.*;
class Numbered {
    final int n;
    Numbered(int n) {
        this.n = n;
    }
    @Override
    public String toString() {
        return "Numbered(" + n + ")";
    }
}
class FunctionMap2 {
    public static void main(String[] args) {
        Stream.of(1, 5, 7, 9, 11, 13)
        .map(Numbered::new)
        .forEach(System.out::println);
    }
}

如果结果是数值类型的一种,我们必须使用合适的 mapTo数值类型 进行替代。代码示例:

// Producing numeric output streams( 产生数值输出流)
import java.util.*;
import java.util.stream.*;
class FunctionMap3 {
    public static void main(String[] args) {
        Stream.of("5", "7", "9")
        .mapToInt(Integer::parseInt)
        .forEach(n -> System.out.format("%d ", n));
        System.out.println();
        Stream.of("17", "19", "23")
        .mapToLong(Long::parseLong)
        .forEach(n -> System.out.format("%d ", n));
        System.out.println();
        Stream.of("17", "1.9", ".23")
        .mapToDouble(Double::parseDouble)
        .forEach(n -> System.out.format("%f ", n));
    }
}

map() 中组合流

flatMap() 做了两件事:将产生流的函数应用在每个元素上(与 map() 所做的相同),然后将每个流都扁平化为元素,因而最终产生的仅仅是元素。

flatMap(Function):当 Function 产生流时使用。

flatMapToInt(Function):当 Function 产生 IntStream 时使用。

flatMapToLong(Function):当 Function 产生 LongStream 时使用。

flatMapToDouble(Function):当 Function 产生 DoubleStream 时使用。

import java.util.stream.*;
public class FlatMap {
    public static void main(String[] args) {
        Stream.of(1, 2, 3)
        .flatMap(i -> Stream.of("Gonzo", "Fozzie", "Beaker"))
        .forEach(System.out::println);
    }
}

从一个整数流开始,然后使用每一个整数去创建更多的随机数。

import java.util.*;
import java.util.stream.*;
public class StreamOfRandoms {
    static Random rand = new Random(47);
    public static void main(String[] args) {
        Stream.of(1, 2, 3, 4, 5)
            .flatMapToInt(i -> IntStream.concat(
        rand.ints(0, 100).limit(i), IntStream.of(-1)))
            .forEach(n -> System.out.format("%d ", n));
    }
}

Optional 类

Optional 可以实现这样的功能。一些标准流操作返回 Optional 对象,因为它们并不能保证预期结果一定存在。包括:

  • findFirst() 返回一个包含第一个元素的 Optional 对象,如果流为空则返回 Optional.empty

  • findAny() 返回包含任意元素的 Optional 对象,如果流为空则返回 Optional.empty

  • max()min() 返回一个包含最大值或者最小值的 Optional 对象,如果流为空则返回 Optional.empty

    reduce() 不再以 identity 形式开头,而是将其返回值包装在 Optional 中。(identity 对象成为其他形式的 reduce() 的默认结果,因此不存在空结果的风险)

对于数字流 IntStreamLongStreamDoubleStreamaverage() 会将结果包装在 Optional 以防止流为空。

简单测试:

import java.util.*;
import java.util.stream.*;
class OptionalsFromEmptyStreams {
    public static void main(String[] args) {
        System.out.println(Stream.<String>empty()
             .findFirst());
        System.out.println(Stream.<String>empty()
             .findAny());
        System.out.println(Stream.<String>empty()
             .max(String.CASE_INSENSITIVE_ORDER));
        System.out.println(Stream.<String>empty()
             .min(String.CASE_INSENSITIVE_ORDER));
        System.out.println(Stream.<String>empty()
             .reduce((s1, s2) -> s1 + s2));
        System.out.println(IntStream.empty()
             .average());
    }
}

结果

Optional.empty
Optional.empty
Optional.empty
Optional.empty
Optional.empty
OptionalDouble.empty

Optional 的两个基本用法:

import java.util.*;
import java.util.stream.*;
class OptionalBasics {
    static void test(Optional<String> optString) {
        if(optString.isPresent())
            System.out.println(optString.get()); 
        else
            System.out.println("Nothing inside!");
    }
    public static void main(String[] args) {
        test(Stream.of("Epithets").findFirst());
        test(Stream.<String>empty().findFirst());
    }
}

收到 Optional 对象时,首先调用 isPresent() 检查其中是否包含元素。如果存在,可使用 get() 获取。

便利函数

有许多便利函数可以解包 Optional ,这简化了上述 “对所包含的对象的检查和执行操作” 的过程:

  • ifPresent(Consumer):当值存在时调用 Consumer,否则什么也不做。
  • orElse(otherObject):如果值存在则直接返回,否则生成 otherObject
  • orElseGet(Supplier):如果值存在则直接返回,否则使用 Supplier 函数生成一个可替代对象。
  • orElseThrow(Supplier):如果值存在直接返回,否则使用 Supplier 函数生成一个异常。
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
public class Optionals {
    static void basics(Optional<String> optString) {
        if(optString.isPresent())
            System.out.println(optString.get()); 
        else
            System.out.println("Nothing inside!");
    }
    static void ifPresent(Optional<String> optString) {
        optString.ifPresent(System.out::println);
    }
    static void orElse(Optional<String> optString) {
        System.out.println(optString.orElse("Nada"));
    }
    static void orElseGet(Optional<String> optString) {
        System.out.println(
        optString.orElseGet(() -> "Generated"));
    }
    static void orElseThrow(Optional<String> optString) {
        try {
            System.out.println(optString.orElseThrow(
            () -> new Exception("Supplied")));
        } catch(Exception e) {
            System.out.println("Caught " + e);
        }
    }
    static void test(String testName, Consumer<Optional<String>> cos) {
        System.out.println(" === " + testName + " === ");
        cos.accept(Stream.of("Epithets").findFirst());
        cos.accept(Stream.<String>empty().findFirst());
    }
    public static void main(String[] args) {
        test("basics", Optionals::basics);
        test("ifPresent", Optionals::ifPresent);
        test("orElse", Optionals::orElse);
        test("orElseGet", Optionals::orElseGet);
        test("orElseThrow", Optionals::orElseThrow);
    }
}

创建 Optional

当我们在自己的代码中加入 Optional 时,可以使用下面 3 个静态方法:

  • empty():生成一个空 Optional
  • of(value):将一个非空值包装到 Optional 里。
  • ofNullable(value):针对一个可能为空的值,为空时自动生成 Optional.empty,否则将值包装在 Optional 中。
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
class CreatingOptionals {
    static void test(String testName, Optional<String> opt) {
        System.out.println(" === " + testName + " === ");
        System.out.println(opt.orElse("Null"));
    }
    public static void main(String[] args) {
        test("empty", Optional.empty());
        test("of", Optional.of("Howdy"));
        try {
            test("of", Optional.of(null));
        } catch(Exception e) {
            System.out.println(e);
        }
        test("ofNullable", Optional.ofNullable("Hi"));
        test("ofNullable", Optional.ofNullable(null));
    }
}

我们不能通过传递 nullof() 来创建 Optional 对象。最安全的方法是,使用 ofNullable() 来优雅地处理 null

Optional 对象操作

当我们的流管道生成了 Optional 对象,下面 3 个方法可使得 Optional 的后续能做更多的操作:

  • filter(Predicate):对 Optional 中的内容应用 Predicate 并将结果返回。如果 Optional 不满足 Predicate ,将 Optional 转化为空 Optional 。如果 Optional 已经为空,则直接返回空 Optional
  • map(Function):如果 Optional 不为空,应用 FunctionOptional 中的内容,并返回结果。否则直接返回 Optional.empty
  • flatMap(Function):同 map(),但是提供的映射函数将结果包装在 Optional 对象中,因此 flatMap() 不会在最后进行任何包装。

以上方法都不适用于数值型 Optional。一般来说,流的 filter() 会在 Predicate 返回 false 时移除流元素。而 Optional.filter() 在失败时不会删除 Optional,而是将其保留下来,并转化为空。下面请看代码示例:

import java.util.*;
import java.util.stream.*;
import java.util.function.*;
class OptionalFilter {
    static String[] elements = {
            "Foo", "", "Bar", "Baz", "Bingo"
    };
    static Stream<String> testStream() {
        return Arrays.stream(elements);
    }
    static void test(String descr, Predicate<String> pred) {
        System.out.println(" ---( " + descr + " )---");
        for(int i = 0; i <= elements.length; i++) {
            System.out.println(
                    testStream()
                            .skip(i)
                            .findFirst()
                            .filter(pred));
        }
    }
    public static void main(String[] args) {
        test("true", str -> true);
        test("false", str -> false);
        test("str != \"\"", str -> str != "");
        test("str.length() == 3", str -> str.length() == 3);
        test("startsWith(\"B\")",
                str -> str.startsWith("B"));
    }
}

for 循环的索引值范围并不是 i < elements.length, 而是 i <= elements.length。所以最后一个元素实际上超出了流。方便的是,这将自动成为 Optional.empty

map() 一样 , Optional.map() 执行一个函数。它仅在 Optional 不为空时才执行这个映射函数。并将 Optional 的内容提取出来,传递给映射函数。代码示例:

import java.util.Arrays;
import java.util.function.Function;
import java.util.stream.Stream;

class OptionalMap {
    static String[] elements = {"12", "", "23", "45"};

    static Stream<String> testStream() {
        return Arrays.stream(elements);
    }

    static void test(String descr, Function<String, String> func) {
        System.out.println(" ---( " + descr + " )---");
        for (int i = 0; i <= elements.length; i++) {
            System.out.println(
                    testStream()
                            .skip(i)
                            .findFirst() // Produces an Optional
                            .map(func));
        }
    }

    public static void main(String[] args) {
        // If Optional is not empty, map() first extracts
        // the contents which it then passes
        // to the function:
        test("Add brackets", s -> "[" + s + "]");
        test("Increment", s -> {
            try {
                return Integer.parseInt(s) + 1 + "";
            } catch (NumberFormatException e) {
                return s;
            }
        });
        test("Replace", s -> s.replace("2", "9"));
        test("Take last digit", s -> s.length() > 0 ?
                s.charAt(s.length() - 1) + "" : s);
    }
    // After the function is finished, map() wraps the
    // result in an Optional before returning it:
}

映射函数的返回结果会自动包装成为 OptionalOptional.empty 会被直接跳过。

OptionalflatMap() 应用于已生成 Optional 的映射函数,所以 flatMap() 不会像 map() 那样将结果封装在 Optional 中。代码示例:

// streams/OptionalFlatMap.java
import java.util.Arrays;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;

class OptionalFlatMap {
    static String[] elements = {"12", "", "23", "45"};

    static Stream<String> testStream() {
        return Arrays.stream(elements);
    }

    static void test(String descr,
                     Function<String, Optional<String>> func) {
        System.out.println(" ---( " + descr + " )---");
        for (int i = 0; i <= elements.length; i++) {
            System.out.println(
                    testStream()
                            .skip(i)
                            .findFirst()
                            .flatMap(func));
        }
    }

    public static void main(String[] args) {
        test("Add brackets",
                s -> Optional.of("[" + s + "]"));
        test("Increment", s -> {
            try {
                return Optional.of(
                        Integer.parseInt(s) + 1 + "");
            } catch (NumberFormatException e) {
                return Optional.of(s);
            }
        });
        test("Replace",
                s -> Optional.of(s.replace("2", "9")));
        test("Take last digit",
                s -> Optional.of(s.length() > 0 ?
                        s.charAt(s.length() - 1) + ""
                        : s));
    }
}

map()flatMap() 将提取非空 Optional 的内容并将其应用在映射函数。唯一的区别就是 flatMap() 不会把结果包装在 Optional 中,因为映射函数已经被包装过了。

Optional 流

假设你的生成器可能产生 null 值,那么当用它来创建流时,你会自然地想到用 Optional 来包装元素。如下是它的样子,代码示例:

import java.util.*;
import java.util.stream.*;
import java.util.function.*;
public class Signal {
    private final String msg;
    public Signal(String msg) { this.msg = msg; }
    public String getMsg() { return msg; }
    @Override
    public String toString() {
        return "Signal(" + msg + ")";
    }
    static Random rand = new Random(47);
    public static Signal morse() {
        switch(rand.nextInt(4)) {
            case 1: return new Signal("dot");
            case 2: return new Signal("dash");
            default: return null;
        }
    }
    public static Stream<Optional<Signal>> stream() {
        return Stream.generate(Signal::morse)
                .map(signal -> Optional.ofNullable(signal));
    }
}

当我们使用这个流的时候,必须要弄清楚如何解包 Optional。代码示例:

import java.util.*;
import java.util.stream.*;
public class StreamOfOptionals {
    public static void main(String[] args) {
        Signal.stream()
                .limit(10)
                .forEach(System.out::println);
        System.out.println(" ---");
        Signal.stream()
                .limit(10)
                .filter(Optional::isPresent)
                .map(Optional::get)
                .forEach(System.out::println);
    }
}

在这里,我们使用 filter() 来保留那些非空 Optional,然后在 map() 中使用 get() 获取元素。由于每种情况都需要定义 “空值” 的含义,所以通常我们要为每个应用程序采用不同的方法。

终端操作

以下操作将会获取流的最终结果。至此我们无法再继续往后传递流。可以说,终端操作(Terminal Operations)总是我们在流管道中所做的最后一件事。

数组

  • toArray():将流转换成适当类型的数组。
  • toArray(generator):在特殊情况下,生成自定义类型的数组。

当我们需要得到数组类型的数据以便于后续操作时,上面的方法就很有用。假设我们需要复用流产生的随机数时,就可以这么使用。代码示例:

import java.util.*;
import java.util.stream.*;
public class RandInts {
    private static int[] rints = new Random(47).ints(0, 1000).limit(100).toArray();
    public static IntStream rands() {
        return Arrays.stream(rints);
    }
}

循环

  • forEach(Consumer) 常见如 System.out::println 作为 Consumer 函数。
  • forEachOrdered(Consumer): 保证 forEach 按照原始流顺序操作。

第一种形式:无序操作,仅在引入并行流时才有意义。这里简单介绍下 parallel():可实现多处理器并行操作。实现原理为将流分割为多个(通常数目为 CPU 核心数)并在不同处理器上分别执行操作。因为我们采用的是内部迭代,而不是外部迭代,所以这是可能实现的。

下例引入 parallel() 来帮助理解 forEachOrdered(Consumer) 的作用和使用场景。代码示例:

import java.util.*;
import java.util.stream.*;
import static streams.RandInts.*;
public class ForEach {
    static final int SZ = 14;
    public static void main(String[] args) {
        rands().limit(SZ)
                .forEach(n -> System.out.format("%d ", n));
        System.out.println();
        rands().limit(SZ)
                .parallel()
                .forEach(n -> System.out.format("%d ", n));
        System.out.println();
        rands().limit(SZ)
                .parallel()
                .forEachOrdered(n -> System.out.format("%d ", n));
    }
}

结果

258 555 693 861 961 429 868 200 522 207 288 128 551 589
551 861 429 589 200 522 555 693 258 128 868 288 961 207
258 555 693 861 961 429 868 200 522 207 288 128 551 589

在第一个流中,未使用 parallel() ,因此以元素从 rands() 出来的顺序输出结果。在第二个流中,引入 parallel() ,即便流很小,输出的结果的顺序也和前面不一样。这是由于多处理器并行操作的原因,如果你将程序多运行几次,你会发现输出都不相同,这是多处理器并行操作的不确定性造成的结果。

在最后一个流中,同时使用了 parallel()forEachOrdered() 来强制保持原始流顺序。因此,对非并行流使用 forEachOrdered() 是没有任何影响的。

集合

  • collect(Collector):使用 Collector 收集流元素到结果集合中。
  • collect(Supplier, BiConsumer, BiConsumer):同上,第一个参数 Supplier 创建了一个新的结果集合,第二个参数 BiConsumer 将下一个元素收集到结果集合中,第三个参数 BiConsumer 用于将两个结果集合合并起来。

假设我们现在为了保证元素有序,将元素存储在 TreeSet 中。Collectors 里面没有特定的 toTreeSet(),但是我们可以通过将集合的构造函数引用传递给 Collectors.toCollection(),从而构建任何类型的集合。下面我们来将一个文件中的单词收集到 TreeSet 集合中。代码示例:

import java.util.*;
import java.nio.file.*;
import java.util.stream.*;
public class TreeSetOfWords {
    public static void
    main(String[] args) throws Exception {
        Set<String> words2 =
                Files.lines(Paths.get("TreeSetOfWords.java"))
                        .flatMap(s -> Arrays.stream(s.split("\\W+")))
                        .filter(s -> !s.matches("\\d+")) // No numbers
                        .map(String::trim)
                        .filter(s -> s.length() > 2)
                        .limit(100)
                        .collect(Collectors.toCollection(TreeSet::new));
        System.out.println(words2);
    }
}

Files.lines() 打开 Path 并将其转换成为由行组成的流。下一行代码以一个或多个非单词字符(\\W+)为分界,对每一行进行分割,结果是产生一个数组,然后使用 Arrays.stream() 将数组转化成为流,最后 flatMap() 将各行形成的多个单词流,扁平映射为一个单词流。使用 matches(\\d+) 查找并移除全部是数字的字符串(注意,words2 是通过的)。然后用 String.trim() 去除单词两边的空白,filter() 过滤所有长度小于 3 的单词,并只获取前 100 个单词,最后将其保存到 TreeSet 中。

也可以在流中生成 Map。代码示例:

import java.util.*;
import java.util.stream.*;
class Pair {
    public final Character c;
    public final Integer i;
    Pair(Character c, Integer i) {
        this.c = c;
        this.i = i;
    }
    public Character getC() { return c; }
    public Integer getI() { return i; }
    @Override
    public String toString() {
        return "Pair(" + c + ", " + i + ")";
    }
}
class RandomPair {
    Random rand = new Random(47);
    // An infinite iterator of random capital letters:
    Iterator<Character> capChars = rand.ints(65,91)
            .mapToObj(i -> (char)i)
            .iterator();
    public Stream<Pair> stream() {
        return rand.ints(100, 1000).distinct()
                .mapToObj(i -> new Pair(capChars.next(), i));
    }
}
public class MapCollector {
    public static void main(String[] args) {
        Map<Integer, Character> map =
                new RandomPair().stream()
                        .limit(8)
                        .collect(
                                Collectors.toMap(Pair::getI, Pair::getC));
        System.out.println(map);
    }
}

组合

  • reduce(BinaryOperator):使用 BinaryOperator 来组合所有流中的元素。因为流可能为空,其返回值为 Optional
  • reduce(identity, BinaryOperator):功能同上,但是使用 identity 作为其组合的初始值。因此如果流为空,identity 就是结果。
  • reduce(identity, BiFunction, BinaryOperator):更复杂的使用形式(暂不介绍),这里把它包含在内,因为它可以提高效率。通常,我们可以显式地组合 map()reduce() 来更简单的表达它。
import java.util.*;
import java.util.stream.*;
class Frobnitz {
    int size;
    Frobnitz(int sz) { size = sz; }
    @Override
    public String toString() {
        return "Frobnitz(" + size + ")";
    }
    // Generator:
    static Random rand = new Random(47);
    static final int BOUND = 100;
    static Frobnitz supply() {
        return new Frobnitz(rand.nextInt(BOUND));
    }
}
public class Reduce {
    public static void main(String[] args) {
        Stream.generate(Frobnitz::supply)
                .limit(10)
                .peek(System.out::println)
                .reduce((fr0, fr1) -> fr0.size < 50 ? fr0 : fr1)
                .ifPresent(System.out::println);
    }
}

Lambda 表达式中的第一个参数 fr0reduce() 中上一次调用的结果。而第二个参数 fr1 是从流传递过来的值。

reduce() 中的 Lambda 表达式使用了三元表达式来获取结果,当 fr0size 值小于 50 的时候,将 fr0 作为结果,否则将序列中的下一个元素即 fr1 作为结果。当取得第一个 size 值小于 50 的 Frobnitz,只要得到这个结果就会忽略流中其他元素。

匹配

  • allMatch(Predicate) :如果流的每个元素提供给 Predicate 都返回 true ,结果返回为 true。在第一个 false 时,则停止执行计算。
  • anyMatch(Predicate):如果流的任意一个元素提供给 Predicate 返回 true ,结果返回为 true。在第一个 true 是停止执行计算。
  • noneMatch(Predicate):如果流的每个元素提供给 Predicate 都返回 false 时,结果返回为 true。在第一个 true 时停止执行计算。
import java.util.stream.*;
import java.util.function.*;
import static streams.RandInts.*;

interface Matcher extends BiPredicate<Stream<Integer>, Predicate<Integer>> {}
        
public class Matching {
    static void show(Matcher match, int val) {
        System.out.println(
                match.test(
                        IntStream.rangeClosed(1, 9)
                                .boxed()
                                .peek(n -> System.out.format("%d ", n)),
                        n -> n < val));
    }
    public static void main(String[] args) {
        show(Stream::allMatch, 10);
        show(Stream::allMatch, 4);
        show(Stream::anyMatch, 2);
        show(Stream::anyMatch, 0);
        show(Stream::noneMatch, 5);
        show(Stream::noneMatch, 0);
    }
}

BiPredicate 是一个二元谓词,它接受两个参数并返回 true 或者 false。第一个参数是我们要测试的流,第二个参数是一个谓词 PredicateMatcher 可以匹配所有的 Stream::*Match 方法,所以可以将每一个 Stream::*Match 方法引用传递到 show() 中。对 match.test() 的调用会被转换成 对方法引用 Stream::*Match 的调用。

show() 接受一个 Matcher 和一个 val 参数,val 在判断测试 n < val 中指定了最大值。show() 方法生成了整数 1-9 组成的一个流。peek() 用来展示在测试短路之前测试进行到了哪一步。从输出中可以看到每次都发生了短路。

查找

  • findFirst():返回第一个流元素的 Optional,如果流为空返回 Optional.empty
  • findAny(:返回含有任意流元素的 Optional,如果流为空返回 Optional.empty

代码示例:

import java.util.*;
import java.util.stream.*;
import static streams.RandInts.*;
public class SelectElement {
    public static void main(String[] args) {
        System.out.println(rands().findFirst().getAsInt());
        System.out.println(
                rands().parallel().findFirst().getAsInt());
        System.out.println(rands().findAny().getAsInt());
        System.out.println(
                rands().parallel().findAny().getAsInt());
    }
}

输出结果:

258
258
258
242

无论流是否为并行化,findFirst() 总是会选择流中的第一个元素。对于非并行流,findAny() 会选择流中的第一个元素(即使从定义上来看是选择任意元素)。在这个例子中,用 parallel() 将流并行化,以展示 findAny() 不选择流的第一个元素的可能性。

如果必须选择流中最后一个元素,那就使用 reduce()。代码示例:

import java.util.*;
import java.util.stream.*;
public class LastElement {
    public static void main(String[] args) {
        OptionalInt last = IntStream.range(10, 20)
                .reduce((n1, n2) -> n2);
        System.out.println(last.orElse(-1));
        // Non-numeric object:
        Optional<String> lastobj =
                Stream.of("one", "two", "three")
                        .reduce((n1, n2) -> n2);
        System.out.println(
                lastobj.orElse("Nothing there!"));
    }
}

输出结果:

19

reduce() 的参数只是用最后一个元素替换了最后两个元素,最终只生成最后一个元素。如果为数字流,你必须使用相近的数字 Optional 类型( numeric optional type),否则使用 Optional 类型,就像上例中的 Optional<String>

信息

  • count():流中的元素个数。
  • max(Comparator):根据所传入的 Comparator 所决定的 “最大” 元素。
  • min(Comparator):根据所传入的 Comparator 所决定的 “最小” 元素。

String 类型有预设的 Comparator 实现。代码示例:

// streams/Informational.java
import java.util.stream.*;
import java.util.function.*;
public class Informational {
    public static void
    main(String[] args) throws Exception {
        System.out.println(
                FileToWords.stream("Cheese.dat").count());
        System.out.println(
                FileToWords.stream("Cheese.dat")
                        .min(String.CASE_INSENSITIVE_ORDER)
                        .orElse("NONE"));
        System.out.println(
                FileToWords.stream("Cheese.dat")
                        .max(String.CASE_INSENSITIVE_ORDER)
                        .orElse("NONE"));
    }
}

输出结果:

32
a
you

min()max() 的返回类型为 Optional,这需要我们使用 orElse() 来解包。

数字流信息

  • average() :求取流元素平均值。
  • max()min():数值流操作无需 Comparator
  • sum():对所有流元素进行求和。
  • summaryStatistics():生成可能有用的数据。目前并不太清楚这个方法存在的必要性,因为我们其实可以用更直接的方法获得需要的数据。
import java.util.stream.*;
import static streams.RandInts.*;
public class NumericStreamInfo {
    public static void main(String[] args) {
        System.out.println(rands().average().getAsDouble());
        System.out.println(rands().max().getAsInt());
        System.out.println(rands().min().getAsInt());
        System.out.println(rands().sum());
        System.out.println(rands().summaryStatistics());
    }
}复制ErrorOK!

输出结果:

507.94
998
8
50794
IntSummaryStatistics{count=100, sum=50794, min=8, average=507.940000, max=998}

上例操作对于 LongStreamDoubleStream 同样适用。