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

Java Stream 中 map 方法的实战技巧

2024-05-102.9k 阅读

Java Stream 中 map 方法的基础概念

在 Java 8 引入 Stream API 之后,极大地简化了集合操作。Stream 是一种来自数据源的元素队列,它支持顺序和并行的聚合操作。而 map 方法是 Stream API 中非常重要的一个中间操作。它的主要作用是对 Stream 中的每个元素应用一个函数,然后将其映射成一个新的元素,形成一个新的 Stream。

从本质上来说,map 方法是一种函数式编程的体现,它将一个函数应用到流中的每一个元素上,类似于数学中的映射概念。给定一个函数 ( f ) 和一个集合 ( S = {x_1, x_2, \cdots, x_n} ),经过 ( map(f, S) ) 操作后,会得到一个新的集合 ( T = {f(x_1), f(x_2), \cdots, f(x_n)} )。

在 Java 代码中,map 方法的定义如下:

<R> Stream<R> map(Function<? super T,? extends R> mapper);

这里,T 是原始 Stream 中元素的类型,R 是映射后新 Stream 中元素的类型。Function 是一个函数式接口,它接收一个参数并返回一个结果。

下面通过一个简单的示例来展示 map 方法的基本用法。假设我们有一个整数列表,我们想将列表中的每个整数乘以 2:

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

public class MapExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
        List<Integer> doubledNumbers = numbers.stream()
              .map(n -> n * 2)
              .collect(Collectors.toList());
        System.out.println(doubledNumbers);
    }
}

在上述代码中,我们首先创建了一个包含整数的列表 numbers。然后通过 stream() 方法将列表转换为 Stream。接着使用 map 方法,传入一个 lambda 表达式 n -> n * 2,这个表达式定义了如何将每个整数映射为新的整数(即乘以 2)。最后通过 collect(Collectors.toList()) 将 Stream 转换回列表并打印出来。运行这段代码,会输出 [2, 4, 6, 8, 10]

对自定义对象集合使用 map 方法

在实际开发中,我们经常会处理自定义对象的集合。假设我们有一个 Person 类,包含姓名和年龄属性:

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

现在我们有一个 Person 对象的列表,我们想获取每个人的姓名并形成一个新的字符串列表。可以这样做:

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

public class CustomObjectMapExample {
    public static void main(String[] args) {
        List<Person> people = Arrays.asList(
                new Person("Alice", 25),
                new Person("Bob", 30),
                new Person("Charlie", 35)
        );
        List<String> names = people.stream()
              .map(Person::getName)
              .collect(Collectors.toList());
        System.out.println(names);
    }
}

在这段代码中,map(Person::getName) 使用了方法引用,它等同于 map(p -> p.getName())。这里 Person::getName 是一个 Function<Person, String>,它将 Person 对象映射为其姓名(类型为 String)。最终结果是一个包含所有人姓名的字符串列表,运行代码会输出 [Alice, Bob, Charlie]

多层 map 嵌套使用

有时候,我们可能需要对 Stream 中的元素进行多层映射。例如,假设我们有一个字符串列表,每个字符串包含多个单词,我们想将每个单词都转换为大写形式并收集到一个新的列表中。

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

public class NestedMapExample {
    public static void main(String[] args) {
        List<String> sentences = Arrays.asList(
                "hello world",
                "java programming",
                "stream api"
        );
        List<String> upperCaseWords = sentences.stream()
              .map(s -> s.split(" "))
              .flatMap(Arrays::stream)
              .map(String::toUpperCase)
              .collect(Collectors.toList());
        System.out.println(upperCaseWords);
    }
}

在上述代码中,首先通过 map(s -> s.split(" ")) 将每个句子映射为一个单词数组。这里得到的是一个 Stream<String[]>。由于我们最终想要的是一个包含所有单词的 Stream,而不是一个包含数组的 Stream,所以使用 flatMap(Arrays::stream)Stream<String[]> 扁平化为 Stream<String>。然后再通过 map(String::toUpperCase) 将每个单词转换为大写形式,最后收集到列表中。运行代码会输出 [HELLO, WORLD, JAVA, PROGRAMMING, STREAM, API]

map 方法与并行流的结合使用

Stream API 支持并行处理,这在处理大数据集时可以显著提高性能。当使用并行流时,map 方法同样可以发挥作用。

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

public class ParallelMapExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        List<Integer> squaredNumbers = numbers.parallelStream()
              .map(n -> n * n)
              .collect(Collectors.toList());
        System.out.println(squaredNumbers);
    }
}

在上述代码中,通过 parallelStream() 将列表转换为并行流。然后使用 map 方法对每个元素进行平方操作。并行流会将数据分成多个部分,在不同的线程中并行处理这些部分,最后将结果合并。这样在处理大量数据时可以利用多核 CPU 的优势,提高处理速度。

map 方法在数据过滤与转换结合场景中的应用

在实际开发中,我们常常需要先对数据进行过滤,然后再进行转换。例如,假设我们有一个整数列表,我们只想对其中的偶数进行平方操作。

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

public class FilterAndMapExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
        List<Integer> squaredEvenNumbers = numbers.stream()
              .filter(n -> n % 2 == 0)
              .map(n -> n * n)
              .collect(Collectors.toList());
        System.out.println(squaredEvenNumbers);
    }
}

在这段代码中,首先使用 filter(n -> n % 2 == 0) 过滤出偶数,然后使用 map(n -> n * n) 对这些偶数进行平方操作,最后收集到列表中。运行代码会输出 [4, 16, 36]

