概述
这些操作会过滤出以字母“H”开头的元素,将其余元素转换为大写字母,然后以字母顺序)对其进行排序,最后将它们放到一个列表中。 因此,上述的结果输出为[HELLO, HI]。
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class StreamDemo {
public static void main(String[] args) {
List<String> list = Stream.of("Hi", "stream","Hello", "world")
.filter(s -> s.startsWith("H"))
.map(String::toUpperCase)
.sorted()
.collect(Collectors.toList());
//[HELLO, HI]
System.out.println(list);
}
}
Stream 的来源操作
Stream 主要适合处理对象的集合并且可以在任何类型的元素上做一些操作。虽然提供了三种特别的 Stream 实现:IntStream、LongStream、DoubleStream,它们只能处理相对应的原始类型。
任何类型的空的 Stream 可以调用 Stream.empty() 来生成,例如下面的方法:
Stream<T> Stream.empty()
IntStream IntStream.empty()
LongStream LongStream.empty()
DoubleStream DoubleStream.empty()
通过集合创建 Stream
Java 8 的java.util.Collection 接口被扩展,提供了两个获取流的默认方法:
- default Stream
stream( ):返回一个串行流(顺序流),串行流从集合中取数据是按照集合的顺序的。 - default Stream
parallelStream(): 返回一个并行流,行流是并行操作的,获取到的数据是无序的。
public class SteamCollection {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("a");
stringList.add("b");
stringList.add("c");
// 通过集合获取串行 stream 对象
Stream<String> s1 = stringList.stream();
// 通过集合获取并行 stream 对象
Stream<String> s2 = stringList.parallelStream();
}
}
通过数组创建 Stream
Java 8 中的java.util.Arrays的静态方法stream()可以获取数组流:
- static
Stream stream(T[] array):返回一个数组流。
此外,stream()还有几个重载方法,能够处理对应的基本数据类型的数组:
- public static IntStream stream(int[] array):返回以指定数组作为其源的连续IntStream;
- public static LongStream stream(long[] array):返回以指定数组作为其源的连续LongStream;
- public static DoubleStream stream(double[] array):返回以指定数组作为其源的连续DoubleStream。
public class StreamArray {
public static void main(String[] args) {
int[] arr = new int[]{1,2,3};
// 通过整型数组,获取整形的 stream 对象
IntStream s1 = Arrays.stream(arr);
// 通过字符串类型的数组,获取泛型类型为 String 的 stream 对象
String[] stringArr = new String[]{"Hello", "world"};
Stream<String> s2 = Arrays.stream(stringArr);
}
}
of
public class StreamOf {
public static void main(String[] args) {
Stream<Integer> stream = Stream.of(1, 2, 3);
}
}
中间操作 Intermediate Operations
过滤 Filter
根据经验,filter() 是 Stream API 最有用处的操作之一。可以用对应的限制条件将 Stream 中的元素过滤掉。此类标准必须表示为 Predicate(产生布尔值的函数),例如 一个 Lambda 表达式。以下代码的目的是查找以字母“h”开头的字符串,并丢弃其他字符串。
Stream.of("hi", "stream", "hello", "world").
filter(s -> s.startsWith("h"))
.forEach(System.out::println);
数量限制Limit
有一些非常简单但功能强大的操作,提供了一种基于元素在 Stream 中的位置来选择或丢弃元素的方法。 这些操作中的第一个是 limit(n),它创建一个新 Stream,该 Stream 仅包含应用该 Stream 的前 n 个元素。
Stream.of("hi", "stream", "hello", "world").
limit(2)
.forEach(System.out::println);
跳过元素Skip
类似地,如果我们只对某些元素感兴趣,则可以使用 .skip(n) 操作。 如果将 skip(2) 应用于 Stream,则会留下两个元素“Giraffe”和“Lemur”。
Stream.of("hi", "stream", "hello", "world").
skip(2)
.forEach(System.out::println);
hello
world
去重Distinct
在某些情况下,Stream 中每个元素只需要出现一次即可。 无需手动筛选出任何重复项,而是为此目的而存在一个指定的操作 distinct() 。它将使用 Object :: equals 检查是否相等,并返回仅包含唯一元素的新 Stream。
Stream.of("hi", "stream", "hello", "world","hi").
distinct()
.forEach(System.out::println);
排序 Sorted
该操作将以自然顺序排列元素。对于以下字符串,使用的是字母顺序。
Stream.of("b", "a", "d","c").
sorted()
.forEach(System.out::println);
结果
a
b
c
d
sorted(Comparator com)
有时只能以自然顺序排序可能会有些局限,可以应用自定义比较器来检查元素的某些属性。例如,我们可以按照字符串的长度顺序对它们进行排序:
Employee
public class Employee implements Comparable<Employee> {
private Integer id;
private Double salary;
public Employee(Integer id, double salary) {
this.id = id;
this.salary = salary;
}
public double getSalary() {
return salary;
}
@Override
public int compareTo(Employee e) {
return this.salary.compareTo(e.getSalary());
}
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", salary=" + salary +
'}';
}
}
比较
List<Employee> list = new ArrayList<>();
list.add(new Employee(1, 8000));
list.add(new Employee(2, 6000));
list.add(new Employee(3, 7000));
list.stream()
.sorted(Comparator.comparing(Employee::getSalary).reversed())
.forEach(System.out::println);
映射 Map
- map(Function f):接收一个方法作为参数,该方法会被应用到每个元素上,并将其映射成一个新的元素;
- mapToDouble(ToDoubleFunction f):接收一个方法作为参数,该方法会被应用到每个元素上,产生一个新的DoubleStream;
- mapToLong(ToLongFunction f):接收一个方法作为参数,该方法会被应用到每个元素上,产生一个新的LongStream;
- flatMap(Function f):接收一个方法作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。
Stream 的另一个最通用的操作是 map()。通过它可以将 Stream 的元素映射到另一个值或类型。这意味着此操作的结果可以是任何类型 R 的 Stream。下面的示例执行从 String 到 String 的简单映射,将所有大写字母替换为它们的小写字母。
public class StreamMap {
public static void main(String[] args) {
Stream<String> lowerCase = Stream.of("Monkey", "Lion", "Giraffe", "Lemur" ).map(String::toLowerCase);
lowerCase.forEach(System.out::println);
}
}
Map 操作还有三种特殊的实现方式,它们仅限于将元素映射到基本类型 int、double 和 long。
.mapToInt(); .mapToDouble(); .mapToLong();
因此,这些操作的结果始终对应于 IntStream、DoubleStream 或 LongStream。下面,我们演示如何使用 .mapToInt() 将动物映射到其名称的长度:
public class StreamMap {
public static void main(String[] args) {
IntStream lengths = Stream.of("Monkey", "Lion", "Giraffe", "Lemur" ).mapToInt(String::length);
lengths.forEach(System.out::println);
}
}
String::length 等于 lambda 表达式的 s-> s.length()。
FlatMap
它与 map() 操作有关,但不是采用从类型 T 到返回类型 R 的函数,而是采用从类型 T 到返回 R 的 Stream 的函数。函数将元素将其展平为 Stream,flatMap() 将 Stream 的所有元素串联在一起。
public class StreamFlatMap {
public static void main(String[] args) {
Stream<Character> chars = Stream.of("Hello", "World").flatMap(s -> s.chars().
mapToObj(i -> (char) i));
chars.forEach(System.out::println);
}
eg:
public class StreamFlatMap {
public static void main(String[] args) {
List<String> l1 = Arrays.asList("a","b");
List<String> l2 = Arrays.asList("c","d");
List<String> l = new ArrayList<>();
l.addAll(l1);
l.addAll(l2);
List<String> letters = l.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
letters.forEach(System.out::println);
List<String> betterLetters = Stream.of(l1, l2)
.flatMap(List::stream)
.map(String::toUpperCase)
.collect(Collectors.toList());
betterLetters.forEach(System.out::println);
}
}
终端操作 Terminal Operations
现在我们熟悉了 Stream 管道的初始化和构造,接下来需要一种处理输出的方法。 终端操作通过从其余元素操作(例如 count() )或 side-effect 操作(例如 forEach(Consumer))中产生结果。
在启动终端操作之前,Stream 将不会对源的元素执行任何计算。 这意味着仅在需要时才使用源元素,这是避免不必要工作的明智方法。这也意味着一旦应用了终端操作,Stream 将被消耗,并且无法再添加其他操作。
遍历
Stream 的可能用处是更新某些或所有元素的属性,或者只是出于调试目的而将它们打印出来。无论哪种方式,我们都不希望收集(collecting)或计数(counting)输出,而是通过产生 side-effect 不返回值来进行。
这是 forEach() 或 forEachOrdered() 的目的。他们都使用一个 Consumer 并终止流而不返回任何东西。 这些操作之间的区别仅在于 forEachOrdered() 承诺以元素在 Stream 中出现的顺序调用提供的 Consumer,而 forEach() 仅承诺以任何顺序调用 Consumer。后一种变体对并行流很有用。
在下面的简单情况下,我们在一行中打印出 Stream 的每个元素。
Stream.of( "Monkey", "Lion", "Giraffe", "Lemur", "Lion" ).forEachOrdered(System.out::print);
产生的结果如下:
MonkeyLionGiraffeLemurLion
收集Collector
Collector 接口中的实现决定了如何对流执行收集的操作(如收集到 List、Set、Map)。java.util.stream.Collectors 类提供了很多静态方法,可以方便地创建常用收集器实例,常用静态方法如下:
- static List
toList():把流中元素收集到List; - static Set
toSet():把流中元素收集到Set; - static Collection
toCollection():把流中元素收集到创建的集合。 3.2.1 将数据放到 Set 集合中
使用 toSet() 可以把 Stream 中的所有元素都放到一个 Set 集合中。
SetcollectToSet = Stream.of( “Monkey”, “Lion”, “Giraffe”, “Lemur”, “Lion” ) .collect(Collectors.toSet());
toSet: [Monkey, Lion, Giraffe, Lemur]3.2.2 将数据放到 List 集合中
使用 toList() 收集器将元素收集到 List 中。
ListcollectToList = Stream.of( “Monkey”, “Lion”, “Giraffe”, “Lemur”, “Lion” ) .collect(Collectors.toList());
collectToList: [Monkey, Lion, Giraffe, Lemur, Lion]3.2.3 将数据放到通用集合中
在更一般的情况下,可以通过提供 Collection 类型提供构造函数来将 Stream 的元素收集到任何 Collection 中。构造函数的示例为 LinkedList::new、LinkedHashSet::new 和 PriorityQueue::new。
LinkedListcollectToCollection = Stream.of( “Monkey”, “Lion”, “Giraffe”, “Lemur”, “Lion” ) .collect(Collectors.toCollection(LinkedList::new));
collectToCollection: [Monkey, Lion, Giraffe, Lemur, Lion]3.2.4 将结果转换为数组
由于数组是固定大小的容器,并不像“Collection”那样的灵活,因此需要有特殊的终端操作来在数组中创建和存储元素,这个操作就是 toArray()。 注意,仅调用 toArray() 会生成元素类型未 Objects 的 Array,因为该方法无法自行创建类型化数组。下面,我们展示如何使用 String 数组的构造函数来提供类型化数组 String[]。
String[] toArray = Stream.of( “Monkey”, “Lion”, “Giraffe”, “Lemur”, “Lion” ) .toArray(String[]::new)
toArray: [Monkey, Lion, Giraffe, Lemur, Lion]3.2.5 将结果转换为 Map
我们可能想从元素中提取信息,并将结果以 Map 的形式输出。 为此,我们可以使用 .toMap(),该命令具有两个与之相对应的“函数”,即 key-mapper 和 value-mapper。
该例子显示了不同的动物如何与它们名称中不同字符的数量相关联。 我们使用中间操作 distinct() 来确保只在 Map 中添加唯一 key(如果键不相同,则必须提供 toMap() 收集器,其中解析器时用于合并来自相等键的结果)。
MaptoMap = Stream.of( “Monkey”, “Lion”, “Giraffe”, “Lemur”, “Lion” ) .distinct() .collect(Collectors.toMap( Function.identity(), //Function keyMapper s -> (int) s.chars().distinct().count()// Function valueMapper ));
toMap: {Monkey=6, Lion=4, Lemur=5, Giraffe=6} (*)
注意:key 的顺序是无法保证的。3.2.6 使用 GroupingBy 来收集结果
有一个非常有用的名为 groupingBy() 的 Collector,它根据某些属性将元素划分为不同的组,它是通过使用称为“分类器”来提取属性。 这个操作的输出是 Map。下面,我们演示如何根据动物的名字的首字母对动物进行分组。
Map> groupingByList = Stream.of( “Monkey”, “Lion”, “Giraffe”, “Lemur”, “Lion” ) .collect(Collectors.groupingBy( s -> s.charAt(0) // Function classifier ));
groupingByList: {G=[Giraffe], L=[Lion, Lemur, Lion], M=[Monkey]}3.2.6 使用带有下游收集器的 GroupingBy 来收集结果
在前面的示例中,默认情况下,会为 Map 中的值应用**下游收集器(downstream collector)toList(),将每个存储的元素收集到 List 中。 有一个重载的 groupingBy() 版本,它允许使用自定义的“下游收集器”来更好地控制生成的 Map。下面是一个示例,说明如何使用特殊的下游收集器 counting() 来计数。
MapgroupingByCounting = Stream.of( “Monkey”, “Lion”, “Giraffe”, “Lemur”, “Lion” ) .collect(Collectors.groupingBy( s -> s.charAt(0), // Function classifier counting() // Downstream collector ));
groupingByCounting: {G=1, L=3, M=1}
下图说明了这个处理过程:
任何收集器都可以用作下游收集器。 特别值得一提的是,收集器 groupingBy() 可以采用下游收集器,该下游收集器也是 groupingBy() 收集器,从而可以对第一个分组操作的结果进行二次分组。 在我们的动物案例中,我们可能会创建一个 Map
查找
- findFirst():返回第一个元素;
-
匹配
allMatch(Predicate p):检查是否匹配所有元素;
- anyMatch(Predicate p):检查是否至少匹配一个元素;
- noneMatch(Predicate p):检查是否没有匹配所有元素;
中间操作 filter() 是消除与给定条件不匹配的元素的好方法。 在某些情况下,我们只是想知道是否存在至少一个满足该条件的元素。 如果是这样,那么使用 anyMatch() 更方便和有效。 在这里,我们寻找数字 2 是否存在:
public class StreamMatch {
public static void main(String[] args) {
// 创建一个整型列表
List<Integer> integers = Arrays.asList(10, 12, 9, 8, 20, 1);
// 使用 allMatch(Predicate p) 检查是否匹配所有元素,如果匹配,则返回 true;否则返回 false
boolean b1 = integers.stream().allMatch(integer -> integer > 0);
if (b1) {
System.out.println(integers + "列表中所有的元素都大于0");
} else {
System.out.println(integers + "列表中不是所有的元素都大于0");
}
// 使用 anyMatch(Predicate p) 检查是否至少匹配一个元素
boolean b2 = integers.stream().anyMatch(integer -> integer >= 20);
if (b2) {
System.out.println(integers + "列表中至少存在一个的元素都大于等于20");
} else {
System.out.println(integers + "列表中不存在任何一个大于等于20的元素");
}
// 使用 noneMath(Predicate p) 检查是否没有匹配所有元素
boolean b3 = integers.stream().noneMatch(integer -> integer > 100);
if (b3) {
System.out.println(integers + "列表中不存在大于100的元素");
} else {
System.out.println(integers + "列表中存在大于100的元素");
}
}
}
计算
- count():返回流中元素总数;
- max(Comparator c):返回流中最大值;
min(Comparator c):返回流中最小值;
最简单的是 count(),它可以应用于任何 Stream。例如,它可以用于计算动物的数量:
long nrOfAnimals = Stream.of( "Monkey", "Lion", "Giraffe", "Lemur" ) .count();
nrOfAnimals: 4
某些终端操作仅适用于我们在第一篇文章中提到的特殊 Stream 实现,IntStream、LongStream 和 DoubleStream。我们可以简单地将所有元素汇总如下:
int sum = IntStream.of(1, 2, 3).sum();
sum: 6
或者也可以使用 .average() 来计算这些整数的平均值:
OptionalDouble average = IntStream.of(1, 2, 3).average();
average: OptionalDouble[2.0]
也可以使用 .max() 来查找他们中的最大值:
int max = IntStream.of(1, 2, 3).max().orElse(0);
max: 3
与 average() 一样,max() 运算符的结果是可选的,因此通过声明 .orElse(0),我们将自动检索值(如果它存在或降为0)作为默认值 。如果我们想处理原始返回类型,也可以将相同的解决方案应用于求平均值的例子。
如果我们对所有这些统计数据都感兴趣,那么创建几个相同的流并对每个流应用不同的终端操作是非常麻烦的。幸运地是,有一个方便的名为 summaryStatistics 的操作,该操作允许将几个常见的统计属性合并到一个“SummaryStatistics”对象中。
IntSummaryStatistics statistics = IntStream.of(1, 2, 3).summaryStatistics();
statistics: IntSummaryStatistics{count=3, sum=6, min=1, average=2.000000,
规约reduce
- reduce(T identity, BinaryOperator b):可以将流中的元素反复结合起来,得到一个值。返回 T;
- reduce(BinaryOperator b):可以将流中的元素反复结合起来,得到一个值,返回 Optional
。
public class StreamReduce {
public static void main(String[] args) {
// 创建一个整型列表
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
// 使用 reduce(T identity, BinaryOperator b) 计算列表中所有整数和
Integer sum = integers.stream().reduce(0, Integer::sum);
System.out.println(sum);
// 使用 reduce(BinaryOperator b) 计算列表中所有整数和,返回一个 Optional<T>
Optional<Integer> reduce = integers.stream().reduce(Integer::sum);
System.out.println(reduce);
}
}