由于 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 的例子。
List items = new ArrayList();
items.add("one");
items.add("two");
items.add("three");
Stream stream = items.stream();
复制代码
集合对象都实现了 Collection 接口,所以通过接口里定义的 stream 方法获救获取到由集合元素构成的 Steam。
流处理的构成
在对流进行处理时,不同的流操作以级联的方式形成处理链。一个流的处理链由一个源(source),0 到多个中间操作(intermediate operation)和一个终结操作(terminal operation)完成。
- 源:源代表 Stream 中元素的来源,比如我们上面看到的集合对象。
- 中间操作:中间操作,在一个流上添加的处理器方法,他们的返回结果是一个新的流。这些操作是延迟执行的,在终结操作启动后才会开始执行。
- 终结操作:终结流操作是启动元素内部迭代、调用所有处理器方法并最终返回结果的操作。
概念听起来有点模糊,我们通过流处理的例子再理解一下。
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class StreamExamples {
public static void main(String[] args) {
List stringList = new ArrayList<>();
stringList.add("ONE");
stringList.add("TWO");
stringList.add("THREE");
Stream stream = stringList.stream();
long count = stream
.map((value) -> value.toLowerCase())
.count();
System.out.println("count = " + count);
}
}
复制代码
map() 方法的调用是一个中间操作。它只是在流上设置一个 Lambda 表达式,将每个元素转换为小写形式。而对 count() 方法的调用是一个终结操作。此调用会在内部启动迭代,开始流处理,这将导致每个元素都转换为小写然后计数。
将元素转换为小写实际上并不影响元素的计数。转换部分只是作为 map() 是一个中间操作的示例。
流的中间操作
Stream API 的中间(非终结)流操作是转换或者过滤流中元素的操作。当我们把中间操作添加到流上时,我们会得到一个新的流作为结果。下面是一个添加到流上的中间操作的示例,它的执行结果会产生一个新的流。
List stringList = new ArrayList<>();
stringList.add("ONE");
stringList.add("TWO");
stringList.add("THREE");
Stream stream = stringList.stream();
Stream stringStream =
stream.map((value) -> value.toLowerCase());
复制代码
上面例子中,流上添加的 map() 调用,此调用实际上返回一个新的 Stream 实例,该实例表示原始字符串流应用了 map 操作后的新流。 只能将单个操作添加到给定的 Stream 实例上。如果需要将多个操作链接在一起,则只能将第二个操作应用于第一个操作产生的 Stream 实例上。
Stream stringStream1 =
stream.map((value) -> value.toLowerCase());
Stream<½String> stringStream2 =
stringStream1.map((value) -> value.toUpperCase());
复制代码
注意第二个 map() 调用是如何在第一个 map() 调用返回的 Stream 上进行调用的。
我们一般是将 Stream 上的所有中间操作串联成一个调用链:
Stream 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() 可以将每个字符串转换为小写、大写或原始字符串的子字符串,或完全不同的东西。
List list = new ArrayList();
Stream stream = list.stream();
Stream streamMapped = stream.map((value) -> value.toUpperCase());
复制代码
filter
filter() 用于从 Stream 中过滤掉元素。 filter 方法接受一个 Predicate (也是一个函数式接口),filter() 为流中的每个元素调用 Predicate。如果元素要包含在 filter() 返回结果的流中,则 Predicate 应返回 true。如果不应包含该元素,则 Predicate 应返回 false。
Stream 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 方法。 在效果上看是把原来流中的每个元素进行了“展平”
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 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 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 方法,遍历并输出了每个单词。
One
flew
over
the
cuckoo's
nest
To
kill
a
muckingbird
Gone
with
the
wind
复制代码
distinct
distinct() 会返回一个仅包含原始流中不同元素的新 Stream 实例,任何重复的元素都将会被去掉。
List stringList = new ArrayList();
stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");
Stream stream = stringList.stream();
List distinctStrings = stream
.distinct()
.collect(Collectors.toList());
System.out.println(distinctStrings);
复制代码
在这个例子中,元素 "one" 在一开始的流中出现了两次,原始流应用 distinct 操作生成的新流中将会丢弃掉重复的元素,只保留一个 "one" 元素。所以这个例子最后的输出是:
[one, two, three]
复制代码
limit
limit 操作会截断原始流,返回最多只包含给定数量个元素的新流。
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class StreamLimitExample {
public static void main(String[] args) {
List stringList = new ArrayList();
stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");
Stream stream = stringList.stream();
stream.limit(2)
.forEach( element -> System.out.println(element));
}
}
复制代码
这个例子中,因为对原始流使用了 limit(2) 操作,所以只会返回包含两个元素的新流,随后使用 forEach 操作将它们打印了出来。程序最终将会输出:
one
two
复制代码
peek
peek() 方法是一个以 Consumer (java.util.function.Consumer,Consumer 代表的是消费元素但不返回任何值的方法) 作为参数的中间操作,它返回的流与原始流相同。当原始流中的元素开始迭代时,会调用 peek 方法中指定的 Consumer 实现对元素进行处理。
正如 peek 操作名称的含义一样,peek() 方法的目的是查看流中的元素,而不是转换它们。跟其他中间操作的方法一样,peek() 方法不会启动流中元素的内部迭代,流需要一个终结操作才能开始内部元素的迭代。
peek() 方法在流处理的 DEBUG 上的应用甚广,比如我们可以利用 peek() 方法输出流的中间值,方便我们的调试。
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());
复制代码
上面的例子会输出以下调试信息。
Filtered value: three
Mapped value: THREE
Filtered value: four
Mapped value: FOUR
复制代码
流的终结操作
Stream 的终结操作通常会返回单个值,一旦一个 Stream 实例上的终结操作被调用,流内部元素的迭代以及流处理调用链上的中间操作就会开始执行,当迭代结束后,终结操作的返回值将作为整个流处理的返回值被返回。
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。
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class StreamAnyMatchExample {
public static void main(String[] args) {
List 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 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。
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class StreamAllMatchExample {
public static void main(String[] args) {
List 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 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。
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class StreamNoneExample {
public static void main(String[] args) {
List stringList = new ArrayList<>();
stringList.add("abc");
stringList.add("def");
Stream stream = stringList.stream();
boolean noneMatch = stream.noneMatch((element) -> {
return "xyz".equals(element);
});
System.out.println("noneMatch = " + noneMatch); //输出 noneMatch = true
}
}
复制代码
collect
collect() 方法被调用后,会启动元素的内部迭代,并将流中的元素收集到集合或对象中。
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 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 stream = stringList.stream();
List 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 中元素的迭代,并对元素进行计数。
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 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 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 中的任何位置。且它不提供从流中的哪个位置获取元素的保证。
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 stringList = new ArrayList<>();
stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");
Stream stream = stringList.stream();
Optional 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() 方法检查是否找到了元素。
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 stringList = new ArrayList<>();
stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");
Stream stream = stringList.stream();
Optional 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。
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class StreamExamples {
public static void main(String[] args) {
List stringList = new ArrayList();
stringList.add("one");
stringList.add("two");
stringList.add("three");
Stream stream = stringList.stream();
stream.forEach(System.out::println);
}
}
复制代码
注意,上面例程中 forEach 的参数我们直接用了Lambda 表达式引用方法的简写形式。
min
min() 方法返回 Stream 中的最小元素。哪个元素最小是由传递给 min() 方法的 Comparator 接口实现来确定的。
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 stringList = new ArrayList<>();
stringList.add("abc");
stringList.add("def");
Stream stream = stringList.stream();
// 作为 min 方法参数的Lambda 表达式可以简写成 String::compareTo
// Optional min = stream.min(String::compareTo);
Optional min = stream.min((val1, val2) -> {
return val1.compareTo(val2);
});
String minString = min.get();
System.out.println(minString); // abc
}
}
复制代码
min() 方法返回的是一个 Optional ,也就是它可能不包含结果。如果为空,直接调用 Optional 的 get() 方法将抛出 异常--NoSuchElementException。比如我们把上面的 List 添加元素的两行代码注释掉后,运行程序就会报
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() 即可。
Optional min = stream.max(String::compareTo);
复制代码
reduce
reduce() 方法,是 Stream 的一个聚合方法,它可以把一个 Stream 的所有元素按照聚合函数聚合成一个结果。reduce()方法接收一个函数式接口 BinaryOperator 的实现,它定义的一个apply()方法,负责把上次累加的结果和本次的元素进行运算,并返回累加的结果。
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 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 stream = stringList.stream();
Optional reduced = stream.reduce((value, combinedValue) -> combinedValue + " + " + value);
// 写程序的时候记得别忘了 reduced.ifPresent() 检查结果里是否有值
System.out.println(reduced.get());
}
}
复制代码
reduce() 方法的返回值同样是一个 Optional 类的对象,所以在获取值前别忘了使用 ifPresent() 进行检查。
streadm 实现了多个版本的reduce() 方法,还有可以直接返回元素类型的版本,比如使用 reduce 实现整型Stream的元素的求和
import java.util.ArrayList;
import java.util.List;
public class IntegerStreamReduceSum {
public static void main(String[] args) {
List 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 求和,总和为%s\n", sum);
}
}
复制代码
toArray
toArray() 方法是一个流的终结操作,它会启动流中元素的内部迭代,并返回一个包含所有元素的 Object 数组。
List 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 stream = stringList.stream();
Object[] objects = stream.toArray();
复制代码
不过 toArray 还有一个重载方法,允许传入指定类型数组的构造方法,比如我们用 toArray 把流中的元素收集到字符串数组中,可以这么写:
String[] strArray = stream.toArray(String[]::new);
复制代码
流的拼接
Java 的Stream 接口包含一个名为 concat() 的静态方法,它可以将两个流连接成一个。
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 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 stream1 = stringList.stream();
List stringList2 = new ArrayList<>();
stringList2.add("Lord of the Rings");
stringList2.add("Planet of the Rats");
stringList2.add("Phantom Menace");
Stream stream2 = stringList2.stream();
Stream concatStream = Stream.concat(stream1, stream2);
List stringsAsUppercaseList = concatStream
.collect(Collectors.toList());
System.out.println(stringsAsUppercaseList);
}
}
复制代码
从数组创建流
上面关于 Stream 的例子我们都是从 Collection 实例的 stream() 方法获取的集合包含的所有元素的流,除了这种方法之外,Java 的 Stream 接口中提供了一个名为 of 的静态方法,能支持从单个,多个对象或者数组对象快速创建流。
import java.util.stream.Stream;
public class StreamExamples {
public static void main(String[] args) {
Stream stream1 = Stream.of("one", "two", "three");
Stream stream2 = Stream.of(new String[]{"one", "two"});
System.out.println(stream1.count()); // 输出3
System.out.println(stream2.count()); // 输出2
}
}
复制代码
总结
上面我们把 Stream 的两大类操作:流的中间操作、流的终结操作都有哪些方法给大家列举了一遍,让大家对 Stream 能完成的操作有了大致的印象。不过为了讲解这些操作用的都是非常简单的例子,流操作的数据也都是简单类型的,主要的目的是让大家能更快速地理解 Stream 的各种操作应用在数据上后,都有什么效果。
下一篇我会演示一些在项目开发中我们会高频用到的,使用 Stream 完成各种复杂操作的示例,让大家做项目的时候可以直接进行参考,进一步提升你用 Java 编程、开发项目的体验。