2021-09-06

谓词(predicate)

在数学上常常用来代表一个类似函数的东西,它接受一个参数值,并返回true或false。你在后面会看到,Java 8允许写Function<Apple,Boolean>——函数,但用Predicate是更标准的方式,效率也会更高一点儿,这避免了把boolean封装在Boolean里面

//传递方法
@Data
@AllArgsConstructor
public class Apple {
    private String color;
    private Integer weight;

    public static boolean isGreenApple(Apple apple) {
        return "green".equals(apple.getColor());
    }

    public static boolean isHeavyApple(Apple apple) {
        return apple.getWeight() > 150;
    }
}
public class Test1 {
    public static void main(String[] args) {
        ArrayList<Apple> apples = new ArrayList<>();
        Apple aa = new Apple("aa", 20);
        Apple bb = new Apple("bb", 30);
        Apple green = new Apple("green", 160);
        Apple green1 = new Apple("green", 60);
        apples.add(aa);
        apples.add(bb);
        apples.add(green);
        apples.add(green1);
        List<Apple> apples1 = filterApples(apples, Apple::isGreenApple);
        System.out.println(apples1);
        List<Apple> apples2 = filterApples(apples, Apple::isHeavyApple);
        System.out.println(apples2);
    }

    public static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p) {
        ArrayList<Apple> apples = new ArrayList<>();
        for (Apple apple : inventory) {
            if (p.test(apple)) {
                apples.add(apple);
            }
        }
        return apples;
    }
}

不需要为只用一次的方法写定义;代码更干净、更清晰,用不着去找自己到底传递了什么代码。

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

        ArrayList<Apple> apples = new ArrayList<>();
        Apple aa = new Apple("aa", 20);
        Apple bb = new Apple("bb", 30);
        Apple green = new Apple("green", 160);
        Apple green1 = new Apple("green", 60);
        apples.add(aa);
        apples.add(bb);
        apples.add(green);
        apples.add(green1);
        List<Apple> apples1 = filterApples(apples, (Apple apple) -> "green".equals(apple.getColor()));
        System.out.println(apples1);
        List<Apple> apples2 = filterApples(apples, (Apple apple) -> apple.getWeight() > 60);
        System.out.println(apples2);
    }

    public static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p) {
        ArrayList<Apple> apples = new ArrayList<>();
        for (Apple apple : inventory) {
            if (p.test(apple)) {
                apples.add(apple);
            }
        }
        return apples;
    }

}

但要是Lambda的长度多于几行(它的行为也不是一目了然)的话,应该用方法引用来指向一个有描述性名称的方法,而不是使用匿名的Lambda。以代码的清晰度为准绳。

Java中的并行与无共享可变状态

Java里面并行很难,而且和synchronized相关的都容易出问题。在Java 8里面,首先,库会负责分块,即把大的流分成几个小的流,以便并行处理。其次,流提供的这个几乎免费的并行,只有在传递给filter之类的库方法的方法不会互动(比方说有可变的共享对象)时才能工作。举个例子,我们的Apple::isGreenApple就是这样。确实,虽然函数式编程中的函数的主要意思是“把函数作为一等值”,不过它也常常隐含着第二层意思,即“执行时在元素之间无互动”。

默认方法

Java 8中加入默认方法主要是为了支持库设计师,让他们能够写出更容易改进的接口。在Java 8之前,List并没有stream或parallelStream方法,它实现的Collection接口也没有,因为当初还没有想到这些方法嘛!可没有这些方法,这些代码就不能编译。换作你自己的接口的话,最简单的解决方案就是让Java 8的设计者把stream方法加入Collection接口,并加入ArrayList类的实现。可要是这样做,对用户来说就是噩梦了。有很多的替代集合框架都用Collection API实现了接口。但给接口加入一个新方法,意味着所有的实体类都必须为其提供一个实现。语言设计者没法控制Collections所有现有的实现,这下你就进退两难了:你如何改变已发布的接口而不破坏已有的实现呢?
Java 8的解决方法就是打破最后一环——接口如今可以包含实现类没有提供实现的方法签名了!那谁来实现它呢?缺失的方法主体随接口提供了(因此就有了默认实现),而不是由实现类提供。
这就给接口设计者提供了一个扩充接口的方式,而不会破坏现有的代码。Java 8在接口声明中使用新的default关键字来表示这一点

例如,在Java8里,你现在可以直接对List调用sort方法。它是用Java 8 List接口中如下所示的默认方法实现的,它会调用Arrays.sort静态方法:

 @SuppressWarnings({"unchecked", "rawtypes"})
    default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }

这意味着List的任何实体类都不需要显式实现sort,而在以前的Java版本中,除非提供了sort的实现,否则这些实体类在重新编译时都会失败

可能存在的问题

如果在好几个接口里有多个默认实现,是否意味着Java中有了某种形式的多重继承?是的,在某种程度上是这样。Java 8用一些限制来避免出现类似于C++中臭名昭著的菱形继承问题

菱形继承问题

在C++中允许多继承,D类继承自B、C,而B、C有同一个父类A。那么这个时候调用say方法是否成功?答案是不能,编译器并不能判断这个say来自哪个父类

在这里插入图片描述
public class DiamondExtendProblem {
    private interface A {
        default void test() {
            System.out.println("A");
        }
    }

    private interface B1 extends A {
        @Override
        default void test() {
            System.out.println("B1");
        }
    }

    private interface B2 extends A {
        @Override
        default void test() {
            System.out.println("B2");
        }
    }

    private static class C implements B1, B2 {
    }

    public static void main(String[] args) {
        C c = new C();
        c.test();//菱形继承问题,Error:(32, 20) java: 类C从类型B1和B2中继承了test()的不相关默认值
    }
}

C++的解决办法有两个:一是指定域,使用::,二是虚继承

目前Java8解决这种冲突一般遵循三个原则:

  1. 类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。
  2. 如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果B继承了A,那么B就比A更加具体。
  3. 最后,如果还是无法判断,继承了多个接口的类必须通过显式调用期望的方法。
//显示覆盖
public class DiamondExtendProblemSolve {
    private interface A {
        default void test() {
            System.out.println("A");
        }
    }

    private interface B1 extends A {
        @Override
        default void test() {
            System.out.println("B1");
        }
    }

    private interface B2 extends A {
        @Override
        default void test() {
            System.out.println("B2");
        }
    }

    private static class C implements B1, B2 {
        @Override
        public void test() {
            B1.super.test();//显示调用B1的test方法
        }
    }

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

Java从函数式编程中引入的两个核心思想

  1. 将方法和Lambda作为一等值
  2. 在没有可变共享状态时,函数或方法可以有效、安全地并行执行
一等值和二等值
  • 一等值

    称作java的一等公民,即java可以操作的值狭义上来说,是可以作为参数传递给方法的值

  • 二等值

    有助于表示值的结构,但在程序执行期间不能传递的结构,通俗意义上来说就是不能作为参数传递的结构

其他好思想
  1. 在Java 8里有一个Optional类,如果你能一致地使用它的话,就可以帮助你避免出现NullPointer异常。它是一个容器对象,可以包含,也可以不包含一个值。Optional中有方法来明确处理值不存在的情况,这样就可以避免NullPointer异常了。换句话说,它使用类型系统,允许你表明我们知道一个变量可能会没有值。

  2. (结构)模式匹配

    f(0) = 1 
    f(n) = n*f(n-1) otherwise 
    

    在Java中,你可以在这里写一个if-then-else语句或一个switch语句。其他语言表明,对于更复杂的数据类型,模式匹配可以比if-then-else更简明地表达编程思想。对于这种数据类型,你也可以使用多态和方法重载来替代if-then-else,但对于哪种方式更合适,就语言设计而言仍有一些争论。两者都是有用的工具,你都应该掌握。不幸的是,Java 8对模式匹配的支持并不完全。

为什么Java中的switch语句应该限于原始类型值和Strings呢?函数式语言倾向于允许switch用在更多的数据类型上,包括允许模式匹配(在Scala代码中是通过match操作实现的)。在面向对象设计中,常用的访客模式可以用来遍历一组类(如汽车的不同组件:车轮、发动机、底盘等),并对每个访问的对象执行操作。模式匹配的一个优点是编译器可以报告常见错误,如:“Brakes类属于用来表示Car类的组件的一族类。你忘记了要显式处理它。”

通过行为参数传递代码

行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式

例如,你可以将代码块作为参数传递给另一个方法,稍后再去执行它。这样,这个方法的行为就基于那块代码被参数化了。

DRY(Don’t Repeat Yourself,不要重复自己)软件工程原则

行为参数化

把所有属性结合起来的笨拙尝试,糟糕透了!

public static List<Apple> filterApples(List<Apple> inventory, String color, 
 int weight, boolean flag) { 
 List<Apple> result = new ArrayList<Apple>(); 
 for (Apple apple: inventory){ 
 if ( (flag && apple.getColor().equals(color)) ||
 (!flag && apple.getWeight() > weight) ){
 result.add(apple); 
 } 
 } 
 return result; 
} 
List<Apple> greenApples = filterApples(inventory, "green", 0, true); 
List<Apple> heavyApples = filterApples(inventory, "", 150, false); 

我们需要一种比添加很多参数更好的方法来应对变化的需求。让我们后退一步来看看更高层次的抽象。一种可能的解决方案是对你的选择标准建模:你考虑的是苹果,需要根据Apple的某些属性(比如它是绿色的吗?重量超过150克吗?)来返回一个boolean值。我们把它称为谓词(即一个返回boolean值的函数)。让我们定义一个接口来对选择标准建模:

public interface ApplePredicate{ 
 boolean test (Apple apple); 
} 

现在你就可以用ApplePredicate的多个实现代表不同的选择标准了

public class AppleHeavyWeightPredicate implements ApplePredicate{ 
 public boolean test(Apple apple){ 
 return apple.getWeight() > 150; 
 } 
} 
public class AppleGreenColorPredicate implements ApplePredicate{
 public boolean test(Apple apple){ 
 return "green".equals(apple.getColor()); 
 } 
} 

你可以把这些标准看作filter方法的不同行为。你刚做的这些和“策略设计模式”相关,它让你定义一族算法,把它们封装起来(称为“策略”),然后在运行时选择一个算法。在这里,算法族就是ApplePredicate,不同的策略就是AppleHeavyWeightPredicate和AppleGreenColorPredicate。但是,该怎么利用ApplePredicate的不同实现呢?你需要filterApples方法接受ApplePredicate对象,对Apple做条件测试。这就是行为参数化:让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。

public static List<Apple> filterApples(List<Apple> inventory, 
 ApplePredicate p){ 
 List<Apple> result = new ArrayList<>(); 
 for(Apple apple: inventory){ 
 if(p.test(apple)){ 
 result.add(apple); 
 } 
 } 
 return result; 
} 
List<Apple> redAndHeavyApples = filterApples(inventory, new AppleRedAndHeavyPredicate()); 

但令人遗憾的是,由于该filterApples方法只能接受对象,所以你必须把代码包裹在ApplePredicate对象里。你的做法就类似于在内联“传递代码”,因为你是通过一个实现了test方法的对象来传递布尔表达式的。通过使用Lambda,你可以直接把表达式"red".equals(apple.getColor()) &&apple.getWeight() > 150传递给filterApples方法,而无需定义多个ApplePredicate类,从而去掉不必要的代码。

行为参数化的好处在于你可以把迭代要筛选的集合的逻辑与对集合中每个元素应用的行为区分开来。这样你可以重复使用同一个方法,给它不同的行为来达到不
同的目的。

目前,当要把新的行为传递给filterApples方法的时候,你不得不声明好几个实现ApplePredicate接口的类,然后实例化好几个只会提到一次的ApplePredicate对象。这真是很啰嗦,很费时间!

匿名类和Lambda表达式简化代码
  • 匿名类

    List<Apple> redApples = filterApples(inventory, new ApplePredicate() { 
     public boolean test(Apple apple){ 
     return "red".equals(apple.getColor()); 
     } 
    }); 
    

    但匿名类还是不够好。第一,它往往很笨重,因为它占用了很多空间。在只需要传递一段简单的代码时(例如表示选择标准的boolean表达式),你还是要创建一个对象,明确地实现一个方法来定义一个新的行为(例如Predicate中的test方法或是EventHandler中的handler方法)

  • Lambda

    List<Apple> result = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));
    public interface Predicate<T>{
     boolean test(T t);
    }
    public static <T> List<T> filter(List<T> list, Predicate<T> p){
     List<T> result = new ArrayList<>();
     for(T e: list){
     if(p.test(e)){
     result.add(e); 
     }
     }
     return result; 
    } 
    
Lambda表达式

可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表

使用地点

函数式接口

函数式接口就是只定义一个抽象方法的接口。接口现在还可以拥有默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。

用函数式接口可以干什么呢?Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)。你用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后再直接内联将它实例化。

函数描述符

函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作函数描述符。

例如,Runnable接口可以看作一个什么也不接受什么也不返回(void)的函数的签名,因为它只有一个叫作run的抽象方法,这个方法什么也不接受,什么也不返回(void)。使用了一个特殊表示法来描述Lambda和函数式接口的签名。() -> void代表了参数列表为空,且返回void的函数。这正是Runnable接口所代表的。

Lambda表达式是怎么做类型检查的???

现在,只要知道Lambda表达式可以被赋给一个变量,或传递给一个接受函数式接口作为参数的方法就好了,当然这个Lambda表达式的签名要和函数式接口的抽象方法一样

@FunctionInterface

@FunctionalInterface又是怎么回事?
如果你去看看新的Java API,会发现函数式接口带有@FunctionalInterface的标注(3.4节中会深入研究函数式接口,并会给出一个长长的列表)。这个标注用于表示该接口会设计成一个函数式接口。如果你用@FunctionalInterface定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。例如,错误消息可能是“Multiple non-overriding abstract methods found in interface Foo”,表明存在多个抽象方法。请注意,@FunctionalInterface不是必需的,但对于为此设计的接口而言,使用它是比较好的做法。它就像是@Override标注表示方法被重写了。

函数式接口

在java.util.function包中引入了几个新的函数式接口

  1. Predicate

    java.util.function.Predicate接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean.如果你去查Predicate接口的Javadoc说明,可能会注意到诸如and和or等其他方法。

    @FunctionalInterface 
    public interface Predicate<T>{ 
     boolean test(T t); 
    } 
    public static <T> List<T> filter(List<T> list, Predicate<T> p) { 
     List<T> results = new ArrayList<>(); 
     for(T s: list){ 
     if(p.test(s)){ 
     results.add(s); 
     } 
     } 
     return results; 
    } 
    Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty(); 
    List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate); 
    
  2. Consumer

    java.util.function.Consumer定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回(void)。你如果需要访问类型T的对象,并对其执行某些操作,就可以使用这个接口。比如,你可以用它来创建一个forEach方法,接受一个Integers的列表,并对其中每个元素执行操作。在下面的代码中,你就可以使用这个forEach方法,并配合Lambda来打印列表中的所有元素。

    public class TestConsumer {
        public static void main(String[] args) {
            forEach(Arrays.asList(1, 2, 3), System.out::println);
        }
    
      public static <T> void forEach(List<T> list, Consumer<T> consumer) {
            list.forEach(consumer);
        }
    }
    
    
  3. Function

    java.util.function.Function<T, R>接口定义了一个叫作apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象。如果你需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口(比如提取苹果的重量,或把字符串映射为它的长度)。在下面的代码中,我们向你展示如何利用它来创建一个map方法,以将一个String列表映射到包含每个String长度的Integer列表。

    public class TestFunction {
        public static void main(String[] args) {
            List<Integer> result = map(Arrays.asList("12", "3423", "ff543"), String::length);
            System.out.println(result);
        }
    
        public static <T, R> List<R> map(List<T> list, Function<T, R> function) {
            List<R> result = new ArrayList<>();
            list.forEach(v -> result.add(function.apply(v)));
            return result;
        }
    }
    
    
原始类型特化

有些函数式接口专为某些类型而设计

Java类型要么是引用类型(比如Byte、Integer、Object、List),要么是原始类型(比如int、double、byte、char)。但是泛型(比如Consumer中的T)只能绑定到引用类型。这是由泛型内部的实现方式造成的。

在Java里有一个将原始类型转换为对应的引用类型的机制。这个机制叫作装箱(boxing)。相反的操作,也就是将引用类型转换为对应的原始类型,叫作拆箱(unboxing)。

但这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。

Java 8为我们前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。

public class TestIntegerPredicate {
    public static void main(String[] args) {
        IntPredicate evenNumber = (int v) -> v % 2 == 0;
        Scanner sc = new Scanner(System.in);
        boolean test = evenNumber.test(sc.nextInt());
        System.out.println(test);
    }
}

一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,比如DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。Function接口还有针对输出参数类型的变种:ToIntFunction、IntToDoubleFunction等。

函数式接口函数描述符原始类型特化
PredicateT->booleanIntPredicate,LongPredicate, DoublePredicate
ConsumerT->voidIntConsumer,LongConsumer, DoubleConsumer
Function<T,R>T->RIntFunction,
IntToDoubleFunction,
IntToLongFunction,
LongFunction,
LongToDoubleFunction,
LongToIntFunction,
DoubleFunction,
ToIntFunction,
ToDoubleFunction,
ToLongFunction
Supplier()->TBooleanSupplier,IntSupplier, LongSupplier,
DoubleSupplier
UnaryOperatorT->TIntUnaryOperator,
LongUnaryOperator,
DoubleUnaryOperator
BinaryOperator(T,T)->TIntBinaryOperator,
LongBinaryOperator,
DoubleBinaryOperator
BiPredicate<L,R>(L,R)->boolean
BiConsumer<T,U>(T,U)->voidObjIntConsumer,
ObjLongConsumer,
ObjDoubleConsumer
BiFunction<T,U,R>(T,U)->RToIntBiFunction<T,U>,
ToLongBiFunction<T,U>,
ToDoubleBiFunction<T,U>
使用案例Lambda的例子对应的函数式接口
布尔表达式(List list)->list.isEmpty()Predicate<List>
创建对象() -> new Apple(10)Supplier
消费一个对象(Apple a) -> System.out.println(a.getWeight())Consumer
从一个对象中选择/提取(String s) -> s.length()Function<String, Integer>或ToIntFunction
合并两个值(int a, int b) -> a * bIntBinaryOperator
比较两个对象(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())Comparator或BiFunction<Apple,Apple,Integer>或 ToIntBiFunction<Apple, Apple>
异常、Lambda,函数式接口

请注意,任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda包在一个try/catch块中。

@FunctionalInterface 
public interface BufferedReaderProcessor { 
 String process(BufferedReader b) throws IOException; 
} 
BufferedReaderProcessor p = (BufferedReader br) -> br.readLine(); 

但是你可能是在使用一个接受函数式接口的API,比如Function<T, R>,没有办法自己创建一个。这种情况下,你可以显式捕捉受检异常:

Function<BufferedReader, String> f = (BufferedReader b) -> { 
 try {
 return b.readLine(); 
 } 
 catch(IOException e) { 
 throw new RuntimeException(e); 
 } 
}; 
类型检查、类型推断以及限制
类型检查

Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型

List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150); 

从上面看,使用Lambda的上下文是什么呢?该Lambda是作为filter的参数,所以我们进入filter方法中

filter(List<Apple> inventory,Predicate<Apple> p)

从中我们可以很清晰地看到,上下文就是filter方法中的Predicate p参数,那么目标类型就是Predicate,T绑定到Apple,Predicate是一个函数式接口。

boolean test(Apple apple)

这是Predicate的抽象方法,该方法描述了一个函数描述符,Apple->boolean

最后函数描述符Apple->boolean匹配Lambda的签名,返回一个boolean,因此代码类型检查无误。

