zenith-docs 1.0.0 Help

Lambda

JDK1.8 之后,提供了 Lambda 表达式,他可以让我们的程序编写更加简化、显得更加优雅。利用 Lambda 可以更简洁地实现匿名内部类与函数声明调用。在这个基础上,还提供了 stream 流式处理极大地简化了对集合的操作。这篇文档将对 Lambda 表示式做出详细的描述。

与传统写法的对比

我们先来看一个对集合排序的例子,直观地感受一下使用 Lambda 表达式和之前传统的写法之间明显的区别。下面是传统的集合排序写法:

List<String> names = Arrays.asList("PHP", "C", "C++", "Java"); names.sort(new Comparator<String>() { @Override public int compare(String o1, String o2) { return o1.compareTo(o2); } });

如果改成 Lambda 表示式,上面的写法就简化成了如下形式:

List<String> names = Arrays.asList("PHP", "C", "C++", "Java"); names.sort(String::compareTo);

但从代码上看,就直接去掉了 6 行。

基础语法解析

一条 Lambda 表示式由三部分组成,形式如下:

(参数列表) -> 实现语句

解释如下:

  • 参数列表:可以由 1 到多个参数,中间使用逗号分隔。如果是单参数,括号可以省略。参数类型有可以省略。

  • ->: 这是 Lambda 表达式的操作符,固定写法,编译器据此来识别是否是 Lambda 表达式。

  • 实现语句: 如果是单行则直接写就行,如果是多行使用 {} 包裹。

方法引用语法解析

上文中,出现了names.sort(String::compareTo); 这样的写法,当中 String::compareTo 这种就叫做方法引用,表示用来直接访问类或者实例中已经存在的方法或者构造方法。其语法如下:

String :: compareTo 目标引用 双冒号分隔符 方法名

方法引用一共分为四类,如下表所示: |方法引用类型 |表现形式| |---|---| |指向静态方法| Class::staticMethod| |指向现有对象的实例| Object::instanceMethod| |指向任意类型的实例方法| Class::instanceMethod| |指向构造方法| Class::new|

举例来说明,比如我要判断一个列表是否为空,如果为空的话给默认值:

Optional.of(numbers).orElseGet(ArrayList::new);

写法示例

Lambda 表示式就是对实现类的简化写法。比如说定义一个四则运算的接口如下:

public interface MathOperation { public Float operate(Integer a, Integer b); }

然后利用 Lambda 表达式,就可以来实现这个接口:

MathOperation addition = (Integer a, Integer b) -> a + b + 0f; System.out.println(addition.operate(1, 2)); // 输出:3.0

上面的写法还可以继续简化,如下:

MathOperation subtraction = (a, b) -> a - b + 0f; System.out.println(subtraction.operate(4, 2)); // 输出: 2.0

@FunctionalInterface 接口

如果你去看 JDK 中的函数式接口的源码的话,都会在接口的上方看到一个名为@FunctionalInterface 的注解。这个注解对于代码本身来说并没有任何影响,加上或者不加上都是一样的。

@FunctionalInterface public interface Function<T, R> {}

这个注解是给编译器看的,编译器看到之后就会去检查你这个接口是否为函数式接口,是否有且只有一个抽象方法。

函数式编程

函数式编程的核心在于,让函数称为和类一样的一等公民,可以定义为变量、可以在函数之间传递。关于函数式编程和面向对象编程的区别,如下表所示:

面向对象编程

函数式编程

设计思路

侧重过程,重分析,重设计

侧重结果,快速实现

可读性

对于复杂的结构,可读性较差

在特定场景下,可读性较好

代码量

完全面向对象的实现,代码量多

某些特定场景,代码量少

并发问题

设计不当,会出现线程安全问题

不会出现线程安全问题

健壮性

相对差

使用场景

中大型多人协作的项目

小型应用快速实现

而在 Java 中,函数式编程就是基于函数式接口并使用 Lambda 表达式的编程方式,在上面的章节中我们已经演示了函数式接口和 Lambda 表达式。

下面介绍一些常用的函数式接口:

函数式接口

参数类型

返回类型

描述

Consumer<T>

T

void

对应有一个输入参数无输出的功能代码

Function<T,R>

T

R

对应有一个输入参数且需要返回数据的功能代码

Predicate<T>

T

Boolean

用于条件判断,固定返回 Boolean

