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

Java Stream 对比传统 foreach 的代码简洁性

2024-05-296.8k 阅读

Java Stream 与传统 foreach 的概述

Java Stream 简介

Java 8 引入了 Stream API,它为处理集合数据提供了一种全新的方式。Stream 代表一系列支持顺序和并行聚合操作的元素。Stream 并不存储数据,而是通过管道操作对数据源(如集合、数组等)进行处理。它允许以声明式的方式处理数据,将业务逻辑与数据处理逻辑分离,极大地提高了代码的可读性和可维护性。

例如,假设有一个整数列表,要筛选出所有偶数并计算它们的总和。使用 Stream 可以这样写:

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

public class StreamExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
        int sumOfEvens = numbers.stream()
               .filter(n -> n % 2 == 0)
               .mapToInt(Integer::intValue)
               .sum();
        System.out.println("Sum of evens: " + sumOfEvens);
    }
}

在上述代码中,numbers.stream() 将列表转换为 Stream,filter 方法筛选出偶数,mapToIntInteger 类型转换为 int 类型,最后 sum 方法计算总和。整个过程通过链式调用完成,代码简洁明了。

传统 foreach 简介

传统的 foreach 循环是 Java 5 引入的一种遍历集合和数组的简便方式。它使用 for (elementType element : collection) 的语法,简化了对集合和数组的遍历操作。

例如,同样是对上述整数列表筛选出偶数并计算总和,使用传统 foreach 循环可以这样实现:

import java.util.Arrays;
import java.util.List;

public class ForeachExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
        int sumOfEvens = 0;
        for (Integer number : numbers) {
            if (number % 2 == 0) {
                sumOfEvens += number;
            }
        }
        System.out.println("Sum of evens: " + sumOfEvens);
    }
}

这里通过 for (Integer number : numbers) 遍历列表中的每个元素,然后使用 if 语句判断是否为偶数,如果是则累加到 sumOfEvens 中。

代码简洁性在数据筛选方面的对比

Stream 在数据筛选中的简洁表现

Stream 的 filter 方法专门用于筛选数据,使得代码更加声明式。比如有一个包含人员信息的列表,要筛选出年龄大于 30 岁的人员。假设人员信息由一个 Person 类表示,代码如下:

import java.util.ArrayList;
import java.util.List;
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 int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class StreamFilterExample {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 25));
        people.add(new Person("Bob", 35));
        people.add(new Person("Charlie", 40));

        List<Person> peopleOver30 = people.stream()
               .filter(person -> person.getAge() > 30)
               .collect(Collectors.toList());
        System.out.println(peopleOver30);
    }
}

通过 people.stream().filter(person -> person.getAge() > 30) 这一行代码,清晰地表达了筛选年龄大于 30 岁人员的意图。

传统 foreach 在数据筛选中的实现

使用传统 foreach 循环实现相同功能时,代码如下:

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

class Person {
    private String name;
    private int age;

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

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class ForeachFilterExample {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 25));
        people.add(new Person("Bob", 35));
        people.add(new Person("Charlie", 40));

        List<Person> peopleOver30 = new ArrayList<>();
        for (Person person : people) {
            if (person.getAge() > 30) {
                peopleOver30.add(person);
            }
        }
        System.out.println(peopleOver30);
    }
}

可以看到,传统 foreach 循环需要手动创建一个新的列表 peopleOver30,并在循环中进行判断和添加操作,代码相对冗长,并且数据处理逻辑与业务逻辑混合在一起,不如 Stream 代码简洁清晰。

代码简洁性在数据转换方面的对比

Stream 在数据转换中的简洁体现

Stream 的 map 方法用于将 Stream 中的每个元素按照指定的规则进行转换。例如,有一个字符串列表,要将每个字符串转换为大写形式。使用 Stream 可以这样做:

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

public class StreamMapExample {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("java", "python", "c++");
        List<String> upperCaseWords = words.stream()
               .map(String::toUpperCase)
               .collect(Collectors.toList());
        System.out.println(upperCaseWords);
    }
}

