> 文档中心 > 【Java基础系列教程】第二十四章 JDK8新特性_Lambda表达式、函数式接口、方法引用和构造器引用

【Java基础系列教程】第二十四章 JDK8新特性_Lambda表达式、函数式接口、方法引用和构造器引用


一、Lambda 表达式

1.1 Lambda 表达式简介

Lambda 表达式是 JDK8 的一个新特性,可以取代大部分的匿名内部类,写出更优雅的 Java 代码,尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构。

JDK 也提供了大量的内置函数式接口供我们使用,使得 Lambda 表达式的运用更加方便、高效。

Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。

Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。

使用 Lambda 表达式可以使代码变的更加简洁紧凑。

在JDK8之前,一个方法能接受的参数都是变量,例如: object.method(Object o);那么,如果需要传入一个动作呢?比如回调,那么你可能会想到匿名内部类。

例如:匿名内部类是需要依赖接口的,所以需要先定义个接口;

public interface PersonCallBack {    void callBack(Person person);}

Person类

public class Person {    private int id;    private String name;    public Person(int id, String name) { this.id = id; this.name = name;    }    public int getId() { return id;    }    public void setId(int id) { this.id = id;    }    public String getName() { return name;    }    public void setName(String name) { this.name = name;    }    /**     * 创建一个Person后进行回调     *     * @param id     * @param name     * @param personCallBack     */    public static void create(Integer id, String name, PersonCallBack personCallBack) { Person person = new Person(id, name); personCallBack.callBack(person);    }}

调用方法:

public class Test {    public static void main(String[] args) { // 调用方法,传入回调类,传统方式,使用匿名内部类 Person.create(1, "zhangsan", new PersonCallBack() {     @Override     public void callBack(Person person) {  System.out.println("callback -- " + person.getName());     } });    }}

    上面的PersonCallBack其实就是一种动作,但是我们真正关心的只有callback方法里的内容而已,我们用Lambda表示,可以将上面的代码就可以优化成:

public class Test {    public static void main(String[] args) { //调用方法,传入回调类,传统方式,使用匿名内部类 Person.create(1, "zhangsan", new PersonCallBack() {     @Override     public void callBack(Person person) {  System.out.println("callback -- " + person.getName());     } }); //使用lambda表达式实现 Person.create(2, "lisi", (person) -> {     System.out.println("lambda callback -- " + person.getName()); }); //进一步简化: 这归功于Java8的类型推导机制。因为现在接口里只有一个方法,那么现在这个Lambda表达式肯定是对应实现了这个方法,既然是唯一的对应关系,那么入参肯定是Person类,所以可以简写, //并且方法体只有唯一的一条语句,所以也可以简写,以达到表达式简洁的效果。 Person.create(3, "wangwu", person ->  System.out.println("lambda callback -- " + person.getName()) );    }}

    Lambda允许把函数作为一个方法的参数,一个lambda由用逗号分隔的参数列表、–> 符号、函数体三部分表示。

    一个Lambda表达式实现了接口里的有且仅有的唯一一个抽象方法。那么对于这种接口就叫做函数式接口。

    Lambda表达式其实完成了实现接口并且实现接口里的方法这一功能,也可以认为Lambda表达式代表一种动作,我们可以直接把这种特殊的动作进行传递。    

1.2 为什么使用Lambda表达式

    首先说一下为什么要使用Lambda表达式?