👍

类型检查过程可以分解为如下所示。

  • 首先,你要找出filter方法的声明。
  • 第二,要求它是Predicate(目标类型)对象的第二个正式参数。
  • 第三,Predicate是一个函数式接口,定义了一个叫作test的抽象方法。
  • 第四,test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean。
  • 最后,filter的任何实际参数都必须匹配这个要求。

请注意,如果Lambda表达式抛出一个异常,那么抽象方法所声明的throws语句也必须与之匹配。

同样的lambda,不同的函数式接口

有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来,只要它们的抽象方法签名能够兼容。

Comparator<Apple> c1 =  (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); 
ToIntBiFunction<Apple, Apple> c2 =  (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); 
BiFunction<Apple, Apple, Integer> c3 =  (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); 

菱形运算符
那些熟悉Java的演变的人会记得,Java 7中已经引入了菱形运算符(<>),利用泛型推断从上下文推断类型的思想(这一思想甚至可以追溯到更早的泛型方法)。一个类实例表达式可以出现在两个或更多不同的上下文中,并会像下面这样推断出适当的类型参数:

List<String> listOfStrings = new ArrayList<>(); 
List<Integer> listOfIntegers = new ArrayList<>(); 

特殊的void兼容规则
如果一个Lambda的主体是一个语句表达式,它就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)。例如,以下两行都是合法的,尽管List的add方法返回了一个boolean,而不是Consumer上下文(T -> void)所要求的void:

// Predicate返回了一个boolean 
Predicate<String> p = s -> list.add(s); 
// Consumer返回了一个void 
Consumer<String> b = s -> list.add(s); 

用Lambda表达式可以从赋值的上下文、方法调用的上下文(参数和返回值),以及类型转换的上下文中获得目标类型。

类型推断

进一步简化代码。Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。这样做的好处在于,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda语法中省去标注参数类型。换句话说,Java编译器会像下面这样推断Lambda的参数类型:

List<Apple> greenApples = filter(inventory, a -> "green".equals(a.getColor())); 

参数a没有显示类型,当Lambda仅有一个类型需要推断的参数时,参数名称两边的括号也可以省略

Comparator<Apple> c = 
 (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); 
Comparator<Apple> c = 
 (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

请注意,有时候显式写出类型更易读,有时候去掉它们更易读。没有什么法则说哪种更好;对于如何让代码更易读,程序员必须做出自己的选择。

使用局部变量

Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。 它们被称作捕获Lambda。

int portNumber = 1337; 
Runnable r = () -> System.out.println(portNumber);

Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为final,或事实上是final。换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获最终局部变量this。) 例如,下面的代码无法编译,因为portNumber变量被赋值两次:

int portNumber = 1337; 
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337; 
局部变量的限制

为什么局部变量有这些限制?

第一,实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它的副本😄,而不是访问原始变量

第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(这种模式会阻碍很容易做到的并行处理)

闭包

用科学的说法来说,闭包就是一个函数的实例,且它可以无限制地访问那个函数的非本地变量。例如,闭包可以作为参数传递给另一个函数。它也可以访问和修改其作用域之外的变量。现在,Java 8的Lambda和匿名类可以做类似于闭包的事情:它们可以作为参数传递给方法,并且可以访问其作用域之外的变量。但有一个限制:它们不能修改定义Lambda的方法的局部变量的内容。这些变量必须是隐式最终的。可以认为Lambda是对值封闭,而不是对变量封闭。如前所述,这种限制存在的原因在于局部变量保存在栈上,并且隐式表示它们仅限于其所在线程。如果允许捕获可改变的局部变量,就会引发造成线程不安全的新的可能性,而这是我们不想看到的(实例变量可以,因为它们保存在堆中,而堆是在线程之间共享的)。

方法引用

方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。在一些情况下,比起使用Lambda表达式,它们似乎更易读,感觉也更自然。

inventory.sort(comparing(Apple::getWeight)); 

方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法

它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。事实上,方法引用就是让你根据已有的方法实现来创建Lambda表达式。但是,显式地指明方法的名称,你的代码的可读性会更好。

当你需要使用方法引用时,目标引用放在分隔符::前,方法的名称放在后面。例如,Apple::getWeight就是引用了Apple类中定义的方法getWeight。请记住,不需要括号,因为你没有实际调用这个方法。方法引用就是Lambda表达式(Apple a) -> a.getWeight()的快捷写法。

Lambda等效的方法引用
(Apple a) -> a.getWeight()Apple::getWeight
() -> Thread.currentThread().dumpStack()Thread.currentThread()::dumpStack
(str, i) -> str.substring(i)String::substring
(String s) -> System.out.println(s)System.out::println

可以把方法引用看作针对仅仅涉及单一方法的Lambda的语法糖,因为你表达同样的事情时要写的代码更少了

创建方法引用
  1. 指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt)。

  2. 指 向 任意类型实例方法 的方法引用(例如 String 的 length 方法,写作String::length)。

    在引用一个对象的方法,而这个对象本身是Lambda的一个参数。例如,Lambda表达式(String s) -> s.toUppeCase()可以写作String::toUpperCase

  3. 指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction用于存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensiveTransaction::getValue)

    你在Lambda中调用一个已经存在的外部对象中的方法。例如,Lambda表达式()->expensiveTransaction.getValue()可以写作expensiveTransaction::getValue。

还有针对构造函数、数组构造函数和父类调用(super-call)的一些特殊形式的方法引用,方法引用的签名必须和上下文类型匹配

构造函数引用

对于一个现有构造函数,你可以利用它的名称和关键字new来创建它的一个引用:ClassName::new。它的功能与指向静态方法的引用类似。

假设有一个构造函数没有参数。它适合Supplier的签名() -> Apple

//构造函数引用指向默认的Student()构造函数
Supplier<Student> supplier = Student::new;
//调用Supplier的get方法将产生一个新的Student
Student student = supplier.get();
Student student1 = supplier.get();
//false
System.out.println(student == student1);

构造函数的签名是Student(String name),那么它就适合Function接口的签名

Function<String, Student> function = Student::new;
Student apply = function.apply("小明");
Student apply1 = function.apply("小明");
System.out.println(apply);
//false
System.out.println(apply == apply1);

两个参数的构造函数,适合BiFuction接口的签名

//BiFunction
        BiFunction<String, Integer, Student> biFunction = Student::new;
        Student student2 = biFunction.apply("小王", 20);
        Student student3 = biFunction.apply("小王1", 21);
        System.out.println(student2);
        System.out.println(student3);
        //false
        System.out.println(student2==student3);

多个参数,自定义接口

Lambda和方法引用实战

Comparator具有一个叫作comparing的静态辅助方法,它可以接受一个Function来提取Comparable键值,并生成一个Comparator对象。

