当前位置:网站首页>流式编程:流支持,创建,中间操作以及终端操作
流式编程:流支持,创建,中间操作以及终端操作
2022-06-21 09:28:00 【凄戚】
流式编程
前言
集合优化了对象的存储,而流(Streams)则是关于一组组对象的处理。
流是与任何特定存储机制无关的元素序列——实际上,我们说流是没有存储的。
- 取代了在集合中迭代(Iterator)元素的做法,使用流(Streams)可以从管道中提取元素并对其操作。这些管道通常被串联在一起形成了一整套的管线,而流对它们进行操作。
- 大多数情况下,你将对象存储在集合中就是为了处理它们,因此当你编程时你会发现你的主要焦点会从集合转移到流上。
- 流的一个核心好处是:它使得你的程序更短小和更容易去理解。当将 Lambda 表达式和方法引用与流一起使用时你会发现它们自成一体。流使得 Java 8 机具吸引力(真的很棒)。
举个例子,如果你要随机展示5-20 之间的不重复的整数进行排序(4个条件)。
你的想法一开始可能是这样的,首先关注使用哪个有序集合,然后围绕着这个集合进行后续的操作。但是使用流式编程,你就可以这样做:
public class Randoms {
public static void main(String[] args) {
new Random(47)
.ints(5, 20)
.distinct()
.limit(7)
.sorted()
.forEach(System.out::println);
}
}
/* output: 6 10 13 16 17 18 19 */
这真的太简洁了,我们只需要简单称述自己要做什么就可以。
首先我们给Random 对象一个种子值, ints()方法产生一个流并且ints()方法有多种方式的重载—–两个参数限定了产生的数值的边界。这将产生一个随机整数流。之后我们再用流的中间操作distinct()使流中的元素不重复,然后limit()取前七个元素。之后排序并遍历输出,forEach()根据传递给它的函数对流中的每个对象执行操作。
注意:上述示例中没有声明任何变量。流可以在不曾使用赋值或可变数据的情况下,对现有的系统进行建模,这非常有用。
我们之前做的更多地是命令式编程的形式(指明每一步如何做),而现在使用的声明式编程(Declarative Programming)——它声明了要做什么,而不是指明如何做。
public class ImperativeRandoms {
public static void main(String[] args) {
Random rd = new Random(47);
SortedSet<Integer> rints = new TreeSet<>();
while (rints.size() < 7) {
int r = rd.nextInt(20);
if (r < 5) {
//如果满足条件,直接跳出这次循环
continue;
}
rints.add(r);
}
System.out.println(rints);
}
}
/* output: [7, 8, 9, 11, 15, 16, 18] */
我们应该很清楚的看出这两个代码的区别。在 Randoms.java 中,我们不必去定义任何变量,但是在这个示例中我们定义了3个,并且代码更复杂,且nextInt() 方法没有下界,效率不高。
最重要的是:你必须研究代码才能清楚这个示例在做什么,而在Random.java 中,代码会直接告诉你它在做什么。
像在 ImperativeRandoms.java 中那样显示编写迭代过程的方式称为外部迭代。而在 Random.java 中是内部迭代,这是流式编程的核心特征。内部迭代可以产生的代码可读性更强,而且能更简单地使用多核处理器。通过放弃对迭代过程的控制,我们可以把控制权交给并行化机制。
另一个重要方面:流是懒加载的。这代表它只会在绝对必要时才计算。你可以将流看做“延迟列表”,由于计算延迟,流使我们能够表示非常大(甚至无限)的序列,而不需要考虑内存问题。
一、流支持
Java 设计者们面临着这样一个难题:如何将一个全新的流概念融入到现有的类库中呢?通过在之前的类库中添加更多地方法,只要不改变原有的方法,现有代码就不会受到干扰。
其次问题是使用接口的类库。如果将一个新方法添加到接口,那么所有实现这个接口的类库就得改变。Java 8 的解决方案是:在接口中添加被 default 修饰的方法。所以你在一些流接口中会发现一些 default 方法,这种方法不需要子类单独实现,其预置的操作几乎已经满足了我们平常所有的需求。通过这种方案,设计者们可以将流式方法平滑地嵌入到现有类中。
流操作的类型有三种:创建流,修改流元素(中间操作),消费流元素(终端操作)。最后一种类型通常意味着收集流元素(将操作完的流汇入一个集合)。
二、流创建
public class StreamOf {
public static void main(String[] args) {
Stream.of(new Bubble(1), new Bubble(2), new Bubble(3))
.forEach(System.out::println);
Stream.of("It's ", "a ", "wonderful ", "day ", "for ", "pie!")
.forEach(System.out::print);
System.out.println();
Stream.of(3.1415926, 2.718, 1.618)
.forEach(System.out::println);
System.out.println("=====================================");
List<Bubble> bubbles = Arrays.asList(new Bubble(3), new Bubble(4), new Bubble(5));
System.out.println(bubbles.stream()
.mapToInt(b -> b.i)
.sum());
Set<String> w = new HashSet<>(Arrays.asList("It's a wonderful day for pie!".split(" ")));
w.stream()
.map(x -> x + " ")
.forEach(System.out::print);
System.out.println();
Map<String, Double> m = new HashMap<>();
m.put("pi", 3.14159);
m.put("e", 2.718);
m.put("phi", 1.618);
m.entrySet()
.stream()
.map(e->e.getKey() + ":" + e.getValue())
.forEach(System.out::println);
}
}
/* output: Bubble{i=1} Bubble{i=2} Bubble{i=3} It's a wonderful day for pie! 3.1415926 2.718 1.618 ===================================== 12 a pie! It's for wonderful day phi:1.618 e:2.718 pi:3.14159 */
我们可以通过Stream.of()很容易地将一组元素转化为流。除此之外,每个集合都可以通过调用stream()方法来产生一个流。
- 在创建
List<Bubble>对象之后,我们只需要调用所有集合中都有的stream()。 - 中间操作
map()会获取流中的所有元素,并且对流中元素实施操作去创建一个新的元素,之后将其传递回流中。通常map()会获取对象并产生新的对象,但在这里产生了特殊的用于数值类型的流。例如:mapToInt()方法将一个对象流转换成为包含整型数字的IntStream。 - 为了从Map 集合中产生流数据,我们首先调用
entrySet()产生一个对象流,每个对象都包含一个key键以及其相关联的 value 值。然后分别调用getKey()和getValue()。
2.1 随机数流
public class RandomGenerators {
public static <T> void show(Stream<T> stream) {
stream
.limit(4)
.forEach(System.out::println);
System.out.println("+++++++++++++++");
}
public static void main(String[] args) {
Random rd = new Random(47);
/* 不控制上下限,如果没有show方法中的limit,将会无限产生IntStream中的值。 此处boxed方法之前已经生成了流,只是是基本数据类型的(如IntStream),而boxed 可以将基本数据类型进行包装(Stream<Integer>) */
show(rd.ints().boxed());
show(rd.longs().boxed());
show(rd.doubles().boxed());
//控制上下限
show(rd.ints(10, 20).boxed());
show(rd.longs(50, 100).boxed());
show(rd.doubles(20, 30).boxed());
//控制流大小
show(rd.ints(2).boxed());
show(rd.longs(2).boxed());
show(rd.doubles(2).boxed());
//控制流的大小和界限
show(rd.ints(2, 10, 20).boxed());
show(rd.longs(2, 50, 100).boxed());
show(rd.doubles(2, 20, 30).boxed());
}
}
/* output: -1172028779 1717241110 -2014573909 229403722 +++++++++++++++ 2955289354441303771 3476817843704654257 -8917117694134521474 4941259272818818752 +++++++++++++++ 0.2613610344283964 0.0508673570556899 0.8037155449603999 0.7620665811558285 +++++++++++++++ 16 10 11 12 +++++++++++++++ 65 99 54 58 +++++++++++++++ 29.86777681078574 24.83968447804611 20.09247112332014 24.046793846338723 +++++++++++++++ 1169976606 1947946283 +++++++++++++++ 2970202997824602425 -2325326920272830366 +++++++++++++++ 0.7024254510631527 0.6648552384607359 +++++++++++++++ 17 18 +++++++++++++++ 81 86 +++++++++++++++ 21.898377705316413 23.22662025293785 +++++++++++++++ */
为了消除冗余代码,这里使用了泛型方法来创建不同类型的流。其实这里Random类只能生成基本类型的流,但是:boxed()流操作会自动地将基本类型包装成为对应的装箱类型,从而使得show() 方法可以调用。
我们可以使用Random 为任意对象集合创建 Supplier。
public class RandomWords implements Supplier<String> {
List<String> words = new ArrayList<>();
Random rand = new Random(47);
RandomWords(String fname) throws IOException {
List<String> lines = Files.readAllLines(Paths.get(fname));
//略过第一行
for (String line : lines.subList(1, lines.size())) {
for (String word : line.split("[ .?,]+")) {
words.add(word);
}
}
}
@Override
public String get() {
return words.get(rand.nextInt(words.size()));
}
public static void main(String[] args) throws IOException {
System.out.println(
Stream.generate(new RandomWords("C:\\Users\\gt136\\Downloads\\Documents\\cheese.txt"))
.limit(10)
.collect(Collectors.joining(" ")));
}
}
/* cheese.txt // streams/Cheese.dat Not much of a cheese shop really, is it? Finest in the district, sir. And what leads you to that conclusion? Well, it's so clean. It's certainly uncontaminated by cheese. output: it shop sir the much cheese by conclusion district is */
- 在这里可以看到
split()更复杂的运用。在构造器中:每一行都被split 方法通过方括号内的空格或其他标点符号分割。方括号后面的+号表示它前面的东西可以出现一次或多次。 collect()操作会根据参数来结合所有的流元素。当你用Collectors.joining()作为它的参数时,将会得到一个String 类型的结果:即流中的所有元素被joining()中的参数隔开。generate()方法可以把任意Supplier<T>用于生成 T 类型的无序的流,而重写的get() 方法是collect 方法去调用的。
2.2 int 类型的range
public class Range {
//产生从start-end逐步step增长的序列
public static int[] range(int start, int end, int step) {
if (step == 0) {
throw new IllegalArgumentException("Step cannot be zero!");
}
//操作范围
int sz = Math.max(0,step >= 0 ? (end + step -1 -start) / step : (end + step + 1 -start) / step);
int[] result = new int[sz];
for (int i = 0; i < sz; i++) {
result[i] = start + (i * step);
}
return result;
}
//产生从start 到 end 依次递增 1 的序列
public static int[] range(int start, int end) {
return range(start, end, 1);
}
//产生从0 到 n 依次递增 1 的序列
public static int[] range(int n) {
return range(0, n);
}
}
IntStream 类提供了 range()方法用于生成整型:
import static java.util.stream.IntStream.*;
public class Ranges {
public static void repeat(int n, Runnable action) {
range(0, n).forEach(i->action.run());
}
public static void main(String[] args) {
//传统方法
int result = 0;
for (int i = 10; i < 20; i++) {
result += i;
}
System.out.println(result);
//for-in 循环
result = 0;
for (int i : range(10, 20).toArray()) {
result += i;
}
System.out.println(result);
//使用流
System.out.println(range(10,20).sum());
//使用流但是不适用IntStream中的方法
System.out.println(new Random()
.ints(10, 20)
.limit(5).sum());
repeat(3,()-> System.out.println("Looping!"));
}
}
在主方法中的第一种方式是我们传统编写 for 循环的方式;第二种方式用 range()方法(提前引入了包)创建了流并将其转化为数组,然后在 for-in 代码块中使用。但是第三种是全部使用流。
注意:IntStream.range() 相比之前的 Range.range() 受很多限制。这是由于其可选的第三个参数,后者允许步长大于一(前者是固定的1),并且可以从大到小来生成。
2.3 generate()方法可以把任意Supplier<T>用于生成 T 类型的无序的流
public class Generator implements Supplier<String> {
Random rand = new Random(47);
char[] letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
@Override
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);
//可以产生 T 对象包含相同数据的流(因为相当于只有一个值)
Stream.generate(() -> "duplicate")
.limit(3)
.forEach(System.out::println);
Stream.generate(Bubble::bubbler)
.limit(5)
.forEach(System.out::println);
}
}
/* output: YNZBRNYGCFOWZNTCQRGSEGZMMJMROE duplicate duplicate duplicate Bubble{i=0} Bubble{i=1} Bubble{i=2} Bubble{i=3} Bubble{i=4} */
参照 RandomWords.java 中的 Stream.generate() 搭配 Supplier<T>的用法,要记得它可以生成任意T类型的流,同样是collect 调用了get() 方法。
简单总结一下:new Random().ints() 可以产生一个随机数值序列,而 Stream.generate() 则可以在提供的集合或者对象中产生一个随机序列。
如果要创建包含相同对象的流,只需要传递一个生成那些对象的 lambda 到 generate()中,就像上面最后一句表达式那样。
2.4 iterate()
Stream.iterate() 产生的流的第一个元素是种子(iterate 方法的第一个参数),然后将种子传递给方法(iterate 方法的第二个参数)。方法运行的结果被添加到流(作为流的下一个元素),并被存储起来,作为下次调用 iterate()方法的第一个参数,以此类推。我们可以利用它生成一个斐波那契数列。
public class Fibonacci {
int x = 1;
Stream<Integer> number() {
return Stream.iterate(0, i -> {
int result = x + i;
x = i;
return result;
});
}
public static void main(String[] args) {
new Fibonacci().number()
.skip(20)//过滤前20个
.limit(10) //然后取10个
.forEach(System.out::println);
}
}
/* output: 6765 10946 17711 28657 46368 75025 121393 196418 317811 514229 */
三、流的建造者模式
在建造者模式(Builder design pattern)中,首先①创建一个builder 对象,然后②将创建流所需的多个信息传递给它,最后③** builder 对象执行“创建”流**的操作。Stream 库提供了这样的 Builder,下面是改写上面示例的代码:
public class FileToWordsBuilder {
//builder方法返回了一个Builder的实现类
Stream.Builder<String> builder = Stream.builder();
public FileToWordsBuilder(String filePath) throws IOException {
Files.lines(Paths.get(filePath))
.skip(1)//跳过第一行
.forEach(line->{
for (String w : line.split("[ .?,]+")) {
builder.add(w);
}
});
}
Stream<String> stream() {
return builder.build();
}
public static void main(String[] args) throws IOException {
new FileToWordsBuilder("cheese.txt")
.stream()
.limit(7)
.map(w -> w + " ")
.forEach(System.out::print);
}
}
/* output: Not much of a cheese shop really */
注意:构造器会添加文件中的所有单词(除了第一行),但是,并没有调用builder()。只要你不调用stream()方法,就可以继续向 builder 对象中添加单词。
在该类的更完整形式中,你可以添加一个标志位用于查看 build() 是否被调用,并且可能的话增加一个可以添加更多单词的方法。在 Stream.builder 调用 build() 方法后继续尝试添加单词会产生一个异常。
3.1 Arrays
Arrays 类中含有一个一个名为 stream() 的静态方法用于吧数组转换成流。主方法用于创建一个流,并将 excute() 应用于每个元素。
public interface Operations {
void execute();
static void runOps(Operations... ops) {
for(Operations op : ops)
op.execute();
}
static void show(String msg) {
System.out.println(msg);
}
}
/*************************************/
public class Machine2 {
public static void main(String[] args) {
Arrays.stream(new Operations[]{
() -> Operations.show("Bing"),
() -> Operations.show("Crack"),
() -> Operations.show("Twist"),
() -> Operations.show("Pop")
}).forEach(Operations::execute);
}
}
/* output: Bing Crack Twist Pop */
new Operations[] 动态地创建了 Operations对象的数组。
stream() 同样的可以产生IntStream, LongStream和DoubleStream。
public class ArrayStreams {
public static void main(String[] args) {
Arrays.stream(new double[]{
3.14159, 2.718, 1.618 })
.forEach(n->System.out.format("%f ", n));
System.out.println();
Arrays.stream(new int[]{
1, 3, 5})
.forEach(n -> System.out.format("%d ", n));
System.out.println();
Arrays.stream(new long[]{
11, 22, 33, 44, 66})
.forEach(n -> System.out.format("%d ", n));
System.out.println();
//选择一个子域
Arrays.stream(new int[]{
1, 3, 5, 7, 8, 10, 12}, 3, 6)
.forEach(n -> System.out.format("%d ", n));
}
}
/* output: 3.141590 2.718000 1.618000 1 3 5 11 22 33 44 66 7 8 10 */
最后一个 stream()的方法的调用有两个额外的参数,第一个参数告诉 stream() 从数组的那个位置开始选择元素,第二个参数是停止位置。
3.2 正则表达式
Java 8 在 java.util.regex.Pattern中增加了一个新的方法splitAsStream()。这个方法可以根据传入的公式将字符序列转化为流。但有一个限制:输入只能是CharSequence,因此不能将流作为它的参数。
public class FileToWordsRegexp {
private String all;
public FileToWordsRegexp(String filepath) throws IOException {
this.all = Files.lines(Paths.get(filepath))
.skip(1)
.collect(Collectors.joining(" "));
}
public Stream<String> stream() {
return Pattern
.compile("[ .,?]+")
.splitAsStream(all);//splitAsStream() 只能接收charSequence 对象,但是String 符合这种要求
}
public static void main(String[] args) throws IOException {
FileToWordsRegexp fw = new FileToWordsRegexp("cheese.txt");
fw.stream()
.limit(7)
.map(m -> m + " ")
.forEach(System.out::print);
fw.stream()
.skip(7)
.limit(2)
.map(x -> x + " ")
.forEach(System.out::print);
}
}
/*output: Not much of a cheese shop really is it */
这一次我们使用流将文件转换为一个字符串,接着使用正则表达式将字符串转化为单词流。
在构造器中读取了文件中的所有内容,当调用stream()的时候,可以像往常一样获取一个流,但这回我们可以多次调用stream(),每次都从已存储的的字符串中创建一个新的流。这里有个限制,就是整个文件必须存储在内存中;很多时候这并并不是什么问题,但是这丢失了流操作的重要优势:
- “不需要把流存储起来。”当然,流确实需要一部分内存存储,但只会存储序列的一小部分,而不是整个序列。
- 它们是懒加载计算的
后面我们会提到解决方案。就在4.5
四、中间操作
中间操作用于从一个流中获取对象,并将对象作为另一个流从后端输出,以连接到其他地方。
4.1 跟踪和调试
peek()操作的目的是帮助调试。它允许你无修改的查看流中的元素。
public class Peeking {
public static void main(String[] args) throws IOException {
FileToWordsBuilder fw = new FileToWordsBuilder("C:\\Users\\gt136\\Downloads\\Documents\\cheese.txt");
fw.stream()
.skip(21)
.limit(4)
.map(w -> w + " ")
.peek(System.out::print)
.map(String::toUpperCase)
.peek(System.out::print)
.map(String::toLowerCase)
.forEach(System.out::print);
}
}
/* output: Well WELL well it's IT'S it's so SO so clean CLEAN clean */
刚开始对这个流的打印很迷惑,如果你单步查看这个流的执行过程,会发现声明式编程的特点:它是全部按照声明执行完,最后才将你需要的流呈现给你,而中间的操作你看不到。
4.2 流元素排序
fw2.stream()
.skip(10)
.limit(10)
.sorted(Comparator.reverseOrder())
.map(w -> w + " ")
.forEach(System.out::print);
之前我们认识了sorted()方法,但是在这里是它的另一种实现形式:传入了一个Comparator 参数。
4.3 移除元素
public class Prime {
public static Boolean isPrime(long n) {
return rangeClosed(2, (long) Math.sqrt(n))//产生一个从第一个参数到第二个参数依次递增一的序列,包含了初始值与上限值
.noneMatch(i -> n % i == 0);
}
public static LongStream numbers() {
return iterate(2, i -> i + 1)//产生一个从 2 开始的无限序列
.filter(Prime::isPrime);
}
public static void main(String[] args) {
new Prime().numbers()
.limit(10)
.forEach(n -> System.out.format("%d ", n));
System.out.println();
new Prime().numbers()
.skip(90)
.limit(10)
.forEach(n-> System.out.format("%d ",n));
}
}
/* output: 2 3 5 7 11 13 17 19 23 29 467 479 487 491 499 503 509 521 523 541 */
distinct():可以消除重复元素,相比创建一个Set来处理,该方法要简便的多。filter(Predicate):过滤操作,保留如下元素:若元素传递给过滤函数产生的结果为 true 。
上述代码中的的isPrime()是用来检测质数的,rangeClosed(long startInclusive, final long endInclusive)包含了初始与上限值。这个函数的意思是如果不能整除,则 noneMatch()返回true,如果为0,返回false,且这个方法操作一旦有错误就会退出。
4.4 应用函数到元素
map(Function):将函数操作应用在输入流的元素中,并将返回值传递到输出流中。mapToInt(ToIntFuntion):操作同上,但结果是 IntStream 。mapToLong(ToLongFunction):操作同上,但结果是LongStream。mapToDouble(ToDoubleFunction):操作同上,但结果是DoubleStream。
public class FunctionMap {
static String[] elements = {
"12", "", "23", "34"};
//将数组转换为流
static Stream<String> testStream() {
return Arrays.stream(elements);
}
static void test(String descr, Function<String, String> func) {
System.out.println("---( " + descr + ")---");
testStream()
.map(func)
.forEach(System.out::println);
}
public static void main(String[] args) {
//使原数据变为我们想要的形式
test("add brackets", s -> "[" + s + "]");
test("Increment",s->{
try {
return Integer.parseInt(s) + 1 + "";
} catch (NumberFormatException e) {
return s;
}
});
}
}
/* output: ---( add brackets)--- [12] [] [23] [34] ---( Increment)--- 13 24 35 */
在上面的自增示例中,我们将一个字符串转化为整数,如果不能被转化称为整数就会抛出异常,此时就将原始字符串放到输出流中。map()将一个字符串映射为另一个字符串,但是我们完全可以产生和接受类型完全不同的类型,从而改变流的数据类型。只需要将流中的单个数据进行处理,比如:.map(Numbered::new),Numbered 的构造器接收元素。
4.5 在 map() 中组合流
假设现在有一个传入的元素流,并且打算对流元素使用map()函数,但是问题来了,这些函数功能是产生一个流:我们想要产生一个元素流,而实际上却产生了一个元素流的流!请注意:这里的操作是针对传入的是流而不是前面示例中的函数类型
flatMap():做了两件事:将产生流的函数应用到每个元素上(与map()所做的相同),然后将每个流都扁平化为元素,因此它最终产生的仅仅是元素。flatMap(Function):当Function产生流时使用。flatMapToInt(Function):当 Function 产生 IntStream 时使用。flatMapToLong(Function):当 Function 产生 LongStream 时使用。flatMapToDouble(Function):当 Function 产生 DoubleStream 时使用。
为了弄清楚它的工作原理,从传入一个刻意设计的函数给map()开始。
/** * 流中流 其中的 map 方法 :public final <R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper) {} * 这个map中的参数是个 Function:也就是说会调用其中的apply方法,这个是自己实现的;而 Function 中的参数,第一个是指我们输入流的类型, * 就是说如果你这个流的类型时 Integer 型,那这个就是 Integer 型;第二个参数是指你返回的新的流的类型,这个也可以自己定义。 * @Date 2021/6/17 16:41 * @Created by gt136 */
public class StreamOfStreams {
public static void main(String[] args) {
Stream.of(1,2,3)
/*.map(new Function<Integer, Stream<String>>() { @Override public Stream<String> apply(Integer integer) { return Stream.of("null"); } })*/
.map(i->Stream.of("Gonzo","Kermit","Breaker"))
.map(e->e.getClass().getName())
.forEach(System.out::println);
}
}
/* output: java.util.stream.ReferencePipeline$Head java.util.stream.ReferencePipeline$Head java.util.stream.ReferencePipeline$Head */
我们希望能够得到字符串流,但是实际得到的却是“Head”流的流。我们可以使用flatMap()来解决
Stream.of(1, 2, 3)
.flatMap(i -> Stream.of("Gonzo", "Kermit", "Breaker"))
.forEach(System.out::println);
/* output: Gonzo Kermit Breaker Gonzo Kermit Breaker Gonzo Kermit Breaker */
这里从映射返回的每一个元素流转化为了元素。
下面是另一个演示,从一个整数流开始,然后使用每一个整数元素去创建更多的随机数。
Stream.of(1, 2, 3, 4, 5)
.flatMapToInt(i -> IntStream.concat(
rand.ints(0, 100).limit(i), IntStream.of(-1)))
.forEach(n -> System.out.format("%d ", n));
/*output: 58 -1 55 93 -1 61 61 29 -1 68 0 22 7 -1 88 28 51 89 9 -1 */
在这里引入了concat(),它以参数顺序组合两个流。
再次考虑一下之前的将文件划分为单词流的任务。最后使用的是 FileToWordsRegexp.java,它的问题是需要将整个文件读入行列表中—需要存储好该列表。
而我们需要的是创建一个不需要中间存储层的单词流:
public class FileToWords {
public static Stream<String> stream(String filePath) throws IOException {
return Files.lines(Paths.get(filePath))
.skip(1)
.flatMap(line -> Pattern.compile("\\W+").splitAsStream(line));
}
}
stream()现在是一个静态方法,因为它可以自己完成整个流的创建过程。
注意:\\W+是一个正则表达式。表示“非单词字符”,小写形式的w表示“单词字符”。
上述代码中的Pattern.compile().splitAsStream()产生的结果为流,这意味着当我们只是想要一个简单的单词流时,在传入的行流上调用map()会产生一个单词流的流。所以解决的方案就是:使用flatMap()将元素流的流扁平化为一个简单的元素流,或者,我们可以使用String.split()生成一个数组,其可以被Arrays.stream()转化为流。
因为有了真正的流,而不是流的流,所以每一次需要新的流时,我们都必须从头创建,因为流不能被复用。
因为我们在操作流的时候可能会遇到“空流“,而解决空流的方式就是使用 Optional 类。所以这里如果有兴趣可以看这一篇Optional类使用
五、终端操作
以下操作将会获取流的最终结果。至此我们无法再继续往后传递流。可以说:终端操作是我们可以在流管道中做的最后一件事。
5.1 数组
toArray():将流转换成适当类型的数组。toArray(generator):在特殊情况下,生成自定义类型的数组。
当我们需要的到数组类型的数据以便于后续操作时,上面的方法就很有用。
public class RandInts {
private static int[] rints = new Random(47)
.ints(0, 1000)
.limit(100)
.toArray();//将流转化为数组
public static IntStream rands() {
return Arrays.stream(rints);//将数组转化为流
}
}
上例将100 个数值范围在 0 - 1000 之间的随机数流转换为数组并将其存储在 rints 中。这样一来,每次调用 rands() 的时候可以重复获取相同的整数流。
5.2 循环
forEach(Consumer):常见如:System.out::println作为Consumer 函数。forEachOrdered(Consumer):保证forEach按照原始流顺序操作。
第一种形式:无序操作,仅在引入并行流时才有意义,这里只简单介绍 parallel():可以实现多处理器并行操作,实现原理为将流分割为多个(通常数目为CPU核心数)并在不同的处理器上分别执行操作。因为我们采取的是内部迭代,所以这是可以实现的。
parallel() 看似简单,实则棘手。更多的将在并发编程中整理。
下面引入parallel() 来帮助理解forEachOrdered(Consumer)的作用和使用场景。
public class ForEach {
static final int SZ = 14;
public static void main(String[] args) {
rands().limit(SZ)
.forEach(value -> System.out.format("%d ", value));
System.out.println(
);
rands().limit(SZ)
.parallel()
.forEach(n -> System.out.format("%d ", n));
System.out.println();
rands().limit(SZ)
.parallel()
.forEachOrdered(n -> System.out.format("%d ", n));
}
}
/* output: 258 555 693 861 961 429 868 200 522 207 288 128 551 589 551 589 861 288 555 868 693 207 128 200 961 429 258 522 258 555 693 861 961 429 868 200 522 207 288 128 551 589 */
在第一个流中,未使用parallel(),因此以元素从 rands() 出来的顺序输出结果。在第二个流中,引入parallel(),即使流很小,输出的结果也产生了差异,这是由于多处理器并行操作的结果,如果多运行几次,你会发现输出都不一样。
最后一个流中,同时使用parallel()和forEachOrdered(Consumer)来强制保持原始流数据。因此对于非并行流使用后一个方法没有影响。
5.3 集合
collect(Collector):使用 Collector 收集流元素到结果集中。collect(Supplier,BiConsumer,BiConsumer):同上,第一个参数创建了一个新的结果集合,第二个参数将下一个元素收集到结果集中,第三个参数用于将两个结果集合并起来。
假设我们现在的需求是为了保证元素有序,将元素存储在 TreeSet 中。虽然 Collectors 里面没有特定的 toTreeSet(),但是我们可以通过将集合的构造函数引用传递给Collectors.toCollection(),从而构建任何类型的集合。
public class TreeSetOfWords {
public static void main(String[] args) throws IOException {
Set<String> words2 = Files.lines(Paths.get("C:\\demo\\src\\main\\com\\thingInJava\\streamsProgram\\optional\\TreeSetOfWords.java"))
.flatMap(s-> Arrays.stream(s.split("\\W+")))//split将根据参数将String切为数组,
/*.flatMap(new Function<String, Stream<String>>() { @Override public Stream<String> apply(String s) { System.out.println(s); return Arrays.stream(s.split("\\W+")); } })*/
.filter(s->!s.matches("\\d+"))
.map(String::trim)//去掉字符串中的“ ”
.filter(s->s.length()>2)
.limit(100)
.collect(Collectors.toCollection(TreeSet::new));
System.out.println(words2);
}
}
/* output: [Arrays, Collectors, Downloads, Files, IOException, Paths, Set, String, System, TreeSet, TreeSetOfWords, Users, args, class, collect, com, demo, file, filter, flatMap, get, import, java, length, limit, lines, main, map, matches, new, nio, optional, out, package, println, public, split, src, static, stream, streamsProgram, thingInJava, throws, toCollection, trim, util, void, words2] */
整个的执行流程为:Files.lines() 打开 Path 并将其转换为由行组成的流。下一行代码从流集合中取出一行(String)进行分割(数组)并重写转化为流,最后flatMap 将各行形成的多个单词流,扁平映射成一个单词流。下一行移除全部是数字的字符串,然后 trim 去除单词两边的空格,filter方法过滤所有长度小于 3 的单词,并取最后结果的前100个将它们保存在TreeSet 中。
我们也可以在流中生成 Map。
class Pair{
public final Character c;
public final Integer i;
public Pair(Character c, Integer i) {
this.c = c;
this.i = i;
}
@Override
public String toString() {
return "Pair{" +
"c=" + c +
", i=" + i +
'}';
}
public Character getC() {
return c;
}
public Integer getI() {
return i;
}
}
class RandomPair{
Random rand = new Random(47);
//随机大写字母的无限迭代器,调用iterator方法就会返回这个流元素的迭代器;在这里也就是这些大写子母的流的迭代器
Iterator<Character> captures = rand.ints(65, 91)
.mapToObj(i -> (char) i)
.iterator();
public Stream<Pair> stream() {
return rand.ints(100, 1000)
.distinct()
//因为上面是随机产生了int型的流并去重,所以,对应的mapToObject的内部参数为IntFunction<Object>(),
// 它的默认方法是public Object apply(int value) {return null;};在这里我们将Object改为我们需要的Pair类型
.mapToObj(i -> new Pair(captures.next(), i));
}
}
public class MapCollector {
public static void main(String[] args) {
Map<Integer, Character> map = new RandomPair().stream()
.limit(8)
.collect(Collectors.toMap(Pair::getI, Pair::getC));
System.out.println(map);
}
}
/* output: {688=W, 309=C, 293=B, 761=N, 858=N, 668=G, 622=F, 751=N} */
RandomPair 创建了随机生成的 Pair 对象流,在 Java 中,我们不能直接以某种方式组合两个流。所以这里创建了一个整数流,并使用mapToObj()将整数流转化为 Pair 流。capChars 的随机大写字母迭代器创建了流,然后 next() 让我们可以在 stream() 中使用了这个流。据作者所知,这是将多个流组合成一个新的对象流的唯一方法。
在这里,我们只使用最简单形式的 Collectors.toMap(),这个方法只需要两个从流中获取键和值的函数。还有其他重载形式,其中一种是当键发生冲突时,使用一个函数来处理冲突。
大多数情况下,java.util.stream.Collectors预设的Collector就能满足我们的要求。除此之外,你还可以使用第二种形式的 collect()。
public class SpecialCollector {
public static void main(String[] args) throws IOException {
ArrayList<String> words = FileToWords.stream("C:\\Users\\gt136\\Downloads\\Documents\\cheese.txt")
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
words.stream()
.filter(s->s.equals("cheese"))
.forEach(System.out::println);
}
}
/* output: cheese cheese */
5.4 组合
reduce(BinaryOperator):使用 BinaryOperator 来组合所有流中的元素。因为流可能为空,其返回值为 Optional。reduce(identity,BinaryOperator):功能同上,但是使用 identity 作为其组合的初始值。因为如果流为空,identity 就是结果。reduce(identity,BFunction,BinaryOperator):更复杂的使用形式,这里把它包含在内,因为它可以提高效率。通常,我们可以显式的组合map()和reduce()来更简单的表达它。
class Frobnitz {
int size;
Frobnitz(int size){
this.size = size;
}
@Override
public String toString() {
return "Frobnitz{" +
"size=" + size +
'}';
}
static Random rand = new Random(47);
static final int BOUND = 100;
//随机生成100以内的
static Frobnitz supply() {
return new Frobnitz(rand.nextInt(BOUND));
}
}
public class Reduce {
public static void main(String[] args) {
Stream.generate(Frobnitz::supply)
.limit(10)
.peek(System.out::println)
//reduce方法的作用是将一次遍历流中的元素,第一次的时候第一个参数为null,第二个参数为第一个流元素,之后第一个参数皆为上次操作后的元素,第二个参数为下一个流元素
.reduce((fr0, fr1) -> fr0.size < 50 ? fr0 : fr1)
.ifPresent(System.out::println);//到29之前的时候都不满足上面的方法,所以值一直在被更替,之后满足后就一直是这个小于50的29
}
}
/* output: Frobnitz{size=58} Frobnitz{size=55} Frobnitz{size=93} Frobnitz{size=61} Frobnitz{size=61} Frobnitz{size=29} Frobnitz{size=68} Frobnitz{size=0} Frobnitz{size=22} Frobnitz{size=7} Frobnitz{size=29} */
因为supply()方法作为一个 Supplier 是签名兼容的,我们可以把supply方法作为一个方法引用传递给 Stream.generate()(这种签名叫做结构一致性)。我们使用了没有“初始值”作为第一个参数的reduce()方法,所以产生的结果是 Optional 类型。Optional.ifPresent()方法只有在结果非空时才会调用Consumer<>(println 方法可以被调用是因为 Frobnitz 可以通过 toString 方法转化为String)。
Lambda 表达式的第一个参数 fr0 是reduce() 是上一次调用的结果,fr1 是从流传递过来的值。reduce()中的Lambda 表达式使用了三元表达式,当 fr0 的 size 值小于50 时,将 fr0 作为结果,否则将序列中的fr1 当做结果。当取得第一个size值小于50的 Frobnitz,只要得到这个结果就会忽略流中其他元素,这是个非常奇怪的限制。
5.5 查找
findFirst():返回第一个流元素的 Optional,如果流为空返回 Optional.empty.findAny():返回含有任意流元素的 Optional,如果流为空返回 Optional.empty。
import static com.gui.demo.thingInJava.streamsProgram.optional.RandInts.*;
public class SelectElement {
public static void main(String[] args) {
//rands方法将数组转化为流,findFirst
System.out.println(rands().findFirst().getAsInt());
System.out.println(rands().parallel().findFirst().getAsInt());
System.out.println(rands().findAny().getAsInt());
System.out.println(rands().parallel().findAny().getAsInt());
}
}
/* output: 258 258 258 242 */
无论流是否为并行化,findFirst()总是会选择流中的第一个元素。对于非并行流,findAny()会选择流中的第一个元素(即使从定义上来看是选择任意元素)。在这个例子中,用parallel()将流并行化,以展示findAny()不选择流的第一个元素的可能性。
如果必需选择流中最后一个元素,那就使用reduce()。代码如下:
public class LastElement {
public static void main(String[] args) {
OptionalInt last = IntStream.range(10, 20)
.reduce((n1, n2) -> n2);
System.out.println(last.orElse(-1));
Optional<String> lastobj = Stream.of("one", "two", "three")
.reduce((n1, n2) -> n2);
System.out.println(lastobj.orElse("Nothing there"));
}
}
/* output: 19 three */
reduce()的参数只是用最后一个元素替换了最后两个元素,最终只生成最后一个元素。如果是数字流,你必须使用相近的数字 Optional 类型,否则使用 Optional 类型,就像上例中的 Optional<String>。
5.6 信息
count():流中的元素个数。max(Comparator):根据传入的Comparator 所决定的“最大”元素。min(Comparator):根据传入的 Comparator 所决定的“最小”元素。
String 类型有预设的 Comparator 实现。
public class Informational {
public static void main(String[] args) throws IOException {
System.out.println(FileToWords.stream("C:\\Users\\gt136\\Downloads\\Documents\\cheese.txt").count());
System.out.println(FileToWords.stream("C:\\Users\\gt136\\Downloads\\Documents\\cheese.txt")
.min(String.CASE_INSENSITIVE_ORDER)
.orElse("NONE"));
System.out.println(FileToWords.stream("C:\\Users\\gt136\\Downloads\\Documents\\cheese.txt")
.max(String.CASE_INSENSITIVE_ORDER)
.orElse("NONE"));
}
}
/* output: 32 a you */
min 方法和 max 方法的返回类型为Optional,这需要我们使用orElse()来解包。
5.7 数字流信息
average():求取流元素平均值。max()和min():数值流操作无需 Comparator。sum():对所有流元素进行求和。summaryStatistics():生成可能有用的数据。(没什么用)
public class NumericStreamInfo {
public static void main(String[] args) {
System.out.println(rands().average().getAsDouble());
System.out.println(rands().max().getAsInt());
System.out.println(rands().min().getAsInt());
System.out.println(rands().sum());
System.out.println(rands().summaryStatistics());
}
}
/* output: 507.94 998 8 50794 IntSummaryStatistics{count=100, sum=50794, min=8, average=507.940000, max=998} */
5.8 匹配
allMatch(predicate):如果流中的每个元素提供给Predicate 都返回 true,结果返回为true。在第一个 false 时,则停止执行计算。anyMatch(predicate):如果流的任意一个元素提供给 Predicate 返回true,结果返回 true。在第一个true 时停止计算。noneMatch(predicate):如果流的每个元素提供给Predicate都返回false时,结果返回为true。在第一个true时停止执行计算。
我们已经在之前见到过 noneMatch()的使用,其他两个都类似。为了消除冗余,我们创建了show()。首先我们必须知道如何统一的描述这三个匹配器的操作,然后再将其转化为 Matcher 接口。
interface Matcher extends BiPredicate<Stream<Integer>, Predicate<Integer>> {
}
public class Matching {
static void show(Matcher match, int val) {
System.out.println(
match.test(
IntStream.rangeClosed(1, 9)
.boxed()
.peek(n -> System.out.format("%d ", n)),
n -> n < val));
}
public static void main(String[] args) {
show(Stream::allMatch, 10);
show(Stream::allMatch, 4);
show(Stream::anyMatch, 2);
show(Stream::anyMatch, 0);
show(Stream::noneMatch, 5);
show(Stream::noneMatch, 0);
}
}
/* output: 1 2 3 4 5 6 7 8 9 true 1 2 3 4 false 1 true 1 2 3 4 5 6 7 8 9 false 1 false 1 2 3 4 5 6 7 8 9 true */
BiPredicate 是一个二元谓词,它接受两个参数并返回 true 或者 false。第一个参数是我们要测试的流,第二个参数是一个谓词 Predicate。Matcher 可以匹配所有的 Stream::Match 方法,所以可以将每一个Stream::Match 方法引用传递到show方法中,对match.test()的调用会被转换成对方法引用 Stream::Match 的调用。
show()的参数 val 在判断测试 n < val 中指定了最大值。show方法生产了整数 1-9 组成的一个流。peek()用来查看测试短路之前测试进行到了哪一步。从输出中可以看出每次都发生了短路。
边栏推荐
- Application configuration management, basic principle analysis
- 【实战】STM32MP157开发教程之FreeRTOS系统篇6:FreeRTOS 列表和列表项
- Junit5 unit test
- [JUC series] completionservice of executor framework
- android 数据库升级
- Form Validation
- Topic34——31. Next spread
- Introduction to list operation in C #
- Pingcap was selected as the "voice of customers" of Gartner cloud database in 2022, and won the highest score of "outstanding performer"
- Storage of floating point numbers in C language in memory
猜你喜欢

stm32mp1 Cortex M4开发篇13:扩展板按键外部中断
![[actual combat] STM32 FreeRTOS migration series tutorial 7: FreeRTOS event flag group](/img/1c/10add042271c11cd129ddfce66f719.jpg)
[actual combat] STM32 FreeRTOS migration series tutorial 7: FreeRTOS event flag group

Stm32mp1 cortex M4 Development Chapter 11: expansion board buzzer control

Zhihu wanzan: what kind of programmers are still wanted by the company after the age of 35? Breaking the "middle age crisis" of programmers

Application configuration management, basic principle analysis

TC software outline design document (mobile group control)

The skill of using ADB and the principle of USB communication

123. deep and shallow copy of JS implementation -- code text explanation

stm32mp1 Cortex M4开发篇8:扩展板LED灯控制实验
![[vs], [usage problem], [solution] when VS2010 is opened, it stays in the startup interface](/img/04/a7455760caa4fc0480a034de1e24b8.png)
[vs], [usage problem], [solution] when VS2010 is opened, it stays in the startup interface
随机推荐
【实战】STM32 FreeRTOS移植系列教程2:FreeRTOS 互斥信号量
【实战】STM32 FreeRTOS移植系列教程4:FreeRTOS 软件定时器
R language uses as The character function converts date vector data to string (character) vector data
leetcode:19. Delete the penultimate node of the linked list
Embedded remote post, part-time job, order receiving, crowdsourcing platform
[practice] stm32mp157 development tutorial FreeRTOS system 3: FreeRTOS counting semaphore
Wechat applet
R language obtains help information of global, package and function: use the rsitesearch function to search the information of the specified package or function in the R community help manual and arch
stm32mp1 Cortex M4开发篇10:扩展板数码管控制
MOOC course of Nanjing University of Technology: Fundamentals of program design (Ⅰ) Chapter 8 answer and analysis of multiple choice test questions
R language factor variable type: use factor function to convert string vector to factor vector, and use as The factor function converts a factor vector into a string vector and uses as The numeric fun
123. deep and shallow copy of JS implementation -- code text explanation
The spring recruitment is also terrible. Ali asked at the beginning of the interview: how to design a high concurrency system? I just split
远程办公市场调查报告
一条命令开启监控之旅!
Alibaba P6 employees came to a small company for an interview and asked for an annual salary increase of 500000 yuan. How dare you speak
Binary search (non recursive, no repeating elements)
Zhihu wanzan: what kind of programmers are still wanted by the company after the age of 35? Breaking the "middle age crisis" of programmers
Qsort sort string
Compiling 32-bit programs using cmake on 64 bit machines