Java8学习笔记之Lambda表达式

一.前言

   从2014年3月Java8发布到2020年3月17日Java14正式GA,Java版本更新迭代很快。但是公司的一些在维护的旧项目还是停留在JDK1.7,只有新的项目才要求用JDK1.8,然而我们很多人对Java8的一些新特性还不是很熟悉,所以这里做个Java8新特性的总结。
  在Java8中引入了函数式编程方式,它能将一个行为传递作为参数进行传递,如果说面向对象编程是对数据的抽象,那么函数式编程就是对行为进行抽象。现实世界中,数据和行为并存,程序也是如此,因此这两种编程方式都要学。在Java8支持函数式编程以前,我们如果需要传递一个行为通常用的方式就是传递一个对象,而匿名内部类正是为了方便将代码作为数据(参数)进行传递。
  Java8通过引入了Lambda表达式,使Java有了函数式编程的语法特征。Lambda 表达式是一个匿名函数(即没有函数名的函数),Lambda表达式也可以表示闭包。Lambda表达式将行为像数据一样传递。
Java8中的Lambda是使用函数式接口实现的,函数式接口就是指仅有一个抽象方法(默认方法不算)的接口。
Java8中为了解决Lambda引入所带来的一些问题,引入了默认方法和接口的静态方法。

二.Lambda介绍

1.引子–行为参数化传递代码

场景示例:

假设产品提出这样一个需求:从员工列表中筛选出部门编号是"20"的员工,程序员编写代码如下:

  /**
     * 找出部门编号是20的员工
     *
     * @param empList 所有员工列表
     * @return
     */
    public List<Emp> filterDeptNo(List<Emp> empList) {
        List<Emp> resultList = new ArrayList<>();
        for (Emp emp : empList) {
            if (emp.getDeptno() == 20) {
                resultList.add(emp);
            }
        }
        return resultList;
    }

代码写完之后,产品提了一个新需求:要能够筛选出其他部门的员工,这时候程序员修改了代码,将部门编号作为参数,这样就能够灵活的应对需求的变化。代码如下:

    /**
     * 找出部门编号是20的员工
     *
     * @param empList 所有员工列表
     * @param deptNo  部门编号
     * @return
     */
    public List<Emp> filterDeptNo(List<Emp> empList, int deptNo) {
        List<Emp> resultList = new ArrayList<>();
        for (Emp emp : empList) {
            if (emp.getDeptno() == deptNo) {
                resultList.add(emp);
            }
        }
        return resultList;
    }

又过了几个小时,产品说:还需要能够筛选出薪水大于15K的员工,于是,程序员编写了一个新方法,用另外一个参数来代表薪水,代码如下:

/**
     * 筛选出大于指定薪水值的员工
     *
     * @param empList 员工列表
     * @param sale    指定的薪水
     * @return
     */
    public List<Emp> filterSale(List<Emp> empList, double sale) {
        List<Emp> resultList = new ArrayList<>();
        for (Emp emp : empList) {
            if (emp.getSal() >= sale) {
                resultList.add(emp);
            }
        }
        return resultList;
    }

可以看出,除了方法名称、参数、和判断逻辑,其他都是一样的代码,有大量的重复代码出现。这时候程序员想到了把所有判断属性都结合起来,代码如下:

     /**
     * 通过flag判断是筛选部门编号还是筛选薪水
     *
     * @param empList
     * @param deptNo
     * @param sale
     * @param flag
     * @return
     */
    public List<Emp> filterEmp(List<Emp> empList, int deptNo, double sale, boolean flag) {
        List<Emp> resultList = new ArrayList<>();
        for (Emp emp : empList) {
            if ((flag && emp.getDeptno() == deptNo)
                    || (!flag && (emp.getSal() >= sale))) {
                resultList.add(emp);
            }
        }
        return resultList;
    }

这个方法看起来很笨拙,方法参数太多不利于扩展修改。这种通过添加很多参数来应对变化的需求的方式不是很好的方案。

我们这里的场景是根据员工的某些属性(比如员工的编号,员工的薪水等)来筛选过滤,可以把判断员工的不同属性当作不同的算法来对待,使用策略模式来应对不断变化的需求。

策略模式使用的就是面向对象的继承和多态机制,首先将判断员工属性抽象为一个“抽象策略角色”,他是判断员工属性算法的抽象,通常就是一个接口,定义每个策略或算法必须具有的方法和属性。如下:

/**
 * 员工抽象策略角色
 */
public interface EmpPredicate {
    /**
     * 策略模式的算法
     * @return
     */
    public boolean test();
}

用EmpPredicate的多个实现代表不同的选择标准(策略),具体策略就是普通的一个实现类,只要实现接口中的方法就可以。如下:

/**
 * 具体策略(算法)角色1
 */
public class EmpDeptNoPredicate implements EmpPredicate {
    /**
     * 仅仅筛选出部门20的员工
     */
    @Override
    public boolean test(Emp emp) {
        return "20".equals(emp.getDeptno());
    }
}
/**
 * 具体策略(算法)角色2
 */
public class EmpSalePredicate implements EmpPredicate {

    /**
     * 仅仅选出薪水大于1万的
     */
    @Override
    public boolean test(Emp emp) {
        return emp.getSal() > 10000;
    }
}

现在我们修改filterEmp方法,让方法接受empPredicate对象,对Emp做条件测试。这就是行为参数化:让方法接受多种行为(策略)作为参数,并在内部使用,来完成不同的行为。

行为参数化可以让代码更好的适应不断变化的需求,减轻未来的工作量。

/**
     * 员工过滤器
     * @param empList
     * @param predicate
     * @return
     */
    public List<Emp> filterEmp(List<Emp> empList, EmpPredicate predicate) {
        List<Emp> resultList = new ArrayList<>();
        for (Emp emp : empList) {
            if (predicate.test(emp)) {
                resultList.add(emp);
            }
        }
        return resultList;
    }

现在我们可以创建不同的EmpPredicate对象,并将它们传递给filterEmp方法。这样更灵活的应对变化的需求。

//筛选出部门20的员工  
List<Emp> resultList = this.filterEmp(empList, new EmpDeptNoPredicate());
//选出薪水大于1万的
List<Emp> resultList = this.filterEmp(empList, new EmpSalePredicate());

从中可以看出,当要把新的行为(策略)传递给filterEmp方法的时候,我们不得不声明好几个实现EmpPredicate接口的类,
然后实例化好几个只会提到一次的EmpPredicate对象。这就是策略模式的缺点:每一个策略(算法)都是一个类,
复用的可能性很小,类数量增多,并且所有的策略类都需要对外暴露。

下面我们使用匿名内部类改善代码,让它变得更简洁。匿名内部类可以同时声明和实例化一个类。

List<Emp> resultList = this.filterEmp(empList, new EmpSalePredicate() {
            @Override
            public boolean test(Emp emp) {
                return emp.getSal() > 10000;
            }
  });
List<Emp> resultList = this.filterEmp(empList, new EmpDeptNoPredicate() {
            @Override
            public boolean test(Emp emp) {
                return "20".equals(emp.getDeptno());
            }
});

使用匿名内部类处理虽然在某种程度上改善了为一个接口声明好几个实体类的问题,但是还是不够友好,还是存在模板代码。

这时候该Java8的Lambda表达式登场了,Lambda表达式的主要作用就是代替匿名内部类的烦琐语法;如下:

   List<Emp> resultList = this.filterEmp(empList, (Emp emp) -> "20".equals(emp.getDeptno()));

Java8的Lambda表达式是一种更简洁的传递代码的方式。

传递代码:就是将新行为(策略或者算法)作为参数传递给方法。但在Java8之前实现起来比较麻烦(为接口声明许多只用一次的实体类而造成的类数量增多),在Java8之前可以用匿名内部类来简化代码。

下面一节我们详细说明下Lambda表达式。

2.Lambda表达式简介

可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式,一个lambda表达式是一个带有参数的代码块:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。

Lambda表达式有如下几个特点:

  1. 匿名:它不像普通的方法那样有一个明确的名称。
  2. 函数:Lambda函数不像方法那样属于某个特定的类。但是和方法一样,有参数列表、函数体、返回类型,还可以有可以抛出的异常列表。
  3. 传递:Lambda表达式可以作为参数传递给方法或者存储在变量中。
  4. 简洁:无需像匿名类那样写很多模板代码。

Lambda表达式的格式:参数列表、箭头 ->、和一个代码块。基本语法是:

(parameters)-> expression 或者

(parameters)-> { statements }

如果代码块只包含一条语句,Lambda表达式允许省略代码块的花括号。

Lambda代码块只有一条return语句,可以省略return关键字。