inventory.sort(comparing(Apple::getWeight));
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor)
    {
        Objects.requireNonNull(keyExtractor);
        return (Comparator<T> & Serializable)
            (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
    }
复合Lambda表达式的有用方法

Java 8的好几个函数式接口都有为方便而设计的方法。具体而言,许多函数式接口,比如用于传递Lambda表达式的Comparator、Function和Predicate都提供了允许你进行复合的方法。

你可以把多个简单的Lambda复合成复杂的表达式。比如,你可以让两个谓词之间做一个or操作,组合成一个更大的谓词。而且,你还可以让一个函数的结果成为另一个函数的输入。

比较器复合

使用静态方法Comparator.comparing,根据提取用于比较的键值的Function来返回一个Comparator

Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
  1. 逆序

    对苹果按重量递减排序

    inventory.sort(comparing(Apple::getWeight).reversed());
    
  2. 比较器链

    在按重量比较两个苹果之后,按原产国排序

    inventory.sort(comparing(Apple::getWeight) 
     .reversed() 
     .thenComparing(Apple::getCountry)); 
    
谓词复合

谓词接口包括三个方法:negate、and和or,可以重用已有的Predicate来创建更复杂的谓词

使用negate方法来返回一个Predicate的非,比如苹果不是红的

Predicate<Apple> notRedApple = redApple.negate();

请注意,and和or方法是按照在表达式链中的位置,从左向右确定优先级的。因此,a.or(b).and©可以看作(a || b) && c。

函数复合

可以把Function接口所代表的Lambda表达式复合起来。Function接口为此配了andThen和compose两个默认方法,它们都会返回Function的一个实例

andThen方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。

假设有一个函数f给数字加1 (x -> x + 1),另一个函数g给数字乘2,你可以将它们组合成一个函数h,先给数字加1,再给结果乘2

Function<Integer, Integer> f = x -> x + 1; 
Function<Integer, Integer> g = x -> x * 2; 
Function<Integer, Integer> h = f.andThen(g); 
int result = h.apply(1);//4

使用compose方法,先把给定的函数用作compose的参数里面给的那个函数,然后再把函数本身用于结果。比如在上一个例子里用compose的话,它将意味着f(g(x)),而andThen则意味着g(f(x))

Function<Integer, Integer> f = x -> x + 1; 
Function<Integer, Integer> g = x -> x * 2; 
Function<Integer, Integer> h = f.compose(g);
int result = h.apply(1); 

是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。就现在来说,你可以把它们看成遍历数据集的高级迭代器。此外,流还可以透明地并行处理,你无需写任何多线程代码了!

其他库:Guava、Apache和lambdaj

为了给Java程序员提供更好的库操作集合,前人已经做过了很多尝试。比如,Guava就是谷歌创建的一个很流行的库。它提供了multimaps和multisets等额外的容器类。Apache Commons Collections库也提供了类似的功能。最后,本书作者Mario Fusco编写的lambdaj受到函数式编程的启发,也提供了很多声明性操作集合的工具。
如今Java 8自带了官方库,可以以更加声明性的方式操作集合了很多模式,如筛选、切片、查找、匹配、映射和归约

流简单定义:从支持数据处理操作的源生成的元素序列

  • 元素序列

    像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。集合讲的是数据,流讲的是计算

  • 流会使用一个提供数据的源,如集合、数组或输入/输出资源。 请注意,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。

  • 数据处理操作

    流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可并行执行

  • 流水线

    很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。优化如延迟和短路。流水线的操作可以看作对数据源进行数据库式查询

  • 内部迭代

    与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的

//使用流来筛选菜单,找出三个高热量菜肴的名字
List<String> threeHighCaloricDishNames = 
 menu.stream() 
 .filter(d -> d.getCalories() > 300)
 .map(Dish::getName)
 .limit(3) 
 .collect(toList()); 
System.out.println(threeHighCaloricDishNames);
//filter——接受Lambda,从流中排除某些元素,通过传递lambda d -> d.getCalories() > 300,选择出热量超过300卡路里的菜肴
//map——接受一个Lambda,将元素转换成其他形式或提取信息,通过传递方法引用Dish::getName,相当于Lambda d -> d.getName(),提取了每道菜的菜名
//limit——截断流,使其元素不超过给定数量
//collect——将流转换为其他形式。

粗略地说,集合与流之间的差异就在于什么时候进行计算。集合是一个内存中的数据结构,它包含数据结构中目前所有的值——集合中的每个元素都得先算出来才能添加到集合中。(你可以往集合里加东西或者删东西,但是不管什么时候,集合中的每个元素都是放在内存里的,元素都得先算出来才能成为集合的一部分。)
相比之下,流则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计算的

构建一个质数流(2, 3, 5, 7, 11, …)有多简单,尽管质数有无穷多个。这个思想就是用户仅仅从流中提取需要的值,而这些值——在用户看不见的地方——只会按需生成。这是一种生产者-消费者的关系。从另一个角度来说,流就像是一个延迟创建的集合:只有在消费者要求的时候才会计算值(用管理学的话说这就是需求驱动,甚至是实时制造)

与此相反,集合则是急切创建的(供应商驱动:先把仓库装满,再开始卖,就像那些昙花一现的圣诞新玩意儿一样)。以质数为例,要是想创建一个包含所有质数的集合,那这个程序算起来就没完没了了,因为总有新的质数要算,然后把它加到集合里面。当然这个集合是永远也创建不完的,消费者这辈子都见不着了。

流只能遍历一次

请注意,和迭代器类似,流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了。你可以从原始数据源那里再获得一个新的流来重新遍历一遍,就像迭代器一样(这里假设它是集合之类的可重复的源,如果是I/O通道就没戏了)。例如,以下代码会抛出一个异常(java.lang.IllegalStateException: stream has already been operated upon or closed),说流已被消费掉了。

List<String> title = Arrays.asList("Java8", "In", "Action"); 
Stream<String> s = title.stream(); 
s.forEach(System.out::println); 
s.forEach(System.out::println);

集合和流的另一个关键区别在于它们遍历数据的方式

外部迭代和内部迭代

使用Collection接口需要用户去做迭代(比如用for-each),这称为外部迭代。 相反,Streams库使用内部迭代——它帮你把迭代做了,还把得到的流值存在了某个地方,你只要给出一个函数说要干什么就可以了。

用for-each循环外部迭代,for-each结构是一个语法糖,它背后的东西用Iterator对象表达出来更要丑陋得多。

流操作

可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作

操作类型返回类型操作参数函数描述符
filter中间StreamPredicateT -> boolean
map中间StreamFunction<T, R>T -> R
limit中间Stream
sorted中间StreamComparator(T, T) -> int
distinct中间Stream
操作类型目的
forEach终端消费流中的每个元素并对其应用 Lambda。这一操作返回 void
count终端返回流中元素的个数。这一操作返回 long
collect终端把流归约成一个集合,比如 List、Map 甚至是 Integer。
筛选

filter筛选很好理解,就传一个Predicate,里面有一个distinct方法,可以用来去重的筛选方法,通过调用equals和hashCode方法来去重。

limit(n):返回一个不超过n的长度的流。如果流有序,则最多返回前n个元素;如果流无序,limit结果不会以任何顺序排列

skip(n):返回一个扔掉前n个元素的流,如果不足n个,则返回一个空流

映射

流支持map方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一个新版本”而不是去“修改”)。

List<String> dishNames = menu.stream().map(Dish::getName).collect(toList()); 
  • 流的扁平化

    • 需求

      对于一张单词表,如何返回一张列表,列出里面 各不相同的字符 呢?例如,给定单词列表[“Hello”,“World”],你想要返回列表[“H”,“e”,“l”, “o”,“W”,“r”,“d”]

    • 解决方案

      将每个单词转换为由其字母构成的数组,再将各个生成流扁平化为单个流

      List<String> uniqueCharacters = 
       words.stream() //转换为流
       .map(w -> w.split("")) //将每个元素映射为字符串数组的流
       .flatMap(Arrays::stream) //将单独的流合并,扁平化为单个流
       .distinct() //去重
       .collect(Collectors.toList());//流关闭
      

      值得注意的是:Arrays::stream会接受一个数组并产生一个流

      String[] arrayOfWords = {"Goodbye", "World"}; 
      Stream<String> streamOfwords = Arrays.stream(arrayOfWords); 
      
      public class TestFlatMap {
          public static void main(String[] args) {
              List<String> strings = Arrays.asList("hello", "world", "lina");
              Stream<String> stream = strings.stream();
              Stream<String[]> stream1 = stream.map(s -> s.split(""));
              //Stream<Stream<String>> streamStream = stream1.map(Arrays::stream);
              Stream<String> stringStream = stream1.flatMap(Arrays::stream);
              List<String> collect = stringStream.distinct().collect(Collectors.toList());
              System.out.println(collect);
          }
      }
      
    • 测验

      1. 给定一个数字列表,如何返回一个由每个数的平方构成的列表呢?例如,给定[1, 2, 3, 4, 5],应该返回[1, 4, 9, 16, 25]
      public class Example1 {
          public static void main(String[] args) {
              List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
              List<Integer> collect = integers.stream().map(v -> v * v).collect(Collectors.toList());
              System.out.println(collect);
          }
      }
      
      1. 给定两个数字列表,如何返回所有的数对呢?例如,给定列表[1, 2, 3]和列表[3, 4],应该返回[(1, 3), (1, 4), (2, 3), (2, 4), (3, 3), (3, 4)]。为简单起见,你可以用有两个元素的数组来代表数对

        public class Example2 {
            public static void main(String[] args) {
                List<Integer> list1 = Arrays.asList(1, 2, 3);
                List<Integer> list2 = Arrays.asList(3, 4);
                List<Integer[]> collect = list1.stream().flatMap(v -> list2.stream().map(k -> new Integer[]{v, k})).collect(Collectors.toList());
                collect.forEach(i -> System.out.println(i[0] + "," + i[1]));
            }
        }
        
      2. 如何扩展前一个例子,只返回总和能被3整除的数对呢?例如(2, 4)和(3, 3)是可以的

        public class Example3 {
            public static void main(String[] args) {
                List<Integer> list1 = Arrays.asList(1, 2, 3);
                List<Integer> list2 = Arrays.asList(3, 4);
                List<Integer[]> collect = list1.stream().flatMap(v -> list2.stream().filter(k -> (k + v) % 3 == 0).map(k -> new Integer[]{v, k})).collect(Collectors.toList());
                collect.forEach(k -> System.out.println(k[0] + "," + k[1]));
            }
        }
        
查找和匹配
  1. 检查谓词是否至少匹配一个元素

    anyMatch方法,流中是否有一个元素能匹配给定的谓词

  2. 检查谓词是否匹配所有元素

    • allMatch方法,流中的元素是否都能匹配给定的谓词

    • noneMatch方法,流中没有任何元素与给定的谓词匹配

anyMatch、allMatch和noneMatch这三个操作都用到了短路,Java中&&和||运算符短路在流中的版本

短路求值

有些操作不需要处理整个流就能得到结果。例如,假设你需要对一个用and连起来的大布尔表达式求值。不管表达式有多长,你只需找到一个表达式为false,就可以推断整个表达式将返回false,所以用不着计算整个表达式。这就是短路。

对于流而言,某些操作(例如allMatch、anyMatch、noneMatch、findFirst和findAny)不用处理整个流就能得到结果。只要找到一个元素,就可以有结果了。

同样,limit也是一个短路操作:它只需要创建一个给定大小的流,而用不着处理流中所有的元素。在碰到无限大小的流的时候,这种操作就有用了:它们可以把无限流变成有限流。

查找任意元素

findAny方法将返回当前流中的任意元素。流水线将在后台进行优化使其只需走一遍,并在利用短路找到结果时立即结束

Optional<Dish> dish = 
 menu.stream() 
 .filter(Dish::isVegetarian) 
 .findAny(); 

其中Optional类(java.util.Optional)是一个容器类,代表一个值存在或不存在,可以用来避免容易出问题的null。其中有以下常用的方法:

  1. isPresent()将在Optional包含值的时候返回true, 否则返回false
  2. ifPresent(Consumer block)会在值存在的时候执行给定的代码块
  3. T get()会在值存在时返回值,否则抛出一个NoSuchElement异常
  4. T orElse(T other)会在值存在时返回值,否则返回一个默认值
查找第一个元素

findFirst

何时使用findFirst和findAny
为什么会同时有findFirst和findAny呢?答案是并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny,因为它在使用并行流时限制较少。

规约

把一个流中的元素组合起来,使用reduce操作来表达更复杂的查询,此类查询需要将流中所有元素反复结合起来,得到一个值,比如一个Integer。这样的查询可以被归类为归约操作(将流归约成一个值)。用函数式编程语言的术语来说,这称为折叠(fold),因为你可以将这个操作看成把一张长长的纸(你的流)反复折叠成一个小方块,而这就是折叠操作的结果。

案例

1.元素求和

int[] numbers={4,5,3,9};
int sum = numbers.stream().reduce(0, (a, b) -> a + b); 
T reduce(T identity, BinaryOperator<T> accumulator);//源码