map 方法在处理复杂数据结构时的应用

假设我们有一个复杂的数据结构,例如一个包含嵌套列表的列表,每个内部列表包含整数。我们想将所有这些整数提取出来并进行一些转换。

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

public class ComplexDataStructureMapExample {
    public static void main(String[] args) {
        List<List<Integer>> nestedLists = Arrays.asList(
                Arrays.asList(1, 2),
                Arrays.asList(3, 4),
                Arrays.asList(5, 6)
        );
        List<Integer> doubledNumbers = nestedLists.stream()
              .flatMap(List::stream)
              .map(n -> n * 2)
              .collect(Collectors.toList());
        System.out.println(doubledNumbers);
    }
}

在上述代码中,首先通过 flatMap(List::stream) 将嵌套的列表扁平化为一个包含所有整数的 Stream。然后使用 map(n -> n * 2) 将每个整数乘以 2,最后收集到列表中。运行代码会输出 [2, 4, 6, 8, 10, 12]

map 方法与 Optional 的结合使用

在处理可能为空的值时,Optional 是一个非常有用的工具。我们可以在 map 方法中结合 Optional 使用。

import java.util.Optional;

public class MapWithOptionalExample {
    public static void main(String[] args) {
        Optional<String> optionalString = Optional.of("hello");
        Optional<Integer> lengthOptional = optionalString.map(String::length);
        lengthOptional.ifPresent(System.out::println);

        Optional<String> emptyOptional = Optional.empty();
        Optional<Integer> emptyLengthOptional = emptyOptional.map(String::length);
        emptyLengthOptional.ifPresent(System.out::println);
    }
}

在上述代码中,对于 optionalString,它包含一个值 "hello",通过 map(String::length) 可以获取字符串的长度并封装在 Optional<Integer> 中。对于 emptyOptional,由于它不包含值,map 方法返回的 Optional<Integer> 也是空的,所以不会打印任何内容。

map 方法在集合分组场景中的应用

假设我们有一个 Person 对象的列表,我们想按年龄对人员进行分组,并将每组人员的姓名收集到一个列表中。

import java.util.*;
import java.util.stream.Collectors;

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

public class MapInGroupingExample {
    public static void main(String[] args) {
        List<Person> people = Arrays.asList(
                new Person("Alice", 25),
                new Person("Bob", 30),
                new Person("Charlie", 25),
                new Person("David", 30)
        );
        Map<Integer, List<String>> ageToNamesMap = people.stream()
              .collect(Collectors.groupingBy(
                        Person::getAge,
                        Collectors.mapping(Person::getName, Collectors.toList())
                ));
        System.out.println(ageToNamesMap);
    }
}

在上述代码中,Collectors.groupingBy 方法用于按年龄对人员进行分组。第二个参数 Collectors.mapping(Person::getName, Collectors.toList()) 中,Collectors.mapping 类似于 map 操作,它将每个 Person 对象映射为其姓名,并通过 Collectors.toList() 收集到一个列表中。最终结果是一个按年龄分组,每组包含对应人员姓名列表的映射。运行代码会输出 {25=[Alice, Charlie], 30=[Bob, David]}

map 方法在异常处理中的注意事项

当在 map 方法中应用的函数可能抛出异常时,需要特别注意。例如,假设我们有一个字符串列表,其中部分字符串可能无法转换为整数,我们想将这些字符串转换为整数并进行一些操作。

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

public class MapExceptionHandlingExample {
    public static void main(String[] args) {
        List<String> strings = Arrays.asList("1", "2", "abc", "4");
        // 以下代码会抛出 NumberFormatException
        // List<Integer> numbers = strings.stream()
        //      .map(Integer::parseInt)
        //      .collect(Collectors.toList());

        // 正确处理异常的方式
        List<Integer> numbers = strings.stream()
              .map(s -> {
                    try {
                        return Integer.parseInt(s);
                    } catch (NumberFormatException e) {
                        return null;
                    }
                })
              .filter(Objects::nonNull)
              .collect(Collectors.toList());
        System.out.println(numbers);
    }
}

在上述代码中,如果直接使用 map(Integer::parseInt),当遇到无法转换为整数的字符串(如 "abc")时会抛出 NumberFormatException。为了正确处理这种情况,我们在 map 方法中使用了一个 try - catch 块,将无法转换的字符串映射为 null,然后通过 filter(Objects::nonNull) 过滤掉这些 null 值。运行代码会输出 [1, 2, 4]

总结 map 方法的实战要点

  1. 基本使用map 方法用于将 Stream 中的每个元素通过一个函数映射为新的元素,形成新的 Stream。要熟练掌握其基本语法和 lambda 表达式的使用。
  2. 自定义对象:在处理自定义对象集合时,通过方法引用或 lambda 表达式将自定义对象映射为所需的类型。
  3. 多层映射与扁平处理:当需要对复杂数据结构进行多层映射时,结合 flatMap 方法将嵌套结构扁平化为单一 Stream,以便后续处理。
  4. 并行处理:在大数据集处理场景中,利用并行流与 map 方法结合,充分发挥多核 CPU 的性能优势。
  5. 与其他操作结合map 方法常常与 filtercollect 等方法结合使用,实现数据过滤、转换和收集等复杂操作。
  6. 异常处理:当 map 中应用的函数可能抛出异常时,要在 lambda 表达式中进行适当的异常处理,避免程序崩溃。

通过深入理解和掌握这些实战要点,开发者可以在实际项目中更加灵活、高效地使用 Java Stream 中的 map 方法,提升代码的可读性和性能。