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

Java中的Stream API与Lambda表达式

2024-04-171.7k 阅读

Java中的Stream API与Lambda表达式

1. Lambda表达式基础

在Java 8引入Lambda表达式之前,传递代码片段到方法或存储在变量中并不直观。传统方式是通过匿名类,但匿名类语法冗长。例如,假设我们有一个简单的Runnable接口:

Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Using anonymous class");
    }
};

使用Lambda表达式,上述代码可以简化为:

Runnable r2 = () -> System.out.println("Using lambda expression");

Lambda表达式的基本语法为(parameters) -> expression(parameters) -> { statements; }。当只有一个参数时,括号可以省略;当表达式只有一条语句时,花括号和return关键字可以省略。

Lambda表达式可以与函数式接口一起使用。函数式接口是指只包含一个抽象方法的接口。Java 8提供了许多内置的函数式接口,如Predicate<T>Function<T, R>Consumer<T>等。

Predicate<T>接口用于测试对象是否满足某种条件。例如,判断一个整数是否为偶数:

Predicate<Integer> isEven = num -> num % 2 == 0;
System.out.println(isEven.test(4)); // 输出 true

Function<T, R>接口用于将一个类型的对象转换为另一个类型的对象。例如,将字符串转换为其长度:

Function<String, Integer> lengthFunction = String::length;
System.out.println(lengthFunction.apply("Hello")); // 输出 5

Consumer<T>接口用于对对象执行某种操作,而不返回结果。例如,打印一个字符串:

Consumer<String> printer = System.out::println;
printer.accept("Hello, world!");

2. Stream API概述

Stream API是Java 8中引入的一个强大的框架,用于对集合数据进行高效的处理。Stream代表了一系列支持顺序和并行聚合操作的元素。与集合不同,Stream并不存储数据,而是在原数据上进行操作。

Stream的操作分为中间操作和终端操作。中间操作返回一个新的Stream,可以链式调用多个中间操作;终端操作会触发Stream的处理,并返回一个结果或副作用(如打印到控制台)。

创建Stream有多种方式。可以从集合创建Stream,例如:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> numberStream = numbers.stream();

也可以从数组创建Stream:

int[] array = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(array);

还可以使用Stream.of()方法创建Stream:

Stream<String> stringStream = Stream.of("apple", "banana", "cherry");

3. Stream的中间操作

  • 过滤(Filter)filter(Predicate<? super T> predicate)方法用于筛选出满足给定条件的元素。例如,从一个整数列表中筛选出偶数:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> evenNumbers = numbers.stream().filter(num -> num % 2 == 0);
evenNumbers.forEach(System.out::println);
  • 映射(Map)map(Function<? super T,? extends R> mapper)方法用于将Stream中的每个元素转换为另一个元素。例如,将一个字符串列表中的每个字符串转换为其长度:
List<String> words = Arrays.asList("apple", "banana", "cherry");
Stream<Integer> wordLengths = words.stream().map(String::length);
wordLengths.forEach(System.out::println);

如果要处理的是对象流,并且对象包含特定属性,也可以映射该属性。假设我们有一个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对象流中映射出年龄:

List<Person> people = Arrays.asList(
    new Person("Alice", 25),
    new Person("Bob", 30),
    new Person("Charlie", 35)
);
Stream<Integer> ages = people.stream().map(Person::getAge);
ages.forEach(System.out::println);
  • 排序(Sorted)sorted()方法用于对Stream中的元素进行自然排序,sorted(Comparator<? super T> comparator)方法可以使用自定义的比较器进行排序。例如,对一个整数列表进行升序排序:
List<Integer> numbers = Arrays.asList(5, 3, 4, 1, 2);
Stream<Integer> sortedNumbers = numbers.stream().sorted();
sortedNumbers.forEach(System.out::println);

如果要对Person对象按年龄进行排序:

List<Person> people = Arrays.asList(
    new Person("Alice", 25),
    new Person("Bob", 30),
    new Person("Charlie", 35)
);
Stream<Person> sortedPeople = people.stream()
  .sorted(Comparator.comparingInt(Person::getAge));