Supplier<T>

None

T

提供一个对象

UnaryOperator<T>

T

T

接受对象并返回同类型的对象

BinaryOperator<T>

(T,T)

T

接收两个同类型的对象并返回一个原类型的对象

Predicate 函数式接口

在 Java1.8 以后,新增了很多的函数式接口,位于 java.lang.function 包下。其中就包括 Predicate接口,这个接口是为了测试传入的数据是否满足要求返回一个 Boolean 值,它只有唯一的方式名为test

Predicate<Integer> less = (a) -> a > 30; Boolean result = less.test(4); System.out.println(result); // 输出: false

怎么如何应用于集合呢?

public List<Integer> filter(List<Integer> numbers, Predicate<Integer> tester) { List<Integer> list = new ArrayList<>(); for (Integer number: numbers) { if (tester.test(number)) { list.add(number); } } return list; }

测试代码如下:

List<Integer> numbers = Arrays.asList(87, 29, 37, 98, 82, 58, 73); // 输出: [29] System.out.println(filter(numbers, n -> n < 30)); // 输出: [87, 37, 98, 82, 58, 73] System.out.println(filter(numbers, n -> n > 30));

Consumer 函数式接口

还有就是 Consumer 函数式接口,也比较常用。它用于在只有一个参数并且不返回任何参数的接表达式。此外还有 IntConsumer这样的接口,其用于只有一个整数型参数的表达式,类似的还有 DoubleConsumer。

public void output(Consumer<String> consumer) { String text = "hello world"; consumer.accept(text); } output(text -> System.out.println("Output:" + text));

Function 函数式接口

Function 接口接受一个参数,并且可以返回结果。我们以生成随机字符串为例演示其用法:

Function<Integer, String> randomString = length -> { String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; StringBuilder buffer = new StringBuilder(); Random random = new Random(); for (int i = 0;i < length; i++) { int index = random.nextInt(chars.length()); buffer.append(chars.charAt(index)); } return buffer.toString(); }; System.out.println(randomString.apply(10)); // 输出: trDOPBrGDk System.out.println(randomString.apply(16)); // 输出: SCqhTQDXjBLbllVZ

Stream 流

在正式说 Stream 流之前,着重强调一下:Lambda 中所说的 Stream 和 I/O Stream 没有任何关系。这很容易让初学者感到迷惑。

Stream 流式处理是建立在 Lambda 表示式基础上的多数据处理技术。它对集合数据处理进行了高度抽象,极大简化了处理集合数据的代码量。包含了对集合进行迭代、去重、筛选、聚合、排序等一系列的操作。

Stream终端操作 Terminal非短路操作count 计数max 最大值min 最小值forEach 遍历collect 聚合reduce 归约短路操作allMatch 全部匹配noneMatch 不匹配findFirst 查找首个findAny 查找任意一个anyMatch 任意匹配中间操作 Intermediate有状态操作sorted 排序limit 截断distinct 去重skip 跳过无状态操作map 映射peek 遍历filter 过滤flatMap 扁平化

使用流的示例

下面的例子展示了从一组数据中筛选出最大的一个偶数:

Optional<Integer> op = Stream.of(1, 2, 3, 4, 5) .filter(n -> n % 2 == 0) .max(Comparator.comparingInt(a -> a)); op.ifPresent(System.out::println); // 输出: 4

通过这个例子我们可以看到使用 Stream 可以对我们的集合处理代码极度的简化,代码量少了可读性高了。通过流失处理,我们可以一步一步得到我们最终的结果。

创建 stream 的5种方法

在 Java 中,提供了 5 种创建流的方式,第一种是基于数组创建:

String[] languages = {"Java", "python", "Shell", "PHP"}; Stream<String> s = Stream.of(languages); s.forEach(System.out::println);

第二种方法是基于 List 进行创建:

List<String> list = new ArrayList<>(); list.add("Java"); list.add("c"); list.add("VB"); list.stream().sorted().forEach(System.out::println); // 输出: Java VB c

第三种方法是利用 Generate 方法来创建无限长度的流。下面的例子展现了生成无限随机数的例子:

Stream<Integer> generator = Stream.generate(() -> new Random().nextInt(100000)); generator.forEach(System.out::println); // 无限输出: 32135 40533 51978 ....... generator.limit(10).forEach(System.out::println); // 输出十个: 32135 40533 51978 .......

