• lambda表达式,函数式接口和方法引用


    结论

    • 函数式接口是接口的某种特定形式
    • lambda表达式是函数式接口的具体实现
    • lambda表达式是某种特定形式的匿名类的语法糖
    • 方法引用是某种特定形式的lambda表达式的语法糖

    温故一下什么是匿名类

    比如我们有一个接口HelloWorld,可以对这个世界Say Hi。

    • HelloWorld.java
    package com.test;
    
    public interface HelloWorld {
        void greet();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    它有两个实现类,分别为英文版的EnglishHelloWorld和中文版的ChineseHelloWorld。

    • EnglishHelloWorld.java
    package com.test;
    
    public class EnglishHelloWorld implements HelloWorld {
    
        @Override
        public void greet() {
            System.out.println("Hello world!");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • ChineseHelloWorld.java
    package com.test;
    
    public class ChineseHelloWorld implements HelloWorld {
    
        @Override
        public void greet() {
            System.out.println("你好,世界!");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    主程序中有一个greet方法,以HelloWorld接口类型为参数。调用时,如果实际传入的是EnglishHelloWorld类型,则打印【Hello world!】,而如果实际传入的是ChineseHelloWorld类型,则打印【你好,世界!】。下面是我们通常的写法。

    • App.java
    package com.test;
    
    public class App {
    
        public static void greet(HelloWorld helloWorld) {
            helloWorld.greet();
        }
        public static void main(String[] args) {
            HelloWorld englishHelloWorld = new EnglishHelloWorld(); 
            HelloWorld chineseHelloWorld = new ChineseHelloWorld();
            greet(englishHelloWorld);
            greet(chineseHelloWorld);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    如果englishHelloWorld和chineseHelloWorld这两个变量不会再被其它地方使用,我们偶尔也会像下面这样写。

    • App.java
    package com.test;
    
    public class App {
    
        public static void greet(HelloWorld helloWorld) {
            helloWorld.greet();
        }
        public static void main(String[] args) {
            greet(new EnglishHelloWorld());
            greet(new ChineseHelloWorld());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    而当我们觉得EnglishHelloWorld和ChineseHelloWorld这两个实现类实际上只会在App.java中用到,或者我们并不想让别人使用这两个实现类的时候,我们还有一个选择——匿名类。这样,我们的项目中只需保留App.java和HelloWorld.java。

    • App.java
    package com.test;
    
    public class App {
    
        public static void greet(HelloWorld helloWorld) {
            helloWorld.greet();
        }
        public static void main(String[] args) {
            greet(new HelloWorld() {
                @Override
                public void greet() {
                    System.out.println("Hello world!");
                }
            });
            greet(new HelloWorld() {
                @Override
                public void greet() {
                    System.out.println("你好,世界!");
                }
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    从匿名类到lambda表达式

    实际上,lambda表达式是对匿名类的进一步简化。lambda表达式既可以作为值复制给变量再使用,也可以直接作为方法参数使用。当然,通常直接作为方法参数使用。例子如下。

    • 作为值复制给变量再使用
    package com.test;
    
    public class App {
    
        public static void greet(HelloWorld helloWorld) {
            helloWorld.greet();
        }
        public static void main(String[] args) {
            HelloWorld englishHelloWorld = () -> System.out.println("Hello world!");
            HelloWorld chineseHelloWorld = () -> System.out.println("你好,世界!");
            greet(englishHelloWorld);
            greet(chineseHelloWorld);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 直接作为方法参数使用
    package com.test;
    
    public class App {
    
        public static void greet(HelloWorld helloWorld) {
            helloWorld.greet();
        }
        public static void main(String[] args) {
            greet(() -> System.out.println("Hello world!"));
            greet(() -> System.out.println("你好,世界!"));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    有些人刚接触lambda表达式时有一个困惑,为什么调用一下函数式接口中的方法,lambda表达式就执行了。实际上知道它和匿名类的关联后,我们可以把它当作是匿名类的语法糖,() -> System.out.println(“Hello world!”)实际上就是上面的匿名类去掉了类声明和方法声明只留下参数和方法体。换句话说,上面的例子中,lambda表达式中箭头后面的方法体就是HelloWorld接口的匿名实现类的greet方法的方法体,lambda表达式的箭头前面的参数就是HelloWorld接口的匿名实现类的greet方法的参数。

    函数式接口来了

    函数式接口指的是只有一个抽象方法的接口,上面例子中的HelloWorld接口就是一个函数式接口。函数式接口和普通的接口没有太大差别,只不过只有一个抽象方法而已,但是只有这种特定形式的接口才可以以lambda表达式作为实现。此处需要留意的是,函数式接口仍然可以以普通类和匿名类作为自己的实现。下面的例子中,英文版的HelloWorld以匿名类的形式实现,中文版的HelloWorld以lambda表达式的形式实现。

    package com.test;
    
    public class App {
    
        public static void greet(HelloWorld helloWorld) {
            helloWorld.greet();
        }
        public static void main(String[] args) {
            greet(new HelloWorld() {
                @Override
                public void greet() {
                    System.out.println("Hello world!");
                }
            });
            greet(() -> System.out.println("你好,世界!"));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    而函数式接口上面的@FunctionalInterface注解只是一个标记,没有它lambda表达式也可以正常执行。只不过说有了它方便开发者之间的无声沟通,有了它在我们编写函数式接口的过程中IDE就能判断我们是否遵循了函数式接口的规范并在违反时给出提示,有了它在我们编译包含函数式接口的项目时编译器就能判断我们是否遵循了函数式接口的规范并在违反时给出警告。一切的一切,都是为了把原本的运行时错误尽可能地前置到开发阶段,前置到编译阶段。因此,这个注解的作用本质上和前面例子中@Override一样。

    package com.test;
    
    @FunctionalInterface
    public interface HelloWorld {
        void greet();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    相比于lambda表达式更进一步的方法引用

    lambda表达式还不是简化的终点。有些使用场景下,lambda表达式的函数体的内容仅仅是调用一下某个对象或者某个类的一个方法。这种情况下,lambda表达式可以进一步简化为方法引用。

    比如,我们有一个POJO类Person,它有name和age两个属性,以及相应的getter,setter和toString方法。

    • Person.java
    package com.test;
    
    public class Person {
        private String name;
        private Integer age;
        public Person() {
        }
        public Person(String name, Integer age) {
            this.name = name;
            this.age = age;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public Integer getAge() {
            return age;
        }
        public void setAge(Integer age) {
            this.age = age;
        }
        @Override
        public String toString() {
            return "Person [name=" + name + ", age=" + age + "]";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    由于从业务上来讲,有可能按年龄排序,也有可能按姓名首字母排序,我们不想在排序规则变化时修改POJO,所以我们并不会直接让Person类实现Comparable接口,而是提供一个外置比较器。在调用集合工具类的排序方法实际进行排序时,在以匿名类的形式实现的比较器中灵活地调用我们自己写的比较逻辑。

    • PersonComparator.java
    package com.test;
    
    public class PersonComparator {
    
        /**
         * 按年龄比较大小
         * 
         * @param o1 比较对象1
         * @param o2 比较对象2
         * @return 比较结果
         */
        public static int compareByAge(Person o1, Person o2) {
            return o1.getAge() - o2.getAge();
        }
    
        /**
         * 按姓名首字母比较大小
         * 
         * @param o1 比较对象1
         * @param o2 比较对象2
         * @return 比较结果
         */
        public static int compareByName(Person p1, Person p2) {
            return p1.getName().compareTo(p2.getName());
        }  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • App.java
    package com.test;
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.Comparator;
    import java.util.List;
    
    public class App {
        public static void main(String[] args) {
            Person tom = new Person("tom", 19);
            Person jack = new Person("Jack", 18);
            List<Person> persons = new ArrayList<>();
            persons.add(tom);
            persons.add(jack);
            Collections.sort(persons, new Comparator<Person>() {
                @Override
                public int compare(Person o1, Person o2) {
                    return PersonComparator.compareByAge(o1, o2);
                }
            });
            for (Person person : persons) {
                System.out.println(person);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    实际上,java.util.Comparator也是只有一个抽象方法的接口,所以它也是函数式接口,那么我们可以用lambda表达式来实现。

    package com.test;
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    
    public class App {
        public static void main(String[] args) {
            Person tom = new Person("tom", 19);
            Person jack = new Person("Jack", 18);
            List<Person> persons = new ArrayList<>();
            persons.add(tom);
            persons.add(jack);
            Collections.sort(persons, (p1, p2) -> PersonComparator.compareByAge(p1, p2));
            for (Person person : persons) {
                System.out.println(person);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这个地方,我们实际上只是在lambda表达式中调用一下PersonComparator的compareByAge方法以提供具体的比较逻辑。也就是说,lambda表达式的方法体只是一个单纯的方法调用,没有其它处理。那么这种lambda表达式可以简化为方法引用的写法。

    package com.test;
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    
    public class App {
        public static void main(String[] args) {
            Person tom = new Person("tom", 19);
            Person jack = new Person("Jack", 18);
            List<Person> persons = new ArrayList<>();
            persons.add(tom);
            persons.add(jack);
            Collections.sort(persons, PersonComparator::compareByAge);
            for (Person person : persons) {
                System.out.println(person);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    方法引用的4种形式

    方法引用有4种形式,

    • 引用某个类的某个静态方法
    • 引用某个对象的某个实例方法
    • 引用某个内置类型的任意对象的某个方法
    • 引用某个类的构造器

    引用某个类的某个静态方法

    实际上,上面按年龄比较Person对象的例子体现的正是这种形式,此处略过。

    引用某个对象的某个实例方法

    我们把Person类改动一下,新增一个introduceSelf方法,调用该方法时可以自报姓名和年龄。

    package com.test;
    
    public class Person {
        private String name;
        private Integer age;
        public Person() {
        }
        public Person(String name, Integer age) {
            this.name = name;
            this.age = age;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public Integer getAge() {
            return age;
        }
        public void setAge(Integer age) {
            this.age = age;
        }
        @Override
        public String toString() {
            return "Person [name=" + name + ", age=" + age + "]";
        }
        public void introduceSelf() {
            System.out.println(String.format("Hi, I'm %s and I'm %s years old.", this.name, this.age));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    Stream API中的forEach方法接收一个Consumer类型的函数式接口作为参数,关于Consumer后面我们会讲到,此处略过。重点是,forEach方法会循环调用我们提供的lambda表达式,每次把数据源中的下一个对象作为参数传递给lambda表达式。我们可以在lambda表达式中调用参数对象的introduceSelf方法,让每个Person对象做一遍自我介绍。同样由于lambda表达式中只是单纯调用一下参数中Person对象的introduceSelf方法,我们可以使用函数式接口的写法。这种情况就是引用某个对象的某个实例方法。

    package com.test;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class App {
        public static void main(String[] args) {
            List<Person> persons = new ArrayList<>();
            persons.add(new Person("tom", 19));
            persons.add(new Person("Jack", 18));
            persons.stream().forEach(Person::introduceSelf);
        }
    }
    // 输出结果:
    // Hi, I'm tom and I'm 19 years old.
    // Hi, I'm Jack and I'm 18 years old.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    引用某个内置类型的任意对象的某个方法

    Stream API中的map方法接收一个Function类型的函数式接口作为参数,关于Function后面我们会讲到,此处略过。重点是,map方法会循环调用我们提供的lambda表达式,每次把数据源中的下一个对象作为参数传递给lambda表达式,并获取lambda表达式的执行结果。最后,我们通过收集器把lambda表达式每一次的执行结果放到List中。此处,lambda表达式每一次被调用时,其参数为names中的当前要处理的字符串。我们只是想把每一个字符串转成其相应的小写形式,而String本身提供了toLowerCase方法可以实现我们的需求,所以我们可以直接使用当前处理的参数,也就是Java内置类型String的toLowerCase方法进行操作。这种情况就是引用某个内置类型的任意对象的某个方法。其实这个例子中的方法引用和上一个例子没太大区别,只不过这个例子中的参数的类型是Java内置的String类型,所以可以调用String的某个方法,而上一个例子中的参数的类型是我们自定义的Person类型,所以可以调用Person的introduceSelf方法。

    package com.test;
    
    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    
    public class App {
        public static void main(String[] args) {
            List<String> names = Arrays.asList("Tom", "Jack", "Mike");
            List<String> newNames = names.stream().map(String::toLowerCase).collect(Collectors.toList());
            for (String newName: newNames) {
                System.out.println(newName);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    引用某个类的构造器

    比如我们设计了一个通用的集合转换方法transferElements,给定一个原集合和目标集合,该方法可以把原集合中的每一个元素取出来,放到新创建的目标集合中。目标集合参数的类型我们设计为一个lambda表达式,在该表达式的方法体中,调用者可以自定义一些内容,不过,调用者也可以单纯地创建自己想要的目标集合。假如调用者只是单纯的想创建自己想要的目标集合,那么就可以使用引用某个类的构造器这种形式的方法引用。在下面的代码中,原集合是一个ArrayList,目标集合是一个HashSet,这种情况下,写成HashSet::new的形式即可,连HashSet后面的泛型声明都可以省略。

    package com.test;
    
    import java.util.Arrays;
    import java.util.Collection;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Set;
    import java.util.function.Supplier;
    
    public class App {
    
        public static <T, SOURCE extends Collection<T>, DEST extends Collection<T>> DEST transferElements(
            SOURCE sourceCollection,
            Supplier<DEST> collectionFactory
        ) {
            DEST result = collectionFactory.get();
            for (T t : sourceCollection) {
                result.add(t);
            }
            return result;
        }
        public static void main(String[] args) {
            List<String> names = Arrays.asList("Tom", "Jack", "Mike");
            Set<String> newNames = transferElements(names, HashSet::new);
            for (String newName : newNames) {
                System.out.println(newName);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    常用的内置函数式接口

    通过前面的说明,我们可以知道,函数式接口是只有一个抽象方法的接口,lambda表达式是函数式接口的实现,相当于函数式接口的匿名实现类的语法糖,而方法引用是lambda表达式在某些使用场景下的更进一步的简写形式,所以一切开始于函数式接口。我们来看一下函数式接口。

    比如,我们有一个函数式接口Greeting,它接收一个参数,没有返回值。

    • Greeting.java
    package com.test;
    
    @FunctionalInterface
    public interface Greeting<T> {
        void sayHi(T t);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    然后我们有一个greet方法,接收两个参数,第一个是对方的名字,定义了向谁打招呼。第二个是Greeting接口,定义了打招呼的方式,例子中只是简单地向对方说一句Hi。

    package com.test;
    
    public class App {
    
        public static void greet(String name, Greeting<String> greeting) {
            greeting.sayHi(name);
        }
    
        public static void main(String[] args) {
            greet("Tom", name -> System.out.println(String.format("Hi, %s", name)));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    再比如,我们有一个函数式接口RandomValue,它没有参数,但是有返回值。

    package com.test;
    
    @FunctionalInterface
    public interface RandomValue<T> {
        T generate();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    然后我们有一个getRandomNumber方法,以RandomValue接口为参数,以RandomValue接口的generate方法的返回值作为返回值。这个方法由调用者决定产生随机数的逻辑,例子中会产生一个1到10之间的随机正整数。

    package com.test;
    
    import java.util.Random;
    
    public class App {
    
        public static int getRandomNumber(RandomValue<Integer> random) {
            return random.generate();
        }
    
        public static void main(String[] args) {
            System.out.println(getRandomNumber(() -> new Random().nextInt(10)));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    再比如,我们有一个函数式接口Computer,它有一个参数,并且有返回值。

    package com.test;
    
    @FunctionalInterface
    public interface Computer<T, R> {
        R compute(T t);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    然后我们有一个analyze方法,接收两个参数,第一个是字符串,代表分析对象,第二个是Computer接口,代表分析方法。例子中调用analyze方法时,实际传入的是分析句子的长度。

    package com.test;
    
    public class App {
    
        public static int analyze(String sentence, Computer<String, Integer> computer) {
            return computer.compute(sentence);
        }
    
        public static void main(String[] args) {
            System.out.println(
                String.format(
                    "The length of this sentence is %d", 
                    analyze(
                        "Hello World!", 
                        String::length
                    )
                )
            );
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    实际上,上面例子中的3个函数式接口非常通用。只要我们需要一个只接受一个参数,没有返回值的类型,我们就可以写一个Greeting那样的函数式接口,只要我们需要没有参数,只有返回值的类型,我们就可以写一个RandomValue那样的函数式接口,只要我们需要一个既接收一个参数,又有返回值的类型,我们就可以写一个Computer那样的函数式接口。既然如此,我们何不为每种类型创建一个通用的函数式接口呢?

    比如把Greeting这种类型设计为Consumer

    package com.test;
    
    @FunctionalInterface
    public interface Consumer<T> {
        void accept(T t);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    那么可以把greet方法修改成下面这样。以后再有同样类型的函数式接口的需求,都可以来使用Consumer。

    package com.test;
    
    public class App {
    
        public static void greet(String name, Consumer consumer) {
            consumer.accept(name);
        }
    
        public static void main(String[] args) {
            greet("Tom", name -> System.out.println(String.format("Hi, %s", name)));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    再比如把RandomValue这种类型设计为Supplier

    package com.test;
    
    @FunctionalInterface
    public interface Supplier {
        T get();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    那么可以把getRandomNumber方法修改成下面这样。以后再有同样类型的函数式接口的需求,都可以来使用Supplier。

    package com.test;
    
    import java.util.Random;
    
    public class App {
    
        public static int getRandomNumber(Supplier supplier) {
            return supplier.get();
        }
    
        public static void main(String[] args) {
            System.out.println(getRandomNumber(() -> new Random().nextInt(10)));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    再比如把Computer这种类型设计为Function

    package com.test;
    
    @FunctionalInterface
    public interface Function<T, R> {
        R apply(T t);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    那么可以把analyze方法修改成下面这样。以后再有同样类型的函数式接口的需求,都可以来使用Function。

    package com.test;
    
    public class App {
    
        public static int analyze(String sentence, Function<String, Integer> function) {
            return function.apply(sentence);
        }
    
        public static void main(String[] args) {
            System.out.println(
                String.format(
                    "The length of this sentence is %d", 
                    analyze(
                        "Hello World!", 
                        String::length
                    )
                )
            );
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    既然我们都想到了这一点,Java官方早就为我们考虑到了,所以Java内置了很多常用的函数式接口,不需要我们再单独定义了,拿来使用即可。如果官方提供的函数式接口不满足我们的需求,我们再自己定义即可。想必看了上面的那些例子,我们对函数式接口的使用不再陌生,只是想知道Java官方提供了哪些常用的函数式接口。下面列出了最常用的几个函数式接口,请参考java.util.function包以查看所有预定义的函数式接口。

    函数式接口说明
    java.util.function.Consumer有一个参数,没有返回值
    java.util.function.Supplier没有参数,有返回值
    java.util.function.Function有一个参数,有返回值
    java.util.function.Predicate有一个参数,有返回值,返回值为boolean类型

    lambda表达式在Stream API中的应用

    函数式接口和lambda表达式比较适合做数据处理和事件处理,我们主要看一下四大常用内置函数式接口在Stream API中的使用。

    java.util.function.Consumer

    Stream API的forEach方法的参数为函数式接口Consumer,它会从数据源中逐个获取数据并调用lambda表达式,将获取到的数据作为lambda表达式的参数。下面是一个查看SpringBoot的IOC容器中的所有Bean定义的例子。

    package com.test;
    
    import java.util.Arrays;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.ApplicationContext;
    
    @SpringBootApplication
    public class App {
        public static void main(String[] args) {
            ApplicationContext context = SpringApplication.run(App.class, args);
            Arrays.asList(context.getBeanDefinitionNames()).stream().forEach(System.out::println);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    java.util.function.Supplier

    Stream API的静态方法generate的参数为函数式接口Supplier,它获取lambda表达式的执行结果。下面是一个生成10个随机整数的例子。

    package com.test;
    
    import java.util.Random;
    import java.util.stream.Stream;
    
    public class App {
    
        public static void main(String[] args) {
            Random random = new Random();
            Stream.generate(random::nextInt).limit(10).forEach(System.out::println);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    java.util.function.Function

    Stream API的map方法的参数为函数式接口Function,它会从数据源中逐个获取数据并调用lambda表达式,将获取到的数据作为lambda表达式的参数,并且获取lambda表达式的执行结果。下面的例子中,如果学生成绩及格,则给其一个及格标注。

    • Student.java
    package com.test;
    
    public class Student {
        private String name;
        private Integer score;
        private Boolean passed = Boolean.FALSE;
        public Student() {
        }
        public Student(String name, Integer score) {
            this.name = name;
            this.score = score;
        }
        public Student(String name, Integer score, Boolean passed) {
            this.name = name;
            this.score = score;
            this.passed = passed;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public Integer getScore() {
            return score;
        }
        public void setScore(Integer score) {
            this.score = score;
        }
        public Boolean getPassed() {
            return passed;
        }
        public void setPassed(Boolean passed) {
            this.passed = passed;
        }
        @Override
        public String toString() {
            return "Student [name=" + name + ", score=" + score + ", passed=" + passed + "]";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • App.java
    package com.test;
    
    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    
    public class App {
    
        public static void main(String[] args) {
            List<Student> students = Arrays.asList(
                new Student("Tom", 71),
                new Student("Jack", 59),
                new Student("Mike", 99)
            );
            List<Student> mappedStudents = students.stream().map(student ->
                new Student(student.getName(), student.getScore(), student.getScore() >= 60)
            ).collect(Collectors.toList());
            mappedStudents.forEach(System.out::println);
        }
    }
    // 输出结果:
    // Student [name=Tom, score=71, passed=true]
    // Student [name=Jack, score=59, passed=false]
    // Student [name=Mike, score=99, passed=true]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    java.util.function.Predicate

    Stream API的filter方法的参数为函数式接口Predicate,通过返回值是否为true来判断是否满足过滤条件。下面是一个查看成绩及格的学生的例子。

    • Student.java
    package com.test;
    
    public class Student {
        private String name;
        private Integer score;
        public Student() {
        }
        public Student(String name, Integer score) {
            this.name = name;
            this.score = score;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public Integer getScore() {
            return score;
        }
        public void setScore(Integer score) {
            this.score = score;
        }
        @Override
        public String toString() {
            return "Student [name=" + name + ", score=" + score + "]";
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • App.java
    package com.test;
    
    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    
    public class App {
    
        public static void main(String[] args) {
            List<Student> students = Arrays.asList(
                new Student("Tom", 71),
                new Student("Jack", 59),
                new Student("Mike", 99)
            );
            List<Student> filteredStudents = students.stream().filter(student -> student.getScore() >= 60).collect(Collectors.toList());
            filteredStudents.forEach(System.out::println);
        }
    }
    // 输出结果:
    // Student [name=Tom, score=71]
    // Student [name=Mike, score=99]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    lambda表达式在Spring框架中的应用

    Spring框架中大量使用了lambda表达式。比如,Spring框架其实有Servlet和Reactive两种实现版本,前者为经典的多线程框架,后者为异步框架。Spring Boot项目启动时,它会根据当前的应用环境是Servlet还是Reactive去创建相应的应用环境对象,把环境信息封装到该对象中。在早期的Spring Boot版本中,获取到当前应用环境类型后,通过switch表达式匹配相应的应用环境类型,进而通过new创建相应的应用环境对象。

    private ConfigurableEnvironment getOrCreateEnvironment() {
        if (this.environment != null) {
            return this.environment;
        }
        switch (this.webApplicationType) {
        case SERVLET:
            return new ApplicationServletEnvironment();
        case REACTIVE:
            return new ApplicationReactiveWebEnvironment();
        default:
            return new ApplicationEnvironment();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    而在3.0.0版本中,Spring Boot借助spring-boot-x.y.z.jar的META-INF/spring.factories文件,在该文件中定义ApplicationContextFactory的可取值为ReactiveWebServerApplicationContextFactory和ServletWebServerApplicationContextFactory。

    • spring.factories
    # Application Context Factories
    org.springframework.boot.ApplicationContextFactory=\
    org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContextFactory,\
    org.springframework.boot.web.servlet.context.ServletWebServerApplicationContextFactory
    
    • 1
    • 2
    • 3
    • 4

    取消switch表达式,改为从spring.factories中读取ApplicationContextFactory的实现类列表,然后遍历该列表,如果当前实现类支持当前应用环境类型,则返回相应的应用环境对象。例子中的BiFunction也是Java内置的函数式接口,它接收两个参数,有返回值。

    • DefaultApplicationContextFactory.createEnvironment
    @Override
    public ConfigurableEnvironment createEnvironment(WebApplicationType webApplicationType) {
        return getFromSpringFactories(webApplicationType, ApplicationContextFactory::createEnvironment, null);
    }
    
    • 1
    • 2
    • 3
    • 4
    • DefaultApplicationContextFactory.getFromSpringFactories
    private <T> T getFromSpringFactories(WebApplicationType webApplicationType,
            BiFunction<ApplicationContextFactory, WebApplicationType, T> action, Supplier<T> defaultResult) {
        for (ApplicationContextFactory candidate : SpringFactoriesLoader.loadFactories(ApplicationContextFactory.class,
                getClass().getClassLoader())) {
            T result = action.apply(candidate, webApplicationType);
            // 如果webApplicationType为Servlet,则只有遍历到ServletWebServerApplicationContextFactory时result才有值
            if (result != null) {
                return result;
            }
        }
        return (defaultResult != null) ? defaultResult.get() : null;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    所谓支持当前应用环境类型,对于ServletWebServerApplicationContextFactory来说,指的是如果应用环境类型为Servlet,则返回通过new创建的ApplicationServletEnvironment对象。

    • ServletWebServerApplicationContextFactory
    @Override
    public ConfigurableEnvironment createEnvironment(WebApplicationType webApplicationType) {
        return (webApplicationType != WebApplicationType.SERVLET) ? null : new ApplicationServletEnvironment();
    }
    
    • 1
    • 2
    • 3
    • 4

    而对于ReactiveWebServerApplicationContextFactory来说,指的是如果应用环境类型为Reactive,则返回通过new创建的ApplicationReactiveEnvironment对象。

    • ReactiveWebServerApplicationContextFactory
    @Override
    public ConfigurableEnvironment createEnvironment(WebApplicationType webApplicationType) {
        return (webApplicationType != WebApplicationType.REACTIVE) ? null : new ApplicationReactiveWebEnvironment();
    }
    
    • 1
    • 2
    • 3
    • 4

    乍一看,通过lambda表达式实现的版本比switch的版本要显得复杂,但是它也有好处。通过lambda表达式实现的版本中,要创建的应用环境对象的类型被定义到了外部属性文件中,便于维护,而且创建应用环境对象的方式被委托给了调用者,便于扩展。虽然此处只是单纯地通过new的方式创建了ApplicationServletEnvironment,但是如果以后有需要,ServletWebServerApplicationContextFactory可以对createEnvironment方法进行定制处理

    其它

    在函数式编程中,函数是一等公民,也就是说,函数可以作为值赋值给变量,可以作为参数传递给方法,也可以作为方法的返回值,和基本数据类型所能进行的操作几乎别无二致,是该编程语言世界中具有正常国民待遇的实体。在Java8之前,这一待遇由类享有,函数,Java中称为方法,只是类中用来描述某些行为的一段代码的集合,并不能脱离类单独使用。从Java8开始,函数获得了解放,虽然距离像JS那样自由奔放的使用方式还很遥远,但是一定程度上弥补了Java面向对象的不足。编程语言,尤其是以面向对象为主打的高级编程语言,在机器友好和人类友好之间选择了倾向于后者,所以它会不遗余力地模仿人类所处的现实世界。但是,现实世界并非全都是实体对象,也有过程,比如小学生正在作业本上进行的除法和求和,比如以加速度砸向牛顿脑门的那颗苹果的运动轨迹,比如看似重复其实每次都有些许变化的节拍。以实体对象的形式来容纳和描述这些过程并无不可,但是当计算非常庞杂,或者哪怕不庞杂,在这些场合下,相比于面向对象,面向过程的函数式编程显而易见地更加优雅与灵活。

    参考

  • 相关阅读:
    如何转换图片格式?建议收藏这两个方法
    实验23:lcd1602实验
    VUE~富文本简单使用~tinymce
    CSS之浮动Float
    Codeforces Round #833 (Div. 2) B. Diverse Substrings
    EntityFrameworkCore 模型自动更新(下)
    使用Python脚本实现 将多个文件夹下的文件转移到同一文件夹下并重新命名
    android P MediaCodec编解码流程分析
    Python如何判断当前时间是否为夏令时?
    Linux思维导图
  • 原文地址:https://blog.csdn.net/guangtouhan/article/details/133435666