Java8实战,行为参数化,Lambda表达式,Stream

1.行为参数化

1.1 简单说明

你想要写两个只有几行代码不同的方法,那现在你只需要把不同的那部分代码作为参数传递进去就可以了。让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。

行为参数化把代码传递给方法,Java 8里面将代码传递给方法的功能(同时也能够返回代码并将其包含在数据结构中)还让我们能够使用一整套新技巧,通常称为函数式编程。

1.2 实例:

比如现在有一些苹果库存,农民需要根据不同的条件查询出对应的苹果库存。

1.2.1 初始实现

对于不同的条件,刚开始我们可能会这样写

条件1 根据颜色过滤:

java 复制代码
public static List<Apple> filterApplesByColor(List<Apple> inventory, String color) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
        if (apple.getColor().equals(color)) {
            result.add(apple);
        }
    }
    return result;
}

条件2 根据重量过滤:

java 复制代码
public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weight) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
        if (apple.getWeight() > weight) {
            result.add(apple);
        }
    }
    return result;
}

上面的代码,打破了DRY(Don't Repeat Yourself)的软件工程原则。 我们发现除了过滤条件外,其他的代码几乎一模一样。于是考虑把条件代码作为行为传给方法。

1.2.2 第一次优化:行为参数化

为了解决上面代码重复的问题,开始对我们的选择标准进行建模,考虑的是苹果,需要根据Apple的某些属性(是否是绿色的?重量是否超过150g?)来返回一个boolean的值。我们把它称之为谓词(即一个返回boolean值的函数)

定义一个接口对选择标准建模

java 复制代码
public interface ApplePredicate {
    boolean test (Apple apple);
}

行为1

java 复制代码
public class AppleRedPredicate implements ApplePredicate {
    @Override
    public boolean test(Apple apple) {
        return "red".equals(apple.getColor());
    }
}

行为2

java 复制代码
public class AppleHeavyPredicate implements ApplePredicate {
    @Override
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
}

过滤方法改写后

java 复制代码
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate predicate) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
        if (predicate.test(apple)) {
            result.add(apple);
        }
    }
    return result;
}

调用过滤方法,将行为作为方法传递进去

java 复制代码
List<Apple> filterApples = filterApples(inventory,new AppleRedPredicate());

1.2.3 第二次优化:匿名内部类

java 复制代码
List<Apple> filterApplesColor = filterApples(inventory,new ApplePredicate() {
    @Override
    public boolean test(Apple apple) {
        return apple.getColor().equals("red");
    }
});
List<Apple> filterApplesWeight = filterApples(inventory,new ApplePredicate() {
    @Override
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
});

1.2.4 第三次优化:Lambda 表达式

java 复制代码
List<Apple> filterApples = filterApples(inventory, apple -> apple.getColor().equals("red"));
List<Apple> filterApplesColor = filterApples(inventory, apple -> apple.getWeight() > 150);

2. Lambda表达式

2.1 Lambda表达式简介

可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。

Lambda表达式 由 参数,箭头,主体组成

Lambda 的基本语法

java 复制代码
(parameters)-> expression
java 复制代码
(parameters)-> { statements; }

2.2 函数式接口

函数式接口就是只定义一个抽象方法的接口。

接口现在还可以拥有默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。

下面是一个简单的函数式接口

java 复制代码
@FunctionalInterface
public interface Predicate<T>f
    boolean test (T t);
}

用函数式接口可以干什么呢?

Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)。你用匿名内部类也可以完成同样的事情,只不过比较笨拙: 需要提供一个实现,然后再直接内联将它实例化。具体例子可见1.2.3 和1.2.4

2.3 函数描述符

函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作函数描述符。

Lambda表达式可以被赋给一个变量,或传递给一个接受函数式接口作为参数的方法。

java 8中常见的函数式接口

2.4 Lambda 实践

继续以1.2 的例子来说,比如现在有一些苹果库存,农民需要根据不同的条件查询出对应的苹果库存。

2.4.1 第1步:行为参数化

