浅显易懂的Java函数式教学

写在最前

        本篇文章由笔者对函数式长时间使用及归纳而来,作为典型的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不能单独存在,必须被赋予与其型构(即入参类型列表与返回值类型)相当的类型才得以存在;Lambda由小括号组成的参数列表、箭头符号"->"、主体逻辑 三部分组成,举例:       

        // Lambda由小括号组成的参数列表、箭头符号"->"、主体逻辑 三部分组成
        Supplier<String> sup = () -> {
            return "HelloWorld";
        };

        Function<Object,String> func = (o) -> {
            return o.toString();
        };
        
        // 当Lambda只有一个参数时,小括号可以省略
        Function<Object,String> funI = o -> {
            return o.toString();
        };
        
        // 当主体结构只有一行语句时,大括号和语句结尾的分号可以省略,如果是return语句,则return可以省略
        Function<Object,String> funII = o -> o.toString();

        // 当需要匹配多个接口类型时(尤其是标签接口)使用&指明其类型
        Runnable r = (Runnable & Serializable) () -> {};
        System.out.println(Serializable.class.isInstance(r)); // true

        如果JavaScript写的比较多的话,其实上面的例子类似于定义局部作用域函数:

        var sup = function() {
            return "HelloWorld";
        };

        var func = function(o) {
            return o.toString();
        };
        
        var funI = function(o) {
            return o.toString();
        };

        var funII = o => o.toString();// es6

        2.对类型的静态函数进行引用

        函数引用即双冒号“::”,是一种对指定函数式接口的同型方法进行直接引用的特性;对类型的静态引用,使函数式接口与要引用的方法同型即可,举例如下:

        Function<String,Integer> funcStoI = Integer::valueOf;//String -> Integer;
        IntUnaryOperator intUO = Math::abs;// int -> int;
        DoubleBinaryOperator doubleBO = Math::max;// (double,double) -> double;

        // 当然,构造方法也算一种特殊的静态函数
        Supplier<String> strSup = String::new; // new String();
        UnaryOperator<String> strUO = String::new;// new String(str);

        // 同理,数组的构造参数亦可
        IntFunction<int[]> intArray = int[]::new; // new int[x];

        3.对实例的非静态函数函数进行引用

        同理,对实例的非静态函数引用,与对类的静态函数引用遵循相同规则,举例如下:

        Supplier<String> strSup = "WantString?"::toString;

        IntSupplier intSup = "WantSomeInt?"::hashCode;

        Consumer<String> strCons = System.out::println;

        4.对类型的非静态函数进行引用,等价关系的辩证

        在上述观点中,可以看出,Java中“静态/非静态”的概念与“类/实例”的概念高度一致,这些函数引用看起来非常自然;但这里设想一下,对类的非静态函数进行引用,是否有意义呢?答案是肯定的,这里先引入一个有关函数纯粹性的“等价关系”;

        考虑这样一个观点:

        person.say("hello") 与 say(person,"hello")具备某种等价关系;两个say方法在满足条件时可以相互转化;这里我们先简写为 persion.say("hello") <=> say(persion,"hello");依次类推,persion.flyBetween("北京","上海") <=> flyBetween(persion,"北京","上海");它们彼此间的编程语义其实非常明确;

        这暗示了一个规律,即一个实例的非静态方法,可以等价的变为追加该实例类型作为参数的静态方法,这在函数引用中也有所体现,举例如下:

        Function<Object,String> funcIII = Object::toString; // o.toString() <=> toString(o)
        ToIntFunction<Object> toIntFunc = Object::hashCode; // o.hashCode() <=> hashCode(o)
        
        Consumer<Runnable> runCon = Runnable::run; // r.run() <=> run(r)
        
        BiConsumer<PrintStream,String> biCons = PrintStream::println; // out.println(str) <=> println(out,str);

        可以看到,对类型的非静态函数引用中,类型对应的实例会自动追加作为参数到参数列表的第一个位置上;对类的非静态方法引用,实际是上提取了非静态方法中的静态含义