reduce接受两个参数:

  1. 一个初始值,这里是0;
  2. 一个BinaryOperator来将两个元素结合起来产生一个新值,这里我们用的是lambda (a, b) -> a + b。

reduce操作是如何对一个数字流求和的?

首先,0作为Lambda(a)的第一个参数,从流中获得4作为第二个参数(b)。0 + 4得到4,它成了新的累积值。然后再用累积值和流中下一个元素5调用Lambda,产生新的累积值9。接下来,再用累积值和下一个元素3调用Lambda,得到12。最后,用12和流中最后一个元素9调用Lambda,得到最终结果21。

如果流中无数据,返回初始值。

可以使用方法引用让这段代码更简洁。在Java 8中,Integer类现在有了一个静态的sum方法来对两个数求和,用不着反复用Lambda写同一段代码了。

int[] numbers={4,5,3,9};
int sum = numbers.stream().reduce(0, Integer::sum); 

reduce还有一个重载的变体,它不接受初始值,但是会返回一个Optional对象

Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b)); 
Optional<T> reduce(BinaryOperator<T> accumulator);

为什么它返回一个Optional呢?考虑流中没有任何元素的情况。reduce操作无法返回其和,因为它没有初始值。这就是为什么结果被包裹在一个Optional对象里,以表明和可能不存在。如果有一个还是可以计算的

reduce接受三个参数:这个理解不了?存在问题。后续再研究

<U> U reduce(U identity,//返回实例u,传递你要返回的U类型对象的初始化实例u
        BiFunction<U, ? super T, U> accumulator,//参数累加器accumulator
        BinaryOperator<U> combiner);//参数组合器combiner,会将不同线程计算的结果调用combiner做汇总后返回,而这两个值必须与第二个函数参数相兼容,也就是说它们所得的结果类型是一样的。

2.最大值和最小值

Optional<Integer> max = numbers.stream().reduce(Integer::max); 
  • 测验

    怎样用map和reduce方法数一数流中有多少个菜呢?

    int count = vagetables.stream().map(v->1).reduce(0,(a,b)->a+b);
    long count = menu.stream().count(); //使用count也可以计算
    

map和reduce的连接通常称为map-reduce模式,因Google用它来进行网络搜索而出名,因为它很容易并行化。

归约方法的优势与并行化

相比于逐步迭代求和,使用reduce的好处在于,这里的迭代被内部迭代抽象掉了,这让内部实现得以选择并行执行reduce操作。而迭代式求和例子要更新共享变量sum,这不是那么容易并行化的。如果你加入了同步,很可能会发现线程竞争抵消了并行本应带来的性能提升!这种计算的并行化需要另一种办法:将输入分块,分块求和,最后再合并起来。但这样的话代码看起来就完全不一样了,可以使用分支/合并框架来做。但要并行执行这段代码也要付一定代价:传递给reduce的Lambda不能更改状态(如实例变量),而且操作必须满足结合律才可以按任意顺序执行。

流操作:无状态和有状态

诸如map或filter等操作会从输入流中获取每一个元素,并在输出流中得到0或1个结果。这些操作一般都是无状态的:它们没有内部状态(假设用户提供的Lambda或方法引用没有内部可变状态)。

诸如reduce、sum、max等操作需要内部状态来累积结果。在上面的情况下,内部状态很小。在我们的例子里就是一个int或double。不管流中有多少元素要处理,内部状态都是有界的

相反,诸如sort或distinct等操作一开始都和filter和map差不多——都是接受一个流,再生成一个流(中间操作),但有一个关键的区别。从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操
作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题(把质数流倒序会做什么呢?它应当返回最大的质数,但数学告诉我们它不存在)。我们把这些操作叫作有状态操作

操 作类 型返回类型使用的类型/函数式接口函数描述符
filter中间StreamPredicateT -> boolean
distinct中间(有状态-无界)Stream
skip中间(有状态-有界)Streamlong
limit中间(有状态-有界)Streamlong
map中间StreamFunction<T, R>T -> R
flatMap中间StreamFunction<T, Stream>T -> Stream
sorted中间(有状态-无界)StreamComparator(T, T) -> int
anyMatch终端booleanPredicateT -> boolean
noneMatch终端booleanPredicateT -> boolean
allMatch终端booleanPredicateT -> boolean
findAny终端Optional
findFirst终端Optional
forEach终端voidConsumerT -> void
collect终端RCollector<T, A, R>
reduce终端(有状态-有界) OptionalBinaryOperator(T, T) -> T
count终端long
数值流

问题描述:

使用reduce方法计算流中元素的总和,有一个暗含的装箱成本。每个Integer都必须拆箱成一个原始类型,再进行求和。

int calories = menu.stream() 
 .map(Dish::getCalories) 
 .reduce(0, Integer::sum); 

解决方案:

Java 8引入了三个原始类型特化流接口来解决这个问题:IntStream、DoubleStream和LongStream,分别将流中的元素特化为int、long和double,从而避免了暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到最大元素的max。?如果特化为int,long,double需要考虑数值精确度问题吗

此外还有在必要时再把它们转换回对象流的方法。要记住的是,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性——即类似int和Integer之间的效率差异。

映射到数值流

将流转换为特化版本的常用方法是mapToInt、mapToDouble和mapToLong。这些方法和前面说的map方法的工作方式一样,只是它们返回的是一个特化流,而不是Stream。

int calories = menu.stream() 
 .mapToInt(Dish::getCalories) //返回一个IntStream
 .sum(); 
IntStream mapToInt(ToIntFunction<? super T> mapper);
@FunctionalInterface
//函数描述符 T->int 
public interface ToIntFunction<T> {
    /**
     * Applies this function to the given argument.
     *
     * @param value the function argument
     * @return the function result
     */
    int applyAsInt(T value);
}

这里,mapToInt会从每道菜中提取热量(用一个Integer表示),并返回一个IntStream(而不是一个Stream)。然后你就可以调用IntStream接口中定义的sum方法,对卡路里求和了!请注意,如果流是空的,sum默认返回0。IntStream还支持其他的方便方法,如max、min、average等。

转换为对象流

要把原始流转换成一般流(每个int都会装箱成一个Integer),可以使用boxed方法

IntStream intStream = menu.stream().mapToInt(Dish::getCalories); 
Stream<Integer> stream = intStream.boxed();
默认值OptionalInt

Optional可以用Integer、String等参考类型来参数化。对于三种原始流特化,也分别有一个Optional原始类型特化版本:OptionalInt、OptionalDouble和OptionalLong。

例如,要找到IntStream中的最大元素,可以调用max方法,它会返回一个OptionalInt:

OptionalInt maxCalories = menu.stream() 
 .mapToInt(Dish::getCalories) 
 .max(); 
int max = maxCalories.orElse(1); //如果没有最大值的话,显式提供一个默认最大值
数值范围

Java 8引入了两个可以用于IntStream和LongStream的静态方法,帮助生成这种范围:range和rangeClosed。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但range是不包含结束值的,而rangeClosed则包含结束值。

IntStream evenNumbers = IntStream.rangeClosed(1, 100) 
 .filter(n -> n % 2 == 0); //一个从1到100的偶数流
System.out.println(evenNumbers.count()); //从1到100有50个偶数
构建流
由值构建流

使用静态方法Stream.of,通过显式值创建一个流。它可以接受任意数量的参数

Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action"); //创建一个字符串流
stream.map(String::toUpperCase).forEach(System.out::println); 

使用empty得到一个空流

Stream<String> emptyStream = Stream.empty();
由数组创建流

可以使用静态方法Arrays.stream从数组创建一个流。它接受一个数组作为参数。

例如,你可以将一个原始类型int的数组转换成一个IntStream

int[] numbers = {2, 3, 5, 7, 11, 13}; 
int sum = Arrays.stream(numbers).sum();
由文件生成流

Java中用于处理文件等I/O操作的NIO API(非阻塞 I/O)已更新,以便利用Stream API。java.nio.file.Files中的很多静态方法都会返回一个流。

例如,一个很有用的方法是Files.lines,它会返回一个由指定文件中的各行构成的字符串流。

看一个文件中有多少各不相同的词

long uniqueWords = 0; 
try(Stream<String> lines = 
 Files.lines(Paths.get("data.txt"), Charset.defaultCharset())){ 
uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
 .distinct() 
 .count(); 
} 
catch(IOException e){ 
} 
由函数生成流:创建无限流

Stream API提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate和generate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说,应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值

  1. 迭代

    Stream.iterate(0, n -> n + 2) 
     .limit(10)
     .forEach(System.out::println);
    

    iterate方法接受一个初始值(在这里是0),还有一个依次应用在每个产生的新值上的Lambda(UnaryOperator类型)。

  2. 生成

    与iterate方法类似,generate方法也可让你按需生成一个无限流。但generate不是依次对每个新生成的值应用函数的。它接受一个Supplier类型的Lambda提供新的值。

    //生成5个0到1之间的随机双精度数(不包含1)
    Stream.generate(Math::random) 
     .limit(5) 
     .forEach(System.out::println);
    //转换为DoubleStream
    Stream.generate(Math::random).mapToDouble(v->v).limit(5).forEach(System.out::println);
    

    generate方法还有什么用途?

    我们使用的供应源(指向Math.random的方法引用)是无状态的:它不会在任何地方记录任何值,以备以后计算使用。但供应源不一定是无状态的。可以创建存储状态的供应源,它可以修改状态,并在为流生成下一个值时使用。但很重要的一点是,在并行代码中使用有状态的供应源是不安全的。

    //生成斐波纳契数列,不推荐这种用法,因为存在可变状态
            IntStream.generate(new IntSupplier() {
                private int previous = 0;
                private int current = 1;
                @Override
                public int getAsInt() {
                    int oldPrevious = this.previous;
                    int nextValue = this.previous + this.current;
                    this.previous = this.current;
                    this.current = nextValue;
                    return oldPrevious;
                }
            }).limit(20).forEach(System.out::println);
    

    这里的匿名类通过字段定义状态,状态可以通过getAsInt方法修改,有副作用,不推荐使用,在并行代码时不安全。相比之下,使用iterate的方法则是纯粹不变的:它没有修改现有状态,但在每次迭代时会创建新的元组。始终采用不变的方法,以便并行处理流,并保持结果正确。

