在 Java8 中,filter、map 和 flatMap 是 Stream API 中非常重要的三个方法,它们分别用于过滤、映射和拆分集合中的元素。
这三个方法都是中间操作,这意味着它们会返回一个新的Stream,而不会修改原始Stream。此外,这些操作都是惰性的,即它们不会在调用时立即执行,而是在需要结果时(例如,在调用collect 方法时)才执行。
下面将分别详细介绍这三个方法:
filter 方法用于根据某个条件过滤 Stream 中的元素,并返回一个新的 Stream。这个方法接受一个 Predicate 函数式接口的实现作为参数,这个接口定义了一个测试条件,用于确定哪些元素应该被包含在结果中。
方法定义:
Stream<T> filter(Predicate<? super T> predicate)
示例:
假设我们有一个包含字符串的 List,我们通过 filter 过滤出所有以“t”开头的字符串。代码如下:
package com.hxstrive.jdk8.stream_api; import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; /** * filter 方法 * @author hxstrive.com */ public class StreamFilterDemo { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("one"); list.add("two"); list.add("three"); // 传统匿名类方式 list.stream().filter(new Predicate<String>() { @Override public boolean test(String s) { return s.startsWith("t"); } }).forEach(System.out::println); //输出: //two //three // java8 lambda表达式方式 list.stream().filter(e -> { return e.startsWith("t"); // 返回以 t 开头的字符串 }).forEach(System.out::println); //输出: //two //three } }
在使用 filter 方法时,需要注意如下事项:
线程安全: 如果你在多线程环境中使用共享的 Stream,并且这个 Stream 被多个线程同时 filter,那么你需要确保 Predicate 的实现是线程安全的。但通常情况下,Stream 操作不是线程安全的,因此你应该避免在多线程环境中共享 Stream。
短路行为: filter 是一个短路操作,这意味着一旦它确定了足够的元素来满足终端操作(如 findFirst, anyMatch, noneMatch),它就会停止处理剩余的元素。但如果你使用的是 forEach 或 collect 等非短路操作,那么它会处理 Stream 中的所有元素。例如:
List<String> list = new ArrayList<>(); list.add("one"); list.add("two"); list.add("three"); // java8 lambda表达式方式 list.stream().filter(e -> { System.out.println("e = " + e); return e.startsWith("t"); // 返回以 t 开头的字符串 }).findFirst().ifPresent(System.out::println); //输出: //e = one //e = two //two list.stream().forEach(e -> { System.out.println("e = " + e); if(e.startsWith("t")) { System.out.println(e); } }); //输出: //e = one //e = two //two //e = three //three
避免副作用: 在 filter 方法中,你不应该修改被检查的元素或 Stream 的状态。这可能会导致不可预测的结果,因为 Stream 操作可能是并行执行的。
替代方案: 在某些情况下,使用集合的 removeIf 方法或传统的循环结构可能比使用 Stream API 的 filter 方法更直观或更高效。因此,在选择使用 filter 之前,请考虑其他可能的解决方案。例如:
List<String> list = new ArrayList<>(); list.add("one"); list.add("two"); list.add("three"); //删除不要的元素 list.removeIf(e -> !e.startsWith("t")); //输出list list.forEach(System.out::println); //输出: //two //three
注意:removeIf 方法会从原始 List 中将符合条件的元素删除,上面例子将不是以“t”开头的元素全部删除了。
map 方法用于将 Stream 中的元素映射到另一个对象,并返回一个新的 Stream。这个方法接受一个 Function 函数式接口的实现作为参数,这个接口定义了一个将输入对象映射到输出对象的函数。
方法定义:
// 返回由给定函数应用于此流的元素的结果组成的流 <R> Stream<R> map(Function<? super T, ? extends R> mapper) // 其他 map 函数 // 返回一个 DoubleStream ,其中包含将给定函数应用于此流的元素的结果 DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper) // 返回一个 IntStream ,其中包含将给定函数应用于此流的元素的结果 IntStream mapToInt(ToIntFunction<? super T> mapper) // 返回一个 LongStream ,其中包含将给定函数应用于此流的元素的结果 LongStream mapToLong(ToLongFunction<? super T> mapper)
示例:
假设我们有一个字符串 List,我们通过 map 方法将每个元素转换为元素的长度,例如:
package com.hxstrive.jdk8.stream_api; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.function.Function; /** * map 方法 * @author hxstrive.com */ public class StreamMapDemo { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("one"); list.add("two"); list.add("three"); // 传统匿名类方式 list.stream().map(new Function<String, Integer>() { @Override public Integer apply(String s) { // 将字符转换为大写 return Objects.isNull(s) ? -1 : s.length(); } }).forEach(System.out::println); //输出: //3 //3 //5 // java8 lambda表达式方式 list.stream().map(s -> { // 将字符转换为大写 return Objects.isNull(s) ? -1 : s.length(); }).forEach(System.out::println); //输出: //3 //3 //5 } }
注意:map 方法与 filter 方法类似,map 操作本身不是线程安全的。如果在多线程环境中使用共享的 Stream,并且多个线程同时对其进行 map 操作,可能会遇到并发问题。因此,应避免在多线程环境中共享 Stream。
下面是 mapToInt 方法示例,mapToDouble 和 mapToLong 方法类似:
IntStream intStream = list.stream().mapToInt(new ToIntFunction<String>() { @Override public int applyAsInt(String value) { return value.length(); } }); intStream.forEach(System.out::println); //输出: //3 //3 //5
flatMap 方法用于将 Stream 中的元素映射到另一个 Stream,并将所有这些 Stream 连接成一个新的 Stream。这个方法在处理包含多个层级的数据结构(如集合中的集合)时非常有用。它接受一个 Function 函数式接口的实现作为参数,这个接口定义了一个将输入对象映射到输出 Stream 的函数。
方法定义:
返回由通过将提供的映射函数应用于每个元素而产生的映射流的内容来替换该流的每个元素的结果的流 <R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper) // 其他 flatMap 方法 // 返回一个 DoubleStream,其中包含将该流的每个元素替换为通过将提供的映射函数应用于每个元素而产生的映射流的内容的结果 DoubleStream flatMapToDouble(Function<? super T,? extends DoubleStream> mapper) // 返回一个 IntStream,其中包含将该流的每个元素替换为通过将提供的映射函数应用于每个元素而产生的映射流的内容的结果 IntStream flatMapToInt(Function<? super T,? extends IntStream> mapper) // 返回一个 LongStream,其中包含将该流的每个元素替换为通过将提供的映射函数应用于每个元素而产生的映射流的内容的结果 LongStream flatMapToLong(Function<? super T,? extends LongStream> mapper)
示例:
假设我们有两个字符串列表,我们想要将它们合并成一个新的列表。例如:
package com.hxstrive.jdk8.stream_api; import java.util.Arrays; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; /** * flatMap 方法 * @author hxstrive.com */ public class StreamFlatMapDemo { public static void main(String[] args) { List<String> list1 = Arrays.asList("apple", "banana", "cherry"); List<String> list2 = Arrays.asList("dog", "elephant", "fox"); // 1.先将 list1 和 list2 转换为流 Stream<List<String>> stream = Stream.of(list1, list2); // 2.然后使用 flatMap 方法将它们合并为一个流 // 传统匿名函数方式 List<String> mergedList = stream.flatMap(new Function<List<String>, Stream<String>>() { @Override public Stream<String> apply(List<String> strings) { System.out.println("strings = " + strings); return strings.stream(); } }).collect(Collectors.toList()); List<String> mergedList2 = Stream.of(list1, list2).flatMap(e -> { System.out.println("e = " + e); return e.stream(); }).collect(Collectors.toList()); // 3.查看合并 list 中的数据 System.out.println(mergedList); System.out.println(mergedList2); //输出: //strings = [apple, banana, cherry] //strings = [dog, elephant, fox] //e = [apple, banana, cherry] //e = [dog, elephant, fox] //[apple, banana, cherry, dog, elephant, fox] //[apple, banana, cherry, dog, elephant, fox] } }
注意:
flatMap 的返回类型是一个流,其元素类型由传递给它的 Function 接口的 apply 方法的返回类型决定。确保你的 Function 接口的实现返回的是一个流,否则你会得到编译错误。
与其他 Stream 操作一样,flatMap 操作本身不是线程安全的。如果你在多线程环境中共享 Stream 并对其进行 flatMap 操作,可能会遇到并发问题。
如果 Function 接口的 apply 方法抛出了异常,并且没有被适当捕获和处理,那么整个 Stream 操作将失败并抛出异常。确保你的 Function 实现能够正确处理所有可能的输入情况。
在使用 flatMap 时,请注意流的生命周期。由于flatMap将多个流合并成一个流,因此原始流的生命周期可能会受到影响。确保在不再需要流时关闭它,以避免资源泄漏。