我们在前面几篇关于 Java 集合框架中 List、Set、Map 这些容器的文章中,已经给大家演示过一些 Stream 操作了,这篇文章给大家详细梳理
由于 Stream 提供的操作过多,本节内容是 Stream API 中常用操作的学习和理解,下面会专门再有一篇文章介绍在项目开发中那些高频使用的,利用 Stream 处理对象集合的使用示例。
本文大纲如下:
Java 的 Stream API 提供了一种处理对象集合的函数式方法。 Stream 是和 Lambda 表达式等其他几个函数式编程特性一起在 Java 8 被引入的。这个篇教程将解释 Stream API 提供的这些函数式方法是如何工作的,以及怎么使用它们。
注意,Java 的 Stream API 与 Java IO 的 InputStream 和 OutputStream 没有任何关系,不要因为名字类似造成误解。 InputStream 和 OutputStream 是与字节流有关,而 Java 的 Stream API 用于处理对象流。
Stream 的定义
Java 的 Stream 是一个能够对其元素进行内部迭代的组件,这意味着它可以自己迭代其元素。相反地,当我们使用 Collection 的迭代功能,例如,从 Collection 获取Iterator 或者使用 Iterable 接口 的 forEach 方法这些方式进行迭代时,我们必须自己实现集合元素的迭代逻辑。
当然集合也支持获取 Stream 完成迭代,这些我们在介绍集合框架的相关章节都介绍过。
流处理
我们可以将 Listener 方法或者叫处理器方法附加到 Stream 上。当 Stream 在内部迭代元素时,将以元素为参数调用这些处理器。Stream 会为流中的每个元素调用一次处理器。所以每个处理器方法都可以处理 Stream 中的每个元素,我们把这称为流处理。
流的多个处理器方法可以形成一个调用链。链上的前一个处理器处理流中的元素,返回的新元素会作为参数传给链中的下一个处理器处理。当然,处理器可以返回相同的元素或新元素,具体取决于处理器的目的和用途。
怎么获取流
有很多方法获取 Stream ,一般最常见的是从 Collection 对象中获取 Stream。下面是一个从 List 对象获取 Stream 的例子。
代码语言:javascript复制List<String> items = new ArrayList<String>();
items.add("one");
items.add("two");
items.add("three");
Stream<String> stream = items.stream();
集合对象都实现了 Collection 接口,所以通过接口里定义的 stream 方法获救获取到由集合元素构成的 Steam。
流处理的构成
在对流进行处理时,不同的流操作以级联的方式形成处理链。一个流的处理链由一个源(source),0 到多个中间操作(intermediate operation)和一个终结操作(terminal operation)完成。
- 源:源代表 Stream 中元素的来源,比如我们上面看到的集合对象。
- 中间操作:中间操作,在一个流上添加的处理器方法,他们的返回结果是一个新的流。这些操作是延迟执行的,在终结操作启动后才会开始执行。
- 终结操作:终结流操作是启动元素内部迭代、调用所有处理器方法并最终返回结果的操作。
概念听起来有点模糊,我们通过流处理的例子再理解一下。
代码语言:javascript复制import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class StreamExamples {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("ONE");
stringList.add("TWO");
stringList.add("THREE");
Stream<String> stream = stringList.stream();
long count = stream
.map((value) -> value.toLowerCase())
.count();
System.out.println("count = " count);
}
}
map() 方法的调用是一个中间操作。它只是在流上设置一个 Lambda 表达式,将每个元素转换为小写形式。而对 count() 方法的调用是一个终结操作。此调用会在内部启动迭代,开始流处理,这将导致每个元素都转换为小写然后计数。
将元素转换为小写实际上并不影响元素的计数。转换部分只是作为 map() 是一个中间操作的示例。
流的中间操作
Stream API 的中间(非终结)流操作是转换或者过滤流中元素的操作。当我们把中间操作添加到流上时,我们会得到一个新的流作为结果。下面是一个添加到流上的中间操作的示例,它的执行结果会产生一个新的流。
代码语言:javascript复制List<String> stringList = new ArrayList<>();
stringList.add("ONE");
stringList.add("TWO");
stringList.add("THREE");
Stream<String> stream = stringList.stream();
Stream<String> stringStream =
stream.map((value) -> value.toLowerCase());
上面例子中,流上添加的 map() 调用,此调用实际上返回一个新的 Stream 实例,该实例表示原始字符串流应用了 map 操作后的新流。 只能将单个操作添加到给定的 Stream 实例上。如果需要将多个操作链接在一起,则只能将第二个操作应用于第一个操作产生的 Stream 实例上。
代码语言:javascript复制Stream<String> stringStream1 =
stream.map((value) -> value.toLowerCase());
Stream<½String> stringStream2 =
stringStream1.map((value) -> value.toUpperCase());
注意第二个 map() 调用是如何在第一个 map() 调用返回的 Stream 上进行调用的。
我们一般是将 Stream 上的所有中间操作串联成一个调用链:
代码语言:javascript复制Stream<String> stream1 = stream
.map((value) -> value.toLowerCase())
.map((value) -> value.toUpperCase())
.map((value) -> value.substring(0,3));
以 map方法为代表流间操作方法的参数,是一个函数式接口,我们可以直接用 Lambda 表达式作为这些操作的参数。所以在介绍 Lambda 的那一节我们也说过,Lambda 一般是和流操作就结合起来用的。
**参考--Java 的函数式接口: **tutorials.jenkov.com/java-functi…
下面我们说一下常用的流的中间操作。
map
map() 方法将一个元素转换(或者叫映射)到另一个对象。例如,一个字符串列表,map() 可以将每个字符串转换为小写、大写或原始字符串的子字符串,或完全不同的东西。
代码语言:javascript复制List<String> list = new ArrayList<String>();
Stream<String> stream = list.stream();
Stream<String> streamMapped = stream.map((value) -> value.toUpperCase());
filter
filter() 用于从 Stream 中过滤掉元素。 filter 方法接受一个 Predicate (也是一个函数式接口),filter() 为流中的每个元素调用 Predicate。如果元素要包含在 filter() 返回结果的流中,则 Predicate 应返回 true。如果不应包含该元素,则 Predicate 应返回 false。
代码语言:javascript复制Stream<String> longStringsStream = stream.filter((value) -> {
// 元素长度大于等于3,返回true,会被保留在 filter 产生的新流中。
return value.length() >= 3;
});
比如 Stream 实例应用了上面这个 filter 后,filter 返回的结果流里只会包含长度不小于 3 的元素。
flatMap
flatMap方法接受一个 Lambda 表达式, Lambda 的返回值必须也是一个stream类型,flatMap方法最终会把所有返回的stream合并。map 与 flatMap 方法很像,都是以某种方式转换流中的元素。如果需要将每个元素转换为一个值,则使用 map 方法,如果需要将每个元素转换为多个值组成的流,且最终把所有元素的流合并成一个流,则需要使用 flatMap 方法。 在效果上看是把原来流中的每个元素进行了“展平”
代码语言:javascript复制import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class StreamFlatMapExamples {
public static void main(String[] args) {
List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream = stringList.stream();
stream.flatMap((value) -> {
String[] split = value.split(" ");
return Arrays.asList(split).stream();
}).forEach((value) -> System.out.println(value));
}
}
在上面的例子中,每个字符串元素被拆分成单词,变成一个 List,然后从这个 List 中获取并返回流,flatMap 方法最终会把这些流合并成一个,所以最后用流终结操作 forEach 方法,遍历并输出了每个单词。
代码语言:javascript复制One
flew
over
the
cuckoo's
nest
To
kill
a
muckingbird
Gone
with
the
wind
distinct
distinct() 会返回一个仅包含原始流中不同元素的新 Stream 实例,任何重复的元素都将会被去掉。
代码语言:javascript复制List<String> stringList = new ArrayList<String>();
stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");
Stream<String> stream = stringList.stream();
List<String> distinctStrings = stream
.distinct()
.collect(Collectors.toList());
System.out.println(distinctStrings);
在这个例子中,元素 "one" 在一开始的流中出现了两次,原始流应用 distinct 操作生成的新流中将会丢弃掉重复的元素,只保留一个 "one" 元素。所以这个例子最后的输出是:
代码语言:javascript复制[one, two, three]
limit
limit 操作会截断原始流,返回最多只包含给定数量个元素的新流。
代码语言:javascript复制import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class StreamLimitExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<String>();
stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");
Stream<String> stream = stringList.stream();
stream.limit(2)
.forEach( element -> System.out.println(element));
}
}
这个例子中,因为对原始流使用了 limit(2) 操作,所以只会返回包含两个元素的新流,随后使用 forEach 操作将它们打印了出来。程序最终将会输出:
代码语言:javascript复制one
two
peek
peek() 方法是一个以 Consumer (java.util.function.Consumer,Consumer 代表的是消费元素但不返回任何值的方法) 作为参数的中间操作,它返回的流与原始流相同。当原始流中的元素开始迭代时,会调用 peek 方法中指定的 Consumer 实现对元素进行处理。
正如 peek 操作名称的含义一样,peek() 方法的目的是查看流中的元素,而不是转换它们。跟其他中间操作的方法一样,peek() 方法不会启动流中元素的内部迭代,流需要一个终结操作才能开始内部元素的迭代。
peek() 方法在流处理的 DEBUG 上的应用甚广,比如我们可以利用 peek() 方法输出流的中间值,方便我们的调试。
代码语言:javascript复制Stream.of("one", "two", "three","four").filter(e -> e.length() > 3)
.peek(e -> System.out.println("Filtered value: " e))
.map(String::toUpperCase)
.peek(e -> System.out.println("Mapped value: " e))
.collect(Collectors.toList());
上面的例子会输出以下调试信息。
代码语言:javascript复制Filtered value: three
Mapped value: THREE
Filtered value: four
Mapped value: FOUR
流的终结操作
Stream 的终结操作通常会返回单个值,一旦一个 Stream 实例上的终结操作被调用,流内部元素的迭代以及流处理调用链上的中间操作就会开始执行,当迭代结束后,终结操作的返回值将作为整个流处理的返回值被返回。
代码语言:javascript复制long count = stream
.map((value) -> value.toLowerCase())
.map((value) -> value.toUpperCase())
.map((value) -> value.substring(0,3))
.count();
Stream 的终结操作 count() 被调用后整个流处理开始执行,最后将 count() 的返回值作为结果返回,结束流操作的执行。这也是为什么把他们命名成流的终结操作的原因。
上面例子,应用的中间操作 map 对流处理的结果并没有影响,这里只是做一下演示。
下面我们把常用的流终结操作说一下。
anyMatch
anyMatch() 方法以一个 Predicate (java.util.function.Predicate 接口,它代表一个接收单个参数并返回参数是否匹配的函数)作为参数,启动 Stream 的内部迭代,并将 Predicate 参数应用于每个元素。如果 Predicate 对任何元素返回了 true(表示满足匹配),则 anyMatch() 方法的结果返回 true。如果没有元素匹配 Predicate,anyMatch() 将返回 false。
代码语言:javascript复制import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class StreamAnyMatchExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream = stringList.stream();
boolean anyMatch = stream.anyMatch((value) -> value.startsWith("One"));
System.out.println(anyMatch);
}
}
上面例程的运行结果是 true , 因为流中第一个元素就是以 "One" 开头的,满足 anyMatch 设置的条件。
allMatch
allMatch() 方法同样以一个 Predicate 作为参数,启动 Stream 中元素的内部迭代,并将 Predicate 参数应用于每个元素。如果 Predicate 为 Stream 中的所有元素都返回 true,则 allMatch() 的返回结果为 true。如果不是所有元素都与 Predicate 匹配,则 allMatch() 方法返回 false。
代码语言:javascript复制import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class StreamAllMatchExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream = stringList.stream();
boolean allMatch = stream.allMatch((value) -> value.startsWith("One"));
System.out.println(allMatch);
}
}
上面的例程我们把流上用的 anyMatch 换成了 allMatch ,结果可想而知会返回 false,因为并不是所有元素都是以 "One" 开头的。
noneMatch
Match 系列里还有一个 noneMatch 方法,顾名思义,如果流中的所有元素都与作为 noneMatch 方法参数的 Predicate 不匹配,则方法会返回 true,否则返回 false。
代码语言:javascript复制import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class StreamNoneExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("abc");
stringList.add("def");
Stream<String> stream = stringList.stream();
boolean noneMatch = stream.noneMatch((element) -> {
return "xyz".equals(element);
});
System.out.println("noneMatch = " noneMatch); //输出 noneMatch = true
}
}
collect
collect() 方法被调用后,会启动元素的内部迭代,并将流中的元素收集到集合或对象中。
代码语言:javascript复制import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class StreamCollectExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream = stringList.stream();
List<String> stringsAsUppercaseList = stream
.map(value -> value.toUpperCase())
.collect(Collectors.toList());
System.out.println(stringsAsUppercaseList);
}
}
collect() 方法将收集器 -- Collector (java.util.stream.Collector) 作为参数。在上面的示例中,使用的是 Collectors.toList() 返回的 Collector 实现。这个收集器把流中的所有元素收集到一个 List 中去。
count
count() 方法调用后,会启动 Stream 中元素的迭代,并对元素进行计数。
代码语言:javascript复制import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class StreamExamples {
public static void main(String[] args) {
List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream = stringList.stream();
long count = stream.flatMap((value) -> {
String[] split = value.split(" ");
return Arrays.asList(split).stream();
}).count();
System.out.println("count = " count); // count = 14
}
}
上面的例程中,首先创建一个字符串 List ,然后获取该 List 的 Stream,为其添加了 flatMap() 和 count() 操作。 count() 方法调用后,流处理将开始迭代 Stream 中的元素,处理过程中字符串元素在 flatMap() 操作中被拆分为单词、合并成一个由单词组成的 Stream,然后在 count() 中进行计数。所以最终打印出的结果是 count = 14。
findAny
findAny() 方法可以从 Stream 中找到单个元素。找到的元素可以来自 Stream 中的任何位置。且它不提供从流中的哪个位置获取元素的保证。
代码语言:javascript复制import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
public class StreamFindAnyExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");
Stream<String> stream = stringList.stream();
Optional<String> anyElement = stream.findAny();
if (anyElement.isPresent()) {
System.out.println(anyElement.get());
} else {
System.out.println("not found");
}
}
}
findAny() 方法会返回一个 Optional,意味着 Stream 可能为空,因此没有返回任何元素。我们可以通过 Optional 的 isPresent() 方法检查是否找到了元素。
Optional 类是一个可以为 null 的容器对象。如果值存在则 isPresent() 方法会返回true,调用get()方法会返回容器中的对象,否则抛出异常:NoSuchElementException
findFirst
findFirst() 方法将查找 Stream 中的第一个元素,跟 findAny() 方法一样,也是返回一个 Optional,我们可以通过 Optional 的 isPresent() 方法检查是否找到了元素。
代码语言:javascript复制import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
public class StreamFindFirstExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");
Stream<String> stream = stringList.stream();
Optional<String> anyElement = stream.findFirst();
if (anyElement.isPresent()) {
System.out.println(anyElement.get());
} else {
System.out.println("not found");
}
}
}
forEach
forEach() 方法我们在介绍 Collection 的迭代时介绍过,当时主要是拿它来迭代 List 的元素。它会启动 Stream 中元素的内部迭代,并将 Consumer (java.util.function.Consumer, 一个函数式接口,上面介绍过) 应用于 Stream 中的每个元素。 注意 forEach() 方法的返回值是 void。
代码语言:javascript复制import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class StreamExamples {
public static void main(String[] args) {
List<String> stringList = new ArrayList<String>();
stringList.add("one");
stringList.add("two");
stringList.add("three");
Stream<String> stream = stringList.stream();
stream.forEach(System.out::println);
}
}
注意,上面例程中 forEach 的参数我们直接用了Lambda 表达式引用方法的简写形式。
min
min() 方法返回 Stream 中的最小元素。哪个元素最小是由传递给 min() 方法的 Comparator 接口实现来确定的。
代码语言:javascript复制import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
public class StreamMinExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("abc");
stringList.add("def");
Stream<String> stream = stringList.stream();
// 作为 min 方法参数的Lambda 表达式可以简写成 String::compareTo
// Optional<String> min = stream.min(String::compareTo);
Optional<String> min = stream.min((val1, val2) -> {
return val1.compareTo(val2);
});
String minString = min.get();
System.out.println(minString); // abc
}
}
min() 方法返回的是一个 Optional ,也就是它可能不包含结果。如果为空,直接调用 Optional 的 get() 方法将抛出 异常--NoSuchElementException。比如我们把上面的 List 添加元素的两行代码注释掉后,运行程序就会报
代码语言:javascript复制Exception in thread "main" java.util.NoSuchElementException: No value present
at java.util.Optional.get(Optional.java:135)
at com.example.StreamMinExample.main(StreamMinExample.java:21)
所以最好先用 Optional 的 ifPresent() 判断一下是否包含结果,再调用 get() 获取结果。
max
与 min() 方法相对应,max() 方法会返回 Stream 中的最大元素,max() 方法的参数和返回值跟 min() 方法的也都一样,这里就不再过多阐述了,只需要把上面求最小值的方法替换成求最大值的方法 max() 即可。
代码语言:javascript复制Optional<String> min = stream.max(String::compareTo);
reduce
reduce() 方法,是 Stream 的一个聚合方法,它可以把一个 Stream 的所有元素按照聚合函数聚合成一个结果。reduce()方法接收一个函数式接口 BinaryOperator 的实现,它定义的一个apply()方法,负责把上次累加的结果和本次的元素进行运算,并返回累加的结果。
代码语言:javascript复制import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
public class StreamReduceExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream = stringList.stream();
Optional<String> reduced = stream.reduce((value, combinedValue) -> combinedValue " " value);
// 写程序的时候记得别忘了 reduced.ifPresent() 检查结果里是否有值
System.out.println(reduced.get());
}
}
reduce() 方法的返回值同样是一个 Optional 类的对象,所以在获取值前别忘了使用 ifPresent() 进行检查。
streadm 实现了多个版本的reduce() 方法,还有可以直接返回元素类型的版本,比如使用 reduce 实现整型Stream的元素的求和
代码语言:javascript复制import java.util.ArrayList;
import java.util.List;
public class IntegerStreamReduceSum {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
intList.add(10);
intList.add(9);
intList.add(8);
intList.add(7);
Integer sum = intList.stream().reduce(0, Integer::sum);
System.out.printf("List 求和,总和为%sn", sum);
}
}
toArray
toArray() 方法是一个流的终结操作,它会启动流中元素的内部迭代,并返回一个包含所有元素的 Object 数组。
代码语言:javascript复制List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream = stringList.stream();
Object[] objects = stream.toArray();
不过 toArray 还有一个重载方法,允许传入指定类型数组的构造方法,比如我们用 toArray 把流中的元素收集到字符串数组中,可以这么写:
代码语言:javascript复制String[] strArray = stream.toArray(String[]::new);
流的拼接
Java 的Stream 接口包含一个名为 concat() 的静态方法,它可以将两个流连接成一个。
代码语言:javascript复制import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class StreamConcatExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream1 = stringList.stream();
List<String> stringList2 = new ArrayList<>();
stringList2.add("Lord of the Rings");
stringList2.add("Planet of the Rats");
stringList2.add("Phantom Menace");
Stream<String> stream2 = stringList2.stream();
Stream<String> concatStream = Stream.concat(stream1, stream2);
List<String> stringsAsUppercaseList = concatStream
.collect(Collectors.toList());
System.out.println(stringsAsUppercaseList);
}
}
从数组创建流
上面关于 Stream 的例子我们都是从 Collection 实例的 stream() 方法获取的集合包含的所有元素的流,除了这种方法之外,Java 的 Stream 接口中提供了一个名为 of 的静态方法,能支持从单个,多个对象或者数组对象快速创建流。
代码语言:javascript复制import java.util.stream.Stream;
public class StreamExamples {
public static void main(String[] args) {
Stream<String> stream1 = Stream.of("one", "two", "three");
Stream<String> stream2 = Stream.of(new String[]{"one", "two"});
System.out.println(stream1.count()); // 输出3
System.out.println(stream2.count()); // 输出2
}
}
总结
上面我们把 Stream 的两大类操作:流的中间操作、流的终结操作都有哪些方法给大家列举了一遍,让大家对 Stream 能完成的操作有了大致的印象。不过为了讲解这些操作用的都是非常简单的例子,流操作的数据也都是简单类型的,主要的目的是让大家能更快速地理解 Stream 的各种操作应用在数据上后,都有什么效果。
下一篇我会演示一些在项目开发中我们会高频用到的,使用 Stream 完成各种复杂操作的示例,让大家做项目的时候可以直接进行参考,进一步提升你用 Java 编程、开发项目的体验。