2

注明:该笔记是学习 尚硅谷 李贺飞 老师的 java8视频教程


1,为什么需要Lambda?

暂不解释lambda表达式是什么,我们从一个需求一步一步探讨。假如我们目前有一个List,里面的内容是一个Employee类(公司的员工实体类),定义如下:

public class Employee
{
    private int id;
    private String name;
    private int age;
    private double salary;
    // getter setter toString constructor
}

List数据如下

List<Employee> emps = Arrays.asList(
    new Employee(101, "张三", 18, 9999.99),
    new Employee(102, "李四", 59, 6666.66),
    new Employee(103, "王五", 28, 3333.33),
    new Employee(104, "赵六", 8, 7777.77),
    new Employee(105, "田七", 38, 5555.55)
);

现在有以下需求:

  • 获取公司中年龄小于 35 的员工信息
  • 获取公司中工资大于 5000 的员工信息

一般我们会定义两个方法分别来实现:

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

public List<Employee> filterEmployeeSalary(List<Employee> emps){
    List<Employee> list = new ArrayList<>();
    
    for (Employee emp : emps) {
        if(emp.getSalary() >= 5000){
            list.add(emp);
        }
    }
    
    return list;
}

我们注意到上面两个方法的非常相似,主要是if判断的逻辑不同,如果需求增加,就不得不增加方法,比如我们新增下面几个需求

  • 查找员工工资小于 5000 的所有员工
  • 查找年龄小于30岁的所有员工

优化方式一:策略设计模式

1,定义一个接口,用于判断是否满足某个条件。

public interface MyPredicate<T>
{
    boolean isSelected(T t);
}

2,定义两个实现类,分别实现MyPredicate接口

public class FilterEmployeeForAge implements MyPredicate<Employee>
{
    @Override
    public boolean isSelected(Employee employee)
    {
        return employee.getAge() < 35;
    }
}

public class FilterEmployeeForSalary implements MyPredicate<Employee>
{
    @Override
    public boolean isSelected(Employee employee)
    {
        return employee.getSalary() > 5000;
    }
}

3,定义一个方法filterEmployee,主要是从List<Employee>对象中根据MyPredicate接口的实现类来挑选哪些Employee符合条件。

public List<Employee> filterEmployee(List<Employee> emps, MyPredicate<Employee> mp)
{
    List<Employee> list = new ArrayList<>();

    for (Employee employee : emps)
    {
        if (mp.isSelected(employee))
        {
            list.add(employee);
        }
    }

    return list;
}

4,测试

@Test
public void test()
{
    List<Employee> list1 = filterEmployee(emps, new FilterEmployeeForAge());
    for (Employee employee : list1)
    {
        System.out.println(employee);
    }

    List<Employee> list2 = filterEmployee(emps, new FilterEmployeeForSalary());
    for (Employee employee : list2)
    {
        System.out.println(employee);
    }
}

使用策略设计模式filterEmployee方法只用写一个即可,如果需要添加新的策略那么我们必须再写实现类,然后使用的时候filterEmployee方法第二个参数传入具体的实现类即可完成对应的需求。

比较:我们通过上面的策略设计模式可以看出和最原始的方法相比较,减少方法的编写,但是增加一个接口和多个实现类来实现的。大量重复代码被放入filterEmployee方法中,真正的逻辑代码被isSelected方法所实现。

优化方式二:匿名内部类

这种方式可以避免接口的实现类编写,将实现放入匿名内部类中。相比策略设计模式,我们不需要实现类,并且在使用的时候也有不同。即:

@Test
public void test()
{
    List<Employee> list1 = filterEmployee(emps, new MyPredicate<Employee>()
    {
        @Override
        public boolean isSelected(Employee employee)
        {
            return employee.getAge() < 35;
        }
    });

    List<Employee> list2 = filterEmployee(emps, new MyPredicate<Employee>()
    {
        @Override
        public boolean isSelected(Employee employee)
        {
            return employee.getSalary() > 5000;
        }
    });
}

可以看出匿名内部类方式避免了接口实现类的编写,它是把实现类的逻辑放入到filterEmployee方法使用的时候来实现。如果新增需求只需要再写匿名内部类即可。这样的做法相比策略设计模式,是在filterEmployee方法调用的时候来编写具体逻辑,只能在一处使用,别处的代码不能复用这段逻辑。假如我们不需要复用,那么匿名内部类这样代码还有没有改进呢?

优化方式三:Lambda 表达式

匿名内部类有大量重复的代码,可以使用Lambda表达式即可完成消除重复代码的功能。内容可以暂时不关注,下面详细介绍。

@Test
public void test()
{
    List<Employee> list1 = filterEmployee(emps, (e) -> e.getAge() < 35);
    list1.forEach(System.out::println);

    List<Employee> list2 = filterEmployee(emps, (e) -> e.getSalary() > 5000);
    list2.forEach(System.out::println);
}

优化方式四:Stream API

Lambda 表达式的确简化了代码,但是我们还是需要编写filterEmployee方法,这个方法自然少不了MyPredicate的支持。如果能去除这两处代码则更加简化代码,只要使用Stream API完成这个功能。

