Java 函数式编程&Stream流

之前没学到这个知识点,面试被问到了,没有答上来,学习了一下整理了篇文章记录一下 :学习链接

1.概述

只需要关注函数的含义,这个函数可以对数据进行那些操作,不需要关注具体的对象和类。

优点

  • 代码简洁,
  • 开发快速
  • 接近自然语言
  • 易于理解 易于"并发编程"

下面给出一个案例

查询未成年作家的评分在70以上的书籍 ,需要进行去重

java 复制代码
List<Book> bookList = new ArrayList<>();
Set<Book> uniqueBookValues = new HashSet<>();
Set<Author> uniqueAuthorValues = new HashSet<>();
for (Author author : authors) {
    if (uniqueAuthorValues.add(author)) {
        if (author.getAge() < 18) {
            List<Book> books = author.getBooks();
            for (Book book : books) {
                if (book.getScore() > 70) {
                    if (uniqueBookValues.add(book)) {
                        bookList.add(book);
                    }
                }
            }
        }
    }
}
System.out.println(bookList);
java 复制代码
List<Book> collect = authors.stream()
        .distinct()
        .filter(author -> author.getAge() < 18)
        .map(author -> author.getBooks())
        .flatMap(Collection::stream)
        .filter(book -> book.getScore() > 70)
        .distinct()
        .collect(Collectors.toList());
System.out.println(collect);

2.Lambda表达式

2.1 概述

Java 中的 Lambda 表达式是一种简洁的表示匿名函数的方式,它允许你将函数作为方法参数,或者将代码作为数据对待。Lambda 表达式与函数式接口(只有一个抽象方法的接口)一起使用

2.2 基本格式

rust 复制代码
(参数列表) -> {代码}

案例

1.线程创建

java 复制代码
// 使用匿名内部类创建线程
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("hello");
    }
}).start();

// 使用Lambda 表达式进行修改
new  Thread(()->{
    System.out.println("hello");
}).start();
  1. 使用 Comparator 接口对列表进行排序:

Lambda 表达式:

java 复制代码
List<String> names = Arrays.asList("Jane", "Bob", "Alice", "Tom");
Collections.sort(names, (a, b) -> a.compareTo(b));

传统匿名内部类:

java 复制代码
List<String> names = Arrays.asList("Jane", "Bob", "Alice", "Tom");
Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.compareTo(b);
    }
});

2.3 省略规则

  1. 参数类型 :
    如果编译器能够从上下文推断出参数的类型,则可以省略参数类型。
java 复制代码
// 完整形式
Comparator<String> comparator = (String a, String b) -> a.compareTo(b);

// 省略参数类型
Comparator<String> comparator = (a, b) -> a.compareTo(b);
  1. 小括号 (当只有一个参数时):
    如果 Lambda 表达式只有一个参数,并且其类型可以被省略,那么也可以省略小括号。
java 复制代码
// 完整形式
Consumer<String> consumer = (String s) -> System.out.println(s);

// 省略参数类型和小括号
Consumer<String> consumer = s -> System.out.println(s);
  1. 大括号和 return 关键字 (当主体只有一条语句时):
    如果 Lambda 表达式的主体只包含一个语句,那么可以省略大括号和 return 关键字。对于表达式主体,如果它是一个表达式,那么会隐式返回该表达式的结果。
java 复制代码
// 完整形式
Function<String, Integer> function = (String s) -> { return s.length(); };

// 省略大括号和 return 关键字
Function<String, Integer> function = s -> s.length();
  1. 分号 (当主体只有一条语句时):
    如果 Lambda 表达式的主体是一个语句块,且只有一条语句,那么可以省略分号。
java 复制代码
// 完整形式
Runnable runnable = () -> { System.out.println("Hello, World!"); };

// 省略分号
Runnable runnable = () -> System.out.println("Hello, World!");

需要注意的是,如果 Lambda 表达式的主体包含多条语句,或者你需要明确返回一个值(即使是单一表达式),则必须使用大括号,且在返回值的情况下必须使用 return 关键字。

java 复制代码
// 多条语句需要大括号和分号
Runnable runnable = () -> {
    System.out.println("Statement 1");
    System.out.println("Statement 2");
};

// 需要明确返回值时需要大括号和 return 关键字
BinaryOperator<Integer> operator = (a, b) -> { return a * b; };

总结来说,Lambda 表达式的省略规则旨在让代码更加简洁,但是在使用时也需要保证代码的可读性不受影响。

Stream流

Java 中的 Stream API 是 Java 8 引入的一个强大的数据处理功能,它允许你以声明式的方式处理数据集合。Stream API 可以极大地简化集合操作,并支持顺序和并行处理。

