MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java Stream 中间处理 API 的灵活运用

2022-05-166.9k 阅读

Java Stream 中间处理 API 的灵活运用

一、过滤操作(filter)

在 Java Stream 中,filter 方法用于根据给定的条件对流中的元素进行过滤。它接受一个 Predicate 作为参数,这个 Predicate 定义了过滤条件。只有满足该条件的元素才会被保留在流中。

例如,假设有一个整数列表,我们想过滤出所有的偶数:

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class FilterExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
        numbers.add(4);
        numbers.add(5);

        List<Integer> evenNumbers = numbers.stream()
               .filter(n -> n % 2 == 0)
               .collect(Collectors.toList());

        System.out.println(evenNumbers);
    }
}

在上述代码中,numbers.stream() 将列表转换为流,filter(n -> n % 2 == 0) 使用 Predicate 来检查每个数字是否为偶数,只有偶数会通过过滤,最后 collect(Collectors.toList()) 将过滤后的流收集回一个列表。

从本质上来说,filter 操作是一种惰性求值操作。它并不会立即对所有元素进行过滤,而是在终端操作执行时才会真正处理元素。这使得在处理大数据集时,可以避免不必要的计算。在底层实现中,流在遍历元素时,会将每个元素传递给 Predicate,只有通过测试的元素才会被传递到流的下一个阶段。

二、映射操作(map)

map 方法用于将流中的每个元素按照给定的函数进行转换。它接受一个 Function 作为参数,这个 Function 定义了如何将输入元素转换为输出元素。

比如,有一个字符串列表,我们想将每个字符串转换为其长度:

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class MapExample {
    public static void main(String[] args) {
        List<String> words = new ArrayList<>();
        words.add("apple");
        words.add("banana");
        words.add("cherry");

        List<Integer> wordLengths = words.stream()
               .map(String::length)
               .collect(Collectors.toList());

        System.out.println(wordLengths);
    }
}

这里,map(String::length) 使用 Function 将每个字符串映射为其长度。String::length 是方法引用,等同于 s -> s.length()

map 操作同样是惰性求值的。它构建了一个新的流,该流中的元素是原流元素经过 Function 转换后的结果。在执行终端操作时,才会依次对原流元素应用 Function 进行转换。这一特性在处理复杂对象转换时非常有用,我们可以在不实际执行转换的情况下,构建一系列的流操作,直到最后需要结果时才进行计算。

三、扁平映射操作(flatMap)

flatMap 方法与 map 方法类似,但它主要用于处理流中包含流的情况。flatMap 接受一个 Function,该 Function 会将每个元素转换为一个流,然后 flatMap 会将这些流扁平化为一个单一的流。

假设有一个列表,其中每个元素又是一个列表,我们想将其扁平化为一个单一的列表:

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class FlatMapExample {
    public static void main(String[] args) {
        List<List<Integer>> nestedLists = new ArrayList<>();
        List<Integer> list1 = new ArrayList<>();
        list1.add(1);
        list1.add(2);
        List<Integer> list2 = new ArrayList<>();
        list2.add(3);
        list2.add(4);
        nestedLists.add(list1);
        nestedLists.add(list2);

        List<Integer> flatList = nestedLists.stream()
               .flatMap(List::stream)
               .collect(Collectors.toList());

        System.out.println(flatList);
    }
}

在这个例子中,nestedLists.stream() 将外层列表转换为流,flatMap(List::stream) 将每个内层列表转换为一个流,并将这些流扁平化为一个单一的流,最后收集为一个列表。

从实现角度看,flatMap 内部会迭代原流中的每个元素,将其转换为流,然后将这些流的元素依次添加到新的扁平流中。这一过程确保了所有元素都被正确地合并,而不会保留嵌套的流结构。

四、排序操作(sorted)

sorted 方法用于对流中的元素进行排序。它有两种形式:无参数形式使用元素的自然顺序进行排序,有参数形式接受一个 Comparator 来定义自定义的排序逻辑。

以一个字符串列表为例,使用自然顺序排序:

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class SortedExample1 {
    public static void main(String[] args) {
        List<String> words = new ArrayList<>();
        words.add("banana");
        words.add("apple");
        words.add("cherry");

        List<String> sortedWords = words.stream()
               .sorted()
               .collect(Collectors.toList());

        System.out.println(sortedWords);
    }
}

在上述代码中,sorted() 使用字符串的自然顺序(字典序)对列表进行排序。

如果要使用自定义排序逻辑,比如按照字符串长度排序:

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class SortedExample2 {
    public static void main(String[] args) {
        List<String> words = new ArrayList<>();
        words.add("banana");
        words.add("apple");
        words.add("cherry");

        List<String> sortedWords = words.stream()
               .sorted(Comparator.comparingInt(String::length))
               .collect(Collectors.toList());

        System.out.println(sortedWords);
    }
}

这里,Comparator.comparingInt(String::length) 创建了一个 Comparator,它根据字符串的长度进行比较。

sorted 操作在底层实现上通常会使用高效的排序算法,如归并排序或快速排序的变体。它同样是惰性求值的,只有在终端操作触发时才会进行实际的排序操作。这意味着在构建流操作链时,可以将排序操作放在中间,而不会立即消耗性能。

五、去重操作(distinct)

distinct 方法用于去除流中的重复元素。它通过 equals 方法来判断元素是否重复。

假设有一个包含重复元素的整数列表:

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class DistinctExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(2);
        numbers.add(2);
        numbers.add(3);
        numbers.add(3);

        List<Integer> distinctNumbers = numbers.stream()
               .distinct()
               .collect(Collectors.toList());

        System.out.println(distinctNumbers);
    }
}

在这个例子中,distinct() 方法会去除列表中的重复元素,只保留唯一的元素。

