Java函数式编程
函数式接口
函数式接口在Java中是指:有且仅有一个抽象方法的接口(可以有非抽象方法)。函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。
@FunctionalInterface注解
Java 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface
。该注解可用于一个接口的定义上。一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。需要注意的是,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口。
1 |
|
函数式编程
在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么(What),而不是怎么做(How)。重视的是结果,而不是过程
Lambda表达式
JDK8,必须具有函数式接口才能使用。如:Runnable
、Comparator
Lambda 标准格式
1 | (参数类型 参数名称) -> { 代码语句 } |
Lambda省略格式
- 小括号内参数的类型可以省略;
- 如果小括号内有且仅有一个参,则小括号可以省略;
- 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。
Lambda表达式作为参数
如果方法的参数是一个函数式接口类型,那么就可以使用Lambda表达式进行替代。
1 | // 实现Runnable接口 |
Lambda表达式作为返回值
如果一个方法的返回值类型是一个函数式接口,那么就可以直接返回一个Lambda表达式。
1 | private static Comparator<String> newComparator() { |
Lambda的延迟
有些场景的代码执行后,结果不一定会被使用,从而造成性能浪费。而Lambda表达式是延迟执行的,这正好可以作为解决方案,提升性能。
性能浪费的日志案例
1 | private static void log(int level, String msg) { |
优化
SLF4J在记录日志时为了解决这种性能浪费的问题,并不推荐首先进行字符串的拼接,而是将字符串的若干部分作为可变参数传入方法中,仅在日志级别满足要求的情况下才会进行字符串拼接。
例如: LOGGER.debug("变量{}的取值为{}。", "os", "macOS")
Lambda方式
1 | interface MessageBuilder { |
常用函数式接口
JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景, 主要在java.util.function
包中被提供。下面是最简单的几个接口及使用示例。
Supplier接口
java.util.function.Supplier<T>
接口仅包含一个无参的方法: T get()
。用来获取一个泛型参数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。
1 | public static void main(String[] args) { |
Consumer接口
java.util.function.Consumer<T>
接口则正好与Supplier接口相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型决定。抽象方法 void accept(T t)
,意为消费一个指定泛型的数据。
默认方法:andThen
如果一个方法的参数和返回值有多个Consumer 类型,那么就可以实现:消费数据的时候,先组合, 再调用accept方法消费数据。
1 | // andThen 源码 |
例子
1 | public static void main(String[] args) { |
Predicate接口
有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.Predicate
接口。
抽象方法:
boolean test(T t)
。默认方法:and、or、negate , 对应与、或、非。
例子
1 | public static void main(String[] args) { |
Function接口
java.util.function.Function
接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。
抽象方法: R apply(T t) ,根据类型T的参数获取类型R的结果。
默认方法:andThen 用来进行组合操作。源码如下:
1 | default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { |
例子: 将一个字符型的数字转为int型的,然后再计算这个数的乘方。
1 | public static void main(String[] args) { |
Stream流
Java8引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。
传统集合的多步遍历的弊端:
为什么使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环。前者是目的,后者是方式。
- for循环的语法就是“怎么做”
- for循环的循环体才是“做什么”
流式思想
流式思想类似与工厂车间的“生产流水线。当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们应该首先拼好一个“模型”步骤方案,然后再按照方案一步步的去执行它。
Stream流
- 是一个集合元素的函数模型,不是集合,也不是数据结构,其本身并不存储任何元素或者地址值
- 是一个来自数据源的元素队列,数据源可以是集合,数组 等。
Stream操作特征
- Pipelining: 中间操作都会返回流对象。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
- 内部迭代: 以前对集合遍历都是通过Iterator或者增强for的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式,流可以直接调用遍历方法。
获取流
java.util.stream.Stream
是Java 8新加入的最常用的流接口
所有的
Collection
集合都可以通过stream
默认方法获取流;Stream
接口的静态方法of
可以获取数组对应的流。public static <T> Stream<T> of(T... values)
例子
1 | // Stream.of方法 |
常用方法
- 延迟方法:返回值类型仍然是 Stream 接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为延迟方法。)
- 终结方法:返回值类型不再是 Stream 接口自身类型的方法,因此不再支持链式调用。本小节中,终结方法包括 count 和 forEach 方法。
映射:map
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
:- 将流中的元素映射到另一个流中
逐一处理:forEach: 是一个终结方法
void forEach(Consumer<? super T> action);
过滤:filter
Stream filter(Predicate<? super T> predicate);
- 函数返回false过滤
reduce操作
- reduce(initialValue, accumulator)
- 例:实现累加操作
list.stream().reduce(0, (acc, e) -> acc + e)
- 第一个参数0,是acc的初始值, acc记录每次计算的结果
统计个数:count: 是一个终结方法
long count();
最大值/最小值:max / min
- 需要传递一个Comparator对象
取用前几个:limit
Stream limit(long maxSize);
: 对流进行截取,只取用前n个。
跳过前几个:skip
Stream skip(long n);
: 流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。
组合:concat
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
collect
- collect(Collectors.toList()), 可将流中数据存入一个list集合并返回
例子: 使用Stream流优化下面操作
现在有两个 ArrayList 集合存储队伍当中的多个成员姓名,请依次进行以下若干操作步骤:
- 第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。
- 第一个队伍筛选之后只要前3个人;存储到一个新集合中。
- 第二个队伍只要姓张的成员姓名;存储到一个新集合中。
- 第二个队伍筛选之后不要前2个人;存储到一个新集合中。
- 将两个队伍合并为一个队伍;存储到一个新集合中。
- 根据姓名创建 Person 对象;存储到一个新集合中。
- 打印整个队伍的Person对象信息。
1 | List<String> one = List.of("迪丽热巴", "宋远桥" , "苏星河", "石破天", "石中玉", "老子", "庄子", "洪七公"); |
方法引用
在使用Lambda表达式的时候,我们实际上传递进去的代码就是一种解决方案:拿什么参数做什么操作。那么考虑一种情况:如果我们在Lambda中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复逻辑?
方法引用符
双冒号::
为引用运算符,而它所在的表达式被称为方法引用。如果Lambda要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者。
如果使用Lambda,那么根据“可推导就是可省略”的原则,无需指定参数类型,也无需指定的重载形式——它们都将被自动推导。而如果使用方法引用,也是同样可以根据上下文进行推导。
通过对象名引用成员方法
如果一个类中已经存在一个能过实现我们功能的成员方法,直接引用即可。
1 |
|
通过类名称引用静态方法
如果已经存在了一个能过实现我们功能的静态方法,直接引用即可。
1 |
|
通过super引用成员方法
如果存在继承关系,当Lambda中需要出现super调用时,也可以使用方法引用进行替代。
- Lambda表达式:
() -> super.父类方法
- 方法引用:
super::父类方法
通过this引用成员方法
如果需要引用的方法就是当前类中的成员方法,那么可以使用this::成员方法
的格式来使用方法引用。
类的构造器引用
由于构造器的名称与类名完全一样,并不固定。所以构造器引用使用类名::new
的格式表示。
- Lambda表达式: name -> new Person(name)
- 方法引用: Person::new
1 |
|
数组的构造器引用
数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。
- Lambda表达式:
length -> new int[length]
- 方法引用:
int[]::new
1 |
|