sortedPeople.forEach(p -> System.out.println(p.getName() + " : " + p.getAge()));
  • 去重(Distinct)distinct()方法用于去除Stream中的重复元素。例如,去除一个整数列表中的重复元素:
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3);
Stream<Integer> distinctNumbers = numbers.stream().distinct();
distinctNumbers.forEach(System.out::println);
  • 截断(Limit)limit(long maxSize)方法用于截取Stream的前maxSize个元素。例如,从一个整数列表中截取前3个元素:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> limitedNumbers = numbers.stream().limit(3);
limitedNumbers.forEach(System.out::println);
  • 跳过(Skip)skip(long n)方法用于跳过Stream的前n个元素。例如,从一个整数列表中跳过前2个元素:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> skippedNumbers = numbers.stream().skip(2);
skippedNumbers.forEach(System.out::println);

4. Stream的终端操作

  • 遍历(ForEach)forEach(Consumer<? super T> action)方法用于对Stream中的每个元素执行给定的操作。例如,打印一个字符串列表中的每个字符串:
List<String> words = Arrays.asList("apple", "banana", "cherry");
words.stream().forEach(System.out::println);
  • 归约(Reduce)reduce(T identity, BinaryOperator<T> accumulator)方法用于将Stream中的元素组合成一个值。identity是初始值,accumulator是一个用于组合元素的二元操作。例如,计算一个整数列表的总和:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
System.out.println(sum); // 输出 15

也可以使用方法引用来简化代码:

int sum2 = numbers.stream().reduce(0, Integer::sum);
System.out.println(sum2); // 输出 15

reduce(BinaryOperator<T> accumulator)方法没有初始值,返回一个Optional<T>对象,因为Stream可能为空。例如,计算一个整数列表的乘积:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> product = numbers.stream().reduce((a, b) -> a * b);
product.ifPresent(System.out::println); // 输出 120
  • 收集(Collect)collect(Collector<? super T, A, R> collector)方法用于将Stream中的元素收集到一个集合或其他数据结构中。Collectors类提供了许多预定义的收集器。例如,将一个字符串列表收集到一个Set中:
List<String> words = Arrays.asList("apple", "banana", "cherry");
Set<String> wordSet = words.stream().collect(Collectors.toSet());
System.out.println(wordSet);

收集到一个List中:

List<String> newWordsList = words.stream().collect(Collectors.toList());
System.out.println(newWordsList);

还可以使用Collectors.groupingBy()方法按某个属性对对象进行分组。例如,按年龄对Person对象进行分组:

List<Person> people = Arrays.asList(
    new Person("Alice", 25),
    new Person("Bob", 30),
    new Person("Charlie", 30)
);
Map<Integer, List<Person>> peopleByAge = people.stream()
  .collect(Collectors.groupingBy(Person::getAge));
peopleByAge.forEach((age, personList) -> {
    System.out.println("Age " + age + ": " + personList);
});

Collectors.summingInt()方法可以用于计算对象属性的总和。例如,计算Person对象的年龄总和:

int totalAge = people.stream()
  .collect(Collectors.summingInt(Person::getAge));
System.out.println(totalAge);
  • 查找(Find)findFirst()方法返回Stream中的第一个元素,返回一个Optional<T>对象。例如:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstNumber = numbers.stream().findFirst();
firstNumber.ifPresent(System.out::println); // 输出 1

findAny()方法返回Stream中的任意一个元素,在并行流中可能返回不同的元素。例如:

Optional<Integer> anyNumber = numbers.parallelStream().findAny();
anyNumber.ifPresent(System.out::println);
  • 匹配(Match)allMatch(Predicate<? super T> predicate)方法用于判断Stream中的所有元素是否都满足给定条件。例如,判断一个整数列表中的所有元素是否都大于0:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean allGreaterThanZero = numbers.stream().allMatch(num -> num > 0);
System.out.println(allGreaterThanZero); // 输出 true

anyMatch(Predicate<? super T> predicate)方法用于判断Stream中是否有任何一个元素满足给定条件。例如,判断一个整数列表中是否有偶数:

boolean hasEven = numbers.stream().anyMatch(num -> num % 2 == 0);
System.out.println(hasEven); // 输出 true

noneMatch(Predicate<? super T> predicate)方法用于判断Stream中的所有元素是否都不满足给定条件。例如,判断一个整数列表中是否没有负数:

boolean noNegative = numbers.stream().noneMatch(num -> num < 0);
System.out.println(noNegative); // 输出 true
  • 计数(Count)count()方法用于返回Stream中元素的数量。例如:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
long count = numbers.stream().count();
System.out.println(count); // 输出 5

5. 并行Stream

Stream API支持并行处理,通过parallelStream()方法可以将一个顺序流转换为并行流。并行流会利用多线程对元素进行处理,从而提高处理速度,特别是对于大数据集。

例如,计算一个大整数列表的总和,使用并行流:

List<Integer> largeNumbers = IntStream.rangeClosed(1, 1000000)
  .boxed()
  .collect(Collectors.toList());
long parallelSum = largeNumbers.parallelStream()
  .reduce(0L, Long::sum);
System.out.println(parallelSum);

在使用并行流时,需要注意以下几点:

  • 线程安全:如果在并行处理过程中修改共享状态,可能会导致线程安全问题。例如,在forEach操作中修改外部变量,需要使用线程安全的集合或Atomic类型。
  • 性能开销:并行流的创建和管理会有一定的性能开销。对于小数据集,并行流可能反而会比顺序流慢。因此,需要根据数据集的大小和操作的复杂度来选择使用顺序流还是并行流。
  • 数据独立性:并行流的元素处理顺序是不确定的。如果操作依赖于元素的顺序,如findFirst,在并行流中可能得到不同的结果。

6. Stream与Lambda表达式的结合应用

Stream API和Lambda表达式紧密结合,Lambda表达式为Stream的操作提供了简洁的代码实现。例如,在过滤、映射、排序等操作中,Lambda表达式作为参数传递,使得代码更加简洁易读。

假设我们有一个Book类:

class Book {
    private String title;
    private String author;
    private double price;

    public Book(String title, String author, double price) {
        this.title = title;
        this.author = author;
        this.price = price;
    }

    public String getTitle() {
        return title;
    }

    public String getAuthor() {
        return author;
    }

    public double getPrice() {
        return price;
    }
}

现在我们有一个Book对象列表,要找出价格低于50的书,并按价格升序排序,然后打印出书名和作者:

List<Book> books = Arrays.asList(
    new Book("Effective Java", "Joshua Bloch", 45.0),
    new Book("Clean Code", "Robert C. Martin", 55.0),
    new Book("The Pragmatic Programmer", "Andrew Hunt", 40.0)
);
books.stream()
  .filter(book -> book.getPrice() < 50)
  .sorted(Comparator.comparingDouble(Book::getPrice))
  .forEach(book -> System.out.println(book.getTitle() + " by " + book.getAuthor()));

在这个例子中,filtersorted操作使用Lambda表达式定义条件和比较器,forEach操作使用Lambda表达式定义对每个满足条件的Book对象的处理方式。

7. 总结Stream API和Lambda表达式的优势

  • 代码简洁:Lambda表达式减少了匿名类的冗长代码,Stream API通过链式调用和简洁的操作方法,使代码更易读和维护。
  • 提高生产力:Stream API提供了丰富的聚合操作,减少了手动编写循环和复杂数据处理逻辑的工作量,提高了开发效率。
  • 并行处理:Stream API支持并行流,能够充分利用多核处理器的性能,提高大数据集的处理速度。
  • 函数式编程风格:Lambda表达式和Stream API引入了函数式编程的概念,使代码更具声明性,易于理解和推理。

通过深入理解和应用Stream API与Lambda表达式,Java开发者能够编写出更高效、简洁和可读的代码,提升程序的性能和可维护性。无论是处理集合数据、文件I/O还是其他数据处理任务,这两个强大的特性都为Java编程带来了新的活力。