Stream API 主要提供了以下几类操作:

  1. 创建 Stream :
    可以通过 Collection.stream()Stream.of() 等方法创建 Stream。
  2. 中间操作 (Intermediate Operations):
    这些操作是惰性的,它们不会立即执行,而是创建一个新的 Stream,当终端操作被触发时才会执行。中间操作包括 filter, map, flatMap, sorted, distinct, limit, skip 等。
  3. 终结操作 (Terminal Operations):
    终结操作会触发流的处理,它们会消耗流并产生结果。这些操作包括 forEach, collect, reduce, findFirst, findAny, allMatch, noneMatch, count, min, max 等。
  4. 短路操作 (Short-circuiting Operations):
    某些操作,如 limitfindFirst, 不需要处理整个流就可以返回结果。这些操作在处理无限流时特别有用。

以下是一些使用 Java Stream API 的例子:

创建 Stream:

1.单列集合: 集合对象.stream()

java 复制代码
List authors = getAuthors();
Stream stream = authors.stream();

2.数组: Arrays.stream(数组) 或者使用 Stream.of 来创建

java 复制代码
Integer[] arr = {12,3,4,5};
Stream<Integer> stream = Arrays.stream(arr);
Stream<Integer> arr1 = Stream.of(arr);

3.双列集合:转换成单列集合后再创建(用于存储

java 复制代码
Map<String,Integer> map = new HashMap<>();
map.put("tom",21);
map.put("zeus",19);
map.put("sunly",16);
Stream<Map.Entry<String, Integer>> stream = map.entrySet().stream();

中间操作:

  1. filter:

    • 用于过滤流中的元素。
    • 接受一个 Predicate 函数式接口。
    • 返回一个只包含满足条件的元素的新流。
java 复制代码
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
Stream<String> filteredNames = names.stream().filter(name -> name.startsWith("A"));
  1. map:

    • 用于将流中的每个元素映射到另一个元素。
    • 接受一个 Function 函数式接口。
    • 返回一个包含映射后元素的新流。
java 复制代码
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
Stream<Integer> nameLengths = names.stream().map(String::length);
  1. flatMap:

    • 用于将流中的每个元素映射到一个流。
    • 接受一个 Function 函数式接口,该接口返回一个流的对象。
    • 返回一个包含所有映射流中所有元素的新流。
java 复制代码
List<List<String>> listOfLists = Arrays.asList(
    Arrays.asList("a", "b"),
    Arrays.asList("c", "d")
);
Stream<String> flatStream = listOfLists.stream().flatMap(List::stream);
  1. distinct:

    • 用于去除流中的重复元素。
    • 不接受任何参数。
    • 返回一个去除重复元素后的新流。
java 复制代码
Stream<String> distinctElements = Stream.of("a", "b", "a", "c", "b", "d").distinct();

注意:distinct方法是依赖Object的equals方法来判断是否是相同对象的。所以需要注意重写equals 方法。

  1. sorted:

    • 用于对流中的元素进行排序。
    • 可以接受一个 Comparator 函数式接口来定义排序逻辑,也可以不接受参数使用元素的自然顺序。
    • 返回一个排序后的新流。
arduino 复制代码
Stream<String> sortedStream = Stream.of("a", "d", "b", "c").sorted();

注意:如果调用空参的sorted()方法,需要流中的元素是实现了Comparable。

  1. peek:

    • 主要用于调试目的,在流的每个元素恢复运行时执行提供的操作。
    • 接受一个 Consumer 函数式接口。
    • 返回一个与原始流具有相同元素的新流。
java 复制代码
Stream<String> peekStream = Stream.of("a", "b", "c", "d").peek(System.out::println);
  1. limit:

    • 用于截取流使其最大长度不超过给定数量。
    • 接受一个 long 类型的参数作为最大长度。
    • 返回一个长度不超过指定最大长度的新流。
java 复制代码
Stream<String> limitedStream = Stream.of("a", "b", "c", "d").limit(2);
  1. skip:

    • 用于跳过流中的前N个元素。
    • 接受一个 long 类型的参数作为要跳过的元素数量。
    • 返回一个排除了前N个元素的新流。
java 复制代码
Stream<String> skippedStream = Stream.of("a", "b", "c", "d").skip(2);

以上中间操作是构建流水线的基础,可以组合使用它们来实现复杂的数据处理逻辑。记住,这些操作不会立即执行,它们需要一个终结操作,比如 forEachcollectreduce 等来触发执行。

终结操作:

Stream API 的终结操作是那些实际执行流管道并产生结果的操作。终结操作会触发流中所有惰性操作的执行,并且在执行完毕后流将不再可用。以下是一些常见的终结操作:

  1. forEach:

    • 用于迭代流中的每个元素。
    • 接受一个 Consumer 函数式接口。
    • 通常用于执行副作用,如打印。
java 复制代码
Stream<String> stream = Stream.of("a", "b", "c");
stream.forEach(System.out::println); // 打印每个元素
  1. collect:

    • 用于将流转换成其他形式,如集合或者 Map。
    • 接受一个 Collector 实例。
    • 非常灵活,是最常用的终端操作之一。
java 复制代码
List<String> list = Stream.of("a", "b", "c").collect(Collectors.toList()); // 收集到 List
  1. toArray:

    • 将流转换为数组。
    • 可以提供一个生成器函数来指定数组的类型。
java 复制代码
String[] array = Stream.of("a", "b", "c").toArray(String[]::new); // 转换为数组
  1. reduce:

    • 将流中的元素组合起来,使用一个二元函数累加器,并返回一个 Optional。
    • 有多种重载方法,可以有初始值,也可以没有。
java 复制代码
Optional<String> concatenated = Stream.of("a", "b", "c").reduce((acc, elem) -> acc + elem); // 组合字符串
concatenated.ifPresent(System.out::println);  // res:abc 
  1. count:

    • 返回流中元素的数量。
    • 返回一个 long 类型的值。
java 复制代码
long count = Stream.of("a", "b", "c").count(); // 计算元素数量
  1. minmax:

    • 返回流中的最小或最大元素,基于提供的 Comparator
    • 返回一个 Optional 类型的值。
java 复制代码
Optional<String> min = Stream.of("a", "b", "c").min(String::compareTo); // 获取最小值
Optional<String> max = Stream.of("a", "b", "c").max(String::compareTo); // 获取最大值
  1. findFirstfindAny:

    • 返回流中的第一个元素或任意一个元素(对于并行流来说)。
    • 返回一个 Optional 类型的值。
java 复制代码
Optional<String> first = Stream.of("a", "b", "c").findFirst(); // 获取第一个元素
Optional<String> any = Stream.of("a", "b", "c").findAny(); // 获取任意一个元素
  1. allMatch , anyMatch , noneMatch:

    • 检查流中的元素是否满足给定的谓词。
    • allMatch 检查是否所有元素都匹配,anyMatch 检查是否至少有一个元素匹配,noneMatch 检查是否没有元素匹配。
    • 返回一个 boolean 类型的值。
java 复制代码
boolean allMatch = Stream.of("a", "b", "c").allMatch(s -> s.length() == 1); // 检查所有元素长度是否为1
boolean anyMatch = Stream.of("a", "bb", "c").anyMatch(s -> s.length() > 1); // 检查是否存在元素长度大于1
boolean noneMatch = Stream.of("a", "b", "c").noneMatch(s -> s.length() > 1); // 检查是否没有元素长度大于1

终端操作是流处理的最后一个步骤,一旦执行,流就会被关闭,不能再被使用。这些操作可以生成一个结果,或者像 forEach 这样的操作执行一些副作用。

注意事项

  • 惰性求值(如果没有终结操作,没有中间操作是不会得到执行的)
  • 流是一次性的(一旦一个流对象经过一个终结操作后。这个流就不能再被使用)
  • 不会影响原数据(我们在流中可以多数据做很多处理。但是正常情况下是不会影响原来集合中的元 素的。这往往也是我们期望的)

Optional

Optional 是 Java 8 引入的一个容器类,用于封装可能为 null 的值。Optional 类的目的是提供一种更好的方法来处理 null,避免直接使用 null 值,从而减少空指针异常(NullPointerException)的风险

创建对象

我们一般使用Optional静态方法ofNullable来把数据封装成一个Optional对象。无论传入的参数是否 为null都不会出现问题

java 复制代码
Author author = getAuthor();
Optional authorOptional = Optional.ofNullable(author);

如果你确定一个对象不是空的则可以使用Optional的静态方法of来把数据封装成Optional对象。

java 复制代码
Author author = new Author();
Optional authorOptional = Optional.of(author);

安全使用

我们获取到一个Optional对象后肯定需要对其中的数据进行使用。这时候我们可以使用其ifPresent方法 对来消费其中的值。 这个方法会判断其内封装的数据是否为空,不为空时才会执行具体的消费代码。这样使用起来就更加安 全了。 例如,以下写法就优雅的避免了空指针异常。

java 复制代码
Optional authorOptional = Optional.ofNullable(getAuthor()); 
authorOptional.ifPresent(author -> System.out.println(author.getName()));

常用方法使用

  1. empty() :
    创建一个空的 Optional 对象。
java 复制代码
Optional<String> emptyOptional = Optional.empty();
System.out.println(emptyOptional.isPresent()); // 输出: false
  1. of(value) :
    创建一个包含给定非 null 值的 Optional 对象。
java 复制代码
Optional<String> nonEmptyOptional = Optional.of("Hello");
System.out.println(nonEmptyOptional.isPresent()); // 输出: true
  1. ofNullable(value) :
    创建一个允许包含 null 值的 Optional 对象。
java 复制代码
String nullString = null;
Optional<String> nullableOptional = Optional.ofNullable(nullString);
System.out.println(nullableOptional.isPresent()); // 输出: false
  1. isPresent() :
    检查 Optional 对象是否包含非 null 值。
java 复制代码
if (nonEmptyOptional.isPresent()) {
    System.out.println("Value exists"); // 输出: Value exists
}
  1. get() :
    如果 Optional 包含值,则返回该值,否则抛出 NoSuchElementException
java 复制代码
String value = nonEmptyOptional.get();
System.out.println(value); // 输出: Hello
  1. orElse(other) :
    如果 Optional 包含值,则返回该值,否则返回 other
java 复制代码
Optional<Object> empty = Optional.empty();
System.out.println(empty.orElse("default value")); //输出:default value
  1. orElseGet(supplier) :
    如果 Optional 包含值,则返回该值,否则返回由 Supplier 接口提供的值。
java 复制代码
String suppliedValue = nullableOptional.orElseGet(() -> "Supplied Value");
System.out.println(suppliedValue); // 输出: Supplied Value
  1. orElseThrow(exceptionSupplier) :
    如果 Optional 包含值,则返回该值,否则抛出由 Supplier 接口提供的异常。
java 复制代码
try {
    value = nullableOptional.orElseThrow(IllegalStateException::new);
} catch (IllegalStateException e) {
    System.out.println(e.getMessage()); // 输出: null
}
  1. ifPresent(consumer) :
    如果 Optional 包含值,则执行 Consumer 接口的操作。
java 复制代码
nonEmptyOptional.ifPresent(v -> System.out.println("Value: " + v)); // 输出: Value: Hello
  1. map(function) :
    如果 Optional 包含值,则对该值应用 Function 接口的函数,然后返回一个包含函数结果的 Optional
java 复制代码
Optional<Integer> lengthOptional = nonEmptyOptional.map(String::length);
lengthOptional.ifPresent(length -> System.out.println("Length: " + length)); // 输出: Length: 5
  1. flatMap(function) :
    类似于 map,但要求 Function 接口的函数返回 Optional 对象。
java 复制代码
Optional<Optional<String>> nestedOptional = Optional.of(Optional.of("Nested"));
Optional<String> flatOptional = nestedOptional.flatMap(o -> o);
flatOptional.ifPresent(System.out::println); // 输出: Nested
  1. ifPresentOrElse(consumer, runnable) :
    如果 Optional 包含值,则执行 Consumer 接口的操作,否则执行 Runnable 接口的操作(Java 9 及更高版本)。
java 复制代码
nullableOptional.ifPresentOrElse(
    v -> System.out.println("Value: " + v),
    () -> System.out.println("No Value") // 输出: No Value
);
  1. stream() :
    如果 Optional 包含值,则返回一个单元素流,否则返回一个空流(Java 9 及更高版本)。
java 复制代码
Stream<String> optionalStream = nonEmptyOptional.stream();
optionalStream.forEach(System.out::println); // 输出: Hello

这些案例展示了 Optional 类中方法的用法,以及如何利用 Optional 来优雅地处理可能为 null 的情况。

Stream进阶

基本数据类型优化

我们之前用到的很多Stream的方法由于都使用了泛型。所以涉及到的参数和返回值都是引用数据类型。 即使我们操作的是整数小数,但是实际用的都是他们的包装类。JDK5中引入的自动装箱和自动拆箱让我 们在使用对应的包装类时就好像使用基本数据类型一样方便。但是你一定要知道装箱和拆箱肯定是要消 耗时间的。虽然这个时间消耗很下。但是在大量的数据不断的重复装箱拆箱的时候,你就不能无视这个 时间损耗了。 所以为了让我们能够对这部分的时间消耗进行优化。Stream还提供了很多专门针对基本数据类型的方 法。 例如:mapToInt,mapToLong,mapToDouble,flatMapToInt,flatMapToDouble等。

java 复制代码
List<Author> authors = getAuthors();
authors.stream()
        .map(author -> author.getAge())
        .map(age -> age + 10)
        .filter(age->age>18)
        .map(age->age+2)
        .forEach(System.out::println);
authors.stream()
        .mapToInt(author -> author.getAge())
        .map(age -> age + 10)
        .filter(age->age>18)
        .map(age->age+2)
        .forEach(System.out::println);

并行流

在 Java 中,Stream API 提供了一种高级迭代数据集合的方式。并行流(Parallel Stream)是 Stream API 的一个特性,它利用多核处理器的优势,将数据集合的操作分配到多个线程上执行,从而实现并行处理。并行流使用 ForkJoinPool 来分割任务并执行它们,通常可以提高大数据集合的处理速度。

并行流的创建

并行流可以通过在一个现有流上调用 parallel() 方法来创建,或者直接从集合的 parallelStream() 方法创建。

java 复制代码
// 从集合创建并行流
List<String> myList = Arrays.asList("a", "b", "c", "d", "e");
Stream<String> parallelStream = myList.parallelStream();

// 从顺序流创建并行流
Stream<String> sequentialStream = myList.stream();
Stream<String> anotherParallelStream = sequentialStream.parallel();

并行流的操作

并行流支持与顺序流相同的操作,如 filter, map, reduce, collect 等。但是,由于并行流的操作可能在多个线程上同时执行,因此需要注意线程安全和无状态行为。

注意事项

  • 当使用并行流时,确保操作是无状态的,并且没有共享的可变状态。
  • 对于有副作用的操作(如修改共享变量),并行流可能会导致不确定的结果。
  • 对于小数据集,由于线程管理的开销,使用并行流可能不会带来性能提升。
  • 在并行流中使用排序操作(如 sorted())可能会导致性能下降,因为它需要跨线程的额外协调。
  • 使用并行流时,最终操作可能需要合并各个线程的结果,这可能是一个开销较大的过程。

案例

java 复制代码
public class Test {
    public static void main(String[] args) {
        // 生成一个大的随机数组
        int size = 10_000_0000; // 一亿个元素
        int[] numbers = new int[size];
        for (int i = 0; i < size; i++) {
            numbers[i] = (int) (Math.random() * 100);
        }

        // 顺序流处理
        long startTime = System.currentTimeMillis();
        long sumSequential = IntStream.of(numbers).map(x -> x * x).asLongStream().sum();
        long endTime = System.currentTimeMillis();
        System.out.println("Sequential stream time: " + (endTime - startTime) + "ms");  // 83ms
        System.out.println("Sequential stream result: " + sumSequential);

        // 并行流处理
        startTime = System.currentTimeMillis();
        long sumParallel = IntStream.of(numbers).parallel().map(x -> x * x).asLongStream().sum();
        endTime = System.currentTimeMillis();
        System.out.println("Parallel stream time: " + (endTime - startTime) + "ms");  // 43ms
        System.out.println("Parallel stream result: " + sumParallel);
    }
}

请注意,输出的顺序在并行流中可能是不确定的,因为各个元素的处理可能会在不同的时间完成。如果需要保持特定的顺序,可以在最终操作中使用像 forEachOrdered 这样的顺序保持操作,或者在流的末端进行排序。然而,这可能会降低并行流的性能优势。

并行流输出顺序不一致&forEachOrdered保持输出顺序案例

java 复制代码
public class Test {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        System.out.println("Parallel Stream (forEach):");
        numbers.parallelStream().forEach(number -> System.out.print(number + " "));

        System.out.println("\n\nParallel Stream (forEachOrdered):");
        numbers.parallelStream().forEachOrdered(number -> System.out.print(number + " "));
    }
}
相关推荐
全栈凯哥几秒前
Java详解LeetCode 热题 100(26):LeetCode 142. 环形链表 II(Linked List Cycle II)详解
java·算法·leetcode·链表
chxii2 分钟前
12.7Swing控件6 JList
java
全栈凯哥3 分钟前
Java详解LeetCode 热题 100(27):LeetCode 21. 合并两个有序链表(Merge Two Sorted Lists)详解
java·算法·leetcode·链表
YuTaoShao4 分钟前
Java八股文——集合「List篇」
java·开发语言·list
PypYCCcccCc9 分钟前
支付系统架构图
java·网络·金融·系统架构
华科云商xiao徐30 分钟前
Java HttpClient实现简单网络爬虫
java·爬虫
扎瓦43 分钟前
ThreadLocal 线程变量
java·后端
BillKu1 小时前
Java后端检查空条件查询
java·开发语言
jackson凌1 小时前
【Java学习笔记】String类(重点)
java·笔记·学习
刘白Live1 小时前
【Java】谈一谈浅克隆和深克隆
java