Java流(Stream)详解
1. Java8对流的支持
流是一个与任何特定的存储机制都没有关系的元素序列。不同于在集合中遍历元素,使用流的时候,我们是从一个管道中抽取元素,并对它们逬行操作。这些管道通常会被串联到一起,形成这个流上的一个操作管线。
流的一个核心优点是,它们能使我们的程序更小,也更好理解。当配合流使用时,Lambda 表达式和方法引用就发挥出其威力了。流大大提升了 Java 8 的吸引力。
假设我们想按照有序方式显示随机选择的5~20范围内的,不重复的整数,借助流我们只需要说明想做什么即可实现:
package stream;
import java.util.Random;
public class Randoms {
public static void main(String[] args) {
new Random(47)
.ints(5, 20)
.distinct().limit(5)
.sorted()
.forEach(System.out::println);
/*
* 6
* 10
* 13
* 16
* 18
*/
}
}
我们先为 Random
对象设置一个随机种子,ints()
方法会生成一个流,该方法有多个重载版本,其中两个参数的版本可以设置所生成值的上下界。使用中间流操作 distinct()
去掉重复的值,再使用 limit()
选择前5个值,sorted()
表示元素是有序的,最后我们想显示每一个条目,所以使用了 forEach()
,它会根据我们传递的函数,在每个流对象上执行一个操作。这里我们传递了一个方法引用 System.out::println
,用于将每个条目显示在控制台上。
使用流实现的代码我们看不到任何显式的迭代机制,因此成为内部迭代,这是流编程的一个核心特性。
2. 流的创建
使用 Stream.of()
,可以轻松地将一组条目变成一个流:
package stream;
import java.util.stream.Stream;
public class StreamOf {
public static void main(String[] args) {
Stream.of(4, 1, 7).forEach(System.out::println);
Stream.of("Hello ", "World ", "AsanoSaki").forEach(System.out::print);
/*
* 4
* 1
* 7
* Hello World AsanoSaki
*/
}
}
此外,每个 Collection
都可以使用 stream()
方法来生成一个流:
package stream;
import java.util.*;
import java.util.stream.Collectors;
class A {
int x;
A(int x) { this.x = x; }
}
public class CollectionToStream {
public static void main(String[] args) {
List<A> listA = Arrays.asList(new A(1), new A(2), new A(3));
System.out.println(listA.stream().mapToInt(a -> a.x).sum());
Set<String> st = new TreeSet<>(Arrays.asList("Hello world and java".split(" ")));
System.out.println(st.stream().map(String::toUpperCase).collect(Collectors.joining(" ")));
Map<String, Double> mp = new HashMap<>();
mp.put("PI", 3.14);
mp.put("E", 2.718);
mp.put("PHI", 1.618);
mp.entrySet().stream()
.map(e -> e.getKey() + ": " + e.getValue())
.forEach(System.out::println);
/*
* 6
* HELLO AND JAVA WORLD
* PHI: 1.618
* E: 2.718
* PI: 3.14
*/
}
}
在创建了一个 List<A>
之后,只需要调用一下 stream()
这个所有集合类都有的方法。中间的 map()
操作接受流中的毎个元素,在其上应用一个操作来创建一个新的元素,然后将这个新元素沿着流继续传递下去,这里的 mapToInt()
将一个对象流转变成了一个包含 Integer
的 IntStream
。对于 Float
和 Double
也有名字类似的操作。
collect()
操作会根据其参数将所有的流元素组合起来,当我们使用 Collectors.joining()
时,得到的结果是一个 String
,每个元素都会以 joining()
的参数分隔,还有其他很多 Collectors
,可以生成不同的结果。
为了从 Map
集合生成一个流,我们首先调用 entrySet()
来生成一个对象流,其中每个对象都包含着一个键和与其关联的值,然后再使用 getKey()
和 getValue()
将其分开。
2.1 随机数流
Ramdom
类在 Java 8 引入了流,有一组可以生成流的方法:
package stream;
import java.util.Random;
import java.util.stream.Stream;
public class RandomGenerators {
public static <T> void show(Stream<T> stream) {
stream.limit(3).map(x -> x + " ").forEach(System.out::print);
System.out.println();
}
public static void main(String[] args) {
Random rand = new Random(47);
show(rand.ints().boxed());
show(rand.doubles().boxed());
show(rand.ints(10, 20).boxed()); // 设置上下边界
show(rand.ints(2).boxed()); // 设置流的大小
show(rand.ints(2, 10, 20).boxed()); // 设置流的大小与上下边界
/*
* -1172028779 1717241110 -2014573909
* 0.053412216308810656 0.5779976127815049 0.4170137422770571
* 17 18 18
* 1122537102 491149179
* 19 18
*/
}
}
为消除冗余代码,上面的示例创建了泛型方法 show(Stream<T> stream)
,这个特性在之后会讲。类型参数 T
可以是任何东西,所以使用 Integer
、Long
和 Double
都可以。然而,Random
类只会生成 int
、long
和 double
等基本类型的值。幸运的是,boxed()
流操作会自动将基本类型转换为其对应的包装器类型,使得 show()
能够接受这个流。
2.2 整型的区间范围
IntStream
类提供了一个 range()
方法,可以生成一个流(由 int
值组成的序列),在编写循环时非常方便:
package stream;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class IntStreamRange {
public static void main(String[] args) {
// for-in循环
for (int i: IntStream.range(1, 5).toArray())
System.out.print(i + " "); // 1 2 3 4
System.out.println();
// Stream
System.out.println(IntStream.range(1, 5).boxed().map(Object::toString).collect(Collectors.joining(" "))); // 1 2 3 4
IntStream.range(1, 5).boxed().forEach(x -> System.out.print(x + " ")); // 1 2 3 4
System.out.println();
}
}
现在我们编写一个 repeat()
工具函数取代简单的 for
循环:
package stream;
import java.util.stream.IntStream;
public class Repeat {
static void repeat(int n, Runnable action) {
IntStream.range(0, n).forEach(i -> action.run());
}
static void hello() {
System.out.println("Hello");
}
public static void main(String[] args) {
repeat(3, () -> System.out.println("Lambda"));
repeat(2, Repeat::hello);
/*
* Lambda
* Lambda
* Lambda
* Hello
* Hello
*/
}
}
2.3 Stream.generate()
Stream.generate()
可以接受任何的 Supplier<T>
(java.util.function
中的接口),并生成一个由 T
类型的对象组成的流。如果想创建一个由完全相同的对象组成的流,只需要将一个生成这些对象的 Lambda 表达式传给 generate()
:
package stream;
import java.util.Random;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Generator implements Supplier<String> {
Random rand = new Random(47);
char[] letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
@Override
public String get() {
return String.valueOf(letters[rand.nextInt(letters.length)]);
}
public static void main(String[] args) {
String str = Stream.generate(new Generator()).limit(10).collect(Collectors.joining());
System.out.println(str); // YNZBRNYGCF
Stream.generate(() -> "AsanoSaki").limit(2).forEach(s -> System.out.print(s + " ")); // AsanoSaki AsanoSaki
}
}