@Test
public void test()
{
    emps.stream()
        .filter((e) -> e.getAge() < 35)
        .forEach(System.out::println);

    emps.stream()
        .filter((e) -> e.getSalary() > 5000)
        .forEach(System.out::println);
}

注意这里代码只需要一个Employee类和一些数据即可,其他的代码都可以不要。

List<Employee> emps = Arrays.asList(
    new Employee(101, "张三", 18, 9999.99),
    new Employee(102, "李四", 59, 6666.66),
    new Employee(103, "王五", 28, 3333.33),
    new Employee(104, "赵六", 8, 7777.77),
    new Employee(105, "田七", 38, 5555.55)
);    

2,Lambda表达式基础

上面优化方式三可以看出Lambda表达式是针对匿名内部类大量重复代码进行优化的,本质上是一个匿名方法,因此学习Lambda表达式也必须紧紧围绕匿名内部类来展开。

当我们定义一个接口,假如接口定义了一个方法,那么我们在使用这个接口的方法时,放在以前一般有两中方式:一种是定义一个类实现这个接口,使用时直接使用实现类即可;另一种方式是使用匿名内部类,无需定义实现类,在使用接口的时候直接new出接口并实现方法即可。这两种方式在上面策略设计模式和匿名内部类已经看到了。我们知道接口可以定义很多方法,如果只是定义了一个方法,那么这种接口被称为函数式接口,Lambda表达式就必须依托这个前提。我们再把上面的代码比较一下,看看Lambda表达式的特点。

接口

public interface MyPredicate<T>
{
    boolean isSelected(T t);
}

过滤方法(emps见上面定义)

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

匿名内部类

List<Employee> list = filterEmployee(emps, new MyPredicate<Employee>() {
    @Override
    public boolean isSelected(Employee t) {
        return t.getAge() < 35;
    }
});

Lambda表达式

List<Employee> list = filterEmployee(emps, (e) -> e.getAge() < 35);

3, Lambda表达式结构

Java8中引入了一个新的操作符 "->" 该操作符称为箭头操作符或 Lambda 操作符,它将 Lambda 表达式拆分成两部分:

  • 左侧:Lambda 表达式的参数列表,就是isSelected方法的参数列表(Employee t)
  • 右侧:Lambda 表达式中所需执行的功能, 即 Lambda 体:return t.getAge() < 35;
new MyPredicate<Employee>() {
    @Override
    public boolean isSelected(Employee t) {
        return t.getAge() < 35;
    }
}

被下面的lambda表达式替换

(e) -> e.getAge() < 35

e只是一个名称,代表Employee对象,e.getAge() < 35return t.getAge() < 35;一句功能一样。

4, lambda语法格式

(1)无参数,无返回值(接口的方法是没有参数和返回值的)

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

(2)有一个参数,并且无返回值

(x) -> System.out.println(x)

(3)若只有一个参数,小括号可以省略不写(建议写)


x -> System.out.println(x) 

(4) 有两个以上的参数,有返回值,并且 Lambda 体中有多条语句,需要大括号,需要return。

Comparator<Integer> com = (x, y) -> {
     System.out.println("函数式接口");
     return Integer.compare(x, y);
};

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

Comparator<Integer> com = (x, y) -> Integer.compare(x, y);

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

(Integer x, Integer y) -> Integer.compare(x, y);

注意:Lambda 表达式需要“函数式接口”的支持,函数式接口:接口中只有一个抽象方法的接口,称为函数式接口。 可以使用注解 @FunctionalInterface 修饰,可以检查是否是函数式接口。

5, Lambda接口编程流程

1,定义一个函数式接口

2,编写一个方法,输入需要操做的数据和接口

3,在调用方法时传入数据 和 lambda 表达式,用来操作数据

举例,定义一个可以对两个整数进行加减乘除的操作。以前我们可能定义四个方法,但是如果增加操作类型则需要再定义对应的方法。可以使用Lambda表达式来实现。

1,定义一个函数式接口,使用@FunctionalInterface注解标注

@FunctionalInterface
public interface MyFunction<R,T>
{
    R operator(T t1, T t2);
}

2,编写一个方法,输入需要操做的数据和接口,基本数据类型需要包装类型

public Integer operator(Integer x, Integer y, MyFunction<Integer, Integer> mf)
{
    return mf.getValue(x, y);
}

3,在调用方法时传入数据 和 lambda 表达式,用来操作数据

@Test
public void test()
{
    System.out.println(operator(20,5, (x, y) -> x + y ));
    System.out.println(operator(20,5, (x, y) -> x - y ));
    System.out.println(operator(20,5, (x, y) -> x * y ));
    System.out.println(operator(20,5, (x, y) -> x / y ));
}

6, java8内置接口

上面我们看到要使用Lambda表达式必须先定义接口,创建相关方法之后才可使用,这样做十分不便,其实java8已经内置了许多接口,方便我们使用Lambda表达式。

