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

相关推荐
杨充几秒前
13.观察者模式设计思想
java·redis·观察者模式
Lizhihao_3 分钟前
JAVA-队列
java·开发语言
喵叔哟12 分钟前
重构代码之移动字段
java·数据库·重构
喵叔哟12 分钟前
重构代码之取消临时字段
java·前端·重构
fa_lsyk15 分钟前
maven环境搭建
java·maven
Daniel 大东34 分钟前
idea 解决缓存损坏问题
java·缓存·intellij-idea
wind瑞40 分钟前
IntelliJ IDEA插件开发-代码补全插件入门开发
java·ide·intellij-idea
HappyAcmen40 分钟前
IDEA部署AI代写插件
java·人工智能·intellij-idea
马剑威(威哥爱编程)1 小时前
读写锁分离设计模式详解
java·设计模式·java-ee
鸽鸽程序猿1 小时前
【算法】【优选算法】前缀和(上)
java·算法·前缀和