概念
函数式接口在
Java
中是指:
有且仅有一个抽象方法的接口
。
函数式接口,即适用于函数式编程场景的接口。而
Java
中的函数式编程体现就是
Lambda
,所以函数式接口就是可
以适用于
Lambda
使用的接口。只有确保接口中有且仅有一个抽象方法,
Java
中的
Lambda
才能顺利地进行推导。
备注:
“
语法糖
”
是指使用更加方便,但是原理不变的代码语法。例如在遍历集合时使用的
for-each
语法,其实
底层的实现原理仍然是迭代器,这便是
“
语法糖
”
。从应用层面来讲,
Java
中的
Lambda
可以被当做是匿名内部
类的
“
语法糖
”
,但是二者在原理上是不同的。
格式
只要确保接口中有且仅有一个抽象方法即可:
修饰符
interface
接口名称
{
public abstract
返回值类型 方法名称
(
可选参数信息
);
//
其他非抽象方法内容
}
由于接口当中抽象方法的
public abstract
是可以省略的,所以定义一个函数式接口很简单
public interface
MyFunctionalInterface
{
void
myMethod
();
}
@FunctionalInterface
注解
与
@Override
注解的作用类似,
Java 8
中专门为函数式接口引入了一个新的注解:
@FunctionalInterface
。该注
解可用于一个接口的定义上:
@FunctionalInterface
public interface
MyFunctionalInterface
{
void
myMethod
();
}
一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。需要
注
意
的是,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
自定义函数式接口
对于刚刚定义好的
MyFunctionalInterface
函数式接口,典型使用场景就是作为方法的参数:
函数式编程
在兼顾面向对象特性的基础上,
Java
语言通过
Lambda
表达式与方法引用等,为开发者打开了函数式编程的大门。
下面我们做一个初探。
Lambda
的延迟执行
有些场景的代码执行后,结果不一定会被使用,从而造成性能浪费。而
Lambda
表达式是延迟执行的,这正好可以
作为解决方案,提升性能。
性能浪费的日志案例
注
:
日志可以帮助我们快速的定位问题,记录程序运行过程中的情况,以便项目的监控和优化。
一种典型的场景就是对参数进行有条件使用,例如对日志消息进行拼接后,在满足条件的情况下进行打印输出:
public class
Demo01Logger
{
private static
void
log
(
int
level
,
String
msg
) {
if
(
level
==
1
) {
System
.
out
.
println
(
msg
);
}
}
public static
void
main
(
String
[]
args
) {
String
msgA
=
"Hello"
;
String
msgB
=
"World"
;
String
msgC
=
"Java"
;
log
(
1
,
msgA
+
msgB
+
msgC
);
}
}
这段代码存在问题:无论级别是否满足要求,作为
log
方法的第二个参数,三个字符串一定会首先被拼接并传入方
法内,然后才会进行级别判断。如果级别不符合要求,那么字符串的拼接操作就白做了,存在性能浪费
备注:
SLF4J
是应用非常广泛的日志框架,它在记录日志时为了解决这种性能浪费的问题,并不推荐首先进行
字符串的拼接,而是将字符串的若干部分作为可变参数传入方法中,仅在日志级别满足要求的情况下才会进
行字符串拼接。例如:
LOGGER.debug("
变量
{}
的取值为
{}
。
", "os", "macOS")
,其中的大括号
{}
为占位
符。如果满足日志级别要求,则会将
“os”
和
“macOS”
两个字符串依次拼接到大括号的位置;否则不会进行字
符串拼接。这也是一种可行解决方案,但
Lambda
可以做到更好。
体验
Lambda
的更优写法
使用
Lambda
必然需要一个函数式接口:
@FunctionalInterface
public interface
MessageBuilder
{
String
buildMessage
();
}
然后对
log
方法进行改造:
public class
Demo02LoggerLambda
{
private static
void
log
(
int
level
,
MessageBuilder builder
) {
if
(
level
==
1
) {
System
.
out
.
println
(
builder
.
buildMessage
());
}
}
public static
void
main
(
String
[]
args
) {
String
msgA
=
"Hello"
;
String
msgB
=
"World"
;
String
msgC
=
"Java"
;
log
(
1
, ()
‐>
msgA
+
msgB
+
msgC
);
}
这样一来,只有当级别满足要求的时候,才会进行三个字符串的拼接;否则三个字符串将不会进行拼接
扩展:实际上使用内部类也可以达到同样的效果,只是将代码操作延迟到了另外一个对象当中通过调用方法
来完成。而是否调用其所在方法是在条件判断之后才执行的。
使用
Lambda
作为参数和返回值
如果抛开实现原理不说,
Java
中的
Lambda
表达式可以被当作是匿名内部类的替代品。如果方法的参数是一个函数
式接口类型,那么就可以使用
Lambda
表达式进行替代。使用
Lambda
表达式作为方法参数,其实就是使用函数式
接口作为方法参数。
例如
java.lang.Runnable
接口就是一个函数式接口,假设有一个
startThread
方法使用该接口作为参数,那么就
可以使用
Lambda
进行传参。这种情况其实和
Thread
类的构造方法参数为
Runnable
没有本质区别。
public class
Demo04Runnable
{
private static
void
startThread
(
Runnable task
) {
new
Thread
(
task
).
start
();
}
public static
void
main
(
String
[]
args
) {
startThread
(()
‐>
System
.
out
.
println
(
"
线程任务执行!
"
));
}
}
类似地,如果一个方法的返回值类型是一个函数式接口,那么就可以直接返回一个
Lambda
表达式。当需要通过一
个方法来获取一个
java.util.Comparator
接口类型的对象作为排序器时
,
就可以调该方法获取。
import
java
.
util
.
Arrays
;
import
java
.
util
.
Comparator
;
public class
Demo06Comparator
{
private static
Comparator
<
String
>
newComparator
() {
return
(
a
,
b
)
‐>
b
.
length
()
‐
a
.
length
();
}
public static
void
main
(
String
[]
args
) {
String
[]
array
=
{
"abc"
,
"ab"
,
"abcd"
};
System
.
out
.
println
(
Arrays
.
toString
(
array
));
Arrays
.
sort
(
array
,
newComparator
());
System
.
out
.
println
(
Arrays
.
toString
(
array
));
}
}
常用函数式接口
JDK
提供了大量常用的函数式接口以丰富
Lambda
的典型使用场景,它们主要在
java.util.function
包中被提供。
下面是最简单的几个接口及使用示例。
Supplier
接口
java.util.function.Supplier<T>
接口仅包含一个无参的方法:
T get()
。用来获取一个泛型参数指定类型的对
象数据。由于这是一个函数式接口,这也就意味着对应的
Lambda
表达式需要
“
对外提供
”
一个符合泛型类型的对象
数据。
Consumer
接口
java.util.function.Consumer<T>
接口则正好与
Supplier
接口相反,它不是生产一个数据,而是
消费
一个数据,
其数据类型由泛型决定。
抽象方法:
accept
默认方法:
andThen
如果一个方法的参数和返回值全都是
Consumer
类型,那么就可以实现效果:消费数据的时候,首先做一个操作,
然后再做一个操作,实现组合。而这个方法就是
Consumer
接口中的
default
方法
andThen
。下面是
JDK
的源代码:
default
Consumer
<
T
>
andThen
(
Consumer
<?
super
T
>
after
) {
Objects
.
requireNonNull
(
after
);
return
(
T t
)
‐>
{
accept
(
t
);
after
.
accept
(
t
); };
}
备注:
java.util.Objects
的
requireNonNull
静态方法将会在参数为
null
时主动抛出
NullPointerException
异常。这省去了重复编写
if
语句和抛出空指针异常的麻烦
要想实现组合,需要两个或多个
Lambda
表达式即可,而
andThen
的语义正是
“
一步接一步
”操作。
Predicate
接口
有时候我们需要对某种类型的数据进行判断,从而得到一个
boolean
值结果。这时可以使用
java.util.function.Predicate<T>
接口
抽象方法:
test
Predicate
接口中包含一个抽象方法:
boolean test(T t)
。用于条件判断的场景:
默认方法:
and
既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个
Predicate
条件使用
“
与
”
逻辑连接起来实
现
“
并且
”
的效果时,可以使用
default
方法
and
。其
JDK
源码为:
default
Predicate
<
T
>
and
(
Predicate
<?
super
T
>
other
) {
Objects
.
requireNonNull
(
other
);
return
(
t
)
‐>
test
(
t
)
&&
other
.
test
(
t
);
}
默认方法:
or
与
and
的
“
与
”
类似,默认方法
or
实现逻辑关系中的
“
或
”
。
JDK
源码为:
default
Predicate
<
T
>
or
(
Predicate
<?
super
T
>
other
) {
Objects
.
requireNonNull
(
other
);
return
(
t
)
‐>
test
(
t
)
||
other
.
test
(
t
);
}
默认方法:
negate
“
与
”
、
“
或
”
已经了解了,剩下的
“
非
”
(取反)也会简单。默认方法
negate
的
JDK
源代码为
default
Predicate
<
T
>
negate
() {
return
(
t
)
‐> !
test
(
t
);
}
从实现中很容易看出,它是执行了
test
方法之后,对结果
boolean
值进行
“!”
取反而已。一定要在
test
方法调用之前
调用
negate
方法,正如
and
和
or
方法一样:
Function
接口
java.util.function.Function<T,R>
接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,
后者称为后置条件。
抽象方法:
apply
Function
接口中最主要的抽象方法为:
R apply(T t)
,根据类型
T
的参数获取类型
R
的结果。
使用的场景例如:将
String
类型转换为
Integer
类型
默认方法:
andThen
Function
接口中有一个默认的
andThen
方法,用来进行组合操作。
JDK
源代码如:
default
<
V
>
Function
<
T
,
V
>
andThen
(
Function
<?
super
R
,
?
extends
V
>
after
) {
Objects
.
requireNonNull
(
after
);
return
(
T t
)
‐>
after
.
apply
(
apply
(
t
));
}