用流收集数据

collect是一个归约操作,就像reduce一样可以接受各种做法作为参数,将流中的元素累积成一个汇总结果。具体的做法是通过定义新的
Collector接口来定义的,因此区分Collection、Collector和collect是很重要的。

收集器用作高级归约

收集器非常有用,因为用它可以简洁而灵活地定义collect用来生成结果集合的标准。更具体地说,对流调用collect方法将对流中的元素触发一个归约操作(由Collector来参数化)。

一般来说,Collector会对元素应用一个转换函数(很多时候是不体现任何效果的恒等转换,例如toList),并将结果累积在一个数据结构中,从而产生这一过程的最终输出。

预定义收集器

可以从Collectors类提供的工厂方法(例如groupingBy)创建的收集器,主要提供三大功能:

  1. 将流元素规约和汇总为一个值
  2. 元素分组
  3. 元素分区
规约和汇总

案例:利用counting工厂方法返回的收集器,数一数菜单里有多少种菜

long howManyDishes = menu.stream().collect(Collectors.counting()); 
long howManyDishes = menu.stream().count(); //写成这种更直观

如果已导入了Collectors类的所有静态工厂方法,就可以写counting()而用不着写Collectors.counting()

import static java.util.stream.Collectors.*; 
查看流中的最大值和最小值

可以使用两个收集器,Collectors.maxBy和Collectors.minBy,来计算流中的最大或最小值。这两个收集器接收一个Comparator参数来
比较流中的元素。

Comparator<Dish> dishCaloriesComparator = 
 Comparator.comparingInt(Dish::getCalories); 
Optional<Dish> mostCalorieDish = 
 menu.stream() 
 .collect(maxBy(dishCaloriesComparator)); 
汇总

Collectors类专门为汇总提供了一个工厂方法:Collectors.summingInt。它可接受一个把对象映射为求和所需int的函数,并返回一个收集器;该收集器在传递给普通的collect方法后即执行我们需要的汇总操作。

//求出菜单列表的总热量
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));

但汇总不仅仅是求和;还有Collectors.averagingInt,连同对应的averagingLong和averagingDouble可以计算数值的平均数

double avgCalories = 
 menu.stream().collect(averagingInt(Dish::getCalories)); 

很多时候,你可能想要得到两个或更多这样的结果,而且你希望只需一次操作就可以完成。在这种情况下,你可以使用summarizingInt工厂方法返回的收集器。

例如,通过一次summarizing操作你可以就数出菜单中元素的个数,并得到菜肴热量总和、平均值、最大值和最小值。

IntSummaryStatistics menuStatistics = 
 menu.stream().collect(summarizingInt(Dish::getCalories)); 

这个收集器会把所有这些信息收集到一个叫作IntSummaryStatistics的类里,它提供了方便的取值(getter)方法来访问结果。

IntSummaryStatistics{count=9, sum=4300, min=120, 
 average=477.777778, max=800} 

同样,相应的summarizingLong和summarizingDouble工厂方法有相关的LongSummaryStatistics和DoubleSummaryStatistics类型,适用于收集的属性是原始类型long或double的情况。

连接字符串

joining工厂方法返回的收集器会把对流中每一个对象应用toString方法得到的所有字符串连接成一个字符串。

String shortMenu = menu.stream().map(Dish::getName).collect(joining());//把菜单中所有菜肴的名称连接起来

请注意,**joining在内部使用了StringBuilder**来把生成的字符串逐个追加起来。此外还要注意,如果Dish类有一个toString方法来返回菜肴的名称,那你无需用提取每一道菜名称的函数来对原流做映射就能够得到相同的结果

String shortMenu = menu.stream().collect(joining());//有一个toString方法返回菜肴的名称

joining工厂方法有一个重载版本可以接受元素之间的分界符

public static Collector<CharSequence, ?, String> joining(CharSequence delimiter) {
        return joining(delimiter, "", "");
    }
//prefix为前缀,suffix为后缀(针对于joining的整个字符串)
  public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,
                                                             CharSequence prefix,
                                                             CharSequence suffix) {
        return new CollectorImpl<>(
                () -> new StringJoiner(delimiter, prefix, suffix),
                StringJoiner::add, StringJoiner::merge,
                StringJoiner::toString, CH_NOID);
    }
广义的规约汇总

上面的所有收集器,都是一个可以用reducing工厂方法定义的归约过程的特殊情况,Collectors.reducing工厂方法是所有这些特殊情况的一般化。

案例:用reducing方法创建的收集器来计算你菜单的总热量

int totalCalories = menu.stream().collect(reducing( 
 0, Dish::getCalories, (i, j) -> i + j)); 

需要三个参数:

  1. 第一个参数是归约操作的起始值,也是流中没有元素时的返回值,所以很显然对于数值和而言,0是一个合适的值。
  2. 第二个参数就是使用的函数,将菜肴转换成一个表示其所含热量的int。
  3. 第三个参数是一个BinaryOperator,将两个项目累积成一个同类型的值。这里它就是对两个int求和。

同样,你可以使用下面这样单参数形式的reducing来找到热量最高的菜。