五、Optional与Stream

        关于函数式的概念整理告一段落,这里开始介绍其主要的使用场景,即围绕着Optional与Stream的 构造操作、变换操作、终止操作 展开;在文末会对函数式的其它使用场景做一个总结;Optional与Stream的核心思想有很多一致性,但前者要简单很多;在未定义终止操作时,Optional与Stream中定义的中间逻辑(构造过程、变换过程)不会执行;

        1.Optional;Optional本意为“可选的”这里引申为“可有可无的”,Optional<T> 定义了一个可有可无的T实例,我们可以对T进行一系列操作,若在操作中的任何一个步骤T变为null,则终止操作;

        1.1 Optional的构造 Optional常由其静态函数Optional.ofNullable(T t);进行构造,其中t可为null;

        Optional<Integer> optionalInteger = Optional.ofNullable(1);

        1.2 Optional的变换 Optional<T>.map 定义了对T的变换操作,此处可以传入一个Function<T,R> 将T变换为R;即Optional<T> 变为 Optional<R>;

        // 构造Optional<Integer>
        Optional<Integer> optionalInteger = Optional.of(1);
        // 变换为Optional<String>
        Optional<String> optionalStr = optionalInteger.map(String::valueOf);

        Optional<T>.filter 定义了一个对T的断言,此处传入一个Predicate<T>为T下一个判断,需要满足断言条件为true时才可继续执行操作;

        // 构造包含随机Integer的Optional
        Optional<Integer> optionalRnd = Optional.of(new Random().nextInt());
        // 传入偶数断言,得到一个仅存在偶数的Optional
        Optional<Integer> optionalEven = optionalRnd.filter(i -> i % 2 == 0);

        1.3 Optional的终止 这里列举了一些终止操作,其中ifPresent较为常用;

        // Optional.ifPresent
        Optional.of(new Random().nextInt()) // 获得一个包含随机数的Optional实例
                .filter(i -> i % 2 == 0) // 断言 使用传入的Predicate<T> 保证数据为偶数
                .map(String::valueOf) // 变换 使用传入的Function<T,R>变换类型(Integer -> String)
                .ifPresent(System.out::println); // 若满足条件则使用传入的Consumer<T>进行消费(打印)

        // Optional.orElse
        String evenStr = Optional.of(new Random().nextInt()) // 获得一个包含随机数的Optional实例
                .filter(i -> i % 2 == 0) // 变换数据为偶数
                .map(String::valueOf) // 变换类型为字符串
                .orElse("not even"); // 若不满足条件 则使用orElse传入的默认值
        
        // Optional.orElseGet
        String evenStrI = Optional.of(new Random().nextInt()) // 获得一个包含随机数的Optional实例
                .filter(i -> i % 2 == 0) // 变换数据为偶数
                .map(String::valueOf) // 变换类型为字符串
                .orElseGet(String::new); // 若不满足条件 则调用orElseGet传入的Supplier<T>获取一个默认值

        // Optional.orElseThrow
        String evenStrII = Optional.of(new Random().nextInt()) // 获得一个包含随机数的Optional实例
                .filter(i -> i % 2 == 0) // 变换数据为偶数
                .map(String::valueOf) // 变换类型为字符串
                .orElseThrow(RuntimeException::new); // 若不满足条件 则抛出一个异常

        这里我用常见的防御性编程再举个例子,我们经常有需要取出组合对象的多级结构,但是为了防止空指针,我们在每次取出对象时判断一下空:

        A a = getAFromSomeWhere();
        if(a == null) {
            return "a is null";
        }
        
        B b = a.getB();
        if(b == null) {
            return "b is null";
        }
        
        C c = b.getC();
        if(c == null) {
            return "c is null";
        }
        
        if(!c.isLegal()) {
            return "c is illegal";
        }
        
        doSomething(c);

        使用Optional的版本为则简化为:

        Optional.ofNullable(getAFromSomeWhere())
        .map(A::getB)
        .map(B::getC)
        .filter(C::isLegal)
        .ifPresent(this::doSomething);

        此外,按照函数式接口的设计思想,也存在一组描述基本类型的Optional类,它们的命名规律为Optional+类型名,由于是基本数据类型,不存在为null的情况;这里对基本类型是否存在的判断是靠Optional类中一个单独的布尔值控制的;

        // int
        OptionalInt intOp = OptionalInt.of(1);
        // long
        OptionalLong longOp = OptionalLong.of(1L);
        // double
        OptionalDouble doubleOp = OptionalDouble.of(1.);

        2.Stream;Stream<T>的抽象含义为一系列待处理的多个对象,类比Optional,Stream对一系列对象定义一系列统一操作,在终止操作前中间操作并不会执行;Stream更加常用也更为复杂,这里会以相当的篇幅对Stream详细介绍;

        2.1 Stream的构造

        这里提供了一些Stream的直接构造方式

        // 空Stream
        Stream<Object> empty = Stream.empty();
        
        // 用可枚举的已有对象构造一个有限流
        Stream<Integer> limitedStream = Stream.of(1,2,3);
        
        // 用迭代方式构造一个无限流 0,1,2,3...
        Stream<Integer> unlimitedStream = Stream.iterate(0, i -> i + 1);
        
        // 在无限流上增加限制
        Stream<Integer> subStream = unlimitedStream.limit(100);

        但上述对Stream的构造在实际应用中并不多,从已有的Array或Collection组件得到Stream则更为常用: 

        // 由List得到Stream
        List<String> strList = Lists.newArrayList("a","b","c");
        Stream<String> strStream = strList.stream();
        
        // 由Set得到Stream
        Set<Long> longSet = Sets.newHashSet(1L,2L,3L);
        Stream<Long> longStream = longSet.stream();
        
        // Map则常用entrySet得到流
        Map<Integer,String> map = new HashMap<>();
        Stream<Entry<Integer, String>> kvStream = map.entrySet().stream();

        // 数组构造,这里可以注意一下基本类型流的命名规则,类比于OptionalInt这里有IntStream
        // int stream
        IntStream intS = Arrays.stream(new int[] {1,2,3});
        // double stream
        DoubleStream doubleS = Arrays.stream(new double[] {1.,2.,3.});
        // String stream
        Stream<String> strS = Arrays.stream(new String[]{"a","1","2"});

        可以合并多个流得到一个新的流:

        // 合并两个流 a b 
        Stream.concat(a, b);

        2.2 Stream的变换 这里介绍Stream的变换操作;类比Optional中的内容,有很多相似之处:

        Stream<T>.map 定义了对T的变换操作,此处可以传入一个Function<T,R> 将T变换为R;即Stream<T> 变为 Stream<R>;

        // String stream
        Stream<String> strStream = Arrays.stream(new String[]{"0","1","2"});
        // map to Integer
        Stream<Integer> integerStream = strStream.map(Integer::valueOf);

        Stream<T>.filter 定义了一个对T的断言,此处传入一个Predicate<T>为T下一个判断,将满足断言条件为true的对象构造为一个新的Stream;

        // 过滤得到一个偶数流
        Stream<Integer> evenStream = Stream.iterate(0, i -> i + 1).filter(i -> i % 2 == 0);
        // 过滤得到一个字符串流
        Stream<String> strStream = Stream.of("123",1L,123.456).filter(String.class::isInstance).map(String.class::cast);

        此外,正如之前提到的,Stream本身的含义是多个对象组成的流,因此一些变换操作是围绕着数量进行的:

        // limit限制了流的数量(对于无限流而言)
        Stream<Integer> limitedStream = Stream.iterate(0, i -> i + 1).limit(100);
        
        // skip可以跳过若干个流的对象
        Stream<Integer> skiptedStream = limitedStream.skip(90);// 90,91,92...99
        
        // flatMap 映射为多个流并打平数据
        // 1 -> 1
        // 2 -> 1,2
        // 3 -> 1,2,3
        // integerStream: 1,1,2,1,2,3
        Stream<Integer> integerStream = Stream.of(1,2,3).flatMap(i -> Stream.iterate(1, j -> j + 1).limit(i));
        
        // sorted排序 sortedStream: 1,1,1,2,2,3 
        Stream<Integer> sortedStream = integerStream.sorted();
        
        // distinct去重 distinctStream: 1,2,3
        Stream<Integer> distinctStream = sortedStream.distinct();

        2.3 Stream的终止 执行终止操作后,会触发中间操作的依次执行,并最终得到希望的结果

        // forEach 是常用终止操作,这里打印了所有对象
        Stream.iterate(0, i -> i + 1).limit(10).forEach(System.out::println);
        
        // collect 这里将Stream收集变为list
        List<Integer> list = Stream.iterate(0, i -> i + 1).limit(10).collect(Collectors.toList());
        // collect 亦可收集为Set
        Stream.iterate(0, i -> i + 1).limit(10).collect(Collectors.toSet());
        // toArray 将Stream的对象变为数组,注意这里toArray入参为函数式接口IntFunction<Integer[]> 
        Integer[] arr = Stream.iterate(0, i -> i + 1).limit(10).toArray(Integer[]::new);
        // 将Stream变为map toMap需要定义两个关于Stream类型T的Function以决定map的key-value类型 请注意toMap的重载
        Map<Integer, Integer> map = Stream.iterate(0, i -> i + 1).limit(10).collect(Collectors.toMap(Functions.identity(), Functions.identity()));
        // 分组聚合 这里按照基偶将流分裂为两个list 并放置于map中 请注意groupingBy方法的入参是一个Function
        // 0 -> [0,2,4,6,8]
        // 1 -> [1,3,5,7,9]
        Map<Integer, List<Integer>> groupByMap = Stream.iterate(0, i -> i + 1).limit(10).collect(Collectors.groupingBy(i -> i % 2));
        
        // 下面介绍一些列尝试合并整个Stream流对象得到一个值的功能
        // 统计流中对象的数量
        long count = Stream.iterate(0, i -> i + 1).limit(10).count();
        // 对流使用断言 allMatch即全部满足时返回true
        boolean allBiggerThanTen = Stream.iterate(0, i -> i + 1).limit(15).allMatch(i -> i > 10);
        // 对流使用断言 anyMatch即流中至少有一个对象满足时返回true
        boolean hasBiggerThanTen = Stream.iterate(0, i -> i + 1).limit(15).anyMatch(i -> i > 10);
        // reduce 常用操作之一,传入BinaryOperator<T>并对流对象进行迭代,最终合并为一个结果;由于Stream中可能为0个对象,故这里得到的结果为Optional(可有可无)
        // 这里是用reduce求和
        Optional<Integer> sum = Stream.iterate(0, i -> i + 1).reduce((i,j) -> i + j);
        // 得到第一个对象
        Optional<Integer> first = Stream.iterate(0, i -> i + 1).reduce((i,j) -> i);
        // 得到最后一个对象
        Optional<Integer> last = Stream.iterate(0, i -> i + 1).reduce((i,j) -> j);
        // 得到第一个对象(相对于上面reduce来讲更为正统方式)
        Optional<Integer> realFirst = Stream.iterate(0, i -> i + 1).findFirst();
        // 一个map+reduce,搞大数据的看了可能会比较亲切
        Stream.of("1","2","3").map(Integer::valueOf).reduce((x,y) -> x + y).ifPresent(System.out::println);// 打印6
        // 求最大,这里注意max的入参 Comparator<T>,已经被@FunctionalInterface所标记
        Optional<Integer> max = Stream.iterate(0, i -> i + 1).max(Integer::compareTo);
        // 求最小
        Optional<Integer> min = Stream.iterate(0, i -> i + 1).min(Integer::compareTo);

        此外,继续类比Optional,我们也有针对基本数据类型的Stream:

        // int stream
        IntStream intS = IntStream.iterate(0, i -> i + 1);
        // long stream
        LongStream longS = LongStream.iterate(0, i -> i + 1L);
        // double stream
        DoubleStream doubleS = DoubleStream.iterate(0, i -> i + 1.);
        // 亦可以由对象流映射到基本类型流
        IntStream intFromInteger = Stream.iterate(0, i -> i + 1).mapToInt(Integer::intValue);
        
        // 此外,基本类型流定义了一些使用方法 这里的结果也会对应基本类型的Optional
        OptionalInt max = intS.max();