Java8 内置的四大核心函数式接口

  • Consumer<T> : 消费型接口:有入参,无返回值

        void accept(T t);
    
  • Supplier<T> : 供给型接口:无入参,有返回值

        T get(); 
    
  • Function<T, R> : 函数型接口:有入参,有返回值

        R apply(T t);
    
  • Predicate<T> : 断言型接口:有入参,有返回值,返回值类型确定是boolea

        boolean test(T t);    
    

其实这些内置接口只是定义了接口和方法,在第五章 Lambda接口编程流程 中第一步可以省略,第二步和第三步不可或缺。我们记住这些接口使用的场景,在使用的时候可以不必自己定义接口。

举例:对字符串进行操作,有输入有输出,使用函数型接口 Function<T, R>

1,定义一个函数式接口

//无代码,使用内置接口的好处

2,编写一个方法,输入需要操做的数据和接口

public String strHandler(String str, Function<String, String> fun){
    return fun.apply(str);
}

3,在调用方法时传入数据 和 lambda 表达式,用来操作数据

@Test
public void test()
{
    System.out.println(strHandler("ABC",(x) -> x.toLowerCase()));
    System.out.println(strHandler("  aaf  ",(x) -> x.trim()));
}

特别注意:当strHandler方法只是把传入的数据str放入到apply(str)进行执行的时候,也就是strHandler方法只是简单的调用apply方法而没有其他逻辑,那么strHandler方法也可以省略掉,直接使用如下代码替换步骤3

@Test
public void test()
{
    Function<String, String> fun = (x) -> x.toLowerCase();
    System.out.println(fun.apply("ABC"));
    fun = (x) -> x.trim();
    System.out.println(fun.apply("  aaf  "));
}

但是如果strHandler方法有很多特殊的逻辑,比如上面的filterEmployee方法,就不能省略该方法。这一点需要特别注意。

7, 引用

引用理解为 Lambda 表达式的另外一种表现形式,提供了一种简短的语法而已。主要有三种:

  • 方法引用
  • 构造器引用
  • 数组引用

方法引用

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

1. 对象的引用 :: 实例方法名

 @Test
 public void test2(){
    Employee emp = new Employee(101, "张三", 18, 9999.99);
    
    Supplier<String> sup1 = () -> emp.getName();
    System.out.println(sup1.get());
    
    Supplier<String> sup2 = emp::getName;
    System.out.println(sup2.get());
}

其中

() -> emp.getName()

被替换为

emp::getName

注意 getName的参数和返回值与Supplier接口的get方法的参数和返回值一致,否则会出现错误。

@Test
public void test1(){
    PrintStream ps = System.out;
    Consumer<String> con = (str) -> ps.println(str);
    con.accept("Hello Java8!");
    
    Consumer<String> con2 = ps::println;
    con2.accept("Hello Java8!");
    
    Consumer<String> con3 = System.out::println;
    con3.accept("Hello Java8!");
}

这个例子中有入参,但是经过方法引用变形之后,似乎没有入参了,其实这是由于println方法和accept方法的入参和返回值一致,因此无需传入。

2. 类名 :: 静态方法名

@Test
public void test()
{
    Comparator<Integer> com = (x, y) -> Integer.compare(x, y);

    Comparator<Integer> com2 = Integer::compare;
}  

下面的也是:

@Test
public void test()
{
    BiFunction<Double, Double, Double> fun = (x, y) -> Math.max(x, y);
    System.out.println(fun.apply(2.5, 222.2));
    
    BiFunction<Double, Double, Double> fun2 = Math::max;
    System.out.println(fun2.apply(3.2, 21.5));
}

3. 类名 :: 实例方法名

@Test
public void test()
{
    BiPredicate<String, String> bp = (x, y) -> x.equals(y);
    System.out.println(bp.test("abcde", "abcde"));

    BiPredicate<String, String> bp2 = String::equals;
    System.out.println(bp2.test("abc", "abc"));
}

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

构造器引用

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

使用方式:类名 :: new

@Test
public void test()
{
    //获取一个Employee对象,调用的是Employee无参构造器
    Supplier<Employee> sup = () -> new Employee();
    System.out.println(sup.get());
    
    //也是调用Employee无参构造器,因为Supplier的get方法没有入参
    Supplier<Employee> sup2 = Employee::new;
    System.out.println(sup2.get());
}    

@Test
public void test()
{
    //意味着Employee有一个带一个参数的构造器,参数类型是String
    Function<String, Employee> fun = Employee::new;

    //意味着Employee有一个带两个个参数的构造器,参数类型是String,Integer
    BiFunction<String, Integer, Employee> fun2 = Employee::new;
}

因为Function接口的方法需要一个String类型的参数,因此Employee::new是调用带有一个String类型参数的构造器。同理 BiFunction 调用的是带有一个String类型参数和Integer参数的的构造器 。

数组引用

类型[] :: new;


@Test
public void test()
{
    Function<Integer, String[]> fun = (args) -> new String[args];
    String[] strs = fun.apply(10);
    System.out.println(strs.length);
    
    Function<Integer, Employee[]> fun2 = Employee[] :: new;
    Employee[] emps = fun2.apply(20);
    System.out.println(emps.length);
}


指尖改变世界
27 声望6 粉丝