可见公共部分的代码是:对苹果列表进行遍历,如果符合条件就添加到新的列表,后续返回新的列表。 不同的是,每次对每个苹果的查询条件不一样。即

java 复制代码
if (apple.getColor().equals(color))
if (apple.getWeight() > weight)

于是需要把判断条件行为参数化,给filterApples方法传递不同的行为(不同的判断条件),以便对苹果进行不同条件的筛选。

传递行为正是Lambda的拿手好戏。那要是查询颜色为红色的苹果,这个新的filterApples方法看起来又该是什么样的呢?基本上,你需要一个接收Apple并返回boolean的Lambda。

例如,下面就是查询颜色为红色的苹果的写法

java 复制代码
filterApples(inventory, apple -> apple.getColor().equals("red"));

2.4.2 第2步:使用函数式接口来传递行为

Lambda仅可用于上下文是函数式接口的情况。你需要创建一个能匹配 Apple -> boolean。我们发现java中已经有的一个函数式接口可以直接使用

java 复制代码
@FunctionalInterface
public interface Predicate<T>f
    boolean test (T t);
}

现在你就可以把这个接口作为新的filterApples方法的参数了

java 复制代码
public static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> predicate) {
    ......
}

2.4.3 第3步:执行一个行为

任何 Apple -> boolean 形式的Lambda 都可以作为参数来传递,因为它们符合Predicate接口中定义的test方法的签名。现在你只需要一种方法在filterApples主体内执行Lambda所代表的代码。请记住,Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。因此,你可以在filterApples主体内,对得到的Predicate对象调用test方法执行处理。

2.4.4 第4步:传递Lambda

java事实上,方法引用就是让你根据已有的方法实现来创建Lambda表达式。但是,显式地指明方法的名称,你的代码的可读性会更好。它是如何工作的呢?当你需要使用方法引用时,目标引用放在分隔符::前,方法的名称放在后面。例如,Apple::getWeight就是引用了Apple类中定义的方法getWeight。请记住,不需要括号,因为你没有实际调用这个方法。方法引用就是Lambda表达式(Apple 复制代码
表3-4 Lambda及其等效方法引用的例子

你可以把方法引用看作针对仅仅涉及单一方法的Lambda的语法糖
List<Apple> filterApples =  filterApples(inventory, apple -> apple.getColor().equals("red"));

List<Apple> filterApplesColor = filterApples(inventory, apple -> apple.getWeight() > 150);

2.5 方法引用

方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。

Java 8中的集合支持一个新的stream方法,它会返回一个流(接口定义在java.util.stream.Stream里)。方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法。它的基本思想是,如果一个Lambda代表的只是"直接调用这个方法",那最好还是用名称来调用它,而不是去描述如何调用它。你可以把方法引用看作针对仅仅涉及单一方法的Lambda的语法糖。

当你需要使用方法引用时,目标引用放在分隔符::前,方法的名称放在后面。例如,Apple::getWeight就是引用了Apple类中定义的方法getWeight。

3. Stream流

3.1 流简介

Java 8中的集合支持一个新的stream方法,它会返回一个流,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。

比如java7对一个集合处理过滤+排序+获取排序后的某个字段

引入流后的写法

3.2 流使用

流的使用一般包括三件事:

❑ 一个数据源(如集合)来执行一个查询;

❑ 一个中间操作链,形成一条流的流水线;

❑ 一个终端操作,执行流水线,并能生成结果。
一些开始操作

操作 类型 返回类型 目的
stream() 开始 Stream 创建出一个新的stream串行流对象
parallelStream() 开始 Stream 创建出一个可并行执行的stream流对象
Stream.of() 开始 Stream 通过给定的一系列元素创建一个新的Stream串行流对象

一些中间操作

一些终端操作

3.2.1 map与flatMap

3.2.1.1 map

  • map 是对每个元素进行转换的操作。它接受一个函数作为参数,该函数将每个输入元素映射到一个输出元素。
  • map 的操作是一对一的,即每个输入元素都对应一个输出元素。
  • 结果是一个新的集合,其中包含了经过函数转换的每个元素。
java 复制代码
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squaredNumbers = numbers.stream()
        .map(x -> x * x)
        .collect(Collectors.toList());
        System.out.println(squaredNumbers);

如上面的例子中,map 操作将每个数的平方映射到新的集合中。输出结果为
[1, 4, 9, 16, 25]

3.2.1.2 flatMap

  • flatMap 也对每个元素进行转换,但是它的转换函数返回的是一个包含零个或多个元素的流。
  • flatMap 的操作是一对多的,即每个输入元素可以对应零个或多个输出元素。
  • 结果是一个扁平化的集合,其中包含了所有转换后的元素。
  • flatMap操作的时候其实是先每个元素处理并返回一个新的Stream,然后将多个Stream展开合并为了一个完整的新的Stream
java 复制代码
List<List<Integer>> nestedNumbers = Arrays.asList(
        Arrays.asList(1, 2, 3),
        Arrays.asList(4, 5, 6),
        Arrays.asList(7, 8, 9)
);
List<Integer> flattenedNumbers = nestedNumbers.stream()
        .flatMap(Collection::stream)
        .collect(Collectors.toList());
System.out.println(flattenedNumbers);

在上面的例子中,flatMap 操作将嵌套的列表转换成了一个扁平的列表。输出结果为
[1, 2, 3, 4, 5, 6, 7, 8, 9]

3.2.2 filter、sorted、distinct、limit

3.2.2.1 filter
  • filter 用于根据指定的条件筛选流中的元素。它接受一个 Predicate 函数式接口作为参数,该函数决定了哪些元素会被保留下来。
java 复制代码
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
List<Integer> evenNumbers = numbers.stream()
                                  .filter(n -> n % 2 == 0)
                                  .collect(Collectors.toList());
System.out.println(evenNumbers);

上面的例子输出结果为 [2, 4, 6, 8]

3.2.2.2 sorted
  • sorted 用于对流中的元素进行排序。
  • 它可以接受一个比较器,或者直接使用元素的自然顺序进行排序。
java 复制代码
List<String> words = Arrays.asList("banana", "apple", "orange", "grape");
List<String> sortedWords = words.stream()
                                .sorted()
                                .collect(Collectors.toList());
System.out.println(sortedWords)

上面的例子输出结果为 [apple, banana, grape, orange]

3.2.2.3 distinct
  • distinct 用于去除流中重复的元素,得到不重复的元素集合。
  • 它依赖元素的 equals 方法来判断是否相同。
java 复制代码
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5);
List<Integer> distinctNumbers = numbers.stream()
                                      .distinct()
                                      .collect(Collectors.toList());
