java8新特性

Lambda类型检查,类型推断以及限制

类型检查

从上下文推断出来,上下文(接受它传递的方法的参数或接受它的值得局部变量)中lambda表达式需要的类型称为目标类型。
​​​​​​​​在这里插入图片描述
类型检查过程可以分解为如下所示。

 首先,你要找出filter方法的声明。

 第二,要求它是Predicate<Apple>(目标类型)对象的第二个正式参数。

 第三,Predicate<Apple>是一个函数式接口,定义了一个叫作test的抽象方法。

 第四,test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean。

 最后,filter的任何实际参数都必须匹配这个要求。

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

  1. 赋值操作获取
    Callable c=()-> 42;
    PrivilegedAction p=()-> 42;
    这里,第一个赋值的目标类型是Callable,第二个赋值的目标类型是PrivilegedAction。
  2. 方法调用获取
 	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 listOfStrings=new ArrayList<>();
List listOfIntegers=new ArrayList<>();
特殊的void兼容规则如果一个Lambda的主体是一个语句表达式,它就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)。例如,以下两行都是合法的,尽管List的add方法返回了一个boolean,而不是Consumer上下文(T-> void)所要求的void:
Predicate p=s-> list.add(s); // Predicate返回了一个boolean
Consumer b=s-> list.add(s); // Consumer返回了一个void

类型推断

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

Lambda表达式有多个参数,代码可读性的好处就更为明显。例如,你可以这样来创建一个Comparator对象:
在这里插入图片描述

使用局部变量

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

    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=3698;

对局部变量的限制

  1. 第一,实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。
    如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制。
  2. 第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(我们会在以后的各章中解释,这种模式会阻碍很容易做到的并行处理)。

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

方法引用

可以把它们视为某些Lambda的快捷写法。

      先前
      inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
      方法引用后:
      inventory.sor(comparing(Apple::getWeight));

方法引用可以被看作Lambda的一种快捷写法。

它是如何工作的呢?

Lambda及其等效方法引用的例子
在这里插入图片描述
如何构建方法引用
方法引用主要有三类

  1. 指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt)。
  2. 指向任意类型实例方法的方法引用(例如String的length方法,写作String::length)。
  3. 指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction用于存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensive-Transaction::getValue)。

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

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

为三种不同类型的Lambda表达式构建方法引用的办法
方法引用的签名必须和上下文类型匹配。

构造函数引用
可以利用它的名称和关键字new来创建它的一个引用:ClassName::new。
无参构造函数
在这里插入图片描述
有参构造函数
在这里插入图片描述

复合Lambda表达式的有用方法

就是函数式接口默认方法的使用
比较器复合

@FunctionalInterface
public interface Comparator
以下方法都是使用的Comparator抽象函数的默认方法
1.逆序
default Comparator reversed()
2.比较器链:就是多个比较条件
default Comparator thenComparing(Comparator<? super T> other)

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

@FunctionalInterface
public interface Predicate
以下方法都是使用的Predicate抽象函数的默认方法
default Predicate and(Predicate<? super T> other)
default Predicate negate()
default Predicate or(Predicate<? super T> other)

and和or从左向右确定优先级的

函数复合

@FunctionalInterface
public interface Function<T,R>

andThen方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。
比如,假设有一个函数f给数字加1 (x-> x+1),另一个函数g给数字乘2,你可以将它们组合成一个函数h,先给数字加1,再给结果乘2:
default Function<T,V> andThen(Function<? super R,? extends V> after)
在这里插入图片描述
compose方法,先把给定的函数用作compose的参数里面给的那个函数,然后再把函数本身用于结果。
default Function<V,R> compose(Function<? super V,? extends T> before)在这里插入图片描述
andThen从左往右复合,compose从右往左复合

第4章 引入流

流是什么

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

    List<String> lowCaloricDishesName=  menu.parallelStream()
                        .filter(d-> d.getCalories() < 400)
                        .sorted(comparing(Dishes::getCalories))
                        .map(Dish::getName)
                        .collect(toList());

新的方法有几个显而易见的好处。

  1. 代码是以声明性方式写的:说明想要完成什么(筛选热量低的菜肴)而不是说明如何实现一个操作(利用循环和if条件等控制流语句)。
  2. 你可以把几个基础操作链接起来,来表达复杂的数据处理流水线(在filter后面接上sorted、map和collect操作,如以上代码所示),同时保持代码清晰可读。filter的结果被传给了sorted方法,再传给map方法,最后传给collect方法。