从本质上讲,distinct 操作在流遍历过程中,会维护一个已见过元素的集合(通常是一个 HashSet)。当新元素到达时,会检查该元素是否已在集合中。如果不在,则将其保留在流中,并添加到集合中;如果已存在,则丢弃该元素。这种实现方式保证了流中最终只包含唯一的元素。

六、限制操作(limit)

limit 方法用于限制流中元素的数量。它接受一个 long 类型的参数,表示要保留的元素个数。

例如,有一个整数列表,我们只想获取前三个元素:

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class LimitExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
        numbers.add(4);
        numbers.add(5);

        List<Integer> limitedNumbers = numbers.stream()
               .limit(3)
               .collect(Collectors.toList());

        System.out.println(limitedNumbers);
    }
}

在上述代码中,limit(3) 会截断流,只保留前三个元素。

limit 操作在底层实现上是一种短路操作。一旦收集到足够数量的元素(即达到 limit 指定的数量),流的遍历就会停止,不会再处理剩余的元素。这在处理大数据集时非常有用,可以避免不必要的计算。

七、跳过操作(skip)

skip 方法与 limit 方法相反,它用于跳过流中的前 n 个元素。接受一个 long 类型的参数,表示要跳过的元素个数。

假设有一个整数列表,我们想跳过前两个元素:

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class SkipExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
        numbers.add(4);
        numbers.add(5);

        List<Integer> skippedNumbers = numbers.stream()
               .skip(2)
               .collect(Collectors.toList());

        System.out.println(skippedNumbers);
    }
}

这里,skip(2) 会跳过列表中的前两个元素,返回剩余元素组成的流。

skip 操作同样是惰性求值的。在流遍历过程中,它会在跳过指定数量的元素后,才开始将后续元素传递到流的下一个阶段。这一操作在处理有序数据集,需要从中间位置开始处理时非常有用。

八、peek 操作

peek 方法主要用于调试目的,它允许我们在流的处理过程中查看元素。它接受一个 Consumer 作为参数,会对流中的每个元素执行该 Consumer,但不会改变元素本身。

例如,我们想在过滤偶数的过程中,查看每个经过过滤的元素:

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class PeekExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
        numbers.add(4);
        numbers.add(5);

        List<Integer> evenNumbers = numbers.stream()
               .filter(n -> n % 2 == 0)
               .peek(System.out::println)
               .collect(Collectors.toList());

        System.out.println(evenNumbers);
    }
}

在这个例子中,peek(System.out::println) 会在每个偶数元素通过过滤后,将其打印出来。

从实现角度看,peek 操作类似于一个中间的监听器。它在流的处理管道中插入了一个操作,该操作会对每个元素执行 Consumer,但不会改变流的结构或元素的值。这使得我们可以在不改变流处理逻辑的情况下,观察元素在流中的流动情况,有助于调试复杂的流操作。

九、中间操作的组合使用

Java Stream 的强大之处在于可以将多个中间操作组合在一起,形成复杂的处理逻辑。

例如,我们有一个字符串列表,我们想先过滤出长度大于 3 的字符串,然后将其转换为大写,再按照长度排序,最后去重:

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class CombinedOperationsExample {
    public static void main(String[] args) {
        List<String> words = new ArrayList<>();
        words.add("apple");
        words.add("banana");
        words.add("cat");
        words.add("dog");
        words.add("elephant");

        List<String> result = words.stream()
               .filter(w -> w.length() > 3)
               .map(String::toUpperCase)
               .sorted(Comparator.comparingInt(String::length))
               .distinct()
               .collect(Collectors.toList());

        System.out.println(result);
    }
}

在上述代码中,我们依次使用了 filtermapsorteddistinct 操作,展示了如何通过组合中间操作来实现复杂的数据处理任务。

这种组合使用的方式基于流的惰性求值特性。每个中间操作只是构建了一个操作链,只有在终端操作执行时,才会按照顺序依次对元素进行处理。这使得我们可以以一种声明式的方式编写代码,而不需要关心底层的迭代和状态管理,大大提高了代码的可读性和可维护性。

十、并行流中的中间操作

Java Stream 支持并行处理,通过 parallelStream() 方法可以将顺序流转换为并行流。在并行流中,中间操作的执行方式会有所不同。

filter 操作为例,在并行流中,流中的元素会被分成多个部分,每个部分由不同的线程并行处理。例如:

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class ParallelFilterExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        for (int i = 0; i < 1000000; i++) {
            numbers.add(i);
        }

        List<Integer> evenNumbers = numbers.parallelStream()
               .filter(n -> n % 2 == 0)
               .collect(Collectors.toList());

        System.out.println(evenNumbers.size());
    }
}

在这个例子中,parallelStream() 将列表转换为并行流,filter 操作会在多个线程中并行执行,从而提高处理速度。

然而,在并行流中使用中间操作时需要注意一些问题。例如,一些操作(如 sorted)在并行流中的性能可能不如顺序流,因为并行流需要额外的同步和合并操作。此外,对于有状态的中间操作(如 distinct),并行执行可能会导致结果不一致,除非流是无序的或者使用了特殊的并发数据结构来保证一致性。

总的来说,合理使用并行流中的中间操作可以显著提高大数据集的处理效率,但需要深入理解其底层实现和潜在的问题,以确保程序的正确性和性能。

通过对这些 Java Stream 中间处理 API 的灵活运用,开发者可以以一种简洁、高效且声明式的方式处理各种数据集合,无论是简单的过滤和映射,还是复杂的组合操作和并行处理,都能轻松应对。这些 API 不仅提升了代码的可读性和可维护性,还充分利用了现代多核处理器的性能优势,为 Java 开发带来了新的活力。