表达式和语句的区别:

1.表达式总是能够返回一个值;而语句则只干活,并不返回,一个语句由1个或多个表达式组成

2.表达式有值,语句没有值, 能作为函数参数即为表达式,否则为语句。

如果Lambda表达式没有参数,可以只提供一个小括号,如下:

//返回int
() -> 100

如果Lambda表达式的参数类型可以被推导的,那么可以省略它们的类型

  EmpPredicate predicate = (e) -> "20".equals(e.getDeptno());

如果方法只包含一个参数,并且该参数的类型可以被推导出来,就可以省略小括号。

 EmpPredicate predicate = e -> "20".equals(e.getDeptno());

3.函数式接口

只包含一个抽象方法的接口就是函数式接口。函数式接口可以包含多个默认方法、类方法,但只能声明一个抽象方法。例如下面的例子就是定义一个函数式接口

@FunctionalInterface
public interface AddOperation {
    int add(int optA, int optB);
}

Java8专门为函数式接口提供了@FunctionalInterface注解,这个注解对程序功能没有任何作用,主要用于编译器执行更严格检查。

如果采用匿名内部类来创建函数式接口的实例,则只需要实现一个抽象方法,在这种情况下即可采用Lambda表达式来创建对象,Lambda表达式创建出来的对象的目标类型就是这个函数式接口。

Lambda表达式的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中
Lambda表达式需要的类型称为目标类型。目标类型决定了在什么时候以及在哪里可以使用lambda表达式。它们可以从赋值的上下文、方法调用的上下文(参数和返回值),以及类型转换的上下文中获得目标类型。

目标类型->函数式接口->Lambda表达式签名->在Lambda表达式中省去标注参数类型

Lambda的两个限制:

  1. Lambda表达式的目标类型必须是明确的函数式接口
  2. Lambda表达式只能为函数式接口创建对象。Lambda表达式只能实现一个方法,因此它只能为只有一个抽象方法的接口(函数式接口)创建对象。

为了保证Lambda表达式的目标类型是一个明确的函数式接口,一般采用如下三种方式

  1. 将Lambda表达式赋值给函数式接口类型的变量。
  2. 将Lambda表达式作为函数式接口类型的参数传给某个方法。
  3. 使用函数式接口对Lambda表达式进行强制类型转换。

同一个Lambda表达式可以与不同的函数式接口联系起来,只要它们的抽象方法签名能兼容。
也就是说同样的Lambda表达式的目标类型完全可能是变化的。唯一的要求是Lambda表达式实现的匿名方法与目标类型(函数式接口)中唯一的抽象方法有相同的形参列表。

比如下面的lambda表达式

EmpPredicate predicate = e -> "20".equals(e.getDeptno());
Predicate<Emp> pre = e -> "20".equals(e.getDeptno());

EmpPredicate是自定义的函数式接口,Predicate是Java8预定义的函数式接口。

Java8在java.util.function包下预定义了大量函数式接口,主要有以下4种接口

  • XxxPredicate:这类接口中包含一个test(T t)抽象方法,这个方法通常用来对参数进行某种判断(test()方法的判断逻辑由lambda表达式来实现),然后返回一个boolean值。这个函数式接口通常用于判断参数是否满足特定条件,经常用于进行筛选过滤数据。
  • XxxConsumer:这类接口中通常包含一个accept(T t)抽象方法,这个方法与XxxFunction接口中的 apply(T)方法基本一样,都是负责对参数进行处理,只是这个方法不会返回处理结果。
  • XxxFunction:这类接口中包含一个apply(T)抽象方法,该方法对参数进行处理、转换,然后返回一个新的值。该函数式接口通常用于对指定数据进行转换处理。
  • XxxSupplier:这类接口通常包含一个get()抽象方法,这个方法不需要输入参数,该方法会按照某种逻辑算法(逻辑算法由Lambda表达式来实现)返回一个数据。

4.使用局部变量

Java8之前,被局部内部类、匿名内部类访问的局部变量必须使用final修饰,从Java8开始这个限制被取消了,如果局部变量被匿名内部类访问,那么该局部变量相当于自动使用了final修饰。也就是说对于被匿名内部类访问的局部变量,可以用final修饰,也可以不用final修饰,但必须按照有final修饰的方式来用,也就是一次赋值之后,以后不能重复赋值。

