Java中的流库
从迭代到流的操作
案例:在处理集合时,我们通常会迭代遍历它的元素,并在每个元素上执行某项操作,如下所示:
List<String> strs = Files.readAllLines(Paths.get("src\\eight\\a.txt"), StandardCharsets.UTF_8);
long count = strs.stream()
.filter(str -> str.length() < 12)
.count();
可以将stream
修改成parallelStream
就可以让流库以并行方式来执行过滤和计数。
List<String> strs = Files.readAllLines(Paths.get("src\\eight\\a.txt"), StandardCharsets.UTF_8);
long count = strs.parallelStream()
.filter(str -> str.length() < 12)
.count();
流遵循了做什么而非怎么做的原则,在流的示例中,我们描述了需要做什么;
流表面上看起来和集合很类似,都可以让我们转换和获取数据,但是它们之间存在着显著的差异:
- 流并不存储其元素。这些元素可能存储在底层的集合中,或者是按需生成的。
- 流的操作不会修改其数据源。例如,filter方法不会从流中移除元素,而是会生成一个新的流,其中不包含被过滤掉的元素。
- 流的操作是尽可能惰性执行的。这意味着直至需要其结果时,操作才会执行。如:我们指向查找前5个长单词而不是所有长单词,那么filter方法就会在匹配到第五个单词后停止过滤,因此,我们甚至可以操作无限流。
案例中的代码,是操作流时的典型流程。我们建立了一个包含三个阶段的操作管道:
- 创建一个流;
- 指定将初始流转换为其他流的中间操作,可能包含多个步骤;
- 应用终止操作,从而产生结果。这个操作会强制执行之前的惰性操作,从此之后,这个流便不能使用了。
流的创建
可以用Collection
接口的stream
方法将任何集合转换为一个流,如果有一个数组,那么可以使用静态的Stream.of
方法。
// 数组转换为流
String test = "123;312;123;1231;23123;123;";
Stream<String> split = Stream.of(test.split(";"));
// 可变长参数
Stream<String> stringStream = Stream.of("1", "2", "3");
// 空的流
Stream<Object> empty = Stream.empty();
Stream接口有两个用于创建无限流的静态方法。generate方法会接收一个不包含任何引元的函数(是一个Supplier<T>
接口的对象)。无论何时,只要需要一个流类型的值,该函数就会被用以产生一个这样的值。
Stream<String> generate = Stream.generate(() -> "qiao");
// 随机数
Stream<Double> generate1 = Stream.generate(Math::random);
如果想要产生像0,1,2,3。。。这样的序列,可以使用iterate方法。它会接收一个种子值,以及一个函数(UnaryOperation<T>
),并且会反复地将该函数应用到之前的结果上。
// 如果输出的话,会一直产生,无限制
Stream<BigInteger> iterate = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE));
有限序列,书上可以传入两个lambda
表达式的方法并没有找到。改写为一下代码:
Stream<BigInteger> iterates = Stream.iterate(BigInteger.ZERO,n -> n.add(BigInteger.ONE)).limit(100L);
filter
、map
和flatMap
方法
流的转换会产生一个新的流,它的元素派生自另一个流中的元素。下面我们将使用filter将字符串流转换为只包含长单词的流。
String test = "123;312;123;1231123123123;23123;123;";
Stream<String> strings = Stream.of(test);
Stream<String> stringStream = strings.filter(s -> s.length() > 12);
filter
的引元是Predicate<T>
,即从T到boolean
的函数。
通常,我们想要按照某种方式来转换流中的值,此时,可以使用map方法并传递执行该转换的函数,如下所示:
public class TestMain {
public static void main(String[] args) throws IOException {
List<User> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
users.add(new User("test" + i));
}
List<Account> collect = users.stream()
.map(user -> userToAccount(user))
.collect(Collectors.toList());
}
private static Account userToAccount(User user) {
return new Account(user.getName(), BigDecimal.ZERO);
}
}
public class User {
private String name;
// 省略构造器,get和set方法
}
public class Account {
private String accountName;
private BigDecimal balance;
// 省略构造器,get和set方法
}
如果我们想把多个相同元素的流合并为一个流,我们应该这样做:
public static void main(String[] args) throws IOException {
String a = "1231233123";
String b = "qwerqwerqwer";
String c = "adsfasdfadsf";
List<String> strings = new ArrayList<>();
strings.add(a);
strings.add(b);
strings.add(c);
// [[1,2,3,4],[1,2,3],[1,2,3]]
List<Stream<String>> collect = strings.stream()
.map(s -> codePoints(s))
.collect(Collectors.toList());
// [1,2,3,4,5,6,7]
List<String> collect1 = strings.stream()
.flatMap(s -> codePoints(s))
.collect(Collectors.toList());
}
private static Stream<String> codePoints(String str) {
List<String> result = new ArrayList<>();
int i = 0;
while (i < str.length()) {
int j = str.offsetByCodePoints(i, 1);
result.add(str.substring(i, j));
i = j;
}
return result.stream();
}
抽取子流和组合流
调用stream.limit(n)
会返回一个新的流,它在n
个元素之后结束,这个方法对于裁剪无限流的尺寸特别有用。
Stream<BigInteger> iterates = Stream.iterate(BigInteger.ZERO,n -> n.add(BigInteger.ONE)).limit(100L);
调用stream.skip(n)
正好相反,它会丢弃前n个元素。
List<String> collect2 = collect1.stream().skip(1).collect(Collectors.toList());
stream.takeWhile(prdicate)
调用会在谓词为真时获取流中的所有元素,然后停止。(JDK9
提供)
Stream<String> test = list.stream().takeWhilte(s -> "0123".contains(s));
其他的流转换
distinct
方法会返回一个流,它的元素是从原有流中产生的,即原来的元素按照同样的顺序剔除重复元素后产生的。这些重复元素并不一定是毗邻的
Stream.of("qwe", "qwe1", "qwe", "qwe1")
.distinct()
.forEach(System.out::println);
对于流的排序,有多种sorted
方法的变体可用。其中一种用于操作Comparable
元素的流,而另一种可以接收一个Comparator
。
Stream.of("qwe1", "qwe12", "qwe123", "qwe")
.sorted(Comparator.comparing(String::length).reversed())
.forEach(System.out::println);
与所有的流转换一样,sorted
方法会产生一个新的流,它的元素是原有流中按照排序排列的元素。
peek方法会产生另一个流,它的元素与原来流中的元素相同,但是在每次获取一个元素时,都会调用一个函数。
Object[] objects = Stream.iterate(1.0, p -> p * 2)
.peek(e -> System.out.println("Fetching " + e))
.limit(20).toArray();
当实际访问一个元素时,就会打印出来一条消息。通过这种方式,你可以验证iterate
返回的无限流时被惰性处理的。
简单约简
约简是一种终结操作,它们会将流约简为可以在程序中使用的非流值。
**count
方法会返回流中元素的数量,这是一种简单约简。**其他简单约简还有max
和min
,它们分别返回最大值和最小值。这些方法返回的是一个类型的Optional<T>
的值,它要么在其中包装了答案,要么表示没有任何值。在过去,碰到这种情况返回null是很常见的,但是这样做会导致在未做完备测试的程序中产生空指针异常。
如果获取流中的最大值:(min
方法同理)
Optional<Double> max = Stream.iterate(1.0, p -> p * 2)
.limit(3)
.max(Double::compareTo);
System.out.println(max.orElse(0.0));
**findFirst
返回的是非空集合中的第一个值,它通常在与filter
组合使用时很有用。**例如如何找到第一个以字母Q
开头的单词
Optional<String> q = list.stream()
.filter(s -> s.startsWith("Q"))
.findFirst();
如果不强调使用第一个匹配,而是使用任意的匹配都可以,那么就可以使用findAny
方法。这个方法在执行并行流时很有效。
Optional<String> optionalString = list.stream()
.filter(s -> s.startsWith("Q"))
.findFirst();
如果只想知道是否存在匹配,那么可以使用anyMatch
。这个方法会接受一个断言引元,因此不需要使用filter
。
boolean q = list.stream().parallel().anyMatch(s -> s.startsWith("Q"));
还有allMatch
和noneMatch
方法,它们分别在所有元素和没有任何元素匹配谓词的情况下返回true
。这些方法也可以通过并行运行而获益。
Optional
类型
Optional<T>
对象是一种包装器对象,要么包装了类型T的对象,要么没有包装任何对象。对于第一种情况,我们称这种值是存在的。
获取Optional
值
有效地使用Optional的关键是要使用这样的方法:它在值不存在的情况下会产生一个可替代物,而只有在值存在的情况下才会使用这个值。
optionalString.orElse("");
你还可以调用代码来计算默认值
optionalString.orElseGet(() -> String.valueOf(1));
或者在没有任何值时抛出异常:
optionalString.orElseThrow(RuntimeException::new);
消费Optional
值
ifPresent
方法会接受一个函数。如果可选值存在,那么它会被传递给该函数。否则,不会发生任何事情。
optionalString.ifPresent(s -> list.add(s));
如果可选值存在时执行一种动作,不存在时执行另一种动作,可以使用ifPresentOrElse
(JDK1.9
提供):
ifPresentOrElse(v -> System.out.println("f" + v), () -> logger.warning("NO match"));
管道化Optional
值
保持Optional
完整,使用map
方法来转换Optional
内部的值:如果optionalString
为空,那么s
也为空。
Optional<String> s = optionalString.map(String::toUpperCase);
我们将一个结果添加到列表中,如果它存在的话就添加,不存在则什么也不会发生
optionalString.map(list::add);
类似地,我们可以使用filter
方法来只处理那些在转换它之前或之后满足某种特定属性的Optional
值。如果不满足该属性,那么管道会产生空的结果:
Optional<String> transformed = optionalString.filter(s -> s.length() > 4)
.map(String::toUpperCase);
你也可以使用or
方法将空Optional
替换为一个可替代的Optional
。这个可替代值将以惰性方式计算。(JDK1.9
提供)
optionalString.or(() -> {});
不适合使用Optional
值的方式
如果没有正确地使用Optional
值,那么相比以往得到某物或null
的方式,你并没有任何好处。
创建Optional
值
Optional.empty();
Optional.of(obj);
收集结果
当处理完流之后,通常会想要查看其结果。因此可以调用iterator
方法打,它会产生用来访问元素的旧式风格的迭代器,或者使用forEach
方法,将某个函数应用于每个元素;
Stream.of("qwe1", "qwe12", "qwe123", "qwe")
.forEach(System.out::println);
我们可以使用toArray()
方法来创建指定类型的数组
String[] strs = stream.toArray(String::new);
针对将流中的元素收集到另一个目标中,有一个便捷方法collect
可用,它会接受一个Collector
接口的实例。
List<String> q = list.stream().filter(s -> s.startsWith("Q")).collect(Collectors.toList());
Set<String> q1 = list.stream().filter(s -> s.startsWith("Q")).collect(Collectors.toSet());
String q2 = list.stream().filter(s -> s.startsWith("Q")).collect(Collectors.joining());
String q3 = list.stream().filter(s -> s.startsWith("Q")).collect(Collectors.joining(","));
收集到映射表中
Collectors.toMap(key, value)
方法会收集为Map
集合
List<User> list = new ArrayList<>();
list.add(new User("qwe", 1));
list.add(new User("qwe1",2));
list.add(new User("qwe2",3));
list.add(new User("qwe3",4));
Map<String, Integer> collect = list.stream().collect(Collectors.toMap(User::getName, User::getNum));
群组和分区
使用Collectors.groupingBy()
进行分组
Map<Integer, List<User>> collect1 = list.stream().collect(Collectors.groupingBy(User::getNum));
输出结果
1=[eight.User@723279cf]
2=[eight.User@10f87f48, eight.User@b4c966a]
4=[eight.User@2f4d3709]
当分类函数是断言函数(返回Boolean
值)时,流的元素可以分为两个列表:该函数返回true
的元素和其他元素。这种情况下,使用partitioningBy
比使用groupingBy
更高效。
Map<Boolean, List<User>> collect = list.stream().collect(Collectors.partitioningBy(s -> s.getNum() > 2));
collect.entrySet().stream().forEach(System.out::println);
输出结果
false=[eight.User@12edcd21, eight.User@34c45dca, eight.User@52cc8049]
true=[eight.User@5b6f7412]
下游收集器
counting
会产生收集到的元素的个数。如
Map<Integer, Long> collect2 = list.stream().collect(Collectors.groupingBy(User::getNum, Collectors.counting()));
collect2.entrySet().stream().forEach(System.out::println);
summing(Int/Long/Double)会接受一个函数作为引元,将该函数应用到下游元素中,并产生它们的和。如
Map<Integer, Integer> collect = list.stream().collect(Collectors.groupingBy(User::getNum, Collectors.summingInt(User::getNum)));
collect.entrySet().stream().forEach(System.out::println);
maxBy
和minBy
会接收一个比较器,并分别产生下游元素中的最大值和最小值。如
Map<Integer, Optional<User>> collect = list.stream()
.collect(Collectors.groupingBy(User::getNum, Collectors.maxBy(Comparator.comparing(User::getNum))));
collectingAndThen
收集器在收集器后面添加了一个最终处理步骤。如:
Map<Integer, Integer> collect2 = list.stream()
.collect(Collectors.groupingBy(
User::getNum,
Collectors.collectingAndThen(Collectors.toSet(), Set::size)));