System.out.println(distinctNumbers);

上面的例子输出结果为 [1, 2, 3, 4, 5]

3.2.2.4 limit
  • limit 用于截取流中的前 N 个元素,返回一个新的流。
  • 它接受一个参数,表示保留的元素个数。
java 复制代码
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
List<Integer> limitedNumbers = numbers.stream()
                                     .limit(5)
                                     .collect(Collectors.toList());
System.out.println(limitedNumbers);

上面的例子输出结果为 [1, 2, 3, 4, 5]

这些操作可以组合使用,以构建复杂的流处理管道。例如,你可以先使用 filter 进行条件筛选,然后使用 sorted 进行排序,接着使用 distinct 去除重复元素,最后使用 limit 截取前几个元素。

3.2.3 peekforEach

3.2.3.1 peek 方法:

  • peek 方法用于在流的每个元素被消费的同时执行一些操作,这些操作可以是一些输出语句、记录日志、或者对元素进行修改等。

  • 它返回的是一个新的流,因此可以链式调用多个 peek 操作。

  • peek 不会触发流的终结操作,因此可以在 peek 之后继续进行其他的中间操作。

java 复制代码
List<String> words = Arrays.asList("apple", "banana", "orange", "grape");
List<String> modifiedWords = words.stream()
                                  .peek(word -> System.out.println("Original: " + word))
                                  .map(String::toUpperCase)
                                  .peek(word -> System.out.println("Uppercase: " + word))
                                  .collect(Collectors.toList());
System.out.println(modifiedWords);

上面的例子输出结果为

