Java8 流接口有终端操作和非终端操作可供选择。非终端流操作是向流中添加监听器而不做任何其他操作的操作。终端流操作是一种启动元素内部迭代、调用所有监听器并返回结果的操作。
下面是一个 Java 流示例,其中包含一个非终端操作和一个终端操作:
package com.hxstrive.jdk8.stream_api; import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; /** * 终端操作和非终端操作 * @author hxstrive.com */ public class StreamExamples { public static void main(String[] args) { List<String> stringList = new ArrayList<String>(); stringList.add("ONE"); stringList.add("TWO"); stringList.add("THREE"); Stream<String> stream = stringList.stream(); long count = stream // 非终端操作 .map((value) -> { return value.toLowerCase(); }) // 终端操作 .count(); System.out.println("count = " + count); //结果:count = 3 } }
上述示例,调用流接口的 map() 方法是一种非终端操作,它只是在流上设置一个 lambda 表达式,将每个元素转换为小写。而对 count() 方法的调用是一个终端操作,该调用会启动内部迭代,将每个元素转换为小写,然后进行计数。
Java Stream API 的非终端流操作是对流中的元素进行转换或过滤的操作。向流添加非终端操作时,会得到一个新流作为结果。新数据流代表原始数据流在应用非终端操作后产生的元素流。下面是一个向数据流添加非终端操作的示例,操作后会产生一个新的数据流:
package com.hxstrive.jdk8.stream_api; import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; /** * 非终端操作 * @author hxstrive.com */ public class StreamExamples2 { public static void main(String[] args) { List<String> stringList = new ArrayList<String>(); stringList.add("ONE"); stringList.add("TWO"); stringList.add("THREE"); Stream<String> stream = stringList.stream(); // 在流上添加 map 非终端操作 // 注意:非终端操作不会执行,只有等到终端操作的时候才会执行 Stream<String> newStream = stream.map((value) -> { return value.toLowerCase(); }); System.out.println("stream: " + stream); System.out.println("newStream: "+ newStream); //输出: //stream: java.util.stream.ReferencePipeline$Head@4eec7777 //newStream: java.util.stream.ReferencePipeline$3@3b07d329 } }
注意:对流 map() 的调用实际上会返回一个新的 Stream 实例,代表应用了 map 操作的原始字符串流。
您只能向给定的 Stream 实例添加一个操作。如果需要在多个操作之后进行链式操作,则需要将第二个操作应用到第一个操作产生的新 Stream 操作中。如下所示:
List<String> stringList = new ArrayList<String>(); stringList.add("ONE"); stringList.add("TWO"); stringList.add("THREE"); Stream<String> stream = stringList.stream(); // 在流上添加 map 非终端操作 // 注意:非终端操作不会执行,只有等到终端操作的时候才会执行 Stream<String> newStream = stream.map((value) -> { return value.toLowerCase(); // 转为小写 }); Stream<String> newStream2 = newStream.map(value -> { return value.toUpperCase(); // 转为大写 }); System.out.println("stream: " + stream); System.out.println("newStream: " + newStream); System.out.println("newStream2: " + newStream2); //输出: //stream: java.util.stream.ReferencePipeline$Head@3b07d329 //newStream: java.util.stream.ReferencePipeline$3@41629346 //newStream2: java.util.stream.ReferencePipeline$3@404b9385
请注意,对 Stream map() 的第二次调用是在第一次 map() 调用所返回的 Stream 上进行的。
在 Java 流上链式调用非终端操作是很常见的。下面是一个在 Java 流上链式调用非终端操作的示例:
List<String> stringList = new ArrayList<String>(); stringList.add("ONE"); stringList.add("TWO"); stringList.add("THREE"); Stream<String> stream = stringList.stream(); // 在流上添加 map 非终端操作 // 注意:非终端操作不会执行,只有等到终端操作的时候才会执行 Stream<String> newStream = stream.map((value) -> { return value.toLowerCase(); // 转为小写 }).map(value -> { return value.toUpperCase(); // 转为大写 });
许多非终端流操作都可以将 Java lambda 表达式作为参数。这个 lambda 表达式实现了与给定的非终端操作相匹配的 Java 函数接口。例如,Function 或 Predicate 接口。非终端操作方法的参数通常是一个函数式接口 —— 这就是为什么它也可以由 Java lambda 表达式来实现。
Java 流 filter() 可用于过滤 Java 流中的元素。filter 方法接收一个 Predicate 内置的函数式接口,该 Predicate 会针对流中的每个元素进行调用。如果该元素要包含在生成的流中,则 Predicate 应返回 true。如果不包含该元素,则 Predicate 应返回 false。
下面是一个调用 Java Stream filter() 方法的示例:
List<String> stringList = new ArrayList<String>(); stringList.add("ONE"); stringList.add("TWO"); stringList.add("THREE"); // 匿名函数写法 Object[] array1 = stringList.stream().filter(new Predicate<String>() { @Override public boolean test(String s) { return s.startsWith("T"); // 仅显示 T 开头的字符串 } }).toArray(); System.out.println("array1=" + Arrays.toString(array1)); // array1=[TWO, THREE] // lambda 写法 Object[] array2 = stringList.stream().filter(s -> { return s.startsWith("T"); }).toArray(); System.out.println("array2=" + Arrays.toString(array2)); // array2=[TWO, THREE]
Java 流 map() 方法将一个元素转换(映射)为另一个对象。例如,如果您有一个字符串列表,它可以将每个字符串转换为小写、大写、原始字符串的子串或其他完全不同的字符串。下面是一个 Java 流 map() 示例:
List<String> stringList = new ArrayList<String>(); stringList.add("ONE"); stringList.add("TWO"); stringList.add("THREE"); // 匿名函数写法 Object[] array1 = stringList.stream().map(new Function<String, String>() { @Override public String apply(String s) { return s.toLowerCase(); // 转换成小写 } }).toArray(); System.out.println("array1=" + Arrays.toString(array1)); // array1=[one, two, three] // lambda 写法 Object[] array2 = stringList.stream().map(s -> { return s.toLowerCase(); }).toArray(); System.out.println("array2=" + Arrays.toString(array2)); // array2=[one, two, three]
Java 流 flatMap() 方法可将单个元素映射为多个元素。其原理是将每个元素从由多个内部元素组成的复杂结构 "扁平化(flatten)" 为仅由这些内部元素组成的 "扁平(flat)" 流。
例如,假设你有一个包含嵌套对象(子对象)的对象。然后,你可以将该对象映射到一个 "扁平" 流中,该流由对象本身及其嵌套对象组成,或者仅由嵌套对象组成。你也可以将元素列表流映射到元素本身。或者将字符串流映射为这些字符串中的单词流,或者映射为这些字符串中的单个字符实例。
下面是一个将字符串列表平面映射到每个字符串中单词的示例。通过这个示例,您可以了解如何使用 flatMap() 将单个元素映射为多个元素。
List<String> stringList = new ArrayList<String>(); stringList.add("ONE one"); stringList.add("TWO two"); stringList.add("THREE three"); Stream<String> stream = stringList.stream() // 将单个元素转换成一个 Stream .flatMap((value) -> { String[] split = value.split(" "); // 返回新的 Stream<String> return (Stream<String>) Arrays.asList(split).stream(); }); // 迭代流 stream.forEach((value) -> { System.out.println(value); }); //输出: //ONE //one //TWO //two //THREE //three
这个 Java 流 flatMap() 示例首先创建了一个包含 3 个字符串的 List。然后获取 List 的流,并调用 flatMap()。
在 Stream 上调用的 flatMap() 操作必须返回代表平面映射元素的另一个 Stream。在上面的示例中,每个原始字符串都被拆分成单词,变成一个 List,然后从该 List 获取并返回流。
请注意,这个示例最后调用了 forEach(),这是一个终端操作。该调用只是为了触发内部迭代,从而触发平面映射操作。如果没有在 Stream 链上调用终端操作,就不会发生任何事情。实际上不会进行平面映射。
Java Stream distinct() 方法是一种非终端操作,它返回一个新的 Stream,其中只包含与原始 Stream 不同的元素。任何重复的元素都将被消除。下面是 Java Stream distinct() 方法的示例:
List<String> stringList = new ArrayList<String>(); stringList.add("one"); stringList.add("two"); stringList.add("three"); stringList.add("one"); List<String> distinctStrings = stringList.stream() .distinct() // 去重复元素 .collect(Collectors.toList()); System.out.println(distinctStrings); // [one, two, three]
在本例中,元素 one 在原始数据流中出现了 2 次。distinct() 函数返回的流中只包含第一次出现的元素。因此,调用 collect() 得到的 List 只包含 one、two 和 three。
Java 流 limit() 方法可将流中元素的数量限制在作为参数给 limit() 方法的数字范围内。limit() 方法会返回一个新的数据流,其中最多包含给定数量的元素。下面是一个 Java 流 limit() 示例:
List<String> stringList = new ArrayList<String>(); stringList.add("one"); stringList.add("two"); stringList.add("three"); stringList.stream().limit(2) // 限制返回前2个元素 .forEach( e -> { System.out.println(e); }); //输出: //one //two
这个示例首先创建了一个流,然后调用 limit(),然后用一个 lambda 调用 forEach(),打印出流中的元素。由于调用了 limit(2),所以只打印了前两个元素。
Java 流 peek() 方法是一种非终端操作,它将 Consumer(java.util.function.Consumer) 作为参数。流中的每个元素都会调用 Consumer。peek() 方法会返回一个新的流,其中包含原始流中的所有元素。
正如该方法所说,peek() 方法的目的是窥视流中的元素,而不是转换它们。请记住,peek 方法不会启动流中元素的内部迭代。为此,您需要调用终端操作。下面是一个 Java 流 peek() 示例:
List<String> stringList = new ArrayList<String>(); stringList.add("one"); stringList.add("two"); stringList.add("three"); // 匿名内部类方式 stringList.stream().peek(new Consumer<String>() { @Override public void accept(String s) { System.out.println("消费:"+s); } }).toArray(); //输出: //消费:one //消费:two //消费:three // lambda 表达式方式 stringList.stream().peek(s -> System.out.println("消费:"+s)).toArray(); //输出: //消费:one //消费:two //消费:three
Java Stream 接口的终端操作通常只返回一个值。一旦在流上调用了终端操作,流和任何链流的迭代就会开始。一旦迭代完成,就会返回终端操作的结果。
终端操作通常不会返回新的流实例。因此,一旦在流上调用终端操作,非终端操作的流实例链就会结束。下面是一个在 Java 流上调用终端操作的示例:
List<String> stringList = new ArrayList<String>(); stringList.add("one"); stringList.add("two"); stringList.add("three"); long count = stringList.stream() // 非终端操作 .map((value) -> { return value.toLowerCase(); }) .map((value) -> { return value.toUpperCase(); }) .map((value) -> { return value.substring(0,3); }) // 终端操作 .count(); System.out.println("count: "+ count); // count: 3
示例结尾处对 count() 的调用才是终端操作。由于 count() 返回一个 long,所以非终端操作(调用 map())的流链结束了。
Java 流 anyMatch() 方法是一种终端操作,它将单个 Predicate 作为参数,启动流的内部迭代,并将 Predicate 参数应用于每个元素。如果 Predicate 对任何元素都返回 true,则 anyMatch() 方法返回 true。如果没有元素与 Predicate 匹配,anyMatch() 方法将返回 false。下面是一个 Java 流 anyMatch() 示例:
List<String> stringList = new ArrayList<String>(); stringList.add("One flew over the cuckoo's nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); // boolean anyMatch = stringList.stream().anyMatch((value) -> { return value.startsWith("One"); }); System.out.println(anyMatch); // true
在上例中,anyMatch() 方法调用将返回 true,因为数据流中的第一个字符串元素以 "One" 开头。
Java Stream allMatch() 方法是一种终端操作,它将单个Predicate 作为参数,开始对 Stream 中的元素进行内部迭代,并将Predicate 参数应用于每个元素。如果Predicate 对流中的所有元素都返回 true,则 allMatch() 将返回 true。如果不是所有元素都与 Predicate 匹配,则 allMatch() 方法返回 false。下面是一个 Java Stream allMatch() 示例:
List<String> stringList = new ArrayList<String>(); stringList.add("One flew over the cuckoo's nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); // 判断 stringList 的元素是否全部以 One 字符串开头 boolean allMatch = stringList.stream().allMatch((value) -> { return value.startsWith("One"); }); System.out.println(allMatch); // false
在上面的示例中,allMatch() 方法将返回 false,因为流中只有一个字符串以 "One" 开头。
Java 流 noneMatch() 方法是一种终端操作,它将遍历流中的元素,并根据流中是否没有元素与作为参数传递给 noneMatch() 的 Predicate 匹配,返回 true 或 false。如果没有元素与 Predicate 匹配,noneMatch() 方法将返回 true;如果有一个或多个元素匹配,noneMatch() 方法将返回 false。下面是一个 Java 流 noneMatch() 示例:
List<String> stringList = new ArrayList<String>(); stringList.add("One flew over the cuckoo's nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); // 判断 stringList 的元素是否全部不以 Two 字符串开头 boolean allMatch = stringList.stream().noneMatch((value) -> { return value.startsWith("Two"); }); System.out.println(allMatch); // true
Java Stream collect() 方法是一种终端操作,用于启动元素的内部迭代,并将流中的元素收集到某个集合或对象中。下面是一个简单的 Java 流 collect() 方法示例:
List<String> stringList = new ArrayList<String>(); stringList.add("One flew over the cuckoo's nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); // 将 stringList 中的元素转换为大写,然后通过 Collectors.toList() 收集到一个新的列表中 List<String> stringsAsUppercaseList = stringList.stream() .map(value -> value.toUpperCase()) .collect(Collectors.toList()); for(String str : stringsAsUppercaseList) { System.out.println(str); } //输出: //ONE FLEW OVER THE CUCKOO'S NEST //TO KILL A MUCKINGBIRD //GONE WITH THE WIND
collect() 方法的参数是一个 Collector(java.util.stream.Collector)。实现 Collector 需要对 Collector 接口进行一些研究。幸运的是,Java 类 java.util.stream.Collectors 包含了一组预实现的 Collector 实现,你可以用它们来实现最常见的操作。在上面的示例中,使用的就是 Collectors.toList() 返回的 Collector 实现。该 Collector 简单地将流中的所有元素收集到一个标准的 Java List。
Java Stream count() 方法是一种终端操作,它启动 Stream 中元素的内部迭代,并对元素进行计数。下面是一个 Java Stream count() 示例:
List<String> stringList = new ArrayList<String>(); stringList.add("One flew over the cuckoo's nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); // 统计个数 long count = stringList.stream().flatMap((value) -> { String[] split = value.split(" "); return (Stream<String>) Arrays.asList(split).stream(); }) .count(); System.out.println("count = " + count); // count = 14
此示例首先创建了一个字符串列表,然后获取了该列表的流,为其添加了一个 flatMap() 操作,最后调用了 count()。count() 方法将开始迭代 Stream 中的元素,这将导致在 flatMap() 操作中将字符串元素分割成字,然后进行计数。最后打印出的结果是 14。
Java Stream findAny() 方法可以从 Stream 中查找单个元素。找到的元素可以来自流中的任何地方。但并不保证元素来自流中的任何位置。下面是一个 Java Stream findAny() 示例:
List<String> stringList = new ArrayList<String>(); stringList.add("one"); stringList.add("two"); stringList.add("three"); stringList.add("one"); Stream<String> stream = stringList.stream(); Optional<String> anyElement = stream.findAny(); System.out.println(anyElement.get());
请注意 findAny() 方法是如何返回一个 Optional 的。流可能是空的,因此不会返回任何元素。你可以通过 Optional isPresent() 方法检查是否找到了元素。
Java Stream findFirst() 方法可查找 Stream 中的第一个元素(如果 Stream 中存在任何元素)。如果存在元素,findFirst() 方法会返回一个 Optional,您可以从中获取该元素。下面是一个 Java Stream findFirst() 示例:
List<String> stringList = new ArrayList<String>(); stringList.add("one"); stringList.add("two"); stringList.add("three"); stringList.add("one"); // 找出第一个元素 Optional<String> result = stringList.stream().findFirst(); result.ifPresent(e -> { System.out.println(e); }); //输出: //one
您可以通过其 isPresent() 方法检查返回的可选项是否包含元素。
Java Stream forEach() 方法是一个终端操作,它开始对 Stream 中的元素进行内部迭代,并对 Stream 中的每个元素应用一个 Consumer(java.util.function.Consumer)。forEach() 方法返回 void。下面是一个 Java Stream forEach() 示例:
List<String> stringList = new ArrayList<String>(); stringList.add("one"); stringList.add("two"); stringList.add("three"); stringList.add("one"); stringList.stream().forEach( element -> { System.out.println(element); }); //输出: //one //two //three //one
Java 流 min() 方法是一种终端操作,可返回流中最小的元素。哪个元素最小由您传递给 min() 方法的比较器实现决定。我在关于 Java 集合排序的教程中解释了比较器接口的工作原理。下面是一个 Java 流 min() 示例:
List<String> stringList = new ArrayList<String>(); stringList.add("abc"); stringList.add("def"); // 找出最小的字符串 Optional<String> min = stringList.stream().min((val1, val2) -> { return val1.compareTo(val2); }); String minString = min.get(); System.out.println(minString); //输出: //abc
请注意 min() 方法如何返回一个 Optional ,其中可能包含也可能不包含结果。如果流为空,则 Optional get() 方法将抛出 NoSuchElementException 异常。
Java 流 max() 方法是一种终端操作,可返回流中最大的元素。哪个元素最大由您传递给 max() 方法的比较器实现决定。我在关于 Java 集合排序的教程中解释了比较器接口的工作原理。下面是一个 Java Stream max() 示例:
List<String> stringList = new ArrayList<String>(); stringList.add("abc"); stringList.add("def"); // 找出最大的字符串 Optional<String> min = stringList.stream().max((val1, val2) -> { return val1.compareTo(val2); }); String minString = min.get(); System.out.println(minString); //输出: //def
请注意 max() 方法如何返回一个 Optional,其中可能包含也可能不包含结果。如果流为空,则 Optional get() 方法将抛出 NoSuchElementException 异常。
Java Stream reduce() 方法是一种终端操作,可将流中的所有元素还原为单个元素。下面是一个 Java Stream reduce() 示例:
List<String> stringList = new ArrayList<String>(); stringList.add("One flew over the cuckoo's nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); // 将 stringList 列表中的元素连接成一个长字符串 Optional<String> reduced = stringList.stream() .reduce((value, combinedValue) -> { return combinedValue + " + " + value; }); System.out.println(reduced.get()); //输出: //Gone with the wind + To kill a muckingbird + One flew over the cuckoo's nest
请注意 reduce() 方法返回的 Optional。该 Optional 包含传递给 reduce() 方法的 lambda 表达式返回的值(如果有的话)。你可以通过调用 Optional get() 方法获得该值。
Java Stream toArray() 方法是一个终端操作,它启动流中元素的内部迭代,并返回一个包含所有元素的对象数组。下面是一个 Java Stream toArray() 示例:
List<String> stringList = new ArrayList<String>(); stringList.add("One flew over the cuckoo's nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); //将 stringList 列表转换成数组 Object[] objects = stringList.stream().toArray(); System.out.println(Arrays.toString(objects)); //输出: //[One flew over the cuckoo's nest, To kill a muckingbird, Gone with the wind]