其他库:Guava、Apache和lambdaj
为了给Java程序员提供更好的库操作集合,前人已经做过了很多尝试。比如,Guava就是谷歌创建的一个很流行的库。
它提供了multimaps和multisets等额外的容器类。
Apache Commons Collections库也提供了类似的功能。
lambdaj受到函数式编程的启发,也提供了很多声明性操作集合的工具。
如今Java 8自带了官方库,可以以更加声明性的方式操作集合了。

Stream API可以让你写出这样的代码:
❑ 声明性——更简洁,更易读
❑ 可复合——更灵活
❑ 可并行——性能更好

流简介

简短的定义就是“从支持数据处理操作的源生成的元素序列”。

  1. ❑ 元素序列——就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访问元素(如ArrayList与LinkedList)。但流的目的在于表达计算,比如你前面见到的filter、sorted和map。集合讲的是数据,流讲的是计算。我们会在后面几节中详细解释这个思想。
  2. ❑ 源——流会使用一个提供数据的源,如集合、数组或输入/输出资源。请注意,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
  3. ❑ 数据处理操作——流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可并行执行。

此外,流操作有两个重要的特点。

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

  • ❑ 内部迭代——与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。
    在这里插入图片描述
    上图是流操作的顺序:filter、map、limit、collect,每个操作简介如下:

  • ❑ filter——接受Lambda,从流中排除某些元素。

  • ❑ map——接受一个Lambda,将元素转换成其他形式或提取信息。

  • ❑ limit——截断流,使其元素不超过给定数量。

  • ❑ collect——将流转换为其他形式。在本例中,流被转换为一个列表。

  • toList()就是将流转换为列表的方案。

流与集合

集合: 是一个内存中的数据结构,它包含数据结构中目前所有的值——集合中的每个元素都得先算出来才能添加到集合中。
流: 则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计算的。是一种生产者-消费者的关系。从另一个角度来说,流就像是一个延迟创建的集合:只有在消费者要求的时候才会计算值

只能遍历一次

和迭代器类似,流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了。你可以从原始数据源那里再获得一个新的流来重新遍历一遍,就像迭代器一样(这里假设它是集合之类的可重复的源,如果是I/O通道就没戏了)。

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

外部迭代与内部迭代

Streams库的内部迭代可以自动选择一种适合你硬件的数据表示和并行实现。
内部迭代与外部迭代

流操作

java.util.stream.Stream中的Stream接口定义了许多操作。它们可以分为两大类。
两类操作:

  • ❑ filter、map和limit可以连成一条流水线;
  • ❑ collect触发流水线执行并关闭它。
    可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作。
    中间操作与终端操作

中间操作

在这里插入图片描述

	上图输出结果:
    filtering pork
    mapping pork
    filtering beef
    mapping beef
    filtering chicken
    mapping chicken
    [pork, beef, chicken]

终端操作

终端操作会从流的流水线生成结果。其结果是任何不是流的值,比如List、Integer,甚至void。

使用流

总而言之,流的使用一般包括三件事:

  1. ❑ 一个数据源(如集合)来执行一个查询;
  2. ❑ 一个中间操作链,形成一条流的流水线;
  3. ❑ 一个终端操作,执行流水线,并能生成结果。
    流的流水线背后的理念类似于构建器模式。在构建器模式中有一个调用链用来设置一套配置(对流来说这就是一个中间操作链),接着是调用built方法(对流来说就是终端操作)。

第5章 使用流

接口
public interface Stream extends BaseStream<T,Stream>