    在回答这个问题之前我先举个例子:在这里有一个员工的集合,员工类也就是id、age、name、salary这几个属性,无参,有参的构造函数,然后实现他们setter和getter方法。

员工类:

public class Employee {    private Integer id;    private String name;    private Integer age;    private Double salary;    public Employee() {    }    public Employee(Integer id, String name, Integer age, Double salary) { super(); this.id = id; this.name = name; this.age = age; this.salary = salary;    }    public Integer getId() { return id;    }    public void setId(Integer id) { this.id = id;    }    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;    }    public Double getSalary() { return salary;    }    public void setSalary(Double salary) { this.salary = salary;    }    @Override    public String toString() { return "Employee{" +  "id=" + id +  ", name='" + name + '\'' +  ", age=" + age +  ", salary=" + salary +  '}';    }}

存储员工的集合:

import java.util.Arrays;import java.util.List;public class EmployeeTest {    public static void main(String[] args) { //存储员工的集合 List emps = Arrays.asList(  new Employee(101, "张三", 18, 9999d),  new Employee(102, "李四", 59, 6666d),  new Employee(103, "王五", 18, 3333d),  new Employee(104, "赵六", 8, 7777d),  new Employee(105, "田七", 28, 5555d) );    }}

现在有这么一个需求,就是获取员工集合中年龄大于20岁的员工的集合,在之前我们大部分我想应该都是这样写的吧:

public static List filterEmployeeAge(List emps) {    List list = new ArrayList();    for (Employee emp : emps) { if (emp.getAge() > 20) {     list.add(emp); }    }    return list;}

    ok,这样是能很完美的解决问题,很符合现实的逻辑,但是现在我们对这段代码进行优化,在优化的过程中会慢慢演变到主题Lambda的使用。

1.2.1 优化方式一:策略模式

创建一个泛型接口,接口里面只有一个方法。

public interface MyPredicate {    boolean test(T t);}

再写一个接口的实现类:

public class FilterEmployeeForAge implements MyPredicate {    @Override    public boolean test(Employee t) { return t.getAge() > 20;    }}

接下来就是优化的关键代码:

public static List filterEmployee(List emps, MyPredicate mp) {    List list = new ArrayList();    for (Employee employee : emps) { if (mp.test(employee)) {     list.add(employee); }    }    return list;}

测试代码:

List employees = filterEmployee(emps, new FilterEmployeeForAge());for (Employee employee : list) {    System.out.println(employee);} 

    上面的测试代码的关键方法是 filterEmployee ,通过传入的员工列表和你过滤的方式 ,而FilterEmployeeForAge 是 MyPredicate 的实现类,在 filterEmployee 中对每个员工进行判断,然后决定是否要加入到集合中。

    如果你下次判断的不是age而是salary,就可以另外写一个MyPredicate的实现类,实现你自己想要的过滤的方式就可以了。

    其实上面的优化方案就是设计模式中的策略设计模式。

    上面是优化方式一的实现的一个思路,现在我在说一下第二种优化方案:

1.2.2 优化方式二:匿名内部类

List list = filterEmployee(emps, new MyPredicate() {    @Override    public boolean test(Employee t) { return t.getAge() > 20;    }});for (Employee employee : list) {    System.out.println(employee);}

    通过匿名内部类就不需要在去写接口的实现类了,看到这里,小伙伴们是不是着急了,想看看我们表达式怎么用呢?好的,接下来我就说一下Lambda表达式怎么使用。

1.2.3 优化方式三:Lambda表达式

List list = filterEmployee(emps, t -> t.getAge() > 20);list.forEach(System.out::println);

    这样代码是不是很简洁。

1.2.4 教你看懂System.out::println

    在不经意间, 我们会看到这样的代码。

List list = new ArrayList();list.add("a");list.add("b");list.add("c");list.forEach(System.out::println);

    第一印象, 哇, 好高大上的写法, 那么这究竟是怎样的一种语法呢。

    其中list.forEach可以改写成以下代码:

for (int i = 0; i < list.size(); i++) {    System.out.println(list.get(i));}//或者等于以下代码:for (String s : list) {    System.out.println(s);}

    我们来看forEach方法的源码:

default void forEach(Consumer action) {    Objects.requireNonNull(action);    for (T t : this) { action.accept(t);    }}

    其实它本质上是调用了for循环,它的参数的是一个Consumer对象,在for循环中调用了对象的accept方法,注意该方法是接口的default方法,是JDK8新增的语法。

    至于为什么可以写成System.out::println,这种语法叫做方法引用。该功能特性也是JDK8以后引入的,你可以把它看做Lambda表达式的语法糖。如果我们不这样写的话,我们可以用Lambda表达式改写成以下代码:

list.forEach((t) -> System.out.println(t));

如果还不明白的话,也可以这样:

list.forEach((String t) -> System.out.println(t));

    这样的效果跟System.out::println是一样的。

1.3 Lambda 表达式的基础语法

    Lambda 表达式的基础语法:Java8中引入了一个新的操作符 "->" 该操作符称为箭头操作符 或 Lambda 操作符,箭头操作符将 Lambda 表达式拆分成两部分:
        左侧:Lambda 表达式的参数列表;
        右侧:Lambda 表达式中所需执行的功能,即 Lambda 体;

    我们可以这样去进行比对,Lambda表达式是用来替换匿名内部类(基于接口,函数式接口- 只要一个抽象方法),Lambda 表达式的参数列表其实就是函数式接口抽象方法定义的参数个数;那么右侧Lambda表达式所需执行的功能就是以前在抽象方法里面写的代码。

    对应的每种语法,这里都会给出一个例子,方便大家的理解。    

1.3.1 语法格式一

    无参数,无返回值。

    语法:
        () -> System.out.println("Hello Lambda!");

@Testpublic void test1() {    // jdk1.8之前,匿名内部类写法    Runnable r = new Runnable() { @Override public void run() {     System.out.println("Hello World!"); }    };    r.run();    System.out.println("-------------------------------");    // jdk1.8之后,Lambda写法    Runnable r1 = () -> System.out.println("Hello Lambda!");    r1.run();}

1.3.2 语法格式二

    有一个参数,并且无返回值。    

    语法:
        (x) -> System.out.println(x)

@Testpublic void test2() {    Consumer con1 = new Consumer() { @Override public void accept(String t) {     System.out.println(t); }    };    con1.accept("欢迎使用匿名内部类方式!");    System.out.println("-------------------------------");    Consumer con = (x) -> System.out.println(x);    con.accept("欢迎使用Lamdba表示式!");}

1.3.3 语法格式三

    若只有一个参数,小括号可以省略不写。

    语法:
        x -> System.out.println(x)

@Testpublic void test2(){    Consumer con = x -> System.out.println(x);    con.accept("欢迎使用Lamdba表示式!");}

1.3.4 语法格式四

    有两个以上的参数,有返回值,并且 Lambda 体中有多条语句。

    语法:
        (x, y) -> {
            System.out.println("多条语句 1");
            System.out.println("多条语句 2");
            return x + y;
        };

@Testpublic void test3() {    Comparator com1 = new Comparator() { @Override public int compare(Integer o1, Integer o2) {     // TODO Auto-generated method stub     return Integer.compare(o1, o2); }    };    int compare1 = com1.compare(10, 20);    System.out.println("使用匿名内部类方式比较:" + compare1);    System.out.println("-------------------------------");    Comparator com = (x, y) -> { System.out.println("函数式接口"); return Integer.compare(x, y);    };    int compare = com.compare(10, 20);    System.out.println("使用Lamdba表达式方式比较:" + compare);}

1.3.5 语法格式五

    若 Lambda 体中只有一条语句, return 和 大括号都可以省略不写。

    语法:
        (x, y) -> x > y ? x : y;

@Testpublic void test4(){    Comparator com = (x, y) -> Integer.compare(x, y);}

1.3.6 语法格式六

    Lambda 表达式的参数列表的数据类型可以省略不写,因为JVM编译器通过上下文推断出,数据类型,即“类型推断”。

    语法:
        (Integer x, Integer y) -> Integer.compare(x, y);
        等价于
        (x,y) -> Integer.compare(x,y)

ok,讲到这里语法就差不多了在这里了,如果掌握了这些我想应该能解决平时正常的需求,这里有一副对联:

    上联:左右遇一括号省
    下联:左侧推断类型省
    横批:能省则省

    这里讲一个大家需要注意的地方:Lambda 表达式需要“函数式接口”的支持。

二、函数式接口

    函数式接口:接口中只有一个抽象方法的接口,称为函数式接口。可以使用注解 @FunctionalInterface 修饰可以检查是否是函数式接口。

    JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在 java.util.function 包中被提供。接下来我将讲一下Java8中内置的核心的四大函数式接口。

2.1 接口一:消费型接口

    Consumer: java.util.function.Consumer 接口则正好与Supplier接口相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型决定。

    void accept(T t):Consumer 接口中包含抽象方法 void accept(T t),意为消费一个指定泛型的数据。

举个例子,转换大写并输出:

import java.util.function.Consumer;public class ConsumerTest {    public static void main(String[] args) { transformUppercase("Hello", new Consumer() {     @Override     public void accept(String s) {  System.out.println(s.toUpperCase());     } }); transformUppercase("Hello", s -> System.out.println(s.toUpperCase()));    }    public static void transformUppercase(String str, Consumer consumer) { consumer.accept(str);    }}

2.2 接口二:供给型接口

    Supplier: java.util.function.Supplier 接口则正好与Consumer接口相反,它是生产一个数据,而不是消费一个数据,其数据类型由泛型决定。
        
    T get():用来获取一个泛型参数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。

供给型就是外部给我们返回一个东西,看代码我相信你会更加的理解:

import java.util.ArrayList;import java.util.List;import java.util.function.Supplier;public class SupplierTest {    public static void main(String[] args) { List numsList1 = getNumsList(new Supplier() {     @Override     public Integer get() {  return (int) (Math.random() * 100);     } }); System.out.println(numsList1); List numsList2 = getNumsList(() -> (int) (Math.random() * 100)); System.out.println(numsList2);    }    // 需求: 获取一个集合,集合里面有十条数据,数据从供给型接口而来    public static List getNumsList(Supplier supplier) { List list = new ArrayList(); for (int i = 1; i <= 10; i++) {     list.add(supplier.get()); } return list;    }}

2.3 接口三:函数型接口

    Function: 函数型接口,接受一个输入参数,返回一个结果。 T,输入参数;R 返回结果;

    R apply(T t):将此函数应用于给定的参数。  

import java.text.SimpleDateFormat;import java.util.Date;import java.util.function.Function;public class FunctionTest {    public static void main(String[] args) { String s1 = dateToString(new Date(), new Function() {     @Override     public String apply(Date date) {  SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  return sdf.format(date);     } }); System.out.println(s1); String s2 = dateToString(new Date(), date -> {     SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");     return sdf.format(date); }); System.out.println(s2);    }    // 需求: 把日期类型转换为年月日时分秒字符串    public static String dateToString(Date date, Function function) { return function.apply(date);    }}

2.4 接口四:断言型接口

    Predicate: 断言式接口其参数是,也就是给一个参数T,返回boolean类型的结果。跟Function一样,Predicate的具体实现也是根据传入的lambda表达式来决定的。

    boolean test(T t):接受一个参数返回一个布尔类型。

import java.util.ArrayList;import java.util.Arrays;import java.util.List;import java.util.function.Predicate;public class PredicateTest {    public static void main(String[] args) { // 创建员工集合 List emps = Arrays.asList(  new Employee(101, "张三", 18, 9999d),  new Employee(102, "李四", 59, 6666d),  new Employee(103, "王五", 18, 3333d),  new Employee(104, "赵六", 8, 7777d),  new Employee(105, "田七", 28, 5555d) ); List employees1 = filterEmp(emps, new Predicate() {     @Override     public boolean test(Employee employee) {  return employee.getAge() > 20;     } }); System.out.println(employees1); List employees2 = filterEmp(emps, employee -> employee.getAge() > 20); System.out.println(employees2);    }    // 需求: 过滤年龄大于20的员工    public static List filterEmp(List emps, Predicate employeePredicate) { List filterEmps = new ArrayList(); for (Employee e : emps) {     if (employeePredicate.test(e)) {  filterEmps.add(e);     } } return filterEmps;    }}

三、方法引用和构造器引用

    什么是方法引用?方法引用是只需要使用方法的名字,而具体调用交给函数式接口,需要和Lambda表达式配合使用。    

List list = Arrays.asList("a", "b", "c");list.forEach(str -> System.out.print(str));list.forEach(System.out::print);

上面两种写法是等价的。

3.1 方法引用

    若 Lambda 体中的功能,已经有方法提供了实现,可以使用方法引用(可以将方法引用理解为 Lambda 表达式的另外一种表现形式)。

    引用的方式有以下几种:
        对象名 :: 实例方法名
        类名 :: 静态方法名
        类名 :: 实例方法名    

3.1.1 方法引用的第一种引用形式

@Testpublic void methodRefTest01() {    // 方法引用: 对象名 :: 实例方法名    // 创建一个员工对象    Employee emp = new Employee(1001, "张三", 22, 5800d);    // 供给型接口: 提供一个数据    /*Supplier sup = new Supplier() {    @Overridepublic String get() {return emp.getName();}};*/    Supplier sup = emp::getName;    System.out.println(sup.get());}

3.1.2 方法引用的第二种引用形式

@Testpublic void methodRefTest02() {    // 方法引用: 类名 :: 静态方法名    // 把字符串转换为Integer类型    /*Function function = new Function() {@Overridepublic Integer apply(String s) {return Integer.parseInt(s);}};*/    Function function = Integer::parseInt;    Integer apply = function.apply("120");    System.out.println("转换后的值加上1:" + (apply + 1));}

3.1.3 方法引用的第三种引用方式

@Testpublic void methodRefTest03() {    // 方法引用: 类名 :: 实例方法名    // BiPredicate 也是断言型接口,可以传递两个参数的断言型接口    /*BiPredicate biPredicate = new BiPredicate() { @Override public boolean test(String s, String s2) {     return s.equals(s2); }    };*/    BiPredicate biPredicate = String::equals;    boolean test = biPredicate.test("abc", "ABC");    System.out.println(test);}

以上就是方法引用的讲解,在这里给大家讲两个需要注意的地方:

    1、方法引用所引用的方法的参数列表与返回值类型,需要与函数式接口中抽象方法的参数列表和返回值类型保持一致!

    2、若Lambda 的参数列表的第一个参数,是实例方法的调用者,第二个参数(或无参)是实例方法的参数时,格式: ClassName::MethodName

看这个就能明白上面说的那两句话:

BiPredicate bp = (x, y) -> x.equals(y);BiPredicate bp2 = String::equals;System.out.println(bp2.test("abc", "abc"));

    函数式接口中抽象方法 boolean test(T t, U u);参数列表是t,u,在上面的代码中是x,y,返回的类型是是boolean类型,x是实例方法的调用者,x调用了equals方法,而y是实例方法的参数。

3.2 构造器引用

    构造器引用: 构造器的参数列表,需要与函数式接口中参数列表保持一致!

    类名 :: new

@Testpublic void constructorRefTest() {    // 构造器的参数列表,需要与函数式接口中参数列表保持一致!    // 类名 :: new    Supplier supplier1 = new Supplier() { @Override public Employee get() {     return new Employee(); }    };    Supplier supplier2 = Employee::new;    System.out.println(supplier2.get());}

    注:需要提供对应参数的构造函数。

3.3 数组引用

    类型[] :: new

@Testpublic void arrayRefTest() {    Function fun1 = new Function() { @Override public Employee[] apply(Integer integer) {     return new Employee[integer]; }    };    Function fun2 = Employee[]::new;    Employee[] emps = fun2.apply(20);    System.out.println(emps.length);}