Optional<Dish> mostCalorieDish = 
 menu.stream().collect(reducing( 
 (d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));

你可以把单参数reducing工厂方法创建的收集器看作三参数方法的特殊情况,它把流中的第一个项目作为起点,把恒等函数(即一个函数仅仅是返回其输入参数)作为一个转换函数。这也意味着,要是把单参数reducing收集器传递给空流的collect方法,收集器就没有起点,因此会返回一个Optional对象。

收集与规约的区别

Stream接口的collect和reduce方法有何不同,因为两种方法通常会获得相同的结果。

reduce方法来实现toListCollector所做的工作:
Stream stream = Arrays.asList(1, 2, 3, 4, 5, 6).stream();
List numbers = stream.reduce(
new ArrayList(),
(List l, Integer e) -> {
l.add(e);
return l; },
(List l1, List l2) -> {
l1.addAll(l2);
return l1; });

这个解决方案有两个问题:一个语义问题和一个实际问题。语义问题在于,reduce方法旨在把两个值结合起来生成一个新值,它是一个不可变的归约。与此相反,collect方法的设计就是要改变容器,从而累积要输出的结果。这意味着,上面的代码片段是在滥用reduce方法,因为它在原地改变了作为累加器的List

  1. 收集框架的灵活性:以不同的方法执行同样的操作

    之前提到的counting收集器也是类似地利用三参数reducing工厂方法实现的。它把流中的每个元素都转换成一个值为1的Long型对象,然后再把它们相加:

    public static <T> Collector<T, ?, Long> counting() { 
     return reducing(0L, e -> 1L, Long::sum); 
    } 
    

    使用泛型?通配符
    在这段代码中,你可能已经注意到了?通配符,它用作counting工厂方法返回的收集器签名中的第二个泛型类型。对这种记法你应该已经很熟悉了,特别是如果你经常使用Java的集合框架的话。在这里,它仅仅意味着收集器的累加器类型未知,换句话说,累加器本身可以是任何类型。

  2. 根据情况选择最佳解决方案

    收集器在某种程度上比Stream接口上直接提供的方法用起来更复杂,但好处在于它们能提供更高水平的抽象和概括,也更容易重用和自定义。尽可能为手头的问题探索不同的解决方案,但在通用的方案里面,始终选择最专门化的一个。无论是从可读性还是性能上看,这一般都是最好的决定。例如,要计菜单的总热量,我们更倾向于最后一个解决方案(使用IntStream),因为它最简明,也很可能最易读。同时,它也是性能最好的一个,因为IntStream可以让我们避免自动拆箱操作,也就是从Integer到int的隐式转换,它在这里毫无用处。

测验:

用reducing连接字符串,以下哪一种reducing收集器的用法能够合法地替代joining收集器

String shortMenu = menu.stream().map(Dish::getName).collect(joining());
(1) String shortMenu = menu.stream().map(Dish::getName)
 .collect( reducing ( (s1, s2) -> s1 + s2 ) ).get();
(2) String shortMenu = menu.stream()
 .collect( reducing( (d1, d2) -> d1.getName() + d2.getName() ) ).get();
(3) String shortMenu = menu.stream()
 .collect( reducing( "",Dish::getName, (s1, s2) -> s1 + s2 ) ); 

答案:语句1和语句3是有效的,语句2无法编译。
(1) 这会将每道菜转换为菜名,就像原先使用joining收集器的语句一样。然后用一个String作为累加器归约得到的字符串流,并将菜名逐个连接在它后面。
(2) 这无法编译,因为reducing接受的参数是一个BinaryOperator,也就是一个BiFunction<T,T,T>。这就意味着它需要的函数必须能接受两个参数,然后返回一个相同类型的值,但这里用的Lambda表达式接受的参数是两个菜,返回的却是一个字符串。
(3) 这会把一个空字符串作为累加器来进行归约,在遍历菜肴流时,它会把每道菜转换成菜名,并追加到累加器上。请注意,我们前面讲过,reducing要返回一个Optional并不需要三个参数,因为如果是空流的话,它的返回值更有意义——也就是作为累加器初始值的空字
符串。
请注意,虽然语句1和语句3都能够合法地替代joining收集器。然而就实际应用而言,不管是从可读性还是性能方面考虑,我们始终建议使用joining收集器

分组
Map<Dish.Type, List<Dish>> dishesByType = 
 menu.stream().collect(groupingBy(Dish::getType));

你给groupingBy方法传递了一个Function(以方法引用的形式),它提取了流中每一道Dish的Dish.Type。我们把这个Function叫作分类函数,因为它用来把流中的元素分成不同的组。

但是,分类函数不一定像方法引用那样可用,因为你想用的分类条件可能比简单的属性访问器要复杂。例如,你可能想把热量不到400卡路里的菜划分为“低热量”(diet),热量400到700卡路里的菜划为“普通”(normal),高于700卡路里的划为“高热量”(fat)。由于Dish类的作者没有把这个操作写成一个方法,你无法使用方法引用,但你可以把这个逻辑写成Lambda表达式。

public enum CaloricLevel { DIET, NORMAL, FAT } 
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect( 
 groupingBy(dish -> { 
 if (dish.getCalories() <= 400) return CaloricLevel.DIET; 
 else if (dish.getCalories() <= 700) return 
 CaloricLevel.NORMAL; 
 else return CaloricLevel.FAT; 
 } ));

目前就只根据一个标准进行分类,如果要根据多个标准进行分类,就需要使用到多级分组

多级分组

可以使用一个由双参数版本的Collectors.groupingBy工厂方法创建的收集器,它除了普通的分类函数之外,还可以接受collector类型的第二个参数。要进行二级分组的话,我们可以把一个内层groupingBy传递给外层groupingBy,并定义一个为流中项目分类的二级标准。

Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel = 
menu.stream().collect( 
 groupingBy(Dish::getType, 
 groupingBy(dish -> { 
 if (dish.getCalories() <= 400) return CaloricLevel.DIET; 
 else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL; 
 else return CaloricLevel.FAT; 
 } ) 
 ) 
); 

这种多级分组操作可以扩展至任意层级,n级分组就会得到一个代表n级树形结构的n级Map

按子组收集数据

把第二个groupingBy收集器传递给外层收集器来实现多级分组。但进一步说,传递给第一个groupingBy的第二个收集器可以是任何类型,而不一定是另一个groupingBy。例如,要数一数菜单中每类菜有多少个,可以传递counting收集器作为groupingBy收集器的第二个参数:

Map<Dish.Type, Long> typesCount = menu.stream().collect( 
 groupingBy(Dish::getType, counting()));

还要注意,普通的单参数groupingBy(f)(其中f是分类函数)实际上是groupingBy(f, toList())的简便写法

public static <T, K> Collector<T, ?, Map<K, List<T>>>
    groupingBy(Function<? super T, ? extends K> classifier) {
        return groupingBy(classifier, toList());
    }
    
    public static <T, K, A, D>
    Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
                                          Collector<? super T, A, D> downstream) {
        return groupingBy(classifier, HashMap::new, downstream);
    }
    
    public static <T, K, D, A, M extends Map<K, D>>
    Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
                                  Supplier<M> mapFactory,
                                  Collector<? super T, A, D> downstream) {
        Supplier<A> downstreamSupplier = downstream.supplier();
        BiConsumer<A, ? super T> downstreamAccumulator = downstream.accumulator();
        BiConsumer<Map<K, A>, T> accumulator = (m, t) -> {
            K key = Objects.requireNonNull(classifier.apply(t), "element cannot be mapped to a null key");
            A container = m.computeIfAbsent(key, k -> downstreamSupplier.get());
            downstreamAccumulator.accept(container, t);
        };
        BinaryOperator<Map<K, A>> merger = Collectors.<K, A, Map<K, A>>mapMerger(downstream.combiner());
        @SuppressWarnings("unchecked")
        Supplier<Map<K, A>> mangledFactory = (Supplier<Map<K, A>>) mapFactory;

        if (downstream.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) {
            return new CollectorImpl<>(mangledFactory, accumulator, merger, CH_ID);
        }
        else {
            @SuppressWarnings("unchecked")
            Function<A, A> downstreamFinisher = (Function<A, A>) downstream.finisher();
            Function<Map<K, A>, M> finisher = intermediate -> {
                intermediate.replaceAll((k, v) -> downstreamFinisher.apply(v));
                @SuppressWarnings("unchecked")
                M castResult = (M) intermediate;
                return castResult;
            };
            return new CollectorImpl<>(mangledFactory, accumulator, merger, finisher, CH_NOID);
        }
    }
  1. 把收集器的结果转换为另一种类型

    把收集器返回的结果转换为另一种类型,可以使用Collectors.collectingAndThen工厂方法返回的收集器

    Map<Dish.Type, Dish> mostCaloricByType = 
     menu.stream() 
     .collect(groupingBy(Dish::getType,
     collectingAndThen( 
     maxBy(comparingInt(Dish::getCalories)), 
     Optional::get))); 
    

    这个工厂方法接受两个参数——要转换的收集器以及转换函数,并返回另一个收集器这个收集器相当于旧收集器的一个包装,collect操作的最后一步就是将返回值用转换函数做一个映射。

    在这里,被包起来的收集器就是用maxBy建立的那个,而转换函数Optional::get则把返回的Optional中的值提取出来。这个操作放在这里是安全的,因为reducing收集器永远都不会返回Optional.empty()。实际上,如果菜单中没有某一类型的Dish,这个类型就不会对应一个Optional. empty()值,而且根本不会出现在Map的键中。groupingBy收集器只有在应用分组条件后,第一次在流中找到某个键对应的元素时才会把键加入分组Map中。这意味着Optional包装器在这里不是很有用,因为它不会仅仅因为它是归约收集器的返回类型而表达一个最终可能不存在却意外存在的值。

  2. 与groupingBy联合使用的其他收集器的例子

一般来说,通过groupingBy工厂方法的第二个参数传递的收集器将会对分到同一组中的所有流元素执行进一步归约操作。例如,你还重用求出所有菜肴热量总和的收集器,不过这次是对每一组Dish求和:

Map<Dish.Type, Integer> totalCaloriesByType = 
 menu.stream().collect(groupingBy(Dish::getType,
 summingInt(Dish::getCalories)));

然而常常和groupingBy联合使用的另一个收集器是mapping方法生成的。这个方法接受两个参数:一个函数对流中的元素做变换,另一个则将变换的结果对象收集起来。其目的是在累加之前对每个输入元素应用一个映射函数,这样就可以让接受特定类型元素的收集器适应不同类型的对象。

//每种类型的Dish,菜单中都有哪些CaloricLevel
Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType = 
menu.stream().collect( 
 groupingBy(Dish::getType, mapping( 
 dish -> { if (dish.getCalories() <= 400) return CaloricLevel.DIET; 
 else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL; 
 else return CaloricLevel.FAT; }, 
 toSet() ))); 

对于返回的Set是什么类型并没有任何保证。但通过使用toCollection,你就可以有更多的控制。例如,你可以给它传递一个构造函数引用来要求HashSet

Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType = 
menu.stream().collect( 
 groupingBy(Dish::getType, mapping( 
 dish -> { if (dish.getCalories() <= 400) return CaloricLevel.DIET; 
 else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL; 
 else return CaloricLevel.FAT; }, 
 toCollection(HashSet::new) )));
分区

分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数。分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它最多可以分为两组——true是一组,false是一组。

例如,如果你是素食者或是请了一位素食的朋友来共进晚餐,可能会想要把菜单按照素食和非素食分开:

Map<Boolean, List<Dish>> partitionedMenu = 
 menu.stream().collect(partitioningBy(Dish::isVegetarian)); 
分区的优势

分区的好处在于保留了分区函数返回true或false的两套流元素列表。

要得到非素食Dish的List,你可以使用两个筛选操作来访问partitionedMenu这个Map中false键的值:一个利用谓词,一个利用该谓词的非。而且就像你在分组中看到的,partitioningBy工厂方法有一个重载版本,可以像下面这样传递第二个收集器:

//对于分区产生的素食和非素食子流,分别按类型对菜肴分组
Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType = 
menu.stream().collect( 
 partitioningBy(Dish::isVegetarian, 
 groupingBy(Dish::getType)));
Collectors类的静态工厂方法
工厂方法返回类型用 于使用示例
toListList把流中所有项目收集到一个 ListList dishes = menuStream.collect(toList());
toSetSet把流中所有项目收集到一个 Set,删除重复项Set dishes = menuStream.collect(toSet());
toCollectionCollection把流中所有项目收集到给定的供应源创建的集合Collection dishes = menuStream.collect(toCollection(),ArrayList::new);
countingLong计算流中元素的个数long howManyDishes = menuStream.collect(counting());
summingIntInteger对流中项目的一个整数属性求和int totalCalories = menuStream.collect(summingInt(Dish::getCalories));
averagingIntDouble计算流中项目 Integer 属性的平均值double avgCalories = menuStream.collect(averagingInt(Dish::getCalories));
summarizingIntIntSummaryStatistics收集关于流中项目 Integer 属性的统计值,例如最大、最小、总和与平均值IntSummaryStatistics menuStatistics = menuStream.collect(summarizingInt(Dish::getCalories));
joiningString连接对流中每个项目调用 toString 方法所生成的字符串String shortMenu = menuStream.map(Dish::getName).collect(joining(", "));
maxByOptional一个包裹了流中按照给定比较器选出的最大元素的 Optional,或如果流为空则为 Optional.empty()Optional fattest = menuStream.collect(maxBy(comparingInt(Dish::getCalories)));
minByOptional一个包裹了流中按照给定比较器选出的最小元素的 Optional,或如果流为空则为 Optional.empty()Optional lightest = menuStream.collect(minBy(comparingInt(Dish::getCalories)));
reducing归约操作产生的类型从一个作为累加器的初始值开始,利用 BinaryOperator 与流中的元素逐个结合,从而将流归约为单个值int totalCalories = menuStream.collect(reducing(0, Dish::getCalories, Integer::sum));
collectingAndThen转换函数返回的类型包裹另一个收集器,对其结果应用转换函数int howManyDishes = menuStream.collect(collectingAndThen(toList(), List::size));
groupingByMap<K, List>根据项目的一个属性的值对流中的项目作问组,并将属性值作为结果 Map 的键Map<Dish.Type,List> dishesByType = menuStream.collect(groupingBy(Dish::getType));
partitioningByMap<Boolean,List>根据对流中每个项目应用谓词的结果来对项目进行分区Map<Boolean,List> vegetarianDishes = menuStream.collect(partitioningBy(Dish::isVegetarian));
收集器接口

