java8新特性专题之一、为什么要使用lambda
写在前面
这是一道真实的面试题,一个读者朋友出去面试,面试官竟然问他这样一个问题:你说说Java8中为什么引入Lambda表达式?引入Lambda表达式后有哪些好处呢?还好这个朋友对Java8早有准备。不过,如果是看文章的你出去面试,面试官问你这样的问题,你是否也能轻松回答呢?
什么是Lambda表达式?
Lambda表达式是一个匿名函数,我们可以这样理解Lambda表达式:Lambda是一段可以传递的代码(能够做到将代码像数据一样进行传递)。使用Lambda表达式能够写出更加简洁、灵活的代码。并且,使用Lambda表达式能够使Java的语言表达能力得到提升。
匿名内部类
在介绍如何使用Lambda表达式之前,我们先来看看匿名内部类,例如,我们使用匿名内部类比较两个Integer类型数据的大小。
Comparator<Integer> com = new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return Integer.compare(o1, o2); } };
在上述代码中,我们使用匿名内部类实现了比较两个Integer类型数据的大小。
接下来,我们就可以将上述匿名内部类的实例作为参数,传递到其他方法中了,如下所示。
TreeSet<Integer> treeSet = new TreeSet<>(com);
完整的代码如下所示。
@Test public void test1(){ Comparator<Integer> com = new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return Integer.compare(o1, o2); } }; TreeSet<Integer> treeSet = new TreeSet<>(com); }
我们分析下上述代码,在整个匿名内部类中,实际上真正有用的就是下面一行代码。
return Integer.compare(o1, o2);
其他的代码本质上都是“冗余”的。但是为了书写上面的一行代码,我们不得不在匿名内部类中书写更多的代码。
Lambda表达式
如果使用Lambda表达式完成两个Integer类型数据的比较,我们该如何实现呢?
Comparator<Integer> com = (x, y) -> Integer.compare(x, y);
看到没,使用Lambda表达式,我们只需要使用一行代码就能够实现两个Integer类型数据的比较。
我们也可以将Lambda表达式传递到TreeSet的构造方法中,如下所示。
TreeSet<Integer> treeSet = new TreeSet<>((x, y) -> Integer.compare(x, y));
直观的感受就是使用Lambda表达式一行代码就能搞定匿名内部类多行代码的功能。
看到这,不少读者会问:我使用匿名内部类的方式实现比较两个整数类型的数据大小并不复杂啊!我为啥还要学习一种新的语法呢?
其实,我想说的是:上面咱们只是简单的列举了一个示例,接下来,咱们写一个稍微复杂一点的例子,来对比下使用匿名内部类与Lambda表达式哪种方式更加简洁。
对比常规方法和Lambda表达式
例如,现在有这样一个需求:获取当前公司中员工年龄大于30岁的员工信息。
首先,我们需要创建一个Employee实体类来存储员工的信息。
@Data @Builder @ToString @NoArgsConstructor @AllArgsConstructor public class Employee implements Serializable { private static final long serialVersionUID = -9079722457749166858L; private String name; private Integer age; private Double salary; }
在Employee中,我们简单存储了员工的姓名、年龄和薪资。
接下来,我们创建一个存储多个员工的List集合,如下所示。
protected List<Employee> employees = Arrays.asList( new Employee("张三", 18, 9999.99), new Employee("李四", 38, 5555.55), new Employee("王五", 60, 6666.66), new Employee("赵六", 16, 7777.77), new Employee("田七", 18, 3333.33) );
1.常规遍历集合
我们先使用常规遍历集合的方式来查找年龄大于等于30的员工信息。
public List<Employee> filterEmployeesByAge(List<Employee> list){ List<Employee> employees = new ArrayList<>(); for(Employee e : list){ if(e.getAge() >= 30){ employees.add(e); } } return employees; }
接下来,我们测试一下上面的方法。
@Test public void test3(){ List<Employee> employeeList = filterEmployeesByAge(this.employees); for (Employee e : employeeList){ System.out.println(e); } }
运行test3方法,输出信息如下所示。
Employee(name=李四, age=38, salary=5555.55)
Employee(name=王五, age=60, salary=6666.66)
总体来说,查找年龄大于或者等于30的员工信息,使用常规遍历集合的方式稍显复杂了。
例如,需求发生了变化:获取当前公司中员工工资大于或者等于5000的员工信息。
此时,我们不得不再次创建一个按照工资过滤的方法。
public List<Employee> filterEmployeesBySalary(List<Employee> list){ List<Employee> employees = new ArrayList<>(); for(Employee e : list){ if(e.getSalary() >= 5000){ employees.add(e); } } return employees; }
对比filterEmployeesByAge()方法和filterEmployeesBySalary方法后,我们发现,大部分的方法体是相同的,只是for循环中对于条件的判断不同。
如果此时我们再来一个需求,查找当前公司中年龄小于或者等于20的员工信息,那我们又要创建一个过滤方法了。 看来使用常规方法是真的不方便啊!
这里,问大家一个问题:对于这种常规方法最好的优化方式是啥?相信有不少小伙伴会说:将公用的方法抽取出来。没错,将公用的方法抽取出来是一种优化方式,但它不是最好的方式。最好的方式是啥?那就是使用 设计模式 啊!设计模式可是无数前辈不断实践而总结出的设计原则和设计模式。大家可以查看《设计模式汇总——你需要掌握的23种设计模式都在这儿了!》一文来学习设计模式专题。
2.使用设计模式优化代码
如何使用设计模式来优化上面的方法呢,大家继续往下看,对于设计模式不熟悉的同学可以先根据《设计模式汇总——你需要掌握的23种设计模式都在这儿了!》来学习。
我们先定义一个泛型接口MyPredicate,对传递过来的数据进行过滤,符合规则返回true,不符合规则返回false。
public interface MyPredicate<T> { /** * 对传递过来的T类型的数据进行过滤 * 符合规则返回true,不符合规则返回false */ boolean filter(T t); }
接下来,我们创建MyPredicate接口的实现类FilterEmployeeByAge来过滤年龄大于或者等于30的员工信息。
public class FilterEmployeeByAge implements MyPredicate<Employee> { @Override public boolean filter(Employee employee) { return employee.getAge() >= 30; } }
我们定义一个过滤员工信息的方法,此时传递的参数不仅有员工的信息集合,同时还有一个我们定义的接口实例,在遍历员工集合时将符合过滤条件的员工信息返回。
//优化方式一 public List<Employee> filterEmployee(List<Employee> list, MyPredicate<Employee> myPredicate){ List<Employee> employees = new ArrayList<>(); for(Employee e : list){ if(myPredicate.filter(e)){ employees.add(e); } } return employees; }
接下来,我们写一个测试方法来测试优化后的代码。
@Test public void test4(){ List<Employee> employeeList = this.filterEmployee(this.employees, new FilterEmployeeByAge()); for (Employee e : employeeList){ System.out.println(e); } }
运行test4()方法,输出的结果信息如下所示。
Employee(name=李四, age=38, salary=5555.55)
Employee(name=王五, age=60, salary=6666.66)
写到这里,大家是否有一种豁然开朗的感觉呢?
没错,这就是设计模式的魅力,对于设计模式不熟悉的小伙伴,一定要参照《设计模式汇总——你需要掌握的23种设计模式都在这儿了!》来学习。
我们继续获取当前公司中工资大于或者等于5000的员工信息,此时,我们只需要创建一个FilterEmployeeBySalary类实现MyPredicate接口,如下所示。
public class FilterEmployeeBySalary implements MyPredicate<Employee>{ @Override public boolean filter(Employee employee) { return employee.getSalary() >= 5000; } }
接下来,就可以直接写测试方法了,在测试方法中继续调用filterEmployee(List<Employee> list, MyPredicate<Employee> myPredicate)
方法。
@Test public void test5(){ List<Employee> employeeList = this.filterEmployee(this.employees, new FilterEmployeeBySalary()); for (Employee e : employeeList){ System.out.println(e); } }
运行test5方法,输出的结果信息如下所示。
Employee(name=张三, age=18, salary=9999.99) Employee(name=李四, age=38, salary=5555.55) Employee(name=王五, age=60, salary=6666.66) Employee(name=赵六, age=16, salary=7777.77)
可以看到,使用设计模式对代码进行优化后,无论过滤员工信息的需求如何变化,我们只需要创建MyPredicate接口的实现类来实现具体的过滤逻辑,然后在测试方法中调用filterEmployee(List<Employee> list, MyPredicate<Employee> myPredicate)
方法将员工集合和过滤规则传入即可。
这里,问大家一个问题:上面优化代码使用的设计模式是哪种设计模式呢?如果是你,你会想到使用设计模式来优化自己的代码吗?小伙伴们自己先思考一下到底使用的设计模式是什么?文末我会给出答案!
使用设计模式优化代码也有不好的地方:每次定义一个过滤策略的时候,我们都要单独创建一个过滤类!!
3.匿名内部类
那使用匿名内部类是不是能够优化我们书写的代码呢,接下来,我们就使用匿名内部类来实现对员工信息的过滤。先来看过滤年龄大于或者等于30的员工信息。
@Test public void test6(){ List<Employee> employeeList = this.filterEmployee(this.employees, new MyPredicate<Employee>() { @Override public boolean filter(Employee employee) { return employee.getAge() >= 30; } }); for (Employee e : employeeList){ System.out.println(e); } }
运行test6方法,输出如下结果信息。
Employee(name=李四, age=38, salary=5555.55)
Employee(name=王五, age=60, salary=6666.66)
再实现过滤工资大于或者等于5000的员工信息,如下所示。
@Test public void test7(){ List<Employee> employeeList = this.filterEmployee(this.employees, new MyPredicate<Employee>() { @Override public boolean filter(Employee employee) { return employee.getSalary() >= 5000; } }); for (Employee e : employeeList){ System.out.println(e); } }
运行test7方法,输出如下结果信息。
Employee(name=张三, age=18, salary=9999.99) Employee(name=李四, age=38, salary=5555.55) Employee(name=王五, age=60, salary=6666.66) Employee(name=赵六, age=16, salary=7777.77)
匿名内部类看起来比常规遍历集合的方法要简单些,并且将使用设计模式优化代码时,每次创建一个类来实现过滤规则写到了匿名内部类中,使得代码进一步简化了。
但是,使用匿名内部类代码的可读性不高,并且冗余代码也比较多!!
那还有没有更加简化的方式呢?
4.重头戏:Lambda表达式
在使用Lambda表达式时,我们还是要调用之前写的filterEmployee(List<Employee> list, MyPredicate<Employee> myPredicate)
方法。
注意看,获取年龄大于或者等于30的员工信息.
@Test public void test8(){ filterEmployee(this.employees, (e) -> e.getAge() >= 30).forEach(System.out::println); }
看到没,使用Lambda表达式只需要一行代码就完成了员工信息的过滤和输出。是不是很6呢。
运行test8方法,输出如下的结果信息。
Employee(name=李四, age=38, salary=5555.55)
Employee(name=王五, age=60, salary=6666.66)
再来看使用Lambda表达式来获取工资大于或者等于5000的员工信息,如下所示。
@Test public void test9(){ filterEmployee(this.employees, (e) -> e.getSalary() >= 5000).forEach(System.out::println); }
没错,使用Lambda表达式,又是一行代码就搞定了!!
运行test9方法,输出如下的结果信息。
Employee(name=张三, age=18, salary=9999.99) Employee(name=李四, age=38, salary=5555.55) Employee(name=王五, age=60, salary=6666.66) Employee(name=赵六, age=16, salary=7777.77)
另外,使用Lambda表达式时,只需要给出需要过滤的集合,我们就能够实现从集合中过滤指定规则的元素,并输出结果信息。
5.重头戏:Stream API
使用Lambda表达式结合Stream API,只要给出相应的集合,我们就可以完成对集合的各种过滤并输出结果信息。
例如,此时只要有一个employees
集合,我们使用Lambda表达式来获取工资大于或者等于5000的员工信息。
@Test public void test10(){ employees.stream().filter((e) -> e.getSalary() >= 5000).forEach(System.out::println); }
没错,只给出一个集合,使用Lambda表达式和Stream API,一行代码就能够过滤出想要的元素并进行输出。
运行test10方法,输出如下的结果信息。
Employee(name=张三, age=18, salary=9999.99) Employee(name=李四, age=38, salary=5555.55) Employee(name=王五, age=60, salary=6666.66) Employee(name=赵六, age=16, salary=7777.77)
如果我们只想要获取前两个员工的信息呢?其实也很简单,如下所示。
@Test public void test11(){ employees.stream().filter((e) -> e.getSalary() >= 5000).limit(2).forEach(System.out::println); }
可以看到,我们在代码中添加了limit(2)
来限制只获取两个员工信息。运行test11方法,输出如下的结果信息。
Employee(name=张三, age=18, salary=9999.99)
Employee(name=李四, age=38, salary=5555.55)
使用Lambda表达式和Stream API也可以获取指定的字段信息,例如获取工资大于或者等于5000的员工姓名。
@Test public void test12(){ employees.stream().filter((e) -> e.getSalary() >= 5000).map(Employee::getName).forEach(System.out::println); }
可以看到,使用map过滤出了工资大于或者等于5000的员工姓名。运行test12方法,输出如下的结果信息。
张三
李四
王五
赵六
是不是很简单呢?
最后,给出文中使用的设计模式:策略模式。
写在最后
如果觉得文章对你有点帮助,请微信搜索并关注「 冰河技术 」微信公众号,跟冰河学习Java8新特性。
最后,附上Java8新特性核心知识图,祝大家在学习Java8新特性时少走弯路。
写在前面:
在熟悉lambda表达式和方法引用(双冒号操作,后面一篇博文讲)后,我们将能够更加简洁的完成需求编码。
例如:按照苹果颜色完成对一个苹果集的排序:
//1、传统的已经够简化的匿名内部类方式 list.sort(new Comparator<Apple>() { @Override public int compare(Apple o1, Apple o2) { return o1.getColor().compareTo(o2.getColor()); } }); //2、使用lambda进一步简化 list.sort((o1,o2)->o1.getColor().compareTo(o2.getColor())); //2、使用方法引用终极简化 list.sort(Comparator.comparing(Apple::getColor));
上面只是一个很肤浅但直观的一种对lambda和方法引用的一种展现,详细的使用和原理学习还得往下看。
一、为什么使用lambda表达式?
现在我们通过《java8 in action》书中Chapter 2部分的2.1. Coping with changing requirements章节,作者提出的一个问题(如何处理多变的需求)来由浅入深的解释为什么要是用lambda。他给了需求背景:
大概意思就是说,现在你需要做一个农场库存管理的应用程序,农场主的有各种各样的处于变化的需求,你如更好的实现这些可能出现变化的需求,下面详细展开。
1、文中首先提出了一个最基础的需求1::在仓库里的众多苹果中,找出颜色为绿色的苹果
要实现这个需求,我们需要以下代码,:
- 1.1 Apple.java 这是一个苹果的bean,我实现了它的构造方法和to String方法(后面的需求举例皆需要用到这个bean):
package com.aigov.java8_newfeatures.lambda; import lombok.Data; /** * @author : aigoV * @date :2019/10/10 * apple **/ @Data public class Apple { private String color; private long weight; public Apple(String color, long weight) { this.color = color; this.weight = weight; } @Override public String toString() { return "Apple{" + "color='" + color + '\'' + ", weight='" + weight + '\'' + '}'; } }
- 1.2同时我们需要创建一个FilterApple.java 过滤器,我把所有实现的相关代码全写在这里面(之后的需求实现也在这个类里,只不过会去掉之前的代码):
package com.aigov.java8_newfeatures.lambda; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * @author : aigoV * @date :2019/10/10 * apple过滤器 * 根据农场主的特定需求找出特定苹果 **/ public class FilterApple { /** 需求1::在众多苹果中,找出颜色为绿色的苹果**/ public static List<Apple> findGreenApple(List<Apple> appleList){ List<Apple> list = new ArrayList<>(); for (Apple apple : appleList){ if ("green".equals(apple.getColor())){ list.add(apple); } } return list; } public static void main(String[] args) { /** 苹果库存集 **/ List<Apple> list = Arrays.asList( new Apple("green",150), new Apple("yellow",120), new Apple("green",170) ); // List<Apple> greenApples = findGreenApple(list); assert greenApples.size() == 2:"过滤后的集合size与预期不符"; //java assert(断言),基本的测试作用,须在idea设置启用。 System.out.print(greenApples); }
到这,这个简单的需求1就实现了。
2、需求2:需要应对随时变化的需求,比如:不找绿色了,找红色的苹果
这个需求比需求1要求多一点,但其实也很简单,想必大家想的和作者提供的方案一样(当然你也可能想的是再多加一个单独的找红色的方法,,),在过滤器的方法中加入一个color参数,就可以动态满足农场主的需求,代码见下:
package com.aigov.java8_newfeatures.lambda; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * @author : aigoV * @date :2019/10/10 * apple过滤器 * 根据农场主的特定需求找出特定苹果 **/ public class FilterApple { /** 需求2:需要应对随时变化的需求,比如:不找绿色了,找红色的苹果**/ public static List<Apple> findApple(List<Apple> appleList,String color){ List<Apple> list = new ArrayList<>(); for (Apple apple : appleList){ if (color.equals(apple.getColor())){ list.add(apple); } } return list; } public static void main(String[] args) { /** 苹果库存集 **/ List<Apple> list = Arrays.asList( new Apple("green",150), new Apple("yellow",120), new Apple("green",170) ); // List<Apple> redApples = findApple(list,"red"); System.out.print(redApples); }
3、需求3:不只是按颜色来找,需要按照重量,颜色去找(也许apple还会有其他属性,需求还会更多变,更复杂,总之你需要满足不停变化的各种需求)
在这里《java8 in action》的作者给我们提供一种解决方案,那就是使用策略模式去应对复杂多变的需求。代码实现:
package com.aigov.java8_newfeatures.lambda; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * @author : aigoV * @date :2019/10/10 * apple过滤器 * 根据农场主的特定需求找出特定苹果 **/ public class FilterApple { /** 需求3:不只是按颜色来找,需要按照重量,颜色去找(你需要满足不停变化的各种需求)**/ //这是抽象出来的过滤接口 public interface AppleFilter{ boolean filter(Apple apple); } //这是一个过滤方法 public static List<Apple> findApple(List<Apple> appleList,AppleFilter appleFilter){ List<Apple> list = new ArrayList<>(); for (Apple apple : appleList){ if (appleFilter.filter(apple)){ list.add(apple); } } return list; } //按照上面策略模式接口,现在需求是找到颜色为绿色 并且重量>=150的苹果 (这是具体策略的策略类,不同需求建不同策略类) public static class GreenAnd160weiFilter implements AppleFilter{ @Override public boolean filter(Apple apple) { return "green".equals(apple.getColor()) && apple.getWeight()>=160; } } //注意:这里你可能需要为多种多样的需求增添多种多样的策略类,这里就暂写上面那一个 public static void main(String[] args) { /** 苹果库存集 **/ List<Apple> list = Arrays.asList( new Apple("green",150), new Apple("yellow",120), new Apple("green",170) ); // List<Apple> redApples2 = findApple(list,new GreenAnd160weiFilter()); //System.out.print(redApples2); }
4、使用内部类简化策略模式
上面三个由简至繁的需求,实际上使我们在实际开发中的一个缩影,需求总是在变,后台方法也在随之而变,在上面的例子中,我们发现使用策略模式其实已经就可以满足复杂多变的需求了,但是作者觉得这样写代码,还不够简洁,使用策略模式,我们可能需要建很多策略类,这似乎很繁琐。
在此情况下作者又给我们多了一个思路:内部类。上面需求3的策略模式实现代码可以改为以下:
package com.aigov.java8_newfeatures.lambda; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * @author : aigoV * @date :2019/10/10 * apple过滤器 * 根据农场主的特定需求找出特定苹果 **/ public class FilterApple { /** 需求3:不只是按颜色来找,需要按照重量,颜色去找(你需要满足不停变化的各种需求)**/ //这是抽象出来的过滤接口 public interface AppleFilter{ boolean filter(Apple apple); } //这是一个过滤方法 public static List<Apple> findApple(List<Apple> appleList,AppleFilter appleFilter){ List<Apple> list = new ArrayList<>(); for (Apple apple : appleList){ if (appleFilter.filter(apple)){ list.add(apple); } } return list; } public static void main(String[] args) { /** 苹果库存集 **/ List<Apple> list = Arrays.asList( new Apple("green",150), new Apple("yellow",120), new Apple("green",170) ); // /** 使用匿名内部类处理多变需求 (实际上就是将需求3的策略类用匿名类直接表示)**/ List<Apple> yellowApple = findApple(list,new AppleFilter(){ @Override public boolean filter(Apple apple) { return "yellow".equals(apple.getColor()) && apple.getWeight()==120; } }); //注意:这里你可能需要为多种多样的需求增添多种多样的上面这种匿名类,这里就暂写上面那一个 System.out.print(yellowApple); }
好了,匿名内部类的使用,我们会发现我们确实不用再去新建很多策略类了,但是不可避免的,我们仍然要为多变的需求去写很多匿名类,并且匿名类的代码也还是不够简洁,而且在书中,作者提到,使用匿名内部类,程序员有时会对内部类里面的成员参数调用产生误解(应该不会吧,,):
好了到这里,我们已经了解到了需求是多变的这个事实,也清楚了在没有lambda之前,我们一般是怎么处理他的,当然从上面例子想必大家也知道了不用lambda或者说以前这些实现方式的一些弊端:
- 代码繁杂
- 作者特意指出的内部类的弊端
- 其实还有个原因:java8 中使用lambda会相对优化内存
二、如何使用lambda表达式?
1、现在我们就用lambda表达式来解决上面的需求3的场景,我们先看代码实现,后面解释lambda表达式是如何使用的:
package com.aigov.java8_newfeatures.lambda; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * @author : aigoV * @date :2019/10/10 * apple过滤器 * 根据农场主的特定需求找出特定苹果 **/ public class FilterApple { /** 需求3:不只是按颜色来找,需要按照重量,颜色去找(你需要满足不停变化的各种需求)**/ //这是抽象出来的过滤接口 /** java8中,如果你的接口只有一个方法(default static方法除外),那么可以用这个注解,也就是你这个接口可以用lambda表达式 **/ @FunctionalInterface public interface AppleFilter{ boolean filter(Apple apple); } //这是一个过滤方法 public static List<Apple> findApple(List<Apple> appleList,AppleFilter appleFilter){ List<Apple> list = new ArrayList<>(); for (Apple apple : appleList){ if (appleFilter.filter(apple)){ list.add(apple); } } return list; } public static void main(String[] args) { /** 苹果库存集 **/ List<Apple> list = Arrays.asList( new Apple("green",150), new Apple("yellow",120), new Apple("green",170) ); // List<Apple> yellowApple2 = findApple(list,(Apple apple)->{ return "yellow".equals(apple.getColor()) && apple.getWeight()==120; }); System.out.print(yellowApple2); }
2、解释lambda表达式具体如何使用
- 2.1这是对上面lambda表达式语法的标注,看了之后应该就已经一目了然了:
对于上面这个,我们可以简化成下面这样:
- 2.1 要使用lambda表达式,那么你自己写的对应接口应该加上 @FunctionalInterface注解,这个注解的使用见下面:
实际上java8源码包下面包含了许多被@FunctionalInterface注解的接口,他们都可以被lambda表达式服务:
比如上图中打开的 接口中的方法,他就可以写出以下的lambda表达式:
Function<String,Integer > f = s -> s.length();
它表示给一个String类型的参数,会返回一个Integer类型的返回值。
三、举个栗子
现在让我们加深一下印象:来看看不用lambda和使用lambda构建一个线程的区别:
来源:
https://blog.csdn.net/l1028386804/article/details/105925811
https://blog.csdn.net/aigoV/article/details/102485823