六、总结与拾遗

        总结一下本文内容;

        ※ 介绍了函数式接口的概念(有且仅有);

        ※ 介绍了函数式设计理念及对应的三大函数式接口(从哪来,经过何种加工,最终归宿);

        ※ 介绍了三大函数式接口的衍生规律(基本类型,参数数量,运算符,混合);

        ※ 介绍了lambda表达式及函数引用相关规律(还记得等价关系吗);

        ※ 介绍了Optional与Stream这两个伴随函数式大展身手的结构;

        下面补充一些经验性质的东西;

        ⚪ 对于初学者,建议在已有代码中,先对List、Set这类结构尝试使用Stream进行处理,尽可能的使用函数式消除一部分控制流程语句,感受一下它带来的方便之处;

        ⚪ 非常建议在定义方法的形参、类成员时使用函数式接口类型,结合函数引用机制可使代码大大简化;

        ⚪ 在定义自己的函数式接口前,尽量先找找有没有满足定义的已存在接口,避免重复定义;

        ⚪ 在一些场景下,使用函数式会造成性能下降,但这类‘下降’很难成为服务瓶颈;

        ⚪ 如果一个函数式接口实现了Serializable,则能通过函数式接口实例得到被引用的方法名,这意味着我们在使用方法名时,能够存在一个编译时的强检查机会,这对于一些中间件来讲至关重要:

//参考位置:https://github.com/abel533/Mapper/blob/master/weekend/src/main/java/tk/mybatis/mapper/weekend/reflection/Reflections.java
// 顺带一提,这个关于mybatis的类库非常棒,强烈推荐!
/**
 * @author Frank
 */
public class Reflections {
    private static final Pattern GET_PATTERN = Pattern.compile("^get[A-Z].*");
    private static final Pattern IS_PATTERN  = Pattern.compile("^is[A-Z].*");

    private Reflections() {
    }

    public static String fnToFieldName(Fn fn) {
        try {
            Method method = fn.getClass().getDeclaredMethod("writeReplace");
            method.setAccessible(Boolean.TRUE);
            SerializedLambda serializedLambda = (SerializedLambda) method.invoke(fn);
            // 注意这里
            String getter = serializedLambda.getImplMethodName();
            if (GET_PATTERN.matcher(getter).matches()) {
                getter = getter.substring(3);
            } else if (IS_PATTERN.matcher(getter).matches()) {
                getter = getter.substring(2);
            }
            return Introspector.decapitalize(getter);
        } catch (ReflectiveOperationException e) {
            throw new ReflectionOperationException(e);
        }
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章