筛选和切片

  • Stream filter(Predicate<? super T> predicate)
    该操作会接受一个谓词(一个返回boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流。简单说就是实现条件查询效果;
    在这里插入图片描述
    执行示意图在这里插入图片描述
  • Stream distinct()
    返回一个元素各异(根据流所生成元素的hashCode和equals方法实现)的流。人话就是实现去重效果
    示例:
List<Integer> numbers=Arrays.asList(1, 2, 1, 3, 3, 2, 4);
    numbers.stream()
            .filter(i-> i % 2==0)
            .distinct()
            .forEach(System.out::println);

在这里插入图片描述
截短流
Stream limit(long maxSize)
流支持limit(n)方法,该方法会返回一个不超过给定长度的流。从流元素左边开始往右截取n个元素
示例:

 List<Dish> dishes=menu.stream()
                              .filter(d-> d.getCalories() > 300)
                              .limit(3)
                              .collect(toList());

在这里插入图片描述
跳过元素
Stream skip(long n)
流还支持skip(n)方法,返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一个空流。从流元素右边开始往左截取n个元素

  List<Dish> dishes=menu.stream()
                              .filter(d-> d.getCalories() > 300)
                              .skip(2)
                              .collect(toList());

在这里插入图片描述

5.2 映射

一个非常常见的数据处理套路就是从某些对象中选择信息。比如在SQL里,你可以从表中选择一列。Stream API也通过map和flatMap方法提供了类似的工具。

5.2.1 对流中每一个元素应用函数

Stream map(Function<? super T,? extends R> mapper)
流支持map方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素;根据传入的自定义函数,生成一个新流;

示例:

 List<String> dishNames=menu.stream()
                                  .map(Dish::getName)
                                  .collect(toList());

5.2.2 流的扁平化
Arrays.stream() 的方法可以接受一个数组并产生一个流

String[] arrayOfWords={"Goodbye", "World"};
    Stream<String> streamOfwords=Arrays.stream(arrayOfWords);

2.使用flatMap
使用flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map(Arrays::stream)时生成的单个流都被合并起来,即扁平化为一个流
在这里插入图片描述
使用flatMap找出单词列表中各不相同的字符

5.3 查找和匹配

另一个常见的数据处理套路是看看数据集中的某些元素是否匹配一个给定的属性。Stream API通过allMatch、anyMatch、noneMatch、findFirst和findAny方法提供了这样的工具。

  • boolean anyMatch(Predicate<? super T> predicate)
if(menu.stream().anyMatch(Dish::isVegetarian)){
        System.out.println("The menu is (somewhat) vegetarian friendly! ! ");
    }

anyMatch的效果是:流中是否有一个元素能匹配给定的谓词
anyMatch方法返回一个boolean,是一个终端操作。

  • 检查是否匹配所有元素

allMatch方法的工作原理和anyMatch类似,但它会看看流中的元素是否都能匹配给定的谓词。

  boolean isHealthy=menu.stream()
                              .allMatch(d-> d.getCalories() < 1000);
  • noneMatch

和allMatch相对的是noneMatch。它可以确保流中没有任何元素与给定的谓词匹配。

  boolean isHealthy=menu.stream()
                              .noneMatch(d-> d.getCalories() >=1000);

anyMatch、allMatch和noneMatch、findAny这三个操作都用到了我们所谓的短路

短路求值
有些操作不需要处理整个流就能得到结果。例如,假设你需要对一个用and连起来的大布尔表达式求值。不管表达式有多长,你只需找到一个表达式为false,就可以推断整个表达式将返回false,所以用不着计算整个表达式。这就是短路。
对于流而言,某些操作(例如allMatch、anyMatch、noneMatch、findFirst和findAny)不用处理整个流就能得到结果。只要找到一个元素,就可以有结果了。
同样,limit也是一个短路操作:它只需要创建一个给定大小的流,而用不着处理流中所有的元素。
在碰到无限大小的流的时候,这种操作就有用了:它们可以把无限流变成有限流。

  • 查找元素
    Optional findAny()
    findAny方法将返回当前流中的任意元素。它可以与其他流操作结合使用。
Optional<Dish> dish=
        menu.stream()
            .filter(Dish::isVegetarian)
            .findAny();
  • 查找第一个元素
    Optional findFirst()
    找到流中第一个元素
 List<Integer> someNumbers=Arrays.asList(1, 2, 3, 4, 5);
      Optional<Integer> firstSquareDivisibleByThree=
          someNumbers.stream()
                      .map(x-> x * x)
                      .filter(x-> x % 3==0)
                      .findFirst(); // 9

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

Optional简介
Optional类(java.util.Optional)是一个容器类,代表一个值存在或不存在。
引入Optional,这样就不用返回容易出问题的null了。
❑ isPresent()将在Optional包含值的时候返回true,否则返回false。
❑ ifPresent(Consumer block)会在值存在的时候执行给定的代码块。我们在第3章介绍了Consumer函数式接口;它让你传递一个接收T类型参数,并返回void的Lambda表达式。
❑ T get()会在值存在时返回值,否则抛出一个NoSuchElement异常。
❑ T orElse(T other)会在值存在时返回值,否则返回一个默认值。
在这里插入图片描述

5.4 归约

如何把一个流中的元素组合起来,使用reduce操作来表达更复杂的查询,比如“计算菜单中的总卡路里”或“菜单中卡路里最高的菜是哪一个”。此类查询需要将流中所有元素反复结合起来,得到一个值,比如一个Integer。这样的查询可以被归类为归约操作(将流归约成一个值),是一种流的终端操作;

  • 元素求和
    reduce,它对这种重复应用的模式做了抽象。

以下是使用reduce对流元素进行求和操作;

 int sum=numbers.stream().reduce(0, (a, b)-> a+b);

reduce接受两个参数:

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

下图展示了reduce操作是如何作用于一个流的:Lambda反复结合每个元素,直到流被归约成一个值。
使用reduce来对流中的数字求和
让我们深入研究一下reduce操作是如何对一个数字流求和的。首先,0作为Lambda(a)的第一个参数,从流中获得4作为第二个参数(b)。0+4得到4,它成了新的累积值。然后再用累积值和流中下一个元素5调用Lambda,产生新的累积值9。接下来,再用累积值和下一个元素3调用Lambda,得到12。最后,用12和流中最后一个元素9调用Lambda,得到最终结果21。

同样,可以通过方法引用使用Integer中的静态方法sum来实现该操作;
int sum=numbers.stream().reduce(0, Integer::sum);

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

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

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

  • 最大值和最小值
最大值:
	Optional<Integer> max=numbers.stream().reduce(Integer::max);
最小值:
	Optional<Integer> min=numbers.stream().reduce(Integer::min);
使用Lambda方式:
只需把Integer::min 换成 (x, y)-> x < y ? x : y

归约方法的优势与并行化
迭代被内部迭代抽象掉了,这让内部实现得以选择并行执行reduce操作。
传递给reduce的Lambda不能更改状态(如实例变量),而且操作必须满足结合律才可以按任意顺序执行。

流操作:无状态和有状态
诸如map或filter等操作会从输入流中获取每一个元素,并在输出流中得到0或1个结果。这些操作一般都是无状态的:它们没有内部状态(假设用户提供的Lambda或方法引用没有内部可变状态)。
但诸如reduce、sum、max等操作需要内部状态来累积结果。在上面的情况下,内部状态很小。在我们的例子里就是一个int或double。不管流中有多少元素要处理,内部状态都是有界的。
相反,诸如sort或distinct等操作一开始都和filter和map差不多——都是接受一个流,再生成一个流(中间操作),但有一个关键的区别。从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题(把质数流倒序会做什么呢?它应当返回最大的质数,但数学告诉我们它不存在)。我们把这些操作叫作有状态操作。

中间操作和终端操作

5.6 数值流

原始类型流特化

Stream API提供了基本数据类型流特化,专门支持处理数值流的方法。
Java 8引入了三个原始类型特化流接口来解决这个问题:IntStream、DoubleStream和LongStream,分别将流中的元素特化为int、long和double,从而避免了暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到最大元素的max。此外还有在必要时再把它们转换回对象流的方法。要记住的是,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性——即类似int和Integer之间的效率差异。

  1. 映射到数值流

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

  1. 转换回对象流

IntStream的map操作接受的Lambda必须接受int并返回int(一个IntUnaryOperator)。但是你可能想要生成另一类值,比如Dish。为此,你需要访问Stream接口中定义的那些更广义的操作。要把原始流转换成一般流(每个int都会装箱成一个Integer),可以使用boxed方法
在这里插入图片描述
在需要将数值范围装箱成为一个一般流时,boxed尤其有用。

  1. 默认值OptionalInt
    如何区分没有元素的流和最大值真的是0的流呢?
    Optional可以用Integer、String等参考类型来参数化。对于三种原始流特化,也分别有一个Optional原始类型特化版本:OptionalInt、OptionalDouble和OptionalLong。

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

OptionalInt maxCalories=menu.stream()
                                      .mapToInt(Dish::getCalories)
                                      .max();

如果没有最大值的话,你就可以显式处理OptionalInt去定义一个默认值了:

maxCalories.orElse(1);

数值范围

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

在这里插入图片描述

数值流应用:勾股数

第6章 用流收集数据

6.1 收集器简介

函数式编程相对于指令式编程的一个主要优势:只需指出希望的结果——“做什么”,而不用操心执行的步骤——“如何做”。

  • 6.1.1 收集器用作高级归约
    java.util.stream.Collectors
    Collectors实用类提供了很多静态工厂方法,可以方便地创建常见收集器的实例,只要拿来用就可以了。最直接和最常用的收集器是toList静态方法,它会把流中所有的元素收集到一个List中:
 List<Transaction> transactions=
        transactionStream.collect(Collectors.toList());
  • 6.1.2 预定义收集器

从Collectors类提供的工厂方法(例如groupingBy)创建的收集器。它们主要提供了三大功能:
❑ 将流元素归约和汇总为一个值
❑ 元素分组
❑ 元素分区

分组的特殊情况“分区”,即使用谓词(返回一个布尔值的单参数函数)作为分组函数。

6.2 归约和汇总

  • 6.2.2 汇总

static Collector<T,?,Integer> summingInt(ToIntFunction<? super T> mapper)
Collectors.summingInt。它可接受一个把对象映射为求和所需int的函数,并返回一个收集器;该收集器在传递给普通的collect方法后即执行我们需要的汇总操作。

求菜总热量的例子:

汇总求和的方法:
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));

