• Java8系列 (一) Lambda表达式


    函数式编程

    在介绍Lambda表达式之前, 首先需要引入另一个概念, 函数式编程

    函数式编程是一种编程范式, 也就是如何编写程序的方法论。它的核心思想是将运算过程尽量写成一系列嵌套的函数调用,关注的是做什么而不是怎么做,因而被称为声明式编程。以 Stateless(无状态)和 Immutable(不可变)为主要特点,代码简洁,易于理解,能便于进行并行执行,易于做代码重构,函数执行没有顺序上的问题,支持惰性求值,具有函数的确定性——无论在什么场景下都会得到同样的结果

    我们把以前的过程式编程范式叫做 Imperative Programming – 指令式编程,而把函数式编程范式叫做 Declarative Programming – 声明式编程。下面通过一个简单的示例介绍两者的区别。

        //指令式编程
        int a = 1;
        int b = 2;
        int c = a+b;
        int d = c - 10;
        //声明式编程
        minus(plus(a, b), 10);

    函数式接口

    在Java8中, 引入了函数式接口这个新的概念, 函数式接口就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法(静态方法和default关键字修饰的默认方法)的接口。

    如果接口中声明的是java.lang.Object类中的 public 方法,那么这些方法就不算做是函数式接口的抽象方法。因为任何一个实现该接口的类都会有Object类中公共方法的默认实现。

    @FunctionalInterface 注解用于标注接口会被设计成一个函数式接口,虽然他不是必须的,但是推荐使用,这样会在编译期检查使用 @FunctionalInterface 的接口是否是一个函数式接口。

    Runnable线程任务类、Comparator比较器都只有一个抽象方法, 所以他们都是函数式接口, 另外Java8新引入了几个常用的泛型函数式接口 Predicate、Consumer、Function、Supplier, 以及在此基础之上扩展的一些函数式接口, 如 BiFunction、BinaryOperator等等。

    为了避免自动装箱操作,Java8对Predicate、Function、Supplier、Consumer等一些通用的函数式接口的原始类型进行了特化,例如: IntFunction。

        @Test
        public void test6() {
            IntPredicate intPredicate = (int i) -> i % 2 == 1;
            intPredicate.test(1000);
            Predicate<Integer> predicate = (Integer i) -> i % 2 == 1;
            predicate.test(1000);
        }

    上面的示例中, Predicate<Integer> 每次调用它的方法时都要进行一次装箱和拆箱, 而 IntPredicate 避免了这个问题, 当处理的数据比较多时, 使用 IntPredicate 可以提高你的程序运行效率。

    你可以像下面这样自定义一个函数式接口:

        @Test
        public void test3() {
            FunctionInterface1<String, Integer, List, Map<String, Object>> f1 = (str, num, list) -> new HashMap<>(16);
        }
        @FunctionalInterface
        public interface FunctionInterface1<O, T, K, R> {
            R apply(O o, T t, K k);
        }

    Lambda表达式

    Lambda表达式的基本语法是: (参数列表) -> 函数主体:

    • (parameters) -> expression
    • (parameters) -> {statements;}
        Runnable r1 = () -> System.out.println("test");
        Runnable r2 = () -> {
            System.out.println("test");
        };

    Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体的说,是函数式接口的一个具体实现的实例)。

    Lambda表达式可以被赋给一个变量,也可以作为参数传递给一个接受函数式接口作为入参的方法, 还可以作为一个返回值类型为函数式接口的方法返回值。

        public Callable<String> fetch() {
            return () -> "测试Lambda表达式";
        }

    上面的示例中, Callable<String> 的抽象方法签名是   () -> String , 和Lambda表达式 () -> "测试Lambda表达式" 的签名是一致的, 所以可以将其作为方法返回值。

    只要Lambda表达式和函数式接口的抽象方法签名(及函数描述符)相同,则同一个Lambda表达式可以与多个不同的函数式接口联系起来。

        @Test
        public void test7() {
            Comparator<Apple> c1 = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
            ToIntBiFunction<Apple, Apple> c2 = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
            BiFunction<Apple, Apple, Integer> c3 = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
        }

    如果一个Lambda的主体是一个表达式,它就和一个返回 void 的函数描述符(即函数式接口的抽象方法签名, 例如 (T, U) -> R)兼容。下面这个语句是合法的,虽然Lambda主体返回的是List,而不是Consumer上下文要求的 void。

        Consumer<String> c = s -> Arrays.asList(s);

    Lambda表达式可以没有限制的在其主体中引用实例变量和静态变量,但如果是局部变量,则必须显式的声明为final或只能被赋值一次,才能在Lambda主体中被引用。

    public class ChapterTest3 {
        String s1 = "";
        static String s2 = "";
    
        @Test
        public void test8() {
            String str = "局部变量";
            str = "局部变量";
            new Thread(() -> System.out.println(str)).start();//局部变量str重新赋值了,这一行就无法通过编译
            new Thread(() -> System.out.println(s1)).start();
            new Thread(() -> System.out.println(s2)).start();
            s1 = "实例变量";
            s2 = "静态变量";
        }
    }

    方法引用主要有三类

    • 指向静态方法的方法引用,例如  s -> String.valueOf(s)  可简写成  String::valueOf 
    • 指向任意类型的实例方法的方法引用,例如  (String s) -> s.length()  可简写成  String::length  (简单的说,就是你在引用一个对象的方法,而这个对象本身是Lambda的一个入参)
    • 指向Lambda表达式外部的已经存在的对象的实例方法的方法引用,下面的示例很好的展示了如何将 Lambda 重构成对应的方法引用
        @Test
        public void test10() {
            Consumer<String> c1 = i -> this.run(i);
            //上面的Lambda表达式可以简写成下面的方法引用,符合方法引用的第三类方式, this引用即所谓的外部对象
            Consumer<String> c2 = this::run;
        }
    
        public void run(String s) { }
    
        @Test
        public void test9() {
            //指向静态方法的方法引用
            Function<Integer, String> f1 = s -> String.valueOf(s);
            Function<Integer, String> f2 = String::valueOf;
            //指向实例方法的方法引用
            List<String> list = Arrays.asList("a", "b", "A", "B");
            list.sort((s1, s2) -> s1.compareToIgnoreCase(s2));
            //上面这个Lambda表达式转变成更简洁的方法引用
            list.sort(String::compareToIgnoreCase);
        }

    下面的转换模板图, 通俗易懂的总结了如何将Lambda表达式重构为等价的方法引用。

    关于构造函数引用,下面展示了一个简单易懂的栗子

        @Test
        public void test11() {
            //无参构造
            Supplier<Apple> c1 = () -> new Apple();
            Supplier<Apple> c2 = Apple::new;
            Apple a1 = c2.get();
            //有参构造
            BiFunction<String, Integer, Apple> f1 = (color, weight) -> new Apple(color, weight);//Lambda表达式
            BiFunction<String, Integer, Apple> f2 = Apple::new;//构造函数引用
            Apple a2 = f2.apply("red", 10);
        }

    最后我们总结一下Lambda表达式的使用, 假设我们需要对一个List集合进行不同规则的排序,这个不同规则对应的就是一个比较器Comparator, 我们可以有多种实现方式。

    最原始的方式就是定义一个Comparator接口的实现类作为入参, 其次就是使用匿名类的方式提供一个Comparator接口的实现作为入参。

    在Java8中, 我们可以不必像上面这么啰嗦, Lambda表达式很好地简化了这个实现过程, 比如我们这里需要按苹果的重量排序, 那么可以这样写

        @Test
        public void test12() {
            List<Apple> inventory = new ArrayList<>();
            inventory.add(new Apple("red", 94));
            inventory.add(new Apple("green", 100));
            inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
        }

    再想想, 还能不能更简化一下, 使用方法引用的方式进一步简化呢? 在Comparator接口中, 提供了静态方法 Comparator<T> comparing(Function<? super T, ? extends U> keyExtractor) , 就是为了简化Lambda表达式准备的, 让我们重新将上面的代码重构成方法引用

        @Test
        public void test12() {
            List<Apple> inventory = new ArrayList<>();
            inventory.add(new Apple("red", 94));
            inventory.add(new Apple("green", 100));
            inventory.sort(Comparator.comparing(Apple::getWeight));
        }

    关于 Comparator比较器、Predicate谓词、Function函数的组合用法

        /**
         * 函数的组合用法
         */
        @Test
        public void test15() {
            Function<String, Integer> f = i -> Integer.valueOf(i);//方法引用写法: Integer::valueOf
            Function<Integer, Apple> g = weight -> new Apple(weight); //构造函数引用写法: Apple::new
            Function<String, Apple> h = f.andThen(g); // andThen()相当于数学上的 g(f(x)) 函数
            Apple apple = h.apply("99"); //result: Apple(color=null, weight=99)
    
            Function<Apple, String> y = Apple::getColor;
            Function<Apple, Integer> z = f.compose(y); // compose()相当于数学上的 f(y(x)) 函数
            Integer result = z.apply(new Apple("red", 78));//会报 java.lang.NumberFormatException: For input string: "red" 异常
        }
    
        /**
         * 谓词的组合用法
         * and和or方法是按照在表达式链中的位置,从左到右确定优先级的,如a.or(b).and(c).or(d) 可以看成 ((a || b) && c) || d
         */
        @Test
        public void test14() {
            Predicate<Apple> p1 = apple -> "green".equals(apple.getColor());
            final Predicate<Apple> negate = p1.negate(); //
            System.out.println(negate.test(new Apple("green", 98)));// result: false
    
            final Predicate<Apple> and = p1.and(apple -> apple.getWeight() > 150);//
            System.out.println(and.test(new Apple("green", 140)));//result: false
    
            final Predicate<Apple> or = p1.or(apple -> apple.getWeight() > 150);//
            System.out.println(or.test(new Apple("blue", 170)));//result: true
        }
    
        /**
         * 比较器组合的用法
         */
        @Test
        public void test13() {
            inventory.sort(Comparator.comparing(Apple::getWeight).reversed());//苹果按重量倒序排序
            System.out.println(inventory);
            //苹果按重量倒序排序,当苹果重量相同时,按颜色升序排序
            inventory.sort(Comparator.comparing(Apple::getWeight).reversed().thenComparing(Apple::getColor));
            System.out.println(inventory);
        }

    匿名类和Lambda的区别

    在匿名类中,this代表的是类自身,但是在Lambda中,它代表的是包含类。其次,匿名类可以屏蔽包含类的变量,而Lambda表达式则不能(即Lambbda表达式内外不能出现同一名称的变量)。

    public class ChapterTest8 {
        int a = 1;
    
        @Test
        public void test1() {
            int a = 2;
            Runnable r1 = () -> {
                //int a = 3;//编译不通过
                System.out.println(this.a);//result: 1
            };
            Runnable r2 = new Runnable() {
                int a = 4;
                @Override
                public void run() {
                    int a = 5;
                    System.out.println(this.a);//result: 4
                }
            };
            new Thread(r1).start();
            new Thread(r2).start();
        }
    }

    上面的代码中,Lambda表式中的 this.a 指向的是 ChapterTest8 类中的实例变量,所以输出是 1,而匿名类中的 this.a 指向的是匿名类自身的实例变量,所以输出是 4

    另外上面代码Lambda表达式中的  int a = 3; 编译是无法通过的,因为在Lambda表达式外面已经有两个同名的局部变量和实例变量。匿名类则不会有这个问题。

    重载方法的Lambda匹配问题

    在涉及重载的上下文里, 将匿名类转换为Lambda表达式可能导致最终的代码更加晦涩(比如重载的方法入参具有相同的函数描述符), 可以使用显式的类型转换来解决这个问题。

        @Test
        public void test2() {
            doSomething((Task) () -> System.out.println());//此处重载的方法入参具有相同的函数描述符() -> void, 可以使用显式的类型转换来解决这个问题
        }
    
        private void doSomething(Runnable r) {
            r.run();
        }
    
        private void doSomething(Task t) {
            t.execute();
        }
    
        @FunctionalInterface
        interface Task {
            void execute();
        }

    上面的代码示例中,重载方法 doSomething() 的入参都是一个函数式接口,他们具有相同的函数描述符 () -> void ,因此这里使用Lambda表达式作为入参,编译器会无法根据上下文推断出你要调用的是哪个方法,你可以对传入的Lambda表达式做一个显式的类型转换,即可解决这个问题。

    使用Lambda表达式重构常用的设计模式

    策略模式

    通过Lambda表达式来直接传递不同的策略, 不需要像Java8之前那样针对每个策略提供具体的实现

        @Test
        public void test4() throws IOException {
            //这里 i -> i.length() > 8 就是一个策略
            boolean r1 = new strategy(i -> i.length() > 8).test("djdjdsjdj");
        }
    
        class strategy {
            //假定这是一个自定义的策略
            private Predicate<String> predicate;
    
            public strategy(Predicate<String> predicate) {
                this.predicate = predicate;
            }
    
            public boolean test(String s) {
                return predicate.test(s);
            }
        }

    模板模式

    在Java8之前, 模板模式通常这样写

        abstract class OnlineBanking {
            /**
             * 模板方法: 封装不变部分,扩展可变部分
             */
            public final void processCustomer(int id) {
                Customer c = DataBase.getCustomerById(id);
                this.makeCustomerHappy(c);
            }
    
            /**
             * 可变部分由子类去实现
             */
            abstract void makeCustomerHappy(Customer c);
        }

    如上, OnlineBanking抽象类将一些通用算法抽象出来, 封装到 processCustomer() 方法中。而其他需要扩展的可变部分, 定义一个抽象类 makeCustomerHappy() , 交由不同的子类去实现。

    现在你可以用Lambda表达式来实现同样的效果, 而且不再需要针对每个不同的算法去创建一个具体的实现类。

        @Test
        public void test5() {
            new OnlineBankingLambda().processCustomer(9527, customer -> System.out.println("不同的行为参数化传递给模板方法"));
        }
    
        class OnlineBankingLambda {
            /**
             * 模板方法: 封装不变部分,扩展可变部分
             */
            public final void processCustomer(int id, Consumer<Customer> consumer) {
                Customer c = DataBase.getCustomerById(id);
                //在Java8, 扩展的可变部分可以直接通过不同的行为参数化传递给模板方法, 不再需要创建一个子类去具体的实现.
                consumer.accept(c);
            }
        }

    上面模板模式的原始写法和Lambda写法仅是作为一个对比, 在实际业务中, 两者没有绝对的优劣之分。比如当需要扩展的可变算法种类比较多时, 如果使用Lambda表达式的写法, 那么每个可变算法就对应一个函数式接口, 这样反而会让代码的结构变得更加混乱, 可阅读性也大大降低。

    参考资料

    函数式编程初探

    Java 8实战

    作者:张小凡
    出处:https://www.cnblogs.com/qingshanli/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如果觉得还有帮助的话,可以点一下右下角的【推荐】。

  • 相关阅读:
    明年,我们依然年轻
    总有些东西会如台风一样的来
    ora00257错误处理方法
    ORACLE登录错误的解决方法
    C#中public new void add()的new在这里的意义
    Oracle Form Builder配置问题的一些总结
    作为程序员,你应该知道的职场晋升之路
    ORA01034错误的解决方法
    【转】JQUERY刷新页面
    【转】对C# 中堆栈,堆,值类型,引用类型的理解
  • 原文地址:https://www.cnblogs.com/qingshanli/p/11743439.html
Copyright © 2020-2023  润新知