第四种方法是基于迭代器创建, 下面的例子输出了 10 个奇数:

Stream<Integer> iterator = Stream.iterate(1, n -> n + 2); iterator.limit(10).forEach(System.out::println);

第五种方法是基于字符序列创建流, 一般用在字符加密或者对字符进行处理的时候:

String str = "abcdefg"; str.chars().forEach((i) -> System.out.println((char)i));

使用流的示例

下面展示了一些上面提到的方法的示例,以供参考。

reduce 方法示例,它是将一组数据然后减少到单个数据,例如计算一组数据的和:

Integer result = Stream.generate(() -> new Random().nextInt(100)) .limit(3) .parallel() // 并行计算需要加上这个方法的调用 .peek(System.out::println) .reduce(0, Integer::sum, Integer::sum); System.out.println(result); // 三个随机数的求和: 34 + 14 + 23 = 71

上面的代码随机生成三个数值,然后打印出来,最后使用 reduce 方法来求和。reduce 方法接受 3 个参数,第一个参数是初始值、第二个参数是两个参数之间的算法(示例中是求和)、第三个参数是并行计算下的算法。

和reduce 相反,collect 是在一组数据中收集符合要求或者经过处理的一组数据。但是用法是相似的,最多接受三个参数,分别是初始值、合并的方法以及并行计算的方法。下面的例子演示了,用户订单额的统计:

// 创建一个订单类,有两个属性,分别是用户名以及订单总金额 class Order { private final String user; private final Double total; // 省略全参构造函数以及 Getter 、Setter } // 构造测试数据 list.add(new Order("ZhangSan", 998D)); list.add(new Order("LiSi", 666D)); list.add(new Order("ZhangSan", 550D)); list.add(new Order("LiSi", 222D));

然后根据上面构造的订单数据,使用 collect 方法来分组统计每个用户下单总金额:

HashMap<String, Double> result = list.stream().parallel().collect( HashMap::new, // 初始值 // 如何将订单数据汇总到 Map 中 (HashMap<String, Double> map, Order order) -> { String user = order.getUser(); if (map.containsKey(user)) { map.put(user, map.get(user) + order.getTotal()); } else { map.put(user, order.getTotal()); } }, // 在并行计算下,如何合并两个计算后的 Map (HashMap<String, Double> map1, HashMap<String, Double> map2) -> { map2.forEach((user, total) -> { map1.merge(user, total, Double::sum); }); } );

上面的代码可以重构的更为简短:

Map<String, Double> result = list.stream().collect( Collectors.groupingBy( Order::getUser, Collectors.reducing(0.0, Order::getTotal, Double::sum) ) );

总结

这篇文档提供了关于 Java 语言中 Lambda 表达式的全面介绍,包括其在 JDK 1.8 之后的引入、基础语法、使用示例以及与传统编程方式的对比。文档首先展 示了 Lambda 表达式简化编程的优势,尤其是在集合排序等操作中,通过对比传统写法和 Lambda 表达式,突出了后者的简洁性。接着,文档详细解释了 Lambda 表达式的三个组成部分:参数列表、操作符(->)和实现语句。

文档还深入探讨了方法引用语法,解释了方法引用的四种类型,并通过实例加以说明。Lambda 表达式作为函数式编程的一个重要组成部分,文档阐述了其在 Java 中的应用,特别是对函数式接口(只有一个抽象方法的接口)的要求,以及如何通过 @FunctionalInterface 注解标记这些接口。

进一步,文档比较了面向对象编程和函数式编程的不同特点,包括设计思路、可读性、代码量、并发问题处理、健壮性和使用场景。此外,还介绍了 Java 中常用 的几种函数式接口,如 Consumer<T>, Function<T,R>, Predicate<T>, Supplier<T>, UnaryOperator<T>BinaryOperator<T>, 并通过示例展示了它们的应用。

最后,文档深入讲解了基于 Lambda 表达式的 Stream 流式处理,强调了其与 I/O Stream 无关。Stream 提供了对集合数据处理的高度抽象,包括迭代、 去重、筛选、聚合和排序等操作。文档通过丰富的示例展示了如何使用 Stream 流,如何创建流,以及如何利用流进行数据处理,最后通过几个具体的例子 (如 reduce 和 collect 方法)展示了 Stream 的强大功能。

Last modified: 04 August 2024