函数式接口
什么是函数式接口?
函数式接口,@FunctionalInterface,简称FI,简单的说,FI就是指仅含有一个抽象方法的接口,以@Functionalnterface标注,这里的抽象方法指的是该接口自己特有的抽象方法,而不包含它从其上级继承过来的抽象方法,例如:
@FunctionalInterface
public interface ApplePredicate {
boolean test(Apple apple);
}
函数接口有哪些?
Java 7 中已经存在的函数式接口:
- java.lang.Runnable
- java.util.concurrent.Callable
- java.security.PrivilegedAction
- java.util.Comparator
- java.io.FileFilter
- java.beans.PropertyChangeListener
除此之外,Java 8中增加了一个新的包:java.util.function,它里面包含了常用的函数式接口,例如:
– Predicate < T > ——接收 T 并返回 boolean
– Consumer< T >——接收 T,不返回值
– Function< T, R >——接收 T,返回 R
– Supplier< T >——提供 T 对象(例如工厂),不接收值
– UnaryOperator< T >——接收 T 对象,返回 T
– BinaryOperator< T >——接收两个 T,返回 T
除了上面的这些基本的函数式接口,我们还提供了一些针对原始类型(Primitive type)的特化(Specialization)函数式接口,例如 IntSupplier 和 LongBinaryOperator。(我们只为 int、long 和 double 提供了特化函数式接口,如果需要使用其它原始类型则需要进行类型转换)同样的我们也提供了一些针对多个参数的函数式接口,例如 BiFunction
如何定义自己的函数式接口?
在上面的ApplePredicate接口中可以看到我们使用了注解@FunctionalInterface来声明这是一个函数式接口,但在实际开发过程中我们并不需要通过注解来声明,编译器会自动根据接口自行判断该接口是否是一个函数式接口(判断过程并非简单的对接口方法计数:一个接口可能冗余的定义了一个 Object 已经提供的方法,比如 toString(),或者定义了静态方法或默认方法,这些都不属于函数式接口方法的范畴),使用注解的好处在于显示声明该接口为函数式接口,防止其他开发人员往该接口添加其他方法。
接口介绍
java.util.function.* 接口中定义了多种不同的函数接口,这里首先介绍三个泛型函数式接口:Predicate,Consumer,Function
@FunctionalInterface
public interface Predicate<T>{
boolean test(T t);
}
java.util.function.Predicate 定义了一个test方法接受一个泛型返回一个布尔型
@FunctionalInterface
public interface Consumer<T>{
void accept(T t);
}
java.util.function.Consumer 定义了一个accept方法接受一个泛型没有返回类型,如:
public static <T> void forEach(List<T> list, Consumer<T> c) {
for (T i : list) {
c.accept(i);
}
}
public static void main(String arg[]) {
forEach(Arrays.asList(1,2,3,4,5,6), integer -> {if (integer % 2 == 0) System.out.println(integer);});
}
@FunctionalInterface
public interface Function<T, R>{
R apply(T t);
}
java.util.function.Function< T, R >定义了一个apply方法接受一个泛型T返回一个泛型R,如:
public static void main(String arg[]) {
List<Integer> l = map(Arrays.asList("lambdas", "in", "action"), s -> s.length());
}
public static <T, R> List<R> map (List<T> list, Function<T, R> f) {
List<R> result = new ArrayList<R>();
for (T s : list) {
result.add(f.apply(s));
}
return result;
}
原始类型特化
Java类型要么是引用类型(比如Byte、Integer、Object、List),要么是原始类型(比如int、double、byte、char)。但是泛型(比如Consumer<T>中的T)只能绑定到引用类型。这是由泛型内部的实现造成的。因此,在Java里有一个将原始类型转换为对应的引用类型的机制。这个机制叫做装箱(boxing)。相反的操作,也就是将引用类型转换为对应的原始类型,叫做拆箱(unboxing)。Java还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱是自动完成的。比如,这就是为什么下面的代码是有效的:
List<Integer> list = new ArrayList<>();
for (int i = 300; i < 400; i++){
list.add(i);
}
但是这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包装起来,并保存在队里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取包装的原始值。
Java8为我们前面所说的函数式接口带来了一个专门的版本,一遍在输入和输出都是原始类型时避免自动装箱的操作。比如,使用IntPredicate就避免了对值1000进行装箱操作,但要是用Predicate<Integer>就会把参数1000装箱到一个Integer对象中:
public interface IntPredicate{
boolean test(int t);
}
IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000);//无装箱
Predicate<Integer> oddNumbers = (Integer i) -> i % 2 == 1;
oddNumbers.test(1000);//装箱
true
下表展示了Java 8中的常用函数式接口,
函数接口 | 函数描述符 | 原始类型特化 |
---|---|---|
Predicate< T > | T->boolean | IntPredicate,LongPredicate,DoublePredicate |
Consumer< T > | T->void | IntConsumer,LongConsumer, DoubleConsumer |
Function< T,R > | T->R | IntFunction< R >, |
IntToDoubleFunction, | ||
IntToLongFunction, | ||
LongFunction< R >, | ||
LongToDoubleFunction, | ||
LongToIntFunction, | ||
DoubleFunction< R >, | ||
ToIntFunction< T >, | ||
ToDoubleFunction, | ||
ToLongFunction | ||
Supplier< T > | ()->T | BooleanSupplier,IntSupplier, LongSupplier,DoubleSupplier |
UnaryOperator | T->T | IntUnaryOperator,LongUnaryOperator,DoubleUnaryOperator |
BinaryOperator | (T,T)->T | IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator |
BiPredicate | (L,R)->boolean | |
BiConsumer | (T,U)->void | ObjIntConsumer, ObjLongConsumer, ObjDoubleConsumer |
BiFunction | (T,U)->R | ToIntBiFunction, ToLongBiFunction, ToDoubleBiFunction |
下表展示了Lambdas及函数式接口的例子
使用案例 | Lambda的例子 | 对应的函数式接口 |
---|---|---|
布尔表达式 | (List< String > list) -> list.isEmpty() | Predicate< List< String > > |
创建对象 | () -> new Apple(10) | Supplier< Apple > |
消费一个对象 | (Apple a) -> System.out.println(a.getWeight()) | Consumer< Apple > |
从一个对象中选择/提取 | (String s) -> s.length() | Function< String, Integer >或 ToIntFunction< String > |
合并两个值 | (int a, int b) -> a * b | IntBinaryOperator |
比较两个对象 | (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) | Comparator< Apple >或 BiFunction< Apple, Apple, Integer > 或 ToIntBiFunction< Apple, Apple > |
之前讲到了双冒号目标引用方法,这里我们来看一些例子:
lambda | 等效的方法引用 |
---|---|
(Apple a) -> a.getWeight() | Apple::getWeight |
() -> Thread.currentThread().dumpStack() | Thread.currentThread()::dumpStack |
(str, i) -> str.substring(i) | String::substring |
(String s) -> System.out.println(s) | System.out::println |
如何构建方法引用
方法引用只是Lamdba单一方法的语法糖。
方法引用主要有三类。
(1) 指向静态方法的方法引用(例如Integer的parseInt方法,写Integer::parseInt)。
(2) 指 向 任 意 类 型 实 例 方 法 的 方 法 引 用 ( 例 如 String 的 length 方 法 , 写 作String::length)。
(3) 指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction用于存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensiveTransaction::getValue)。