函数式编程,在java中是一种语法糖实现。
FunctionalInterface注解
Java中通过@FunctionalInterface
注解来定义函数,这个注解声明一个接口是由Java语言规范所定义的功能接口。
@FunctionalInterface
注解在java.util.function
包下的所有接口上都有注解;
其所注解的接口在不符合功能函数规范时编译器将会抛出异常;
其注解的接口必需遵守以下规则:
- 只能有一个自己的抽象方法
- 可以有多个default methods
- 可以重写
java.lang.Object
类的方法
什么是default methods
在java8中接口类可以有默认方法,这些方法可以被接口示例调用:
public interface Human {
default String lovingHeart() {
return "爱心";
}
}
函数式编程需要Lambda语法支持,我们可以自己写一个功能接口实现,同样能被Lambda语法支持——
定义Man函数接口:
@FunctionalInterface
public interface Man {
void walk();
}
调用Man函数:
Man man = ()->{
System.out.println("行走了5千米");
};
man.walk();
一个函数基于Lambda语法的最简单的展现形式为:()->{}
。()
里的是函数参数,只有一个参数时()
可以省略;{}
里用来写逻辑代码的,只有一句代码时{}
可以省略:(data)->System.out.println(data);
。
虽然说这种实现看起来类似于匿名内部类实现:
Man man = new Man() {
public void walk() {
System.out.println("行走了5千米");
}
};
man.walk();
但在底层实现上它其实并不等同于匿名内部类,因为在编译后的class中,匿名内部类会生成一个类实现这个接口:
class com.test.jdk8.function.Test$1 implements com.test.jdk8.function.Man {
Test$1();
...
public void walk();
...
}
Test是类名,$1
代表新生成的匿名内部类编号。Test$1
代表在Test类下自动生成的类。
这里之所以为Test类名,是因为匿名内部类的实现语句是写在Test类里的。
而lambda语法只是在所在类添加了一个方法:
public class com.test.jdk8.function.Test {
public Test();
...
private static synthetic void lambda$0();
...
}
这个很好理解,因为函数式接口只能有一个自己的抽象方法,所以编译成方法实现会轻量的多。
java.util.function包
所有接口参照
乍一看接口很多,很难学习。实际上如果我们能探索出其中的规律,就能很轻松地掌握这些接口。
接下来,我会将函数接口的命名分为五种模式和四种修饰词来进行说明。
函数的五种模式
从名称上看,function包下的函数大体可以分为五种模式,暂且称之为Pattern:
模式 | 描述 |
---|---|
Consumer(消费者) | 接收值但不返回结果。 |
Function(函数) | 接收值并且返回结果。 |
Predicate(断言) | 接收值并返回boolean结果。原则上这个boolean应当与传入的值有关。 |
Supplier(供应者) | 不接收值但返回结果。 |
Operator(经营者) | 接收值并返回同类型结果。 |
函数的四种修饰
从名称的修饰词上看则有四种修饰类,暂且称之为Modifier:
修饰 | 描述 |
---|---|
Class(类) | 表示函数接收此Class类型的参数 |
To(转换) | 将接收的类型转值换为另一种类型输出。一般用于修饰Function模式 |
Unary(一元的) | 方法只接收一个参数 |
Binary / Bi(二元的) | 方法接收两个参数 |
四种修饰词类型在java.util.function
包下的函数名称中的表现形式:
名称 | Class | To | Unary | Binary |
---|---|---|---|---|
简述 | 类对象 | 转换 | 一元的 | 二元的 |
表现 | Double / Int / Long / Obj | ClassToClass/ ToClass | Unary / ClassUnary | Binary / Bi / ClassBinary |
注:
1、在Class类型的修饰词中,Obj修饰符是Object的缩写。
2、在其它修饰中的Class是指使用Class修饰类型的修饰词(Double / Int / Long / Obj),已用粗体标明。
比如:LongToIntFunction
就是ClassToClass类型。
依据模式和修饰推测函数的用途
function包下的接口大致都以 Pattern 或 ModifiersPattern组成;根据JDK优秀的代码风格,我们只需通过函数的名字就能推测出其用途,比如:
BiConsumer<T,U>
Bi 接收两个参数,Consumer 接收值但不返回结果。
BiConsumer接收两个T和U类型值进行操作,不返回结果。
ToIntBiFunction<T,U>
ToInt 转换为Int类型,Bi 接收两个参数,Function 接收值并返回结果。
接收两个T和U类型值,转换成Int后返回。
ObjIntConsumer<T>
Obj 接收Obj参数,Int 接收Int参数,Consumer 接收值但不返回结果。
接受对象值和int值进行操作,不返回结果
现在,试着推敲下列函数的用途:
IntBinaryOperator
DoublePredicate
ObjIntConsumer<T>
ToIntBiFunction<T,U>
Supplier<T>
函数方法
函数方法分布于不同的接口中,但所有类中的同名方法都是一个概念。所以,我们只需要去了解这些方法的概念即可。
compose方法
返回一个组合的函数,使得传入的函数在当前函数之前运行。如果传入的函数有返回值,则运行传入的这个函数,并将运行结果当做参数值传入当前函数中。
所谓的运行,就是运行函数接口那唯一的抽象方法。
Function中的compose方法源码:
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
这个方法会调用befor
函数的唯一方法apply
:
before.apply(v)
再将返回值当做函数传入返回的函数的apply
方法:
apply(before.apply(v))
最后形成一个函数返回:
return (V v) -> apply(before.apply(v));
举个例子,在调用当前apply方法前,会先调用传入的函数:
Function<String, String> function = (x)->{
System.out.println("本体");
return x;
};
function.compose((x)->{
System.out.println("前");
return String.valueOf(x);
}).apply("传入值");
andThen方法
返回一个组合的函数,使得传入的函数在当前函数之后运行。如果当前函数有返回值,则将当前函数的运行结果当做参数值传入这个被传入的函数中。
Consumer中的方法源码,传入函数无返回值:
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
Function中的方法源码,传入函数有返回值:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
identity方法
identity方法将传入参数当做返回参数返回。
identity方法作用于有返回值的函数。
identity是一个静态方法。
Function中的方法源码:
static <T> Function<T, T> identity() {
return t -> t;
}
假设有一个Int类型传参,那么上面代码等价于:
Function<Integer, Integer> function = (x)->{
return x;
};
当调用运行函数时,仅仅只是返回传入结果
PS:总感觉这个方法像吃饱了没事干,谁能告诉我怎么回事吗?
BinaryOperator函数特有的minBy和maxBy方法
minBy在传入的两个参数中取最小值
maxBy在传入的两个参数中取最大值
BinaryOperator接收两个参数并返回同类型结果。它继承自BiFunction接口,同时只有两个静态方法:
// 取最小值
public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
}
// 取最大值
public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
}
在传入的Comparator函数中只要进行减法计算(x1 - x2)即可比较大小,负数证明x1 < x2,零说明x1 = x2,正数证明x1 > x2
我们来演示一下如何将2和1进行比较,分别输出最大值和最小值:
Comparator<Integer> comparator = (x1,x2)->{
return x1 - x2;
};
BinaryOperator<Integer> binaryOperator = BinaryOperator.maxBy(comparator);
System.out.println("max " + binaryOperator.apply(2, 1));
binaryOperator = BinaryOperator.minBy(comparator);
System.out.println("min " + binaryOperator.apply(2, 1));
输出结果:
max 2
min 1
Predicate模式里的方法
and、negate、or方法大致分布于Predicate和名称以Predicate结尾的接口中,用于对boolean值进行操作。
而isEqual是Predicate接口的独有方法。
isEqual方法
isEqual是一个静态方法。返回一个Predicate函数对象,用于判断传入值是否与运行函数时传入的值一致。两个空对象会被认为是相等的:null==null
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
上面代码中,Objects::isNull是方法引用语法。是调用Objects类的isNull方法判断传入的值是否为null,并在返回或赋值时封装成一个函数,这样写或许更加直观一点:
Predicate predicate = Objects::isNull;
示例,判断a、b两个对象是否相等:
Object a = new Object();
Object b = new Object();
Predicate<Object> predicate = Predicate.isEqual(a);
boolean result = predicate.test(b);
System.out.println(result);
返回结果:
false
and方法
将other函数与当前函数的运行结果进行&&
运算:
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
or方法
将other函数与当前函数的运行结果进行||
运算:
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
negate
将当前函数的运行结果进行取反!
:
default Predicate<T> negate() {
return (t) -> !test(t);
}
——END 感谢观看