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 + " "));
    }
}
相关推荐
m0_5719575835 分钟前
Java | Leetcode Java题解之第543题二叉树的直径
java·leetcode·题解
魔道不误砍柴功3 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2343 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨3 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
测开小菜鸟4 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity5 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天5 小时前
java的threadlocal为何内存泄漏
java
caridle5 小时前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
^velpro^6 小时前
数据库连接池的创建
java·开发语言·数据库
苹果醋36 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx