【翻译】Java 8 - 流
原文地址:Java 8 Streams by Examples
流可以定义为来自数据源的一个连续的元素序列,并支持对其元素进行各类操作。
这里的源指的是集合或数组,它们可以提供一个数据流。流会保持数据在源中的顺序。批量的操作是一种表达方法,可以简洁地描述我们需要对数据进行的操作。
在进一步讨论流之前,我们需要知道的是大部分Java 8中对流的操作都会返回且只返回一个流。这种设计有助于我们创建一个流操作链,这个链我们称之为管线。
一、流 vs. 集合
我们都在优酷或其他一些网站上看过在线视频。当我们开始观看视频时,我们的计算机会先下载视频开始的一小段进行播放,从而我们在开始播放视频时并不需要安全下载整个视频。这就叫作流。下面我们将讲述集合与流之间的差异。
在基础层面,集合和流的差异在于元素计算时的处理方式。集合是一个处于内存中的数据结构,所有的值都存放在内存中。集合中每一个元素参与计算之前都需要添加到集合中。流是一个概念上的数据结构,描述了将要计算的元素。这样可以给编程带来一些显著的优点。基于流,我们可以只从元素抽取我们需要的值,而这些元素的产生对使用者来说是不可见的。这也是一种生产者-消费者关系。
在Java中,java.util.Stream
描述了一个可以在其之上执行多个操作的流。流操作既可以是过程性操作也可以是终止性操作。终止性操作将会返回一个特定类型的数据,而过程性操作将返回流本身。因此,我们可以多个方法形成调用链。流可以在数据源头创建,如:一个java.util.Collection
的List或Set(Map不支持流)。流操作可以串行的,也可以是并行的。
基于以上的讨论,我们来总结一下流的一些特性:
+ 流不是一个数据结构
+ 流是为lambda表达式所设计
+ 流不支持通过下标进行访问
+ 流可以方便地以数组或列表的形式输出
+ 流支持延迟读取
+ 流可以并行化处理
二、不同的方式创建流
从集合中创建流有以下几种方式:
1) 使用Stream.of(val1, val2, val3,...)
import static java.util.stream.Stream.of; //静态包导入。.*结尾表示导入这个类里的静态方法。
import java.util.stream.Stream;
public class StreamBuilders {
public static void main(String[] args) {
Stream<Integer> stream =of(1,2,3,4,5,6,7,8,9);
stream.forEach(p -> {
System.out.print(p + " ");
});
}
}
2) 使用Stream.of(arrayOfElements)
import static java.util.stream.Stream.of; //静态包导入。.*结尾表示导入这个类里的静态方法。
import java.util.stream.Stream;
public class StreamBuilders {
public static void main(String[] args) {
Stream<Integer> stream = of(new Integer[]{1,2,3,4,5,6,7,8,9});
stream.forEach(p -> {
System.out.print(p + " ");
});
}
}
3) 使用someList.stream()
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class StreamBuilders {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list = Arrays.asList(1,2,3,4,5,6,7,8,9);
Stream<Integer> stream = list.stream();
stream.forEach(p -> {
System.out.print(p + " ");
});
}
}
4) 使用Stream.generate()
或Stream.iterate()
函数
import java.util.Date;
import java.util.stream.Stream;
import static java.util.stream.Stream.generate;
public class StreamBuilders {
public static void main(String[] args) {
Stream<Date> stream = generate(() -> {
return new Date();
});
stream.forEach(p -> System.out.println(p));
}
}
5) 使用String.chars()
或字符串tokens
import static java.util.stream.Stream.of;
import java.util.stream.Stream;
public class StreamBuilders {
public static void main(String[] args) {
IntStream stream = "12345_abcdefg".chars();
stream.forEach(p -> System.out.println(p));
//OR
Stream<String> streamStr = of("A$B$C".split("\\$"));
streamStr.forEach(p -> System.out.println(p));
}
}
三、将Stream
转换成Collection
1) 使用stream.collection(Collectors.toList())
将Stream
转换为List
public static void toList(){
List<Integer> list = new ArrayList<>();
list = Arrays.asList(1,2,3,4,5,6,7,8,9);
Stream<Integer> stream = list.stream();
List<Integer> evenNumbersList = stream.filter(i -> i % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbersList);
}
2) 使用stream.toArray(EntryType[]::new)
将Stream
转换为Array
public static void toArray(){
List<Integer> list = new ArrayList<>();
list = Arrays.asList(1,2,3,4,5,6,7,8,9);
Stream<Integer> stream = list.stream();
Integer[] evenNumbersArr = stream.filter(i -> i % 2 == 0).toArray(Integer[] :: new);
for(int i : evenNumbersArr)
System.out.print(i + " ");
}
Java提供了大量的方法用于将stream转换成set,map。了解更多的方法则需要进一步研究工具类Collectors并记住它。
四、流的核心操作
Stream
为我们抽象了很多有用的函数。下面我们讨论其中最重要的一些函数。
在讨论函数之前,让我们先创建一个字符串集合,后续的操作均基于这个字符串集合,以方便我们理解。
List<String> memberNames = new ArrayList<>();
memberNames.add("Amitabh");
memberNames.add("Shekhar");
memberNames.add("Aman");
memberNames.add("Rahul");
memberNames.add("Shahrukh");
memberNames.add("Salman");
memberNames.add("Yana");
memberNames.add("Lokesh");
下面将分两部分讨论相关的函数。
1、过程性操作
过程性操作返回流本身,因此我们可以创建操作链在同一行上完成多个操作。让我们来看其中最重要的一个。
A) filter()
filter()
接受一个条件,然后对流中的所有元素进行过滤。这个操作是过程性的,我们可以在函数返回结果上调用另一个流操作(如:forEach
)。
memberNames.stream().filter(s -> s.startsWith("A")).forEach(System.out::println);
输出结果为:
Amitabh
Aman
B) map()
过程性操作map()
将通过给定的函数将流中的元素转换成另一个类型。下面的代码将每个字符串类型转换成大写字符串。同样,我们可以使用map()
将每个元素转换成另一种类型。
memberNames.stream().filter(s -> s.startsWith("A"))
.map(String::toUpperCase)
.forEach(System.out::println);
输出结果为:
AMITABH
AMAN
C) sorted()
sorted()
将返回所有元素经过排序的流的视图。如果我们不传入一个定制的Comparator
,元素将按自然顺序排序。
memberNames.stream().sorted().forEach(System.out :: println);
输出结果为:
Aman
Amitabh
Lokesh
Rahul
Salman
Shahrukh
Shekhar
Yana
需要记住的是,sorted()
方法仅创建了一个流元素排序后的视图,并不对流背后的集合进行操作。
2、终止性操作
终止性操作是基于流返回特定类型的数据。
A) forEach()
forEach()
方法可以帮助我们遍历流中所有元素,并对其进行一些操作。操作可以通过lambda表达的形式传入。
memberNames.stream().forEach(System.out :: println);
B) collect()
collect()
方法用于从流中接收元素后将其存放在集合中。
List<String> memNamesInUppercase = memberNames.stream().sorted()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(memNamesInUppercase);
输出结果为:
[AMAN, AMITABH, LOKESH, RAHUL, SALMAN, SHAHRUKH, SHEKHAR, YANA]
C) match()
有多种用于检查流中的元素是否与特定的断言相匹配的操作。所有这些操作都是终止性的,且返回一个布尔值。
boolean matchResult = memberNames.stream()
.anyMatch(s -> s.startsWith("A"));
System.out.println(matchResult);
matchResult = memberNames.stream()
.allMatch(s -> s.startsWith("A"));
System.out.println(matchResult);
matchResult = memberNames.stream()
.noneMatch(s -> s.startsWith("A"));
System.out.println(matchResult);
输出结果为:
true
false
false
D) count()
count()
是用长整型值返回流中元素的个数。
long totalMatched = memberNames.stream()
.filter(s -> s.startsWith("A"))
.count();
System.out.println(totalMatched);
输出结果为:
2
E) reduce()
reduce()
操作用给定的函数来降低流元素的维度,它用Optional
来存储降维后值的引用。
Optional<String> reduced = memberNames.stream()
.reduce((s1,s2) -> s1 + "#" + s2);
reduced.ifPresent(System.out::println);
输出结果为:
Amitabh#Shekhar#Aman#Rahul#Shahrukh#Salman#Yana#Lokesh
五、短路操作
尽管,用于判断集合元素是否满足某个断言的流操作需要作用于所有元素上。但,经常希望在遍历过程中发现第一个匹配断言的元素时终止操作。在外部迭代中,我们可以使用if-else
块,在内部迭代中,我们可以使用特定的方法来达到同样的目的。让我们来看两个例子。
A) anyMatch()
这个方法一旦发现某个元素与断言相匹配则返回true
,它不会再处理余下的元素。
boolean matchResult = memberNames.stream()
.anyMatch(s -> s.startsWith("A"));
输出结果为:
true
B) findFirst()
这个方法将返回第一个与断言匹配的元素,并不再处理其他的元素。
String firstMatchedName = memberNames.stream()
.filter(s -> s.startsWith("L"))
.findFirst().get();
System.out.println(firstMatchedName);
输出结果为:
Lokesh
六、并行处理
Java 7中加入了Fork/Join框架,我们在应用开发时使用并行操作有了有效的机制。但实现这一框架是一项复杂的工作,稍有不慎就可能成为导致应用系统崩溃的复杂的多线程问题的源头。引入了内部迭代器,我们也有了更多并行操作的可能。
为了实现并行化处理,我们需要做的是创建一个并行流,用来替代串行流。令我们惊讶的是,实现起来非常简单。在上面列出的任一示例中,如何我们想要使用多线程去处理特定的工作时,我们所需要做的就是调用parallelStream()
方法来替代stream()
方法。
List<Integer> list = new ArrayList<>();
list = Arrays.asList(1,2,3,4,5,6,7,8,9);
Stream<Integer> stream = list.parallelStream();
Integer[] evenNumbersArr = stream.filter(s -> s % 2 == 0)
.toArray(Integer[]::new);
System.out.println(Arrays.toString(evenNumbersArr));
输出结果为:
[2, 4, 6, 8]
上述示例的关键在于并行对开发者来说变得非常简单和可用。随着Java平台对并行和同步提供了强大的支持,开发者将串行处理的代码调整成并行处理时不需要再面对过多的障碍。