Java SE8 之 Lambda 表达式详解

作者: 王永迪 | 来源:发表于2017-08-13 00:08 被阅读1271次
    lambda.jpg

    原文地址:http://stackoverflow.com/documentation/java/91/lambda-expressions#t=201701170111285810613

    简介

    Lambda表达式用一个表达式提供了一个实现单个接口方法(函数式接口)的简洁明了的方式。他允许你减少你必须创建和维护的代码数量,它经常被用作匿名内部类的替代。

    介绍

    函数式接口

    Lambdas 只能在一个仅包含一个抽象方法的函数式接口上操作。函数式接口可以有任意的default或着static方法。(为此, 函数式接口有时候是说具有单个抽象方法的接口, 或着SAM interfaces)。

    interface Foo1 {
        void bar();
    }
    
    interface Foo2 {
        int bar(boolean baz);
    }
    
    interface Foo3 {
        String bar(Object baz, int mink);
    }
    
    interface Foo4 {
        default String bar() { // default so not counted
            return "baz";
        }
        void quux();
    }
    

    当声明 函数式接口时@FunctionalInterface注解可以被加上,虽然它没有明确的作用, 但是如果一个注解被用于非函数式接口一个compiler error 将会产生,因此充当一个接口不应该被改变的提醒者。

    @FunctionalInterface
    interface Foo5 {
        void bar();
    }
    
    @FunctionalInterface
    interface BlankFoo1 extends Foo3 { // inherits abstract method from Foo3
    }
    
    @FunctionalInterface
    interface Foo6 {
        void bar();
        boolean equals(Object obj); // overrides one of Object's method so not counted
    }
    

    相反的, 它不是一个函数式接口, 因为它不止有一个抽象方法。

    interface BadFoo {
        void bar();
        void quux(); // <-- Second method prevents lambda: which one should be considered as lambda?
    }
    

    它也不是一个函数式接口, 因为它没有任何方法。

    interface BlankFoo2 { }
    

    Java 8 也在Java.util.function中提供了很多基本的模版函数式接口, 例如, 内置的接口Predicate<T>包含了一个单个方法, 输入一个值T并且放回一个boolean。

    Lambda Expressions

    Lambda表达式的基本结构是: lambda 表达式结构

    fi将会持有一个实现了FunctionalInterface接口的匿名类的实例,匿名类中一个方法的定义为{System.out.println("Hello"); }。 换句话说,等价于:

    FunctionalInterface fi = new FunctionalInterface() {
        @Override
        public void theOneMethod() {
            System.out.println("Hello");
        }
    };
    

    你不能在使用lambda时明确一个方法名,反而根本不需要,因为函数式接口必须有一个抽象方法, 所以java重写了它。
    一旦lambda的类型不确定,(e.g. 重写方法)你可以给lambda添加一个强转型告诉编译器它的类型,就像:

    Object fooHolder = (Foo1) () -> System.out.println("Hello");
    System.out.println(fooHolder instanceof Foo1); // returns true
    

    如果函数式接口的单个方法包含参数,它们的本地变量名应该出现在lambda的方括号中。没有必要去声明参数的类型或着返回值的类型,因为他们能从接口定义中推理得出(当然如果你想声明参数类型, 这也不是一个错误)。因此,如下两个样例是等价的:

    Foo2 longFoo = new Foo2() {
        @Override
        public int bar(boolean baz) {
            return baz ? 1 : 0;
        }
    };
    Foo2 shortFoo = (x) -> { return x ? 1 : 0; };
    

    如果函数只有一个参数,参数两边的圆括号可以省略:

    Foo2 np = x -> { return x ? 1 : 0; }; // okay
    Foo3 np2 = x, y -> x.toString() + y // not okay
    

    隐式返回

    如果被放在lambda中的代码是一个java 表达式而不是一个声明,它就会被当作一个返回这个表达式值的方法,因此,下面这两个是等价的:

    IntUnaryOperator addOneShort = (x) -> (x + 1);
    IntUnaryOperator addOneLong = (x) -> { return (x + 1); };
    

    访问本地变量(值闭包)

    因为lambdas 是匿名内部类的简化写法,它们遵循在一个闭合的域中访问本地变量相同的规则;变量必须被当作final并且在lambda表达式中不能够被修改。

    IntUnaryOperator makeAdder(int amount) {
        return (x) -> (x + amount); // Legal even though amount will go out of scope
                                    // because amount is not modified
    }
    
    IntUnaryOperator makeAccumulator(int value) {
        return (x) -> { value += x; return value; }; // Will not compile
    }
    

    如果以这种方式包含一个可改变的变量是必要的, 一个包含此变量的拷贝的合法对象应该被使用, Read more in Closures with lambda expressions.

    接收lambdas

    因为lambda是一个接口的实现,去使一个方法接收lambda并没有什么特别的要做:任何函数只要是函数式接口都能够接收一个lambda。

    public void passMeALambda(Foo1 f) {
        f.bar();
    }
    passMeALambda(() -> System.out.println("Lambda called"));
    

    使用lambda表达式去排序一个集合

    在java 8 之前, 当排序一个集合的时候, 用一个匿名(或着 有名字)类去实现java.util.Comparator接口是必要的:

    Java SE 1.2
    Collections.sort(
        personList,
        new Comparator<Person>() {
            public int compare(Person p1, Person p2){
                return p1.getFirstName().compareTo(p2.getFirstName());
            }
        }
    );
    

    从java 8 开始, 匿名内部类能够被lambda表达式替代, 注意到p1和p2参数能够被忽略, 因为编译器能够自动的推断出它们。

    Collections.sort(
        personList, 
        (p1, p2) -> p1.getFirstName().compareTo(p2.getFirstName())
    );
    

    这个例子能够被简化通过使用Comparator.comparing
    和method references(方法引用), 用::(双冒号)符号来表达:

    Collections.sort(
        personList,
        Comparator.comparing(Person::getFirstName)
    );
    

    静态导入允许我们更加简明的去表达它, 但是对于是否能够提高整体可读性是备受争论的。

    import static java.util.Collections.sort;
    import static java.util.Comparator.comparing;
    //...
    sort(personList, comparing(Person::getFirstName));
    

    Comparators构建这种方式可以用来链式调用。例如, 通过名字比较之后, 如果有一些人具有相同的名字, 那么thenComparing方法将会根据性别来接着比较

    sort(personList, comparing(Person::getFirstName).thenComparing(Person::getLastName));
    

    方法引用

    方法引用允许提前定义的静态或着实例方法去绑定到一个合适的函数式接口来当作参数传递,而不是用一个匿名的lambda表达式。
    假设我们有一个模型:

    class Person {
        private final String name;
        private final String surname;
    
        public Person(String name, String surname){
            this.name = name;
            this.surname = surname;
        }
    
        public String getName(){ return name; }
        public String getSurname(){ return surname; }
    }
    
    List<Person> people = getSomePeople();
    

    实例方法引用(对于一个任意的实例)

    people.stream().map(Person::getName)
    

    等价的lambda:

    people.stream().map(person -> person.getName())
    

    在这个例子中, 对于一个Person类的实例方法getName()的一个方法引用被传递。因为它被当作一个集合类型, 实例上的方法(之后被察觉)将会被调用 。

    实例方法引用(对于一个特定类型)

    people.forEach(System.out::println);
    

    因为System.out是一个PrintStream的实例,对这个特定的实例的一个方法引用被当作一个参数传递。等价的lambda表达式:

    people.forEach(person -> System.out.println(person));
    
    

    静态的方法引用

    对于转换流,我们能够使用静态方法引用

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
    numbers.stream().map(String::valueOf)
    

    这个例子传递了一个String类型的valueOf()静态方法引用, 因此valueOf()
    在集合中的实例对象中被当做一个参数传递。等价的lambda:

    numbers.stream().map(num -> String.valueOf(num))
    

    构造器引用

    List<String> strings = Arrays.asList("1", "2", "3");
    strings.stream().map(Integer::new)
    

    Collect Elements of a Stream into a Collection看看如何收集元素到集合中。唯一的一个Integer的String参数构造器在这里被使用, 通过一个被当作参数提供的String来构造一个整数,只要这个string代表一个数字, 流将会被转化为整数。等价的lambda:

    Collect Elements of a Stream into a Collection
    

    备忘单

    方法参考格式 代码 等价于
    Static method TypeName::method (args) -> TypeName.method(args)
    Non-static method (from instance) instance::method (args) -> instance.method(args
    Non-static method (no instance) TypeName::method (instance, args) -> instance.method(args)
    Constructor TypeName::new (args) -> new TypeName(args)

    实现多个接口

    有时候你想使lambda表达式实现多个接口,使用标记式接口(例如java.io.Serializable)是很有用的, 因为他们不添加任何抽象方法。例如你想使用一个客户自定义Comparator创建一个TreeSet, 接着序列化它, 并通过网路发送它 。一般方法:

    TreeSet<Long> ts = new TreeSet<>((x, y) -> Long.compare(y, x));
    

    并不生效, 因为对Comparator的lambda没有实现Serialization
    , 你能够修正它通过使用交叉类型, 并且显式的明确这个lambda是需要序列化的:

    TreeSet<Long> ts = new TreeSet<>(
        (Comparator<Long> & Serializable) (x, y) -> Long.compare(y, x));
    

    如果你平凡的使用交叉类型(例如, 你正在使用一个譬如几乎所有东西都必须序列化的Apache Spark 框架), 你能够创建一个空的接口并在你的代码中使用它们。

    public interface SerializableComparator extends Comparator<Long>, Serializable {}
    
    public class CustomTreeSet {
      public CustomTreeSet(SerializableComparator comparator) {}
    }
    

    这样你就保证了传递的comparator接口将会被序列化。

    lambda表达式闭包

    当一个lambda表达式引用一个封闭域(全局或局部)内的变量, 一个lambda闭包被创建。这样做的规则和内联方法以及匿名类是相同的。来自一个闭合域中的本地变量在一个lambda内部被使用时必须是final。在java 8 (最早的支持lambdas的版本)中不需要在外部上下文中声明final, 但是必须(当作final)来对待。例如:

    int n = 0; // With Java 8 there is no need to explicit final
    Runnable r = () -> { // Using lambda
        int i = n;
        // do something
    };
    

    只要值n变量没有被改变,它就是合法的。如果你尝试去在lambda的外部或内部去改变这个变量, 你将会得到下面的编译错误:

    “local variables referenced from a lambda expression must be final oreffectively final”.

    例如:如果在lambda里面必须使用一个可改变的变量, 正常的方法是声明一个对此变量的final拷贝,然后使用这个拷贝。例如:

    int n = 0;
    final int k = n; // With Java 8 there is no need to explicit final
    Runnable r = () -> { // Using lambda
        int i = k;
        // do something
    };
    n++;      // Now will not generate an error
    r.run();  // Will run with i = 0 because k was 0 when the lambda was created
    

    自然地, lambda的body体里面对原始变量的改变是不可见的。
    注意到java 不支持真正的闭包, 一个java lambda不能够以一种能够看到在它所被实例化的环境中的变量的改变的方式被创建 。如果你想实现一个能够对它的环境进行观察或做出改变的闭包, 你应该使用一个合法的类“聚集”它。例如:

    // Does not compile ...
    public IntUnaryOperator createAccumulator() {
        int value = 0;
        IntUnaryOperator accumulate = (x) -> { value += x; return value; };
        return accumulate;
    }
    

    以上将不会被编译,由于之前讨论的原因。我们能够绕过编译错误,如下:

    // Does not compile ...
    public IntUnaryOperator createAccumulator() {
        int value = 0;
        IntUnaryOperator accumulate = (x) -> { value += x; return value; };
        return accumulate;
    }
    

    这个问题是IntUnaryOperator接口设计契约的打破, 它声明实例应该是函数式的并且无状态的。如果一个闭包被传递进一个可以接收函数式对象的内置函数式接口, 这是很容易造成冲突和错误的行为。解封装的易变状态的闭包应当被实现为一个合法类。例如:

    // Correct ...
    public class Accumulator {
       private int value = 0;
    
       public int accumulate(int x) {
          value += x;
          return value;
       }
    }
    

    Lambda - Listener 示例

    匿名类listener

    JButton btn = new JButton("My Button");
    btn.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("Button was pressed");
        }
    });
    

    Lambda listener

    JButton btn = new JButton("My Button");
    btn.addActionListener(e -> {
        System.out.println("Button was pressed");
    });
    

    在你的函数式接口中使用Lambda

    Lambda 意味着为单个方法的接口提供一个内联实现代码和用一种常规变量的方式来传递它们的能力,正如我们所曾做的。我们把它叫做函数式接口。
    例如, 用一个匿名类来写一个Runnable, 然后启动一个线程,就像这样:

    //Old way
    new Thread(
            new Runnable(){
                public void run(){
                    System.out.println("run logic...");
                }
            }
    ).start();
    
    //lambdas, from Java 8
    new Thread(
            ()-> System.out.println("run logic...")
    ).start();
    

    现在, 和上面一致, 你们有一些客户端接口:

    interface TwoArgInterface {
        int operate(int a, int b);
    }
    

    在你的代码中你怎么使用Lambda去给出这个接口的实现? 和上述

    public class CustomLambda {
        public static void main(String[] args) {
    
            TwoArgInterface plusOperation = (a, b) -> a + b;
            TwoArgInterface divideOperation = (a,b)->{
                if (b==0) throw new IllegalArgumentException("Divisor can not be 0");
                return a/b;
            };
    
            System.out.println("Plus operation of 3 and 5 is: " + plusOperation.operate(3, 5));
            System.out.println("Divide operation 50 by 25 is: " + divideOperation.operate(50, 25));
    
        }
    }
    

    return 仅仅从Lambda中返回, 而不是外部方法

    当心这不同于Scala和Kotlin!

    void threeTimes(IntConsumer r) {
      for (int i = 0; i < 3; i++) {
        r.accept(i);
      }
    }
    
    void demo() {
      threeTimes(i -> {
        System.out.println(i);
        return; // Return from lambda to threeTimes only!
      });
    }
    

    当尝试用特有语言结构, 这会导致无法预期的异常, 譬如内置的结构:for
    循环return表现的不同:

    void demo2() {
      for (int i = 0; i < 3; i++) {
        System.out.println(i);
        return; // Return from 'demo2' entirely
      }
    }
    

    scala 和 Kotlin ,demo和demo2都仅仅打印0, 但这并不是始终如一的, java方法和refactoring和 类的使用是一致的 -return在代码的顶部和底部表现相同:

    void demo3() {
      threeTimes(new MyIntConsumer());
    }
    
    class MyIntConsumer implements IntConsumer {
      public void accept(int i) {
        System.out.println(i);
        return;
      }
    }
    

    因此, java 的return和类方法和refactoring更为一致, 但和内置的for、while不具一致性, 保留了它们的特殊性。由此, 解析来两个案例在java 中是等价的:

    IntStream.range(1, 4)
        .map(x -> x * x)
        .forEach(System.out::println);
    IntStream.range(1, 4)
        .map(x -> { return x * x; })
        .forEach(System.out::println);
    

    此外,try-with-resources的使用在java中是安全的:

    class Resource implements AutoCloseable {
      public void close() { System.out.println("close()"); }
    }
    
    void executeAround(Consumer<Resource> f) {
      try (Resource r = new Resource()) {
        System.out.print("before ");
        f.accept(r);
        System.out.print("after ");
      }
    }
    
    void demo4() {
      executeAround(r -> {
        System.out.print("accept() ");
        return; // Does not return from demo4, but frees the resource.
      });
    }
    

    将会打印before accept() after close()。 在Scala 和Kotlin 语义中try-with-resources将不会被关闭, 将仅仅打印出before accept()。

    Lambdas 和 执行-环绕模式

    在一些简单的场景中, 作为函数式接口, 有一些使用lambdas好的样例, 一个相对常见的能够被lambdas所增强的用例是被称为Execute-Around模式, 在这个模式中, 你有一组标准的setup/teardown 代码, 很多场景需要被用例特定的代码去环绕, 一些通用的示例就是file io , database io , try / catch 代码块。

    interface DataProcessor {
        void process( Connection connection ) throws SQLException;;
    }
    
    public void doProcessing( DataProcessor processor ) throws SQLException{
        try (Connection connection = DBUtil.getDatabaseConnection();) {
            processor.process(connection);
            connection.commit();
        } 
    }
    

    接着用lambda 来调用这个方法,看起来像下面这样:

    public static void updateMyDAO(MyVO vo) throws DatabaseException {
        doProcessing((Connection conn) -> MyDAO.update(conn, ObjectMapper.map(vo)));
    }
    

    它并不限于I/O操作, 它能够应用于和setup/tear down类似且变量较少的任务的任何场景。这种模式的主要好处是代码重用 和 强制DRY(Don’t Repeat Yourself)。

    传统方式 -> Lambda风格

    Traditional way

    interface MathOperation{
        boolean unaryOperation(int num);
    }
    
    public class LambdaTry {
        public static void main(String[] args) {
            MathOperation isEven = new MathOperation() {
                @Override
                public boolean unaryOperation(int num) {
                    return num%2 == 0;
                }
            };
    
            System.out.println(isEven.unaryOperation(25));
            System.out.println(isEven.unaryOperation(20));
        }
    }
    

    Lambda style

    1、移除类名和函数式接口体

    public class LambdaTry {
        public static void main(String[] args) {
            MathOperation isEven = (int num) -> {
                return num%2 == 0;
            };
    
            System.out.println(isEven.unaryOperation(25));
            System.out.println(isEven.unaryOperation(20));
        }
    }
    

    2、可选的类型的声明

    MathOperation isEven = (num) -> {
        return num%2 == 0;
    };
    

    3、可选的参数两边括弧, 如果是一个参数

    MathOperation isEven = num -> {
        return num%2 == 0;
    };
    

    4、可选的花括号, 如果在函数体中只有一行
    5、可选的返回值, 如果在函数体中只有一行

    MathOperation isEven = num -> num%2 == 0;
    

    Lambdas 与内存利用

    因为Java lambda是闭包的, 它们能够 “捕获” 在闭合作用域中变量的值, 然而并不是所有的lambda都能捕获 – 简单的lambdas 就像s -> s.length()
    什么都没有捕获, 被称作 无状态的 – 捕获形lambdas 要求一个临时的对象去持有这个被捕获的变量, 在这个代码片中, 这个lambda() -> j是一个捕获型lambda, 并且在被使用时可能造成一个对象被分配内存。

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 1000000000; i++) {
            int j = i;
            doSomethingWithLambda(() -> j);
        }
    }
    

    虽然并不会很快的变得显而易见, 因为new关键字并没有在任何地方出现, 但是这个代码创建了1000000000 个独立的 () -> j lambda实例。

    使用lambda条件表达式来从列表中获取某些值

    从java 8 开始, 你能够使用lambda 表达式 & predicates。例如: 使用lambda 表达式 & predicates 从列表中获取某个值, 在这个样例中, 如果他们具有大于18岁的事实就会被打印出来, 反之不会。
    Person Class:

    public class Person {
        private String name;
        private int age;
    
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public int getAge() { return age; }
        public String getName() { return name; }
    }
    

    内置的来自java.util.function.Predicate 包中的接口Predicate是一个函数式接口,并有一个boolean test(T t)
    方法。示例用法:

    import java.util.ArrayList;
    import java.util.List;
    import java.util.function.Predicate;
    
    public class LambdaExample {
        public static void main(String[] args) {
            List<Person> personList = new ArrayList<Person>();
            personList.add(new Person("Jeroen", 20));
            personList.add(new Person("Jack", 5));
            personList.add(new Person("Lisa", 19));
    
            print(personList, p -> p.getAge() >= 18);
        }
    
        private static void print(List<Person> personList, Predicate<Person> checker) {
            for (Person person : personList) {
                if (checker.test(person)) {
                    System.out.print(person + " matches your expression.");
                } else {
                    System.out.println(person  + " doesn't match your expression.");
                }
            }
        }
    }
    

    这个print(personList, p -> p.getAge() >= 18);方法采用一个lambda表达式(因为Predicate 被用于作为一个参数), 你能定义自己所需要的表达式, checker的test方法检查表达式正确与否:checker.test(person)。你可以轻易的去把它变成其他的, 例如print(personList, p -> p.getName().startsWith("J")); 它会检查人的名字是否以字母“J”开头。

    相关文章

      网友评论

      • 寄日当归:我就来看看有没有程序员缺个女朋友的,这里有只大三喵~想要一个程序员男朋友(原因:电脑白痴!!!优点:可爱)
        王永迪:@寄日当归 程序员都缺:disappointed_relieved:
      • 3098431114be:good good study, day day up.
        王永迪:@weex_d58d 嗯嗯 对滴
      • eb402464061b::blush:
        王永迪:@dsadasd_c63c :smile:

      本文标题:Java SE8 之 Lambda 表达式详解

      本文链接:https://www.haomeiwen.com/subject/gqtstxtx.html