通过 map(String::toUpperCase),简洁地将每个字符串转换为大写形式,整个过程链式调用,一目了然。

传统 foreach 在数据转换中的实现

使用传统 foreach 循环实现相同功能,代码如下:

import java.util.Arrays;
import java.util.List;

public class ForeachMapExample {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("java", "python", "c++");
        List<String> upperCaseWords = new ArrayList<>();
        for (String word : words) {
            upperCaseWords.add(word.toUpperCase());
        }
        System.out.println(upperCaseWords);
    }
}

传统 foreach 循环需要手动创建一个新的列表 upperCaseWords,并在循环中逐个进行转换和添加操作,代码量相对较多,且不如 Stream 代码简洁直观。

代码简洁性在聚合操作方面的对比

Stream 的聚合操作简洁性

Stream 提供了丰富的聚合操作方法,如 sumaveragemaxmin 等。以计算整数列表的平均值为例,使用 Stream 代码如下:

import java.util.Arrays;
import java.util.List;
import java.util.OptionalDouble;

public class StreamAggregationExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
        OptionalDouble average = numbers.stream()
               .mapToInt(Integer::intValue)
               .average();
        average.ifPresent(System.out::println);
    }
}

通过 mapToIntInteger 转换为 int 类型,然后使用 average 方法直接计算平均值,OptionalDouble 用于处理可能为空的情况,代码简洁高效。

传统 foreach 在聚合操作中的实现

使用传统 foreach 循环计算平均值,代码如下:

import java.util.Arrays;
import java.util.List;

public class ForeachAggregationExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
        int sum = 0;
        for (Integer number : numbers) {
            sum += number;
        }
        double average = numbers.size() > 0? sum / (double) numbers.size() : 0;
        System.out.println(average);
    }
}

传统 foreach 循环需要手动累加每个元素,然后再根据列表大小计算平均值,代码相对繁琐,尤其在处理可能为空的列表时,需要额外的判断逻辑。

复杂数据处理场景下的代码简洁性对比

Stream 在复杂数据处理中的优势

当面临复杂的数据处理场景,如多级筛选、转换和聚合时,Stream 的优势更加明显。例如,假设有一个包含订单信息的列表,订单包含商品列表,要计算所有订单中价格大于 100 的商品的总数量。代码如下:

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

class Product {
    private String name;
    private double price;

    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public double getPrice() {
        return price;
    }
}

class Order {
    private List<Product> products;

    public Order(List<Product> products) {
        this.products = products;
    }

    public List<Product> getProducts() {
        return products;
    }
}

public class StreamComplexExample {
    public static void main(String[] args) {
        List<Order> orders = new ArrayList<>();
        orders.add(new Order(Arrays.asList(
                new Product("Product1", 80),
                new Product("Product2", 120)
        )));
        orders.add(new Order(Arrays.asList(
                new Product("Product3", 90),
                new Product("Product4", 150)
        )));

        long totalCount = orders.stream()
               .flatMap(order -> order.getProducts().stream())
               .filter(product -> product.getPrice() > 100)
               .count();
        System.out.println("Total count of products with price > 100: " + totalCount);
    }
}

这里通过 flatMap 将订单中的商品列表扁平化,然后进行筛选和计数,代码通过链式调用清晰地表达了复杂的数据处理逻辑。

传统 foreach 在复杂数据处理中的挑战

使用传统 foreach 循环实现相同功能,代码如下:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Product {
    private String name;
    private double price;

    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public double getPrice() {
        return price;
    }
}

class Order {
    private List<Product> products;

    public Order(List<Product> products) {
        this.products = products;
    }

    public List<Product> getProducts() {
        return products;
    }
}

public class ForeachComplexExample {
    public static void main(String[] args) {
        List<Order> orders = new ArrayList<>();
        orders.add(new Order(Arrays.asList(
                new Product("Product1", 80),
                new Product("Product2", 120)
        )));
        orders.add(new Order(Arrays.asList(
                new Product("Product3", 90),
                new Product("Product4", 150)
        )));

        int totalCount = 0;
        for (Order order : orders) {
            List<Product> products = order.getProducts();
            for (Product product : products) {
                if (product.getPrice() > 100) {
                    totalCount++;
                }
            }
        }
        System.out.println("Total count of products with price > 100: " + totalCount);
    }
}