static <T> Collector<T,?,Long> summingLong(ToLongFunction<? super T> mapper)
static <T> Collector<T,?,Double> summingDouble(ToDoubleFunction<? super T> mapper)
计算数值的平均数:
double avgCalorie s= menu.stream().collect(averagingInt(Dish::getCalories));

static <T> Collector<T,?,Double> averagingLong(ToLongFunction<? super T> mapper)
static <T> Collector<T,?,Double> averagingDouble(ToDoubleFunction<? super T> mapper)

一次操作得到多个结果:
IntSummaryStatistics menuStatistics = menu.stream().collect(summarizingInt(Dish::getCalories));
收集所有信息存放到一个IntSummaryStatistics的类里,并通过getter方法取值
IntSummaryStatistics{count=9, sum=4300, min=120,average=477.777778, max=800}

summarizingLong
summarizingDouble

summingInt收集器的累积过程

  • 6.2.3 连接字符串
    joining 工厂方法,把流中所有对象通过toString()方法得到字符串连接成一个字符串,joining在内部使用了StringBuilder来把生成的字符串逐个追加起来。
Dish类不带toString()方法
  String shortMenu=menu.stream().map(Dish::getName).collect(joining());toString()方法
 String shortMenu=menu.stream().collect(joining());
两者都会输出下面的字符串:
  porkbeefchickenfrench friesriceseason fruitpizzaprawnssalmon
  