同样的在Lambda表达式中使用局部变量也有同样的限制,因为局部变量保存在栈中,并且隐式表示它们仅限于其所在线程。如果允许使用可改变的局部变量,会造成线程不安全。而实例变量和静态变量没有这个限制,实例变量保存在堆中,而堆是在线程之间共享的。

5.方法引用与构造器引用

方法引用和构造器引用可以让Lambda表达式的代码块更加简洁。方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法。
就是让你根据已有的方法实现来创建Lambda表达式。方法引用和构造器引用都需要使用两个英文冒号。

Lambda表达式支持的方法引用和构造器引用如下

5.1.指向静态方法的方法引用(引用类方法)

@FunctionalInterface
public interface TypeConverter {
    Integer convert(String numStr);
}


  //用Lambda表达式创建TypeConverter对象
TypeConverter typeConverter = numStr -> Integer.valueOf(numStr);
 Integer convertResult = typeConverter.convert("101");

上面Lambda表达式的代码块只有一行调用类方法的代码,因此可以用方法引用进行替换

//方法引用代替Lambda表达式
//函数式接口中被实现方法的全部参数传给该类方法作为参数
TypeConverter typeConverter = Integer::valueOf;

当调用TypeConverter接口中的唯一的抽象方法时,调用参数将会传给Integer类的valueOf类方法。

5.2.指向任意类型实例方法的方法引用

就是引用一个对象的方法,而这个对象本身是Lambda的一个参数。

比如:Lambda表达式(String s) -> s.toUppeCase() 可以写作String::toUpperCase。

5.3.指向现有对象(特定对象)的实例方法的方法引用

就是在lambda中调用一个已存在的外部对象中的方法。如下

 String lang = "Java,C++,C#,Python";
 TypeConverter typeConverter1 = soustr -> lang.indexOf(soustr);

上面lambda表达式的代码块只有一行调用"lang".indexOf()实例方法的代码,因此可以用方法引用替换

//引用特定对象的实例方法
TypeConverter typeConverter2 = lang::indexOf;

5.4.引用构造器

Supplier<Emp> newEmp = () -> new Emp();
//调用Supplier的get方法将产生一个新的Emp对象
 Emp emp = newEmp.get();

上面Lambda表达式的代码块只有一行 new Emp(),因此可以使用构造器引用替换

 Supplier<Emp> supplier = Emp::new;
//调用Supplier的get方法将产生一个新的Emp对象
Emp emp1 = supplier.get();

如果构造函数的签名是Emp(String empName),那么就适合Function接口的签名

  Function<String, Emp> empFunction = (empName) -> new Emp(empName);
  Emp emp2 = empFunction.apply("Smith");

等价于

  Function<String,Emp> function2 = Emp::new;
  Emp smith = function2.apply("Smith");

6.Lambda表达式复合

可以将多个简单的Lambda复合成复杂的表达式,Java8的Comparator、Function、Predict都提供了允许你复合的方法。

例子1:找出部门编号是20并且薪水大于15K的员工

Predicate<Emp> empPredicate = emp -> "20".equals(emp.getDeptno());
Predicate<Emp> empSalPredicate = empPredicate.and(emp -> emp.getSal() > 15000);
List<Emp> list = filterEmp(empList, empSalPredicate);
public List<Emp> filterEmp(List<Emp> empList, Predicate predicate) {
        List<Emp> resultList = new ArrayList<>();
        for (Emp emp : empList) {
            if (predicate.test(emp)) {
                resultList.add(emp);
            }
        }
        return resultList;
}

例子2:对员工先按照部门编号排序,再按照薪水排序

 Comparator<Emp> empComparator = Comparator.comparing(Emp::getDeptno).thenComparing(Emp::getSal);
 empList.sort(empComparator);

7.Lambda表达式与匿名内部类的联系和区别

相同点:

  1. 都可以直接访问局部变量,以及外部变量。
  2. Lambda表达式和匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法。

不同点:

  1. 匿名内部类可以为任意接口创建实例,不管接口包含多少个抽象方法,只要匿名内部类实现所有的抽象方法即可。但是Lambda表达式只能为函数式接口创建实例。
  2. 匿名内部类可以为抽象类或者普通类创建实例,但Lambda表达式只能为函数式接口创建实例。
  3. 匿名内部类实现的抽象方法里面允许调用接口中定义的默认方法,但是Lambda表达式的代码块不允许调用接口中定义的默认方法。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值