Collector接口包含了一系列方法,为实现具体的归约操作(即收集器)提供了范本

public interface Collector<T, A, R> { 
 Supplier<A> supplier(); 
 BiConsumer<A, T> accumulator(); 
 Function<A, R> finisher(); 
 BinaryOperator<A> combiner(); 
 Set<Characteristics> characteristics(); 
} 
  • T是流中要收集的项目的泛型。
  • A是累加器的类型,累加器是在收集过程中用于累积部分结果的对象。
  • R是收集操作得到的对象(通常但并不一定是集合)的类型。

你可以实现一个ToListCollector类,将Stream中的所有元素收集到一个List里

public class ToListCollector<T> implements Collector<T, List<T>, List<T>> 
Collector接口声明的方法

上面提到的前四个方法都会返回一个会被collect方法调用的函数,而第五个方法characteristics则提供了一系列特征,也就是一个提示列表,告诉collect方法在执行归约操作的时候可以应用哪些优化(比如并行化)。

  1. 建立新的结果容器:supplier方法

    supplier方法必须返回一个结果为空的Supplier,也就是一个无参数函数,在调用时它会创建一个空的累加器实例,供数据收集过程使用。例如:

    public Supplier<List<T>> supplier() { 
     return ArrayList::new; 
    } 
    
  2. 将元素添加到结果容器:accumulator方法

accumulator方法会返回执行归约操作的函数。当遍历到流中第n个元素时,这个函数执行时会有两个参数:保存归约结果的累加器(已收集了流中的前 n-1 个项目),还有第n个元素本身。该函数将返回void,因为累加器是原位更新,即函数的执行改变了它的内部状态以体现遍历的元素的效果。例如:

public BiConsumer<List<T>, T> accumulator() { 
 return List::add; 
} 
  1. 对结果容器应用最终转换:finisher方法

    在遍历完流后,finisher方法必须返回在累积过程的最后要调用的一个函数,以便将累加器对象转换为整个集合操作的最终结果。例如:

    public Function<List<T>, List<T>> finisher() { 
     return Function.identity(); 
    } 
    

这三个方法已经足以对流进行顺序归约,实践中的实现细节可能还要复杂一点,一方面是因为流的延迟性质,可能在collect操作之前还需要完成其他中间操作的流水线,另一方面则是理论上可能要进行并行归约。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X7rqL891-1631271113984)(C:\Users\80311062\Desktop\study\java8实战\image\顺序规约.jpg)]

顺序规约过程逻辑步骤
  1. 合并两个结果容器:combiner方法

    四个方法中的最后一个——combiner方法会返回一个供归约操作使用的函数,它定义了对流的各个子部分进行并行处理时,各个子部分归约所得的累加器要如何合并。例如:

    public BinaryOperator<List<T>> combiner() { 
     return (list1, list2) -> { 
     list1.addAll(list2); 
     return list1; } 
    } 
    

    有了这第四个方法,就可以对流进行并行归约了。它会用到Java 7中引入的分支/合并框架和Spliterator抽象。其并行化规约过程如下:

    使用combiner方法来并行规约过程

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hg8nLF61-1631271113986)(C:\Users\80311062\Desktop\study\java8实战\image\combiner合并工作流程.jpg)]

    具体步骤如下:

    1. 原始流会以递归方式拆分为子流,直到定义流是否需要进一步拆分的一个条件为非(如果分布式工作单位太小,并行计算往往比顺序 计算要慢,而且要是生成的并行任务比处理器内核数多很多的话就没有意义)。
    2. 现在,所有的子流都可以并行处理,对每个子流进行顺序规约算法
    3. 最后,使用收集器combiner方法返回的函数,将所有部分结果两两合并。这时会把原始流每次拆分时得到的子流对应的结果合并起来。
  2. characteristics方法

    最后一个方法——characteristics会返回一个不可变的Characteristics集合,它定义了收集器的行为——尤其是关于流是否可以并行归约,以及可以使用哪些优化的提示。Characteristics是一个包含三个项目的枚举。

    1. UNORDERED——归约结果不受流中项目的遍历和累积顺序的影响(unordered)
    2. CONCURRENT——accumulator函数可以从多个线程同时调用,且该收集器可以并行归约流。如果收集器没有标为UNORDERED,那它仅在用于无序数据源时才可以并行归约。(concurrent)
    3. IDENTITY_FINISH——这表明完成器方法返回的函数是一个恒等函数,可以跳过。这种情况下,累加器对象将会直接用作归约过程的最终结果。这也意味着,将累加器A不加检查地转换为结果R是安全的。
public class MyToListCollector<T> implements Collector<T, List<T>, List<T>> {
  
    @Override
    public Supplier<List<T>> supplier() {
        return ArrayList::new;
    }

    @Override
    public BiConsumer<List<T>, T> accumulator() {
        return List::add;
    }

    @Override
    public BinaryOperator<List<T>> combiner() {
        return (list1, list2) -> {
            list1.addAll(list2);
            return list1;
        };
    }

    @Override
    public Function<List<T>, List<T>> finisher() {
        return Function.identity();
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH, CONCURRENT));
    }
}
public class TestMyToListCollector {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        List<Integer> collect = list.stream().collect(new MyToListCollector<Integer>());
        System.out.println(collect);
        //unordered
    }
}

这个实现和标准的List dishes = menuStream.collect(toList()); 构造之间的其他差异在于toList是一个工厂,而ToListCollector必须用new来实例化。

进行自定义收集而不去实现Collector

对于IDENTITY_FINISH的收集操作,还有一种方法可以得到同样的结果而无需从头实现新的Collectors接口。Stream有一个重载的collect方法可以接受另外三个函数——supplier、accumulator和combiner,其语义和Collector接口的相应方法返回的函数完全相同。

List<Integer> collect = list.stream().collect( 
 ArrayList::new,
 List::add,
 List::addAll);

第二种形式虽然比前一个写法更为紧凑和简洁,却不那么易读。此外,以恰当的类来实现自己的自定义收集器有助于重用并可避免代码重复。另外值得注意的是,这第二个collect方法不能传递任何Characteristics,所以它永远都是一个IDENTITY_FINISH和CONCURRENT但并非UNORDERED的收集器。

开发你自己的收集器以获得更好的性能

案例:将前n个自然数按质数和非质数分区

1.通过IntStream.rangeClosed(2, (int) Math.sqrt((double) candidate))来生成小于被除数平方根的整数,但有一个问题,因为我们只判断是否为质数,所以除数不是质数就不需要考虑,但生成流中并没有这个方法;如果使用filter需要处理整个流才能返回结果,如果流很大就存在问题,所以我们可以自定义收集器来生成这些质数

public class PrimeNumbersCollector 
 implements Collector<Integer, 
 Map<Boolean, List<Integer>>, 
 Map<Boolean, List<Integer>>> { 
 @Override 
 public Supplier<Map<Boolean, List<Integer>>> supplier() { 
 return () -> new HashMap<Boolean, List<Integer>>() {{
 put(true, new ArrayList<Integer>()); 
 put(false, new ArrayList<Integer>()); 
 }}; 
 } 
 @Override 
 public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() { 
 return (Map<Boolean, List<Integer>> acc, Integer candidate) -> { 
 acc.get( isPrime( acc.get(true), 
 candidate) ) 
 .add(candidate);
 }; 
 }
/*
*实际上这个收集器是不能并行使用的,因为该算法本身是顺序的。这意味着永远都
*不会调用combiner方法,你可以把它的实现留空(更好的做法是抛出一个UnsupportedOperationException异常)。
*/
 @Override 
 public BinaryOperator<Map<Boolean, List<Integer>>> combiner() { 
 return (Map<Boolean, List<Integer>> map1, 
 Map<Boolean, List<Integer>> map2) -> {
 map1.get(true).addAll(map2.get(true)); 
 map1.get(false).addAll(map2.get(false)); 
 return map1; 
 }; 
 } 
 @Override 
 public Function<Map<Boolean, List<Integer>>, 
 Map<Boolean, List<Integer>>> finisher() { 
 return Function.identity(); 
 } 
 @Override 
 public Set<Characteristics> characteristics() { 
 return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH));
 } 
}
并行
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用python中的pymsql完成如下:表结构与数据创建 1. 建立 `users` 表和 `orders` 表。 `users` 表有用户ID、用户名、年龄字段,(id,name,age) `orders` 表有订单ID、订单日期、订单金额,用户id字段。(id,order_date,amount,user_id) 2 两表的id作为主键,`orders` 表用户id为users的外键 3 插入数据 `users` (1, '张三', 18), (2, '李四', 20), (3, '王五', 22), (4, '赵六', 25), (5, '钱七', 28); `orders` (1, '2021-09-01', 500, 1), (2, '2021-09-02', 1000, 2), (3, '2021-09-03', 600, 3), (4, '2021-09-04', 800, 4), (5, '2021-09-05', 1500, 5), (6, '2021-09-06', 1200, 3), (7, '2021-09-07', 2000, 1), (8, '2021-09-08', 300, 2), (9, '2021-09-09', 700, 5), (10, '2021-09-10', 900, 4); 查询语句 1. 查询订单总金额 2. 查询所有用户的平均年龄,并将结果四舍五入保留两位小数。 3. 查询订单总数最多的用户的姓名和订单总数。 4. 查询所有不重复的年龄。 5. 查询订单日期在2021年9月1日至9月4日之间的订单总金额。 6. 查询年龄不大于25岁的用户的订单数量,并按照降序排序。 7. 查询订单总金额排名前3的用户的姓名和订单总金额。 8. 查询订单总金额最大的用户的姓名和订单总金额。 9. 查询订单总金额最小的用户的姓名和订单总金额。 10. 查询所有名字中含有“李”的用户,按照名字升序排序。 11. 查询所有年龄大于20岁的用户,按照年龄降序排序,并只显示前5条记录。 12. 查询每个用户的订单数量和订单总金额,并按照总金额降序排序。
最新发布
06-03
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值