makefile 复制代码
Original: apple
Uppercase: APPLE
Original: banana
Uppercase: BANANA
Original: orange
Uppercase: ORANGE
Original: grape
Uppercase: GRAPE
[APPLE, BANANA, ORANGE, GRAPE]

3.2.3.2 forEach 方法:

  • forEach 方法用于对流中的每个元素执行指定的操作。与 peek 不同,forEach 是一个终结操作,它不返回新的流。
  • forEach 接受一个 Consumer 函数式接口作为参数,该接口定义了对每个元素执行的操作。
arduino 复制代码
List<String> words = Arrays.asList("apple", "banana", "orange", "grape");
words.stream()
     .forEach(word -> System.out.println("Word: " + word));

上面的例子输出结果为

makefile 复制代码
Word: apple
Word: banana
Word: orange
Word: grape
  • 注意:forEach 是一个终结操作,它会遍历整个流,执行操作。因此,在使用 forEach 之后,不能再继续进行其他中间操作。

总体而言,如果你需要在流的每个元素上执行一些操作,但仍然想保持流的状态,可以使用 peek。如果你只是希望对每个元素执行一些操作而不关心返回结果,并且不再进行其他操作,可以使用 forEach

3.2.4 collect 将流中的元素收集到不同的集合类型

3.2.4.1 Collectors.toSet()

  • Collectors.toSet() 用于将流中的元素收集到一个 Set 集合中,去除重复元素。
java 复制代码
List<String> words = Arrays.asList("apple", "banana", "orange", "apple", "grape");
Set<String> uniqueWords = words.stream()
                              .collect(Collectors.toSet());
System.out.println(uniqueWords);

上面的例子输出结果为

csharp 复制代码
[banana, orange, apple, grape]

3.2.4.2 Collectors.toMap()

  • Collectors.toMap() 用于将流中的元素收集到一个 Map 集合中,需要指定 key 和 value 的提取方式。
java 复制代码
List<String> words = Arrays.asList("apple", "banana", "orange", "grape");
Map<Integer, String> wordLengthMap = words.stream()
        .collect(Collectors.toMap(String::length, Function.identity(), (e, o) -> e));
System.out.println(wordLengthMap);

上面的例子输出结果为

ini 复制代码
{5=apple, 6=banana}
  • 上面的例子中,String::length 是作为 key 提取方式,Function.identity() 是作为 value 提取方式,生成的 Map 包含了单词长度和单词本身的映射。(e, o) -> e 用于处理当key 冲突的情况。

3.2.4.3 Collectors.toList()

  • Collectors.toList() 用于将流中的元素收集到一个 List 集合中。
java 复制代码
List<String> words = Arrays.asList("apple", "banana", "orange", "grape");
List<String> wordList = words.stream()
                            .collect(Collectors.toList());

这些收集器方法是常用的,根据具体需求选择适合的收集器。注意,toMap 中需要处理 key 冲突的情况,可以通过提供合并函数来解决。如果不提供合并函数,当遇到重复的 key 时会抛出 IllegalStateException

相关推荐
侠客行03173 小时前
Mybatis连接池实现及池化模式
java·mybatis·源码阅读
蛇皮划水怪3 小时前
深入浅出LangChain4J
java·langchain·llm
老毛肚4 小时前
MyBatis体系结构与工作原理 上篇
java·mybatis
风流倜傥唐伯虎5 小时前
Spring Boot Jar包生产级启停脚本
java·运维·spring boot
Yvonne爱编码5 小时前
JAVA数据结构 DAY6-栈和队列
java·开发语言·数据结构·python
Re.不晚5 小时前
JAVA进阶之路——无奖问答挑战1
java·开发语言
你这个代码我看不懂5 小时前
@ConditionalOnProperty不直接使用松绑定规则
java·开发语言
fuquxiaoguang5 小时前
深入浅出:使用MDC构建SpringBoot全链路请求追踪系统
java·spring boot·后端·调用链分析
琹箐5 小时前
最大堆和最小堆 实现思路
java·开发语言·算法
__WanG6 小时前
JavaTuples 库分析
java