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

Java Stream map 方法的一对一转换机制

2022-07-183.7k 阅读

Java Stream map 方法概述

在Java 8引入Stream API之后,极大地简化了集合数据处理的操作。Stream提供了一种函数式编程的方式来处理数据集合,使得代码更加简洁、易读且并行化处理更为容易。其中,map方法是Stream API中极为重要的一个中间操作,它的核心功能是对Stream中的每个元素应用一个给定的函数,并将其映射为一个新的元素,从而形成一个新的Stream。这种映射操作是一对一的,即Stream中的每一个输入元素都对应产生一个输出元素。

map方法定义在java.util.stream.Stream接口中,其方法签名如下:

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

这里,<R>表示映射后新Stream中元素的类型。mapper是一个Function接口的实例,它接受类型为T(即原Stream中元素的类型)的参数,并返回类型为R的结果。Function接口是Java 8函数式编程中的一个核心接口,它只有一个抽象方法apply,该方法用于定义具体的映射逻辑。

map方法的一对一转换本质

从函数式编程角度理解

从函数式编程的视角来看,map方法体现了函数的映射概念。在数学中,函数是一种将输入值映射到输出值的关系。类似地,在Java Stream的map方法中,mapper函数将Stream中的每个元素作为输入,经过特定的处理逻辑,产生一个新的输出元素。例如,有一个包含整数的Stream,我们可以通过map方法将每个整数映射为其平方值。这就如同数学函数y = x^2x是输入值(Stream中的元素),y是输出值(映射后的新元素)。

数据处理流程剖析

map方法被调用时,Stream会遍历其中的每一个元素。对于每个元素,map方法会调用mapper函数的apply方法,将该元素作为参数传入。apply方法执行自定义的映射逻辑,并返回一个新的元素。这些新生成的元素会被收集到一个新的Stream中,最终返回给调用者。整个过程是顺序执行的,除非Stream被并行化处理(关于并行化处理会在后续详细讨论)。

例如,假设有一个List<Integer>,我们希望将其中每个整数转换为它的字符串表示形式。代码如下:

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

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

        List<String> stringNumbers = numbers.stream()
               .map(String::valueOf)
               .collect(Collectors.toList());

        System.out.println(stringNumbers);
    }
}

在上述代码中,numbers.stream()List<Integer>转换为一个Stream。map(String::valueOf)使用String.valueOf方法作为mapper函数,将Stream中的每个Integer元素映射为String类型的元素。最后,collect(Collectors.toList())将新生成的Stream收集为一个List<String>。执行这段代码,控制台会输出[1, 2, 3]对应的字符串形式["1", "2", "3"]

map方法的应用场景

数据转换

  1. 基本类型与包装类型转换 在Java中,基本类型和其对应的包装类型有时需要相互转换。例如,将List<Integer>转换为List<int[]>。通过map方法可以很方便地实现这种转换:
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class PrimitiveToWrapperConversion {
    public static void main(String[] args) {
        List<int[]> intArrays = new ArrayList<>();
        intArrays.add(new int[]{1});
        intArrays.add(new int[]{2});
        intArrays.add(new int[]{3});

        List<Integer> wrappedNumbers = intArrays.stream()
               .map(arr -> arr[0])
               .boxed()
               .collect(Collectors.toList());

        System.out.println(wrappedNumbers);
    }
}

在这段代码中,map(arr -> arr[0])首先从每个int[]数组中提取出第一个元素,得到一个IntStream。然后,boxed()方法将IntStream中的基本类型int转换为包装类型Integer,形成一个Stream<Integer>,最后收集为List<Integer>

  1. 自定义对象属性提取与转换 假设有一个表示员工的类Employee,包含员工ID、姓名和薪资等属性。我们有一个List<Employee>,现在希望获取所有员工的姓名,并将其转换为大写形式。代码如下:
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

class Employee {
    private int id;
    private String name;
    private double salary;

    public Employee(int id, String name, double salary) {
        this.id = id;
        this.name = name;
        this.salary = salary;
    }

    public String getName() {
        return name;
    }
}

public class EmployeeNameTransformation {
    public static void main(String[] args) {
        List<Employee> employees = new ArrayList<>();
        employees.add(new Employee(1, "John", 5000.0));
        employees.add(new Employee(2, "Jane", 6000.0));
        employees.add(new Employee(3, "Bob", 5500.0));

        List<String> upperCaseNames = employees.stream()
               .map(Employee::getName)
               .map(String::toUpperCase)
               .collect(Collectors.toList());

        System.out.println(upperCaseNames);
    }
}

这里,第一个map(Employee::getName)提取出每个Employee对象的姓名,形成一个Stream<String>。第二个map(String::toUpperCase)将每个姓名转换为大写形式,最后收集为List<String>

数据过滤与预处理

  1. 过滤空值 在处理可能包含空值的集合时,可以使用map方法结合Optional类来过滤空值。例如,有一个List<String>可能包含空字符串,我们希望过滤掉空字符串并获取非空字符串的长度。代码如下:
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

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

        List<Integer> lengths = strings.stream()
               .map(Optional::ofNullable)
               .filter(Optional::isPresent)
               .map(Optional::get)
               .map(String::length)
               .collect(Collectors.toList());

        System.out.println(lengths);
    }
}

在上述代码中,map(Optional::ofNullable)将每个字符串包装为Optional<String>filter(Optional::isPresent)过滤掉值为nullOptional对象,map(Optional::get)提取出非空的字符串,最后map(String::length)获取字符串的长度并收集为List<Integer>

  1. 数据清洗 假设我们有一个包含用户输入的字符串列表,其中可能包含一些前后空格。我们可以使用map方法去除这些空格,进行数据清洗。代码如下:
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class DataCleaning {
    public static void main(String[] args) {
        List<String> inputStrings = new ArrayList<>();
        inputStrings.add("  hello  ");
        inputStrings.add("world  ");
        inputStrings.add("  java");

        List<String> cleanedStrings = inputStrings.stream()
               .map(String::trim)
               .collect(Collectors.toList());

        System.out.println(cleanedStrings);
    }
}

map(String::trim)方法对每个字符串调用trim方法,去除前后空格,从而实现数据清洗。

map方法与并行流

并行流的概念

在Java Stream中,并行流是一种能够利用多核处理器并行处理数据的机制。通过将Stream的数据分割为多个部分,并行流可以同时对这些部分进行处理,从而提高处理效率。并行流的启用非常简单,只需在Stream上调用parallel()方法即可。例如:

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

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

        List<Integer> squaredNumbers = numbers.stream()
               .parallel()
               .map(n -> n * n)
               .collect(Collectors.toList());

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

在上述代码中,numbers.stream().parallel()List<Integer>转换为并行流,map(n -> n * n)对并行流中的每个元素进行平方运算。

map方法在并行流中的行为

map方法应用于并行流时,其一对一转换的本质依然不变,但处理过程会并行化。并行流会将数据分割为多个子任务,每个子任务在不同的线程中执行map操作。mapper函数必须是线程安全的,因为多个线程可能同时调用它。

例如,假设我们有一个计算复杂数学函数的mapper函数:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

public class ParallelMapExample {
    private static AtomicInteger counter = new AtomicInteger(0);

    public static double complexFunction(int num) {
        // 模拟复杂计算
        counter.incrementAndGet();
        return Math.sqrt(num * num * Math.PI);
    }

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

        List<Double> results = numbers.stream()
               .parallel()
               .map(ParallelMapExample::complexFunction)
               .collect(Collectors.toList());

        System.out.println("Counter value: " + counter.get());
    }
}

在这个例子中,complexFunction函数是线程安全的,因为它使用了AtomicInteger来保证计数器的线程安全。如果mapper函数不是线程安全的,可能会导致数据竞争和不一致的结果。

并行流中map方法的性能考量

虽然并行流可以提高处理大数据集的效率,但并非在所有情况下都适用。对于小数据集或者mapper函数执行时间较短的情况,并行流的额外开销(如任务分割、线程调度等)可能会导致性能下降。因此,在使用并行流时,需要根据具体的数据规模和mapper函数的复杂度进行性能测试和优化。

例如,我们可以对比顺序流和并行流在处理不同规模数据集时的性能:

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

public class PerformanceComparison {
    public static void main(String[] args) {
        int[] sizes = {100, 1000, 10000, 100000, 1000000};

        for (int size : sizes) {
            List<Integer> numbers = new ArrayList<>();
            for (int i = 0; i < size; i++) {
                numbers.add(i);
            }

            long sequentialStartTime = System.nanoTime();
            List<Integer> sequentialResult = numbers.stream()
                   .map(n -> n * n)
                   .collect(Collectors.toList());
            long sequentialEndTime = System.nanoTime();
            long sequentialDuration = TimeUnit.MILLISECONDS.convert(sequentialEndTime - sequentialStartTime, TimeUnit.NANOSECONDS);

            long parallelStartTime = System.nanoTime();
            List<Integer> parallelResult = numbers.stream()
                   .parallel()
                   .map(n -> n * n)
                   .collect(Collectors.toList());
            long parallelEndTime = System.nanoTime();
            long parallelDuration = TimeUnit.MILLISECONDS.convert(parallelEndTime - parallelStartTime, TimeUnit.NANOSECONDS);

            System.out.println("Size: " + size);
            System.out.println("Sequential time: " + sequentialDuration + " ms");
            System.out.println("Parallel time: " + parallelDuration + " ms");
            System.out.println();
        }
    }
}

通过运行上述代码,可以观察到在小数据集时,顺序流可能比并行流更快;而在大数据集时,并行流的优势会逐渐显现。

map方法与其他Stream操作的组合使用

mapfilter

mapfilter是Stream API中常用的两个操作,它们经常组合使用。filter操作用于根据指定的条件过滤Stream中的元素,而map操作则对过滤后的元素进行转换。

例如,我们有一个包含整数的List,希望过滤出偶数并将其平方。代码如下:

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

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

        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)对这些偶数进行平方运算,最后收集为List<Integer>

mapflatMap

flatMap方法与map方法类似,但它用于处理返回值为Stream的mapper函数。flatMap会将所有子Stream中的元素扁平化为一个Stream。

例如,假设有一个List<List<Integer>>,我们希望将其扁平化为一个List<Integer>并对每个元素进行平方。代码如下:

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

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

        List<Integer> squaredNumbers = nestedLists.stream()
               .flatMap(List::stream)
               .map(n -> n * n)
               .collect(Collectors.toList());

        System.out.println(squaredNumbers);
    }
}

在这段代码中,flatMap(List::stream)List<List<Integer>>扁平化为Stream<Integer>,然后map(n -> n * n)对其中的每个元素进行平方运算,最后收集为List<Integer>

mapreduce

reduce方法用于将Stream中的元素通过一个累积函数进行聚合操作。mapreduce可以组合使用,先对元素进行转换,再进行聚合。

例如,我们有一个包含整数的List,希望先将每个元素平方,然后计算它们的总和。代码如下:

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

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

        Optional<Integer> sumOfSquares = numbers.stream()
               .map(n -> n * n)
               .reduce((acc, num) -> acc + num);

        sumOfSquares.ifPresent(System.out::println);
    }
}

在上述代码中,map(n -> n * n)先将每个元素平方,然后reduce((acc, num) -> acc + num)对平方后的元素进行累加,最终得到平方和。Optional用于处理Stream为空的情况。

map方法使用的常见问题与注意事项

mapper函数返回null

如果mapper函数返回null,在后续操作(如collect)中可能会抛出NullPointerException。例如:

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

public class NullReturnInMapper {
    public static String potentiallyNullMapper(Integer num) {
        if (num % 2 == 0) {
            return num.toString();
        }
        return null;
    }

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

        try {
            List<String> strings = numbers.stream()
                   .map(NullReturnInMapper::potentiallyNullMapper)
                   .collect(Collectors.toList());
        } catch (NullPointerException e) {
            System.out.println("Caught NullPointerException: " + e.getMessage());
        }
    }
}

为了避免这种情况,可以在map操作前使用filter方法过滤掉可能导致mapper返回null的元素,或者在mapper函数内部进行更严格的空值处理。

性能问题

如前文所述,在并行流中使用map方法时,需要注意性能问题。此外,即使在顺序流中,如果mapper函数执行时间过长,也可能影响整体性能。在这种情况下,可以考虑优化mapper函数的实现,或者使用并行流进行加速,但要进行充分的性能测试。

数据类型兼容性

在使用map方法时,要确保mapper函数返回的类型与目标Stream的元素类型兼容。例如,如果目标是一个Stream<String>mapper函数必须返回String类型或其子类型。否则,会导致编译错误。

例如:

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

public class DataTypeIncompatibility {
    public static Integer wrongMapper(String str) {
        return str.length();
    }

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

        // 以下代码会导致编译错误
        // List<Integer> lengths = strings.stream()
        //      .map(DataTypeIncompatibility::wrongMapper)
        //      .collect(Collectors.toList());
    }
}

在上述代码中,wrongMapper函数返回Integer类型,与目标Stream<Integer>不兼容,因此会导致编译错误。

通过深入理解Java Stream map方法的一对一转换机制、应用场景、与其他操作的组合使用以及常见问题,开发者能够更加高效、准确地使用这一强大的功能,提升Java程序的数据处理能力和代码质量。无论是简单的数据转换,还是复杂的数据预处理和聚合操作,map方法都为开发者提供了便捷且灵活的解决方案。同时,合理利用并行流和注意性能问题,可以进一步优化程序的执行效率,使其在处理大数据集时也能表现出色。在实际开发中,结合具体的业务需求,熟练运用map方法及其相关特性,将有助于编写简洁、高效且易于维护的Java代码。