Java 8 中的流 API 和收集器完整指南
让我们举个例子,下面将制作一个 Person 实例列表。
List<Person> persons = new ArrayList<>();
现在,假设我们要计算此列表中年龄超过 20 岁的人的平均年龄。
我们将如何进行?
- 映射步骤-map
- 映射采用人员列表并返回整数列表。
- 两个列表的大小相同。
2.过滤步骤-filter
- 获取我们的年龄列表,这是一个整数列表,并返回一个整数列表。
- 如果我的筛选只是一个大于 20 的谓词指令,那么在返回列表中,所有年龄都大于 20。
3. 归约步骤-reduce
- 我们只会说它等效于 SQL 聚合。什么是 SQL 聚合?例如,求元素的总和、最大值、平均值等类似的东西。
- 这只是一个简单的函数,它将恢复我们示例中的所有整数。。
什么是流?
public interface Stream<T> extends BaseStream<T,Stream<T>> {
// ....
}
- 流是 Java 新引入的泛型接口。其类型为 T。这意味着我们可以有各种各样的流,如整数流、人员流、客户流、字符串流等。
- 它提供了一种在 JVM 内部高效处理数据的方法。它可以有效地处理大量数据。
- 它可以并行处理数据,以利用多个CPU的计算能力。
- 该过程在管道中执行,因而它将避免不必要的中间计算。
Java 8 中流的定义
为什么集合不能是流?
- 流是一个新概念,设计者不想改变集合 API 的工作方式。
什么是流?
- 可以在其上定义操作的对象。通常操作,您可以想到映射、筛选或归约等操作。
- 不保存任何数据的对象。
- 不允许流更改其处理的数据的对象。
- 能够在一次传递中处理数据的对象。
- 对象应该从算法的角度进行优化,并且能够并行处理数据。
- 只能使用一次
构建和使用流
我们如何构建流?
嗯,事实上,我们有很多模式来构建流。
让我们看看第一个,可能是最有用的一个。我们有一个流方法,它已被添加到集合接口中,因此调用person.stream()
将从个人列表实例中获得流对象。
List<Person> persons = new ArrayList<Person>
Stream<Person> streams = person.stream();
- forEach 每个流都定义了此方法
- 在流接口上定义的 forEach 方法,并向其传递使用者。
public interface Consumer<T> {
// ..
}
- 让我们看一下该消费者接口。它是一个函数接口,所以它只有一个抽象方法。函数接口可以通过 lambda 表达式实现。
streams.forEach(p -> System.out.println(p));
它也可以写成方法引用,System.out::println
streams.forEach(System.out::println);
- 事实上,消费者使用是有点复杂,以下是它的定义。
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after){
Objects.requireNonNull(after);
return (T t) -> accept(t); after.accept(t); };
}
}
- 它将使我们能够将消费者联系起来,以下代码将展现此功能。
List<String> result = new ArrayList<>();
List<Person> persons = ... ;
Consumer<String> c1 = result::add;
Consumer<String> c2 = System.out::println;
persons.stream().forEach(c1.andThen(c2));
//
- 因为 forEach() 不返回任何内容。
2. 过滤流
- 它采用在数据源上定义的流,并在谓词之后筛选出部分数据。
List<Person> persons = ... ;
Stream<Person> stream = persons.stream();
Stream<Person> filtered = stream.filter( person -> person.age() > 20 );
- 将谓词作为参数,检查该人的年龄是否大于 20。
Predicate<Person> p = person -> person.age() > 20 ;
- 这是一个常规的 lambda 表达式,person.age()>20。
- 让我们看一下该谓词接口。它有一个称为 test 的方法,该方法将对象作为参数并返回布尔值。
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other);
static <T> Predicate<T> isEqual(Object targetRef);
default Predicate<T> negate();
default Predicate<T>or(Predicate<? super T> other);
}
- 我们必须对这种编写方式要小心一点,因为编写布尔运算时通常操作的优先级在这里没有考虑在内。
Predicate<Integer> p1 = i -> i > 20;
Predicate<Integer> p2 = i -> i < 30;
Predicate<Integer> p3 = i -> i == 0;
Predicate<Integer> p = p1.and(p2).or(p3); // (p1 AND p2) OR p3
Predicate<Integer> p = p3.or(p1).and(p2); // (p3 OR p1) AND p2
- 警告:在这种情况下调用方法不处理优先级。
- 谓词接口中也有一个名为isEqual的静态方法。
Predicate<String> p = Predicate.isEqual("two");
- isEqual方法做什么?它通过比较作为参数传递的对象来创建新的谓词。
Predicate<String> p = Predicate.isEqual("two") ;
Stream<String> stream1 = Stream.of("one", "two", "three") ;
Stream<String> stream2 = stream1.filter(p) ;
- 让我们来看一下Stream.of(...)方法,它是一个静态方法,这也是在 Java 中创建流的另一种方式。它是一种在 Java 8 接口上新声明代码模式的方法,同时在接口中也允许编写静态方法。
- 每次由 filter 方法产生的流都是新实例,因此 stream1 和 stream2 对象是不同的对象。
消费者、谓词和筛选流的示例
import java.util.function.Predicate;
import java.util.stream.Stream;
/**
* @author Jack.Yang
*/
public class FirstPredicates {
public static void main(String[] args) {
Stream<String> stream = Stream.of("one", "two", "three", "four", "five");
Predicate<String> p1 = Predicate.isEqual("two");
Predicate<String> p2 = Predicate.isEqual("three");
List<String> list = new ArrayList<>();
Consumer<String> c1 = list::add;
Consumer<String> c2 = System.out::println;
stream
.peek(str->System.out.println("peek:"+str))// Intermediary Operation
.filter(p1.or(p2))
//.peek(list::add); // Intermediary Operation
.forEach(c1.andThen(c2)); // Terminal/Final Operation
System.out.println("Done!");
System.out.println("size = " + list.size());
}
}
流上的延迟操作
Predicate<String> p = Predicate.isEqual("two") ;
Stream<String> stream1 = Stream.of("one", "two", "three") ;
Stream<String> stream2 = stream1.filter(p) ;
在这个新的流Stream2中,有什么?
- 在流中没有任何对象。这是流的定义。流不能保存任何数据。
- 此代码不执行任何操作,即对给定流的操作声明,但在此调用中不处理任何数据。
- 对筛选器方法的调用称为延迟调用。这意味着,当我调用该方法时,实际上,它只是一个被考虑的声明,但没有处理任何数据。
- 返回另一个流的所有 Stream 方法都是懒惰的。
- 另一种说法是,对返回流的流的操作称为中间操作。
List<String> result = new ArrayList<>();
List<Person> persons = ... ;
persons.stream().peek(System.out::println).filter(person -> person.getAge() > 20).peek(result::add);
- 如果我们回到消费者身上,考虑另一种叫做peek的消费方法。
- peek方法类似于 forEach 方法。唯一的区别是 peek 方法返回流,而 forEach 方法不返回任何内容。由于 peek 方法返回一个流,我们可以安全地假设它是一个中间操作。
- 然后调用了过滤器方法。同样,它只是一个声明,最后一个调用是另一个也是声明的 peek 方法。
- 所以答案是这段代码不做任何事情。首先,它不打印任何内容。System.out::println
永远不会被调用,result列表也保持为空,因为 result::add 永远不会在该代码中被调用。
示例:中间和未端操作
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Stream;
/**
*
* @author Jack.Yang
*/
public class IntermediaryAndFinal {
public static void main(String[] args) {
Stream<String> stream = Stream.of("one", "two", "three", "four", "five");
Predicate<String> p1 = Predicate.isEqual("two");
Predicate<String> p2 = Predicate.isEqual("three");
List<String> list = new ArrayList<>();
stream.peek(System.out::println)// Intermediary Operation
.filter(p1.or(p2))// Intermediary Operation
//.peek(list::add) // Intermediary Operation
.forEach(list::add); // Terminal/Final Operation
System.out.println("Done!");
System.out.println("size = " + list.size());
}
}
map操作
List<Person> list = ... ;
Stream<Person> stream = list.stream();
Stream<String> names =stream.map(person -> person.getName());
- map 操作实现了我们在本文开头看到的 map/filter/reduce 算法的第一步。
- 映射操作返回一个 Stream,因此我们可以安全地假设它是一个中间操作。
public interface Function<T, R> {
R apply(T t);
}
- 映射器函数由函数接口建模。事实上,它只执行一种称为 Apply 的方法。此方法将一个对象作为参数,并返回另一个对象。
- 我们还有一组默认方法来链接和组合映射。
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before);
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after);
}
- 也就是说,我们有两个默认方法,compose 和 andThen。
- 这些是完整的签名。小心泛型!在设计此类方法时必须格外小心,例如,如果您希望通过 person 类的扩展来允许调用。
- 而且,我还有一个称为 identity 的静态实用程序方法。identity有什么作用?嗯,这很明显。它接受一个对象并返回相同的对象。
public interface Function<T, R> {
R apply(T t);
// default methods
static <T> Function<T, T> identity() {
return t -> t;
}
}
flatMap操作
- flatMap操作有点难以理解。
- 让我们看一下此方法的签名。
<R> Stream<R> flatMap(Function<T, Stream<R>> flatMapper);
<R> Stream<R> map(Function<T, R> mapper);
- flatMap 将函数作为参数,与 map 方法的函数相同。
- 如果仔细检查会看到map接受一个对象并返回另一个对象,而flatMap接受一个对象并作为返回类型返回一个对象流。
- 因此,flatMapper 接受一个 T 类型的元素,返回一个 Stream 类型的元素。
- 如果 flatMap 是一个常规映射,它将返回由提供的函数返回的那些流的 Stream,从而返回一个流流。
- 但是,由于它是一个 flatMap,它返回一个平展的流,并成为单个流。
- 这是什么意思?这意味着包含的流中的所有对象。
Map和FlatMap示例
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;
/**
*
* @author Jack.Yang
*/
public class FlatMapExample {
public static void main(String... args) {
List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
List<Integer> list2 = Arrays.asList(2, 4, 6);
List<Integer> list3 = Arrays.asList(3, 5, 7);
List<List<Integer>> list = Arrays.asList(list1, list2, list3);
System.out.println(list);
Function<List<?>, Integer> size = List::size;
Function<List<Integer>, Stream<Integer>> flatmapper =
l -> l.stream();
// list.stream()
// .map(size)
// .forEach(System.out::println);
list.stream()
.flatMap(flatmapper)
.forEach(System.out::println);
}
}
在流上使用Map和filter
我们看到了 3 类操作
- 以消费者为参数的ForEach和peek
- 接受一个谓词作为参数的filter方法,
- flatMap方法中的map,它接受mapper作为参数。
- mapper是函数式接口function的一个实例。
Reduction, Functions, and Bifunctions
- 我们的map/filter/reduce算法的最后一步就是Reduce步骤。
- Stream API包含两种归约操作。
- 第一种是基本和经典的SQL操作,如最小值,最大值,总和,平均值等。
List<Integer> ages = Arrays.asList(1,2,3,4) ;
Stream<Integer> stream = ages.stream();
Integer sum = stream.reduce(0, (age1, age2) -> age1 + age2);//0为初始值
System.out.println("sum:"+sum);//0+1+2+3+4
sum:10
- 该归约采用两个整数 age1 和 age2,并返回它们的总和。
- 第一个参数,这个 0 是归约操作的初始值。
- 第二个参数是 BinaryOperator<T> 类型的归约操作。此处,T 是整数类型。事实上,BinaryOperator是BiFunction的一个特例。
public interface BiFunction<T, U, R> {
R apply(T t, U u);
// plus default methods
}
- BiFunction 看起来像一个函数。它在这里接受两个类型 T 和 U 的对象,并返回一个类型 R 的对象。
public interface BinaryOperator<T> extends BiFunction<T, T, T> {
// T apply(T t1, T t2);
// plus static methods
}
- BinaryOperator只是BiFunction的扩展,其中所有这三种类型实际上都是相同的。因此,BiFunction 将两个对象作为相同类型的参数,并返回一个相同类型的对象。
在空集上进行归约:归约操作的第一个参数
- bifunction 有两个参数,所以我们可以问两个问题。
- 如果流为空会发生什么情况?以及,如果流只有一个元素会发生什么?
- 如果流为空,则空流的归约结果就是第一个参数值
- 如果流只有一个元素,则此流的归约结果就是该元素与已提供的第一参数的相结合。
Stream<Integer> stream = ...;
BinaryOperation<Integer> sum = (i1, i2) -> i1 + i2;
Integer id = 0; // identity element for the sum
int red = stream.reduce(id, sum);
- 求和的单位元素(初始值)是 0,然后可以通过提供这个单位元素 0 和 sum 操作来归约流,这个过程用 lambda 表达式建模如上所示。
Stream<Integer> stream = Stream.empty();
int red = stream.reduce(id, sum);
System.out.println(red);
- 让我们拿一个空流来举例,可以通过在流接口上调用静态方法 empty 来构建空流,我们可以运行它,最后该流的归约为 0,因为第一个元素也是,所以0+0还是为0。
Stream<Integer> stream = Stream.of(1);
int red = stream.reduce(id, sum);
System.out.println(red);
- 让我们再看一个例子,通过调用stream接口的静态方法of,提供一个只有一个元素的流。在这里,我们可以看到这个流的归约结果是1。
Stream<Integer> stream = Stream.of(1, 2, 3, 4);
int red = stream.reduce(id, sum);
System.out.println(red);
- 作为最后一个例子,让我们取一个包含几个整数的普通流,1,2,3,4。让我们reduce这个流。它打印了10,当然,这是正确的答案。
Optionals
BinaryOperation<Integer> max = (i1, i2) ->
i1 > i2 ? i1 : i2;
- 问题是 max 操作没有用于归约(reduce)方法的标识元素(初始值)。
- 因此,不能以这种方式定义空流的最大值。
List<Integer> ages = ... ;
Stream<Integer> stream = ages.stream();
... max = stream.max(Comparator.naturalOrder());
- 那么,这个max方法的返回类型是什么?
- 如果返回类型是 int,即 Java 语言中的原始类型,则默认值为 0,而 0 显然不是 max 方法的标识元素。
- 如果返回类型为 Integer,则默认值为 null,在这种情况下,我当然不想返回 null 值,因为我将不得不在我的代码中检查返回值是否为 null 以避免空点异常。
List<Integer> ages = ... ;
Stream<Integer> stream = ages.stream();
Optional<Integer> max = stream.max(Comparator.naturalOrder());
- 实际上,这个调用的返回类型是optional, optional的类型是Integer。
- 什么是optional?这是Java 8中的一个新概念。它是一个类,我们可以将其视为包装类型。
- 好吧,integer类型的optional看起来像一个包装类型,唯一的区别是,在包装类型中,我总是有一个值,而在optional中,我可能没有值。
- 返回optional意味着可能没有结果,这正是我在这里想要的意思,因为如果我取空流的最大值,我不知道最大值是多少。
可选项的模式
Optional<String> opt = ... ;
if (opt.isPresent()) {
String s = opt.get() ;
} else {
...
}
- 如果optional中有一个值,isPresent 方法将返回 true,如果不是这种情况,则返回 false。而且,如果有一个值,可以通过在这个optional对象上调用 get 方法来获取它。
String s = opt.orElse("") ; // defines a default value
- orElse 方法封装了这两个调用。实际上,它只是调用isPresent,如果有对象,它将为我调用get方法。但是,如果我愿意,我也可以决定抛出异常。
String s = opt.orElseThrow(MyException::new) ; // lazy construct.
- 此 orElseThrow 方法将生成一个新异常。我只是提供一个 lambda 表达式,它将以懒惰的方式为我创建该异常。这是例外。我们只会在需要时构建,此方法将返回字符串(如果存在),如果不存在,则会抛出此异常。
归约操作
- 可用归约限制操作 — max(),min() 和 count() 将返回流中的元素数。
- Boolean归约——allMatch()、noneMatch()、anyMatch()。这三个方法都将谓词作为参数,如果谓词对流中的所有元素都返回true,那么allMatch方法将返回true。
- 返回optional对象归约(最大值和平均值除外)。findFirst() 和 findAny() 就是这样的方法。
- 归约类操作称为终端操作。
- 它们触发数据处理。
示例:Reductions, Optionals
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
/**
*
* @author Jack.Yang
*/
public class ReductionExample {
public static void main(String... args) {
List<Integer> list = Arrays.asList();
Optional<Integer> red = list.stream().reduce(Integer::max);
System.out.println("red = " + red);
}
}
包装Operations 与Optionals
- 归约只是一个经典的 SQL 操作。
- 中间操作和终端操作之间的区别,即中间只添加流的操作声明,在终端操作时触发流上的计算。
- Optional返回类型是有必要存在的,因为默认值不能始终在归约步骤上定义。
收集器, 在字符串、列表中收集展示
- 现在让我们看看第二种归约。如果您查看 Java 文档,您将看到这种归约称为可变归约。
- 为什么?因为我们要归约可变容器中的流,因为我们要在该容器中添加流的所有元素。
List<Person> persons = ... ;
String result = persons.stream()
.filter(person -> person.getAge() > 20)
.map(Person::getLastName)
.collect(Collectors.joining(","));
- 这种收集方法需要收集者。使用字符串作为参数连接。
- 因此,由于字符串,我们看到人员列表中超过 20 岁的人员的姓名,用逗号分隔。
List<Person> persons = ... ;
List<String> result =. persons.stream()
.filter(person -> person.getAge() > 20)
.map(Person::getLastName)
.collect(Collectors.toList());
- 我们也可以在列表中收集。这基本上与我们可以对流进行的处理相同,但是这一次,我们不是收集字符串中的所有名称,而是将它们收集在一个列表中。
在Map中Collect
List<Person> persons = ... ;
Map<Integer, List<Person>> result = persons.stream()
.filter(person -> person.getAge() > 20)
.collect(Collectors.groupingBy(Person::getAge));
- 我们把这条流建立在人身上。我们过滤掉所有20岁以下的人,并在地图上收集他们。现在,这张地图是由什么组成的?好吧,我们只是通过Person::getAge。再一次,这是一个方法引用。
List<Person> persons = ... ;
Map<Integer, Long> result = persons.stream()
.filter(person -> person.getAge() > 20)
.collect(Collectors.groupingBy(Person::getAge,
Collectors.counting() // the downstream collector
));
- 可以对值进行后处理。
示例:处理流
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.paumard.stream.model.Person;
/**
*
* @author Jack.Yang
*/
public class CollectorsExample {
public static void main(String... args) {
List<Person> persons = new ArrayList<>();
try (
BufferedReader reader =
new BufferedReader(
new InputStreamReader(
CollectorsExample.class.getResourceAsStream("people.txt")));
Stream<String> stream = reader.lines();
) {
stream.map(line -> {
String[] s = line.split(" ");
Person p = new Person(s[0].trim(), Integer.parseInt(s[1]));
persons.add(p);
return p;
})
.forEach(System.out::println);
} catch (IOException ioe) {
System.out.println(ioe);
}
Optional<Person> opt =
persons.stream().filter(p -> p.getAge() >= 20)
.min(Comparator.comparing(Person::getAge));
System.out.println(opt);
Optional<Person> opt2 =
persons.stream().max(Comparator.comparing(Person::getAge));
System.out.println(opt2);
Map<Integer, String> map =
persons.stream()
.collect(
Collectors.groupingBy(
Person::getAge,
Collectors.mapping(
Person::getName,
Collectors.joining(", ")
)
)
);
System.out.println(map);
}
}
总结
- 我们对map/filter/reduce算法有一个快速的解释,再一次,这个算法不是Java平台的典型算法。这是一种通用算法。
- 然后我们定义了什么是流。我们看到了几种构建流的模式。
- 我们看到了中间操作和最终操作之间的区别——第一个是懒惰的,第二个是触发数据处理的。
- 我们看到了几个消耗性操作 — forEach 操作(最终操作)和 peek 操作(中介操作)。
- 我们看到了两个映射操作 — 首先是 map(),然后是 flatMap()。
- 我们看到了使用过滤器方法的过滤器操作。
- 我们看到了减少步骤,以及减少操作。我们有两种归约运算 — 第一种是 SQL 聚合,最大值、最小值、总和、计数等,第二种是可变归约。
可变缩减是一个非常强大的工具,具有收集方法、收集器接口和收集器类。它使我们能够非常快速地构建一个复杂的结构来减少我们流的元素。
result
-
接受一个谓词作为参数的filter方法