写在最前
本篇文章由笔者对函数式长时间使用及归纳而来,作为典型的Java猿,学习函数式时中间走了很多弯路,在此将相关经验总结归纳,帮助有需要的小伙伴过坑~;
本篇文章将以函数式观点出发学习,请暂且先忘掉关于‘lambda=内部类简化’的类似认知,这对知识体系的整体架构是至关重要的;
一、函数式接口的概念
函数式接口被定义为有且仅有一个抽象方法的接口;与之相关的注解为@FunctionalInterface;
关于定义:由于Java1.8的接口引入了default方法,接口也允许一定程度的实现,故这里强调一下“有且仅有一个抽象方法”;
关于注解:有人经常对@FunctionalInterface这个注解的作用抱有疑问;这里以我们常见的@Override做类比:
众所周知,@Override常被标记到对父类方法重写的子类方法上,表示这个方法重写了父类方法;如果不标记@Override的话,依然也是有效的重写行为;@Override的作用是,当标记方法没有重写父类方法时,给出编译级别的提示信息,这一定程度上避免了修改父类方法名而使子类的方法由重写变为定义成全新方法的问题;
同理,@FunctionalInterface标记在一个接口上,作用为当接口中出现多于一个抽象方法时,给出编译级别的提示信息,这样来避免我们不小心将函数式接口拓展为普通接口的错误;
只要你愿意,你可以按照此概念创造无数的自定义函数式接口;
二、三大基本函数式接口
函数式思维的出发点,是将函数本身作为“一等公民”,将所有问题概括为三个步骤:数据从哪里来?数据经过何种变换?数据的最终归宿是哪里?这三个问题对应了三个基本的函数式接口,这些函数式接口被定义在java.util.function包下;
数据从哪里来:对应java.util.function.Supplier<T> ,此函数式接口定义了数据的产生,其抽象方法 [T get();] 入参为空出参为T,意为“凭空制造了T”或者说“T的诞生”;我们这里简写记为() -> T;
数据经过何种变换:对应java.util.function.Function<T, R>,参考Supplier我们很容易理解,其抽象方法[R apply(T t);]意为“将T加工为R”或“将T变换为R”,记为T -> R;
数据的最终归宿是哪里:对应java.util.function.Consumer<T>,很容易理解,其抽象方法[void accept(T t);]意为“将T消费”,记为T -> ();
此概念贯穿全文,务必牢记;在此准备一个小问题,你能找到型如 [() -> ()]对应的函数式接口吗?提示一下此接口在多线程中非常常用。
三、函数式接口的衍生
如果观察java.util.function包下的函数式接口,数量有40+;死记硬背很难;但只要细心观察,所有函数式接口都有三大基本函数式接口衍生而来,此处将衍生规律做整理,方便记忆:
1.基本类型簇:三大函数式接口均是泛型接口,虽然能够自动装拆箱,但依然有希望对基本类型方法直接抽象的需求,故Java中追加了一些基本类型函数式接口,这里注意一下它们的命名规律,都是把基本类型名写于接口上;举例:
IntConsumer定义为int -> ();
BooleanSupplier定义为() -> boolean;
LongFunction<R>定义为long->R;而ToDoubleFunction<T>定义为T->double;
此外,有一些特例的函数式接口被赋予了别名,例如Predicate<T>定义为T->boolean,Predicate为“推测、下判断”之意,这里是对T判别一个真伪;“ToBooleanFunction<T>”则并不存在;
2.二元参数簇:三大基本函数式接口只操作了一个数据;而实际使用时,操作多个数据的需求很常见,故官方囊括了一些此类接口,举例如下:
BiConsumer<T,U> 定义为一个二元消费 (T,U) -> (); //“Bi”为“Binary”前缀,意为“二元的”;
BiFunction<T, U, R>定义为(T,U) -> R; //将T、R两个参数加工为R;
BiPredicate<T, U>定义为(T,U) -> boolean; //结合T、U下一个判断;
3.运算符簇:考虑Function<T,T>,其定义为T -> T即出入参类型相同,此时我们赋予了它一个新的命名,即Operator,意为运算符;Operator作为函数式接口时常伴有代表参数元数的短语,举例:
UnaryOperator<T> 定义为 T -> T;是一个“单目运算符”,将一个类型运算后得到一个相同的类型;实际上,它继承了Function<T, T>;类比i++中的“++”;
BinaryOperator<T>定义为(T,T) -> T;是一个“双目运算符”,同理,它继承了BiFunction<T,T,T>;类比1+1中的“+”;
4.复合体簇:其实很多函数式接口的名称都是遵循上述规律的复合体,在掌握上面的命名规律后,我们可以做到“见名知型”了;举例:
DoubleBinaryOperator定义为(double,double) -> double;DoubleBinaryOperator中的“Double”代表其与基本数据类型double有关,“Operator”代表其出入参相同,“Binary”代表其有两个入参,这样就很好推测了;
DoubleToLongFunction定义为double -> long;见到“ToLong”一定是返回了long型,“Double”则意味着其入参为基本类型double;
这里建议参考java.util.function包下的函数式接口名称,练习推测一下其抽象方法的型构;另外在自己定义新的函数式接口时建议延用这些规律;
四、Lambda表达式与函数引用
在了解了函数式接口的概念后,这里对Lambda表达式与函数引用进行讲解;它们与函数式接口的概念息息相关;
1.有关Lambda表达式:
我相信很多人在不了解函数式体系时,已经写过很多lambda了;在这里结合函数式接口的概念整体归纳一下;Lambda不能单独存在,必须被赋予与其型构(即入参类型列表与返回值类型&