集合优化了对象的存储,而流则是对数据的处理
流是一系列与特定存储机制无关的元素,利用流,我们无需迭代集合中的元素,就可以提取和操作它们。这些管道通常被组合在一起,在流上形成一条操作管道。
使用流的一个核心好处是,它使得程序更加短小并且更易理解。同时流还可以同 Lambda 表达式和方法引用(method references)一起使用。
流一个重要的特性是:流是懒加载的,意味着它在绝对必要的情况下才计算。这个特性是我们可以表示非常大(甚至无限)的序列,而不用担心内存的问题。
流的支持
Java 设计者在设计流时面临着这样一个难题:现存的大量类库不仅为 Java 所用,同时也被应用在整个 Java 生态圈数百万行的代码中。如何将一个全新的流的概念融入到现有类库中呢?
Java 8 采用的解决方案是:在接口中添加被 default
(默认
)修饰的方法。通过这种方案,设计者们可以将流式(stream)方法平滑地嵌入到现有类中。流方法预置的操作几乎已满足了我们平常所有的需求。
流的操作有三种:创建流、修改流元素(中间操作)、消费流(终端操作)
流的创建:
流的创建方式有以下这几种方式:
-
Stream.of()
:需要传入一组元素参数。
public class StreamOf {
public static void main(String[] args) {
Stream.of("It's ", "a ", "wonderful ", "day ", "for ", "pie!");
.forEach(System.out::println);
}
}
// 中间需要传入一组元素参数
// 代码中传入的是一组字符串元素,forEach() 传入一个方法引用。
-
stream()
: 对于集合类,可以直接调用集合类中的stream()
方法:
List list = Arrays.asList("It's a wonderful day for pie!".split(" "))
list.stream()
.forEach(System.out::print);
-
Stream.generate()
: 需要传入一个 Supplier,Suppier 是一个通用的函数式接口,所以可以配合Lambda 使用。
// Stream.generate() 中间传入的参数 Generator 类实现Supplier 接口,
//所以实现的 get(),Stream.generate(new Generator()) 会自动调用 get() 产生所需要的流元素。
public class Generator implements Supplier {
Random rand = new Random(47);
char[] letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
public String get() {
return "" + letters[rand.nextInt(letters.length)];
}
public static void main(String[] args) {
String word = Stream.generate(new Generator())
.limit(30)
.collect(Collectors.joining());
System.out.println(word);
}
}
搭配 lambda 表达式使用:传入与 supplier 接口中 get() 方法签名相同的方法引用
Stream.generate(() -> "duplicate")
.limit(3)
.forEach(System.out::println);
// 输出
// “duplicate”
// “duplicate”
// “duplicate”
-
Stream.iterate()
: 该函数有两个参数,以种子(第一个参数)开头,并将其传给方法(第二个参数,需要传入一个方法引用)。方法的结果将添加到流,并存储作为第一个参数用于下次调用 iterate()。
基本类型流的生成
-
InStream.range()
: 生成整型序列的流,range()
需要传入两个参数,指定一个序列范围。 -
LongStream
:生成 Long 序列的流 -
DoubleStream
:生成 double 序列的流
public class Ranges {
public static void main(String[] args) {
System.out.println(range(10, 20).sum());
}
}
// sum() :用于将流中的元素相加求和
数组流生成
-
Arrays.stream(数组)
:利用Arrays
类中含有一个名为stream()
的静态方法将数组转换成为流
Arrays.stream(new double[] { 3.14159, 2.718, 1.618 })
.forEach(n -> System.out.format("%f ", n));
同时也可以
Arrays.stream(new int[] { 1, 3, 5, 7, 15, 28, 37 }, 3, 6)
此时Arrays.stream() 的调用有两个额外的参数。第一个参数告诉 stream() 从数组的哪个位置开始选择元素,第二个参数用于告知在哪里停止。
-
splitAsStream()
:Java 8 在 java.util.regex.Pattern 中增加的一个新的方法。这个方法可以根据传入的公式将字符序列转化为流。但是有一个限制,输入只能是 CharSequence,因此不能将流作为 splitAsStream() 的参数。
中间操作
中间操作,用于从一个流中获取对象,并进行处理,再将对象作为另一个流从后端输出,以连接到其他操作。
查看流中元素
peek()
: 该操作的目的是帮助调试。它允许你无修改地查看流中的元素。因为 peek()
符合无返回值的 Consumer
函数式接口,所以我们只能观察,无法使用不同的元素来替换流中的对象
排序
sorted()
操作排序,使用默认比较器实现,另一种形式是传入一个比较器。另一种形式的实现:传入一个 Comparator 参数。
移除元素
distinct()
: 可用于消除流中的重复元素。相比创建一个 Set 集合,该方法的工作量要少得多。
filter(Predicate)
:过滤操作会保留与传递进去的过滤器函数计算结果为 true 元素。
isPrime()
作为过滤器函数,用于检测质数。
应用函数到元素
map()
会获取流中的所有元素,并且对流中元素应用函数操作从而产生新的元素,并将其传递到后续的流中。mapToInt(ToIntFunction)
:操作同上,但结果是IntStream
。mapToLong(ToLongFunction)
:操作同上,但结果是LongStream
。mapToDouble(ToDoubleFunction)
:操作同上,但结果是DoubleStream
。
使用 map 也可以产生和接收类型不同的类型
map(类::new)
传入一个类的构造器。可以把接收类型转换为其他类。例如 int 转成一个自定义的类型。
在 flatMap 中组合流
map() 函数的功能是产生流,而如果我们传入的元素是流,则会产生元素流的流。
此时可以利用 flatMap()
:该函数功能与 map
一样。但不同是该函数会把流扁平化为元素,最终产生的是元素。
public class FlatMap {
public static void main(String[] args) {
Stream.of(1, 2, 3)
.flatMap(i -> Stream.of("Gonzo", "Fozzie", "Beaker"))
.forEach(System.out::println);
}
}
其他操作:
ints()
: 方法产生一个流并且 ints() 方法有多种方式的重载 — 两个参数限定了数值产生的边界.
相应其他数据类型的操作:longs()limit()
: 操作获取指定数量的元素
limit(8)
:或者前 8 个元素。forEach()
方法遍历输出,它根据传递给它的函数对每个流对象执行操作。
通常使用该方法传入System.out::println
来遍历输出流中的每个元素。boxed()
: 流操作将会自动地把基本类型包装成为对应的装箱类型, 对应解箱 unboxed()skip()
操作 : 它根据参数丢弃指定数量的流元素
Optional 对象
一些标准流返回 Optional
对象,此对象可以作为流的持有者,即使查看的元素不存在也能友好的提示我们。
流中的一些操作方法
findFirst() 返回一个包含第一个元素的 Optional 对象,如果流为空则返回 Optional.empty
findAny() 返回包含任意元素的 Optional 对象,如果流为空则返回 Optional.empty
max() 和 min() 返回一个包含最大值或者最小值的 Optional 对象,如果流为空则返回 Optional.empty
class OptionalsFromEmptyStreams {
public static void main(String[] args) {
System.out.println(Stream.empty()
.findFirst());
}
}
// 输出:Optional.empty
当流为空时,返回 Optional.empty
对象。
空流可以通过 Stream.
创建,也可以运用 Stream.empty()
,使用此方法时,需要赋值给一个指定的类型的变量,不然 java 编辑器无法进行类型推导,即
Stream s = Stream.empty()
当接收到 Optional
对象时,应首先调用 isPresent()
检查其中是否包含元素。如果存在,可使用 get() 获取。
class OptionalBasics {
static void test(Optional optString) {
if(optString.isPresent())
System.out.println(optString.get());
else
System.out.println("Nothing inside!");
}
public static void main(String[] args) {
test(Stream.of("Epithets").findFirst());
test(Stream.empty().findFirst());
}
}
便利函数
通常对于流返回的 Optional
对象,我们需要进行解包,才能获取到里面的元素。
流中提供了一些便利函数,用于解包 Optional
对象,简化了 “对所包含的对象的检查和执行操作”:
-
ifPresent(Consumer)
:当值存在时调用Consumer
,否则什么也不做。 -
orElse(otherObject)
:如果值存在则直接返回,否则生成otherObject
。 -
orElseGet(Supplier)
:如果值存在则直接返回,否则使用Supplier
函数生成一个可替代对象。 -
orElseThrow(Supplier)
:如果值存在直接返回,否则使用Supplier
函数生成一个异常。
创建 Optional
当需要在代码中添加 Optional
对象时,可以利用 Optional
对象的静态方法来创建:
-
empty()
:生成一个空 Optional。 -
of(value)
:将一个非空值包装到 Optional 里。 -
ofNullable(value)
:针对一个可能为空的值,为空时自动生成Optional.empty
,否则将值包装在Optional
中。
Optional 对象的操作
当流管道生成了 Optional
对象,可以使用下列一些方法对对象进行操作。
filter(Predicate)
:将Predicate
应用于Optional
中的内容并返回结果。当Optional
不满足Predicate
时返回空。如果Optional
为空,则直接返回。map(Function)
:如果Optional
不为空,应用Function
于Optional
中的内容,并返回结果。否则直接返回Optional.empty
。flatMap(Function)
:同map()
,但是提供的映射函数将结果包装在Optional
对象中,因此flatMap()
不会在最后进行任何包装。
一般来说,流的 filter()
会在 Predicate
返回 false
时移除流元素。而Optional.filter()
在失败时不会删除 Optional
,而是将其保留下来,并转化为空。
终端操作
数组
-
toArray()
:将流转换成适当类型的数组。 -
toArray(generator)
:在特殊情况下,生成自定义类型的数组。
循环
-
forEach(Consumer)
常见如System.out::println
作为 Consumer 函数。 -
forEachOrdered(Consumer)
: 保证forEach
按照原始流顺序操作
集合
-
collect(Collector)
:使用Collector
收集流元素到结果集合中。 -
collect(Supplier, BiConsumer, BiConsumer)
:同上,第一个参数Supplier
创建了一个新结果集合,第二个参数BiConsumer
将下一个元素包含到结果中,第三个参数BiConsumer
用于将两个值组合起来。
Collectors 还有其他更加复杂的实现,可以通过查看文档了解。
例如:实现将流元素收集到 TreeSet 中的集合。可以利用将集合的的构造函数引用传递给 Collectors.toCollection(),从而构建任何类型的集合
public class TreeSetOfWords {
public static void
main(String[] args) throws Exception {
Set words2 =
Files.lines(Paths.get("TreeSetOfWords.java"))
.flatMap(s -> Arrays.stream(s.split("\\W+")))
.filter(s -> !s.matches("\\d+")) // No numbers
.map(String::trim)
.filter(s -> s.length() > 2)
.limit(100)
.collect(Collectors.toCollection(TreeSet::new));
System.out.println(words2);
}
}
Files.lines() 打开 Path 并将其转换成为行流。
组合
-
reduce(BinaryOperator)
:使用BinaryOperator
来组合所有流中的元素。因为流可能为空,其返回值为Optional
。 -
reduce(identity, BinaryOperator)
:功能同上,但是使用 identity 作为其组合的初始值。因此如果流为空,identity 就是结果。 -
reduce(identity, BiFunction, BinaryOperator)
:更复杂的使用形式(暂不介绍),这里把它包含在内,因为它可以提高效率。通常,我们可以显式地组合 map() 和 reduce() 来更简单的表达它。
匹配
-
allMatch(Predicate)
:如果流的每个元素根据提供的 Predicate 都返回 true 时,结果返回为 true。在第一个 false 时,则停止执行计算。 -
anyMatch(Predicate)
:如果流中的任意一个元素根据提供的 Predicate 返回 true 时,结果返回为 true。在第一个 false 是停止执行计算。 -
noneMatch(Predicate)
:如果流的每个元素根据提供的 Predicate 都返回 false 时,结果返回为 true。在第一个 true 时停止执行计算。
查找
-
findFirst()
:返回第一个流元素的 Optional,如果流为空返回 Optional.empty。 -
findAny()
:返回含有任意流元素的 Optional,如果流为空返回 Optional.empty。
信息
-
count()
:流中的元素个数。
max(Comparator):根据所传入的 Comparator 所决定的“最大”元素。 -
min(Comparator)
:根据所传入的 Comparator 所决定的“最小”元素。
数字流信息
-
average()
:求取流元素平均值。 -
max() 和 min()
:数值流操作无需 Comparator。 -
sum()
:对所有流元素进行求和。 -
summaryStatistics()
:生成可能有用的数据。目前并不太清楚这个方法存在的必要性,因为我们其实可以用更直接的方法获得需要的数据。
collect() 收集操作,它根据参数来组合所有流中的元素。
当传入的参数是Collectors.joining()
,你将会得到一个 String 类型的结果,每个元素都根据 joining() 的参数来进行分割。其他不同的 Collectors 的参数则会产生不同的结果