传统 foreach 循环需要嵌套循环来处理订单和商品的关系,并且在循环内部进行条件判断和计数,代码结构相对复杂,可读性较差。

并行处理与代码简洁性

Stream 的并行处理简洁性

Stream API 支持并行处理,通过 parallelStream 方法可以轻松将顺序流转换为并行流,实现数据的并行处理。例如,计算一个大整数列表的总和,使用并行流可以提高计算效率,代码如下:

import java.util.Arrays;
import java.util.List;

public class StreamParallelExample {
    public static void main(String[] args) {
        List<Integer> largeNumbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);
        long sum = largeNumbers.parallelStream()
               .mapToLong(Integer::longValue)
               .sum();
        System.out.println("Sum of large numbers: " + sum);
    }
}

只需将 stream 替换为 parallelStream,即可实现并行处理,代码简洁,并且无需手动管理线程等复杂操作。

传统 foreach 在并行处理方面的局限

传统 foreach 循环本身不支持并行处理。如果要实现并行处理,需要手动使用多线程技术,这会使代码变得复杂。例如,要使用传统多线程方式计算上述大整数列表的总和,代码如下:

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ForeachParallelExample {
    public static void main(String[] args) throws Exception {
        List<Integer> largeNumbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);
        int numThreads = 4;
        ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
        int chunkSize = largeNumbers.size() / numThreads;
        List<Future<Long>> futures = new ArrayList<>();

        for (int i = 0; i < numThreads; i++) {
            int start = i * chunkSize;
            int end = (i == numThreads - 1)? largeNumbers.size() : (i + 1) * chunkSize;
            List<Integer> subList = largeNumbers.subList(start, end);
            futures.add(executorService.submit(() -> {
                long subSum = 0;
                for (Integer number : subList) {
                    subSum += number;
                }
                return subSum;
            }));
        }

        long sum = 0;
        for (Future<Long> future : futures) {
            sum += future.get();
        }

        executorService.shutdown();
        System.out.println("Sum of large numbers: " + sum);
    }
}

可以看到,使用传统多线程方式实现并行计算,需要创建线程池、划分数据块、提交任务、处理返回结果等一系列复杂操作,代码量大幅增加,且维护难度加大。

代码可读性与简洁性的关系

Stream 对代码可读性的提升

Stream 代码通过声明式的方式表达数据处理逻辑,使得代码更接近自然语言描述。例如,list.stream().filter(item -> item.isValid()).map(Item::transform).collect(Collectors.toList()) 这段代码清晰地表达了从列表中筛选有效项,然后进行转换,最后收集为新列表的过程。这种表达方式让代码的意图一目了然,即使对于不熟悉具体实现细节的人来说,也能快速理解代码的功能,从而提高了代码的可读性。

传统 foreach 对代码可读性的影响

传统 foreach 循环在实现复杂数据处理时,由于逻辑分散在循环内部,使得代码的整体意图不够清晰。例如,在多级筛选和转换的场景下,嵌套的 foreach 循环以及大量的临时变量和条件判断会使代码结构变得复杂,增加了理解代码功能的难度,进而影响了代码的可读性。

总结 Stream 与传统 foreach 的代码简洁性差异

在数据筛选、转换、聚合以及复杂数据处理和并行处理等方面,Java Stream 相比传统 foreach 循环在代码简洁性上具有明显优势。Stream 通过链式调用和声明式编程,将数据处理逻辑以一种更加清晰、简洁的方式表达出来,减少了样板代码,提高了代码的可读性和可维护性。而传统 foreach 循环虽然在简单遍历场景下表现良好,但在面对复杂数据处理和并行处理需求时,代码会变得冗长、复杂,可读性降低。因此,在实际开发中,应根据具体场景合理选择使用 Stream 或传统 foreach 循环,以达到最佳的代码质量和开发效率。