Java8的新特性--Lambda表达式
Lambda表达式
Lambda表达式介绍
Lambda是一个匿名函数,我们可以把Lambda表达式理解为是一段可以传递的代码(将代码像数据一样传递)。可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。
- 匿名:没有一个确定的名字
- 函数:Lambda不属于一个特定的类,但是却有参数列表、函数主体、返回类型、异常列表。
- 传递:可以作为参数传递给方法、或者存储在变量中
- 简洁:不需要写很多模板代码
为什么使用Lambda表达式
知道了Lambda表达式是什么之后,我们也没啥感觉,下面我们来体验一下Lambda表达式到底有啥好的。
我们回顾下我们原来学过的的匿名内部类
//原来的匿名内部类
@Test
public void test01(){
//一个Comparator接口比较两个Integer的大小
Comparator<Integer> com = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1,o2);
}
};
//当用匿名内部类实现了一个接口以后我们就可以把这个匿名内部类的实例作为参数进行传递
TreeSet<Integer> set = new TreeSet(com);
}
我们来看这段代码,最核心关键的一行代码就只有一行
return Integer.compare(o1,o2);
所以下面就有请我们的Lambda表达式闪亮登场来解决这个一堆的代码量的问题啦。
下面我们用Lambda表达式来简化上面编写的匿名内部类。
//Lambda表达式
@Test
public void test02(){
//把上面的匿名内部类核心的一行代码Integer.compare(o1,o2)提取作为一个实现
Comparator<Integer> com = (x,y) -> Integer.compare(x,y);
TreeSet<Integer> set = new TreeSet(com);
}
是不是觉得简单了,如果你还没找到感觉,我们再来看一个例子。
假设有这么一个需求,你要获取当前公司中员工年龄大于35的员工信息。
我们一般要怎么实现呢?首先得先有一个员工的实体类Employee
package com.cqq.java8;
public class Employee {
private String name;
private int age;
private double salary;
public Employee(String name, int age, double salary) {
this.name = name;
this.age = age;
this.salary = salary;
}
public Employee() {
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", age=" + age +
", salary=" + salary +
'}';
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
}
然后在写一个用于过滤当前公司员工中年龄大于35的员工信息。
List<Employee> employees = Arrays.asList(
new Employee("张三",68,9000),
new Employee("李四",38,8000),
new Employee("王五",50,4000),
new Employee("赵六",18,3000),
new Employee("田七",8,1000));
//获取当前公司中员工年龄大于35的员工信息
public List<Employee> filterEmployee(List<Employee> employees){
List<Employee> emps = new ArrayList<>();
for (Employee emp: employees) {
if(emp.getAge() > 35){
emps.add(emp);
}
}
return emps;
}
然后写个测试方法
@Test
public void test03(){
List<Employee> list = filterEmployee(employees);
for (Employee employee:list ) {
System.out.println(employee);
}
}
测试结果如下
Employee{name='张三', age=68, salary=9000.0}
Employee{name='李四', age=38, salary=8000.0}
Employee{name='王五', age=50, salary=4000.0}
我们获取到了年龄大于35的员工信息。
这时候,如果我们的需求再增加一个,获取当前公司中员工工资大于5000的员工信息。于是,我们还要在再写一个用于获取当前公司中员工工资大于5000的员工信息的方法。
//当前公司中员工工资大于5000的员工信息
public List<Employee> filterEmployee2(List<Employee> employees){
List<Employee> emps = new ArrayList<>();
for (Employee emp: employees) {
if(emp.getSalary() > 5000){
emps.add(emp);
}
}
return emps;
}
然后我们发现,上面需求的俩方法有大量的冗余代码,除了判断语句之外,其他几乎都是相同的代码,这时候,我们就想要来优化了。原来我们对这种冗余代码最好的优化方式就是用设计模式,想想用什么设计模式呢,这里我们可以使用策略模式。下面我们来进行优化。
优化方式一:策略模式
策略模式 :定义一系列算法,把每一个算法封装起来,并且使它们可相互替换。简单说就是我们可以根据环境或者条件的不同选择不同的算法或者策略来完成某个功能,再通俗点说就是给他什么策略他就用什么策略来过滤。
这里就是根据判断条件的不同选出对应的员工Employee。
由于俩方法是条件不一致,我们可以来定义一个接口来进行条件判断。
//声明一个带泛型的接口
public interface MyPredicate<T> {
//声明一个boolean的方法,传进来一个T,然后对T操作,条件判断,返回一个boolean
public boolean test(T t);
}
然后,如果我们再有一个新的判断规则,只需要定义一个新的实现类实现MyPredicate接口即可。
下面是对员工年龄大于35的判断的实现类
public class FilterEmployeeByAge implements MyPredicate<Employee> {
@Override
public boolean test(Employee employee) {
return employee.getAge() > 35;
}
}
这个是员工员工工资大于5000的实现类
public class FilterEmployeeBySalary implements MyPredicate<Employee> {
@Override
public boolean test(Employee employee) {
return employee.getSalary() > 5000;
}
}
过滤方法如下
//参数传进来一个MyPredicate接口的实现类em
public List<Employee> filterEmployee(List<Employee> list, MyPredicate em){
List<Employee> emps = new ArrayList<>();
for (Employee employee: list) {
//实现类的判断方法
if(em.test(employee)){
emps.add(employee);
}
}
return emps;
}
测试方法
@Test
public void test04(){
System.out.println("========年龄大于35=========");
//传进来的参数为接口年龄判断的实现类
List<Employee> list = filterEmployee(employees,new FilterEmployeeByAge());
for (Employee employee:list ) {
System.out.println(employee);
}
System.out.println("========工资大于5000=========");
//传进来的参数为工资年龄判断的实现类
List<Employee> list2 = filterEmployee(employees,new FilterEmployeeBySalary());
for (Employee employee:list2 ) {
System.out.println(employee);
}
}
测试结果
========年龄大于35=========
Employee{name='张三', age=68, salary=9000.0}
Employee{name='李四', age=38, salary=8000.0}
Employee{name='王五', age=50, salary=4000.0}
========工资大于5000=========
Employee{name='张三', age=68, salary=9000.0}
Employee{name='李四', age=38, salary=8000.0}
虽然说使用了策略模式来重构了代码,但是为每个判断类都定义一个实现类,还是不那么好。接下来,我们直接使用匿名内部类实现接口的方式继续优化代码。
优化方法二:匿名内部类
@Test
public void test05(){
//传进来的参数为一个匿名内部类,不用每加一个判断条件再加一个接口的实现类了
List<Employee> list = filterEmployee(employees, new MyPredicate<Employee>() {
@Override
public boolean test(Employee t) {
return t.getSalary() < 5000;
}
});
System.out.println("========工资小于5000=========");
for (Employee employee:list ) {
System.out.println(employee);
}
}
测试结果
========工资小于5000=========
Employee{name='王五', age=50, salary=4000.0}
Employee{name='赵六', age=18, salary=3000.0}
Employee{name='田七', age=8, salary=1000.0}
这样是不是好多了,传进来的参数为一个匿名内部类,不用每加一个判断条件再加一个接口的实现类了。再来看看这个方式是不是最好的呢,我们发现其实匿名内部类里有用的代码还是就 return t.getSalary() < 5000;这个判断,可读性还是有点差,我们是不是还可以再继续优化呢?接下来我们继续往下看。
优化方式三:Lambda表达式
@Test
public void test06(){
List<Employee> list = filterEmployee(employees, (e) -> e.getSalary() > 5000);
list.forEach(System.out::println );
}
优化二里的匿名内部类用了Lambda表达式(e) -> e.getSalary() > 5000 来替代
测试结果
Employee{name='张三', age=68, salary=9000.0}
Employee{name='李四', age=38, salary=8000.0}
如果你觉得这种方式还不够好,其实还有更好的,我们往下看
优化方式四:Stream API
@Test
public void test5() {
//获取当前公司中员工工资不小于5000的员工信息
employees.stream()
.filter((e) -> e.getSalary() >= 5000)
.forEach(System.out::println);
}
测试结果
Employee{name='张三', age=68, salary=9000.0}
Employee{name='李四', age=38, salary=8000.0}
Lambda表达式的基础语法
Java8中引入了一个新的操作符"->",叫箭头操作符或者称为Lamnbda操作符
该操作符分为两部分:
左侧:Lambda表达式的参数列表(对应接口中抽象方法的参数列表)
右侧:Lambda表达式中所需要执行的功能,即Lambda体(对应接口中抽象方法的实现)
语法格式一:无参无返回值
() -> System.out.println();
这种语法格式说的就是接口中的那个抽象方法无参,并且无返回值。
Lambda表达式需要函数式接口的支持,我们熟悉的Runnable接口就是函数式接口,并且使无参且无返回值的,Runnable接口的定义如下
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
下面我们来看一下,原来我们是怎样通过匿名内部类的方式去实现的接口的呢?
//Lambda表达式需要函数式接口的支持,我们的Runnable接口就是函数式接口
@Test
public void test01(){
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello Runnable");
}
};
}
下面我们通过Lambda表达式的方法实现接口
//通过Lambda表达式实现接口
Runnable runnable1 = () -> System.out.println("Hello Lambda");
runnable1.run();
我们完整的测试方法如下
//Lambda表达式需要函数式接口的支持,我们的Runnable接口就是函数式接口
@Test
public void test01(){
//通过匿名内部类的方式实现接口
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello Runnable");
}
};
runnable.run();
System.out.println("--------------------");
//通过Lambda表达式实现接口
Runnable runnable1 = () -> System.out.println("Hello Lambda");
runnable1.run();
}
运行结果
Hello Runnable
--------------------
Hello Lambda
我们可以看出匿名内部类和Lambda表达式实现接口的效果是一样的,但是Lambda表达式简介了很多。
这里还有一个小点需要注意:如果我们在局部内部类中,应用了一个同级别的局部变量,在jdk1.8以前该变量必须是final的,1.8开始可以不加final了,底层默认给我们加上了,不需要我们手动加了,当我们对变量进行自增(++)或自减(--)操作时还是不行的。
@Test
public void test01(){
//这里fianl可以不加,jdk1.8开始给我们默认记上了final,不需要我们手动加了
/*final*/ int num = 0;
//通过匿名内部类的方式实现接口
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello Runnable"+num);
}
};
runnable.run();
System.out.println("--------------------");
//通过Lambda表达式实现接口
Runnable runnable1 = () -> System.out.println("Hello Lambda"+num);
runnable1.run();
}
运行结果num的数也打印出来了
Hello Runnable0
--------------------
Hello Lambda0
语法格式二:有一个参数并且无返回值
这种语法格式说的就是接口中的那个抽象方法有一个参数,并且无返回值。
我们以Consumer接口为例来说明,下面是Consumer接口的定义
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);
/**
* Returns a composed {@code Consumer} that performs, in sequence, this
* operation followed by the {@code after} operation. If performing either
* operation throws an exception, it is relayed to the caller of the
* composed operation. If performing this operation throws an exception,
* the {@code after} operation will not be performed.
*
* @param after the operation to perform after this operation
* @return a composed {@code Consumer} that performs in sequence this
* operation followed by the {@code after} operation
* @throws NullPointerException if {@code after} is null
*/
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
测试方法
//Lambda表达式
@Test
public void test02(){
Consumer consumer = (x) -> System.out.println(x);
consumer.accept("==这是测试结果==");
}
运行结果
==这是测试结果==
这个例子说明:Lambda表达式就是对Consumer接口的accept(T t)这个抽象方法的实现,实现的方法体为打印一下输入的参数x。
语法格式三:若有一个参数,小括号可以省略不写
这种语法格式说的就是接口中的那个抽象方法只有一个参数时,小括号可以写也可以不写。
//Lambda表达式
@Test
public void test02(){
//只有一个参数,小括号写
Consumer<String> consumer = (x) -> System.out.println(x);
//只有一个参数,小括号不写
Consumer consumer2 = x -> System.out.println(x);
consumer.accept("==这是测试结果==");
}
语法格式四:有两个及以上参数,并且Lambda体中有多条语句
这种语法格式说的就是接口中的那个抽象方法有两个及以上的参数,并且Lambda方法体中有多条语句,此时Lambda体要用大括号{}括起来。
我就以Comparator接口为例来讲述这种语法格式。我们只看Comparator接口的compare(T o1, T o2)方法的定义
@FunctionalInterface
public interface Comparator<T> {
//--省略
int compare(T o1, T o2);
}
测试方法
@Test
public void test03(){
Comparator<Integer> comparator = (x,y) -> {
//方法体中多条语句
System.out.println("方法体中多条语句1");
System.out.println("方法体中多条语句2");
return Integer.compare(x,y);
};
System.out.println(comparator.compare(2,4));
System.out.println(comparator.compare(4,2));
System.out.println(comparator.compare(2,2));
}
测试结果
方法体中多条语句1
方法体中多条语句2
-1
方法体中多条语句1
方法体中多条语句2
1
方法体中多条语句1
方法体中多条语句2
0
语法格式五:当Lambda体中只有一条语句时,return和大括号都可以省略不写
这种语法格式说的就是接口中的那个抽象方法有两个及以上的参数,并且Lambda方法体中只有一条语句,此时return和大括号{}都可以不写。
@Test
public void test04(){
Comparator<Integer> comparator = (x,y) -> Integer.compare(x,y);
System.out.println(comparator.compare(2,4));
}
语法格式六:Lambda表达式中的参数列表的数据类型可以省略不写,因为JVM编译器可以通过上下文来判断出数据类型,即“类型推断”
这种语法格式说的就是Lambda表达式中的参数列表的数据类型可以省略不写,因为JVM的编译器可以通过上下文来推断出数据类型,这个过程我们称之为"类型推断",其实,“类型推断"也是一个语法糖。
@Test
public void test05(){
Comparator<Integer> comparator = (x,y) -> Integer.compare(x,y);
//x,y的数据类型Integer可以不写,JVM根据上下文Comparator<Integer>这个泛型的Integer可以推断出来
Comparator<Integer> comparator2 = (Integer x,Integer y) -> Integer.compare(x,y);
}
这里x,y的数据类型Integer可以不写,JVM根据上下文Comparator
总结
我们总结下语法的原则:
上联:左右遇一括号省(左边一个参数,右边一个返回值时,参数的小括号和方法体的大括号都可以省略)
下联:左侧推断类型省(箭头操作符的左侧参数类型可以通过目标上下文推断出来,即可以省略不写)
横批:能省则省