使用分隔符:
 String shortMenu=menu.stream().map(Dish::getName).collect(joining(", "));
 输出:
 pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon

以上这种简单形式的归约过程,其实都是Collectors.reducing工厂方法提供的更广义归约收集器的特殊情况。

  • 6.2.4 广义的归约汇总

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

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

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

单参数reducing工厂方法创建的收集器看作三参数方法的特殊情况:

Optional<Dish> mostCalorieDish=
        menu.stream().collect(reducing(
            (d1, d2)-> d1.getCalories() > d2.getCalories() ? d1 : d2));
它把流中的第一个项目作为起点,把恒等函数(即一个函数仅仅是返回其输入参数)作为一个转换函数。这也意味着,要是把单参数reducing收集器传递给空流的collect方法,收集器就没有起点;因此将返回一个Optional<Dish>对象。

收集与归约
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。
以错误的语义使用reduce方法还会造成一个实际问题:这个归约过程不能并行工作,因为由多个线程并发修改同一个数据结构可能会破坏List本身。在这种情况下,如果你想要线程安全,就需要每次分配一个新的List,而对象分配又会影响性能。
这就是collect方法特别适合表达可变容器上的归约的原因,更关键的是它适合并行操作。

1.收集框架的灵活性:以不同的方法执行同样的操作
使用方法引用Integer的静态方法sum简化lambda表达式:
在这里插入图片描述
归约原理:利用累积函数,把一个初始化为起始值的累加器,和把转换函数应用到流中每个元素上得到的结果不断迭代合并起来。
归约操作的工作原理
counting收集器也是类似地利用三参数reducing工厂方法实现的。它把流中的每个元素都转换成一个值为1的Long型对象,然后再把它们相加:

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

使用泛型?通配符
在刚刚提到的代码片段中,使用到了 ? 通配符,它用作counting工厂方法返回的收集器签名中的第二个泛型类型。在这里,它仅仅意味着收集器的累加器类型未知,换句话说,累加器本身可以是任何类型。这里原封不动地写出了Collectors类中原始定义的方法签名。

6.3 分组

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值