【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);}