# Lambda 表达式

# 前言

Lambda 是一个匿名函数,我们可以把 Lambda 表达式理解为是一段可以传递的代码

它可以让我们在 Java 8 的环境下写出更简洁、更灵活的代码。

作为一种更紧凑的代码风格,使 Java 的语言表达能力得到了一定的提升。

如果你学过ECMAScript6简称ES6语法,那么你会发现 Lambda 和它的箭头表达式的确是有点相像的。

# 学习动力

我相信有很多的朋友,因为觉得 Lambda 代码难以阅读,且大部分人觉得没有学习的必要,当公司有同事在使用时,则心里暗暗烦躁,代码读起来极度难以理解。

相信你能看到这里,证明你已经被困扰过了,没错,我就是这样走过来的,希望在这里能让你学好 Lambda 。

# 学前场景案例

# 简单的例子

首先我们看一个例子:

Runnable rb = new Runnable() {
    @Override
    public void run(){
        System.out.println("浅梦博客");
    }
};
rb.run();
1
2
3
4
5
6
7

上述例子中是一个简单的最原始的线程执行方法,如果我们使用Lambda表达式,可以达到如下效果:

Runnable rb = () -> System.out.println("浅梦博客");
rb.run();
1
2

是不是看起来简洁多了?这就是Lambda最为直观的魅力。


# 学生案例

现在我们有了这么些信息:

List<Student> students = Arrays.asList(
            new Student(101, "小明", 12, 88.88),
            new Student(102, "小杰", 13, 66.66),
            new Student(103, "浅梦", 27, 99.99),
            new Student(104, "中杨", 10, 77.77),
            new Student(105, "大健", 5, 55.55)
    );
1
2
3
4
5
6
7

然后我们现在有一个需求:

需求:获取学生中年龄小于等于 12 的学生信息

public List<Student> filterStudentAge(List<Student> students){
    List<Student> list = new ArrayList<>();
    for (Student student : students) {
        if (student.getAge() <= 12){
            list.add(student);
        }
    }
    return list;
}
1
2
3
4
5
6
7
8
9

上述书写了一个过滤方法,把年龄小于等于12的学生对象返回出去,在需要的地方我们可以直接调用了

List<Student> list = this.filterStudentAge(students);
for (Student student : list) {
    System.out.println(student);
}
1
2
3
4

这是最简单也是大家一般都能想到的方法,毫无美感可言,可以看出我这种是为了完成需求而工作。

当需求发生改变时,比如我又需要获取成绩大77的学生信息,是不是又要写一个filterStudentAge方法呢?

然后我们就会将这些代码进行优化。


# 学生案例优化一:策略设计模式

不得不说,我们伟大的设计模式,能为我们解决很多问题,就比如这个学生案例的需求。

现在我们来改造一下:

首先定义一个接口:

public interface MyHandler<T> {
    boolean test(T t);
}
1
2
3

定义两个需求实现:

public class FilterStudentForAge implements MyHandler<Student> {
    @Override
    public boolean test(Student student) {
        return student.getAge() <= 12;
    }
}
1
2
3
4
5
6
public class FilterStudentForScore implements MyHandler<Student> {
    @Override
    public boolean test(Student student) {
        return student.getScore() > 77.00;
    }
}
1
2
3
4
5
6

定义一个功能方法:

public List<Student>filterStudent(List<Student> students , MyHandler<Student> myHandler){
        List<Student> list = new ArrayList<>();
        for (Student student : students) {
            if(myHandler.test(student)){
                list.add(student);
            }
        }
        return list;
    }
1
2
3
4
5
6
7
8
9

具体需求执行过滤:

@Test
public void test(){
    // 获取年龄小于等于12的学生信息
    List<Student> list = this.filterStudent(students, new FilterStudentForAge());
    for (Student student : list) {
        System.out.println(student);
    }
    System.out.println("------------------");
    // 获取成绩大于77的学生信息
    List<Student> list2 = this.filterStudent(students, new FilterStudentForScore());
    for (Student student : list2) {
        System.out.println(student);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

可以看到,上述的例子中,运用了策略设计模式,这样每当我们新加需求,只需要创建一个实现类即可。


# 学生案例优化二:匿名内部类

在上述例子中不知道有没有看出来一些弊端,就是当每次有新的需求时,我们每次都要创建一个实现类。

那么如果我有20多个需求,是不是就需要创建20多个类来完成需求了?

此时我们使用匿名内部类来简化一下,不再创建实现类来完成需求了。

@Test
public void test() {
    // 获取 id 小于等于 103 的学生信息
    List<Student> list = filterStudent(students, new MyHandler<Student>() {
        @Override
        public boolean test(Student t) {
            return t.getId() <= 103;
        }
    });

    for (Student student : list) {
        System.out.println(student);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

可以看到,我们直接在调用时new一个MyHandler接口,并直接进行接口的方法重写进行实现。


# 学生案例优化三:Lambda表达式

可以在简洁一点吗?完全可以的,因为我们Java 8给我们提供了Lambda表达式,也是本文所述的主要知识点。

@Test
public void test6() {
    // 获取大于等于13岁的学生信息
    List<Student> list = filterStudent(students, student -> student.getAge() >= 13);
    for (Student student : list) {
        System.out.println(student);
    }
}
1
2
3
4
5
6
7
8

是不是感觉到了Lambda表达式的强大之处呢?省略了多少代码?省略了多少个实现类?

虽然难懂一点。

记住,写难懂的代码才能让你在公司拥有不可替代性

# Lambda 语法

Java 8中引入了一个新的操作符 ->该操作符称为箭头操作符 或 Lambda 操作符。

箭头操作符将 Lambda 表达式拆分成两部分:

左侧:Lambda 表达式的参数列表

右侧:Lambda 表达式中所需执行的功能,意指Lambda体。

在实际场景应用中,存在着三种语法格式。

# 一、无参数,无返回值

语法

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

Demo

@Test
public void test(){
    Runnable r = new Runnable() {
        @Override
        public void run() {
            System.out.println("Hello World!");
        }
    };
    r.run();
}
1
2
3
4
5
6
7
8
9
10

Lambda

当我们使用一个Runnable时,它是一个接口,该接口提供了一个无参方法,并且是无返回值的,使用Lambda表达式可以达到如下效果。

@Test
public void test2(){
    Runnable r1 = () -> System.out.println("Hello Lambda!");
    r1.run();
}
1
2
3
4
5

# 二、有一个参数,无返回值

语法

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

Demo

@Test
public void test(){
    Consumer<String> con = new Consumer<String>() {
        @Override
        public void accept(String s) {
            System.out.println(s);
        }
    };
    con.accept("Hello World!");
}
1
2
3
4
5
6
7
8
9
10

Lambda

使用Comsumer消费接口,它有一个有参方法,但是无返回值,可以得到如下效果。

@Test
public void test2(){
    Consumer<String> con = s -> System.out.println(s);
    con.accept();
}
1
2
3
4
5

细心的你应该留意到小括号不翼而飞了,这是一个语法糖。

小括号当只有一个参数时,可以省略不写。

# 三、两个参数,有返回值,多条执行语句

语法

(x, y) -> {
    System.out.println("xxx");
    xxx.xxx(x, y);
};
1
2
3
4

Demo

@Test
public void test() {
    Comparator<Integer> com = new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            System.out.println("xxx");
            return Integer.compare(o1, o2);
        }
    };
}
1
2
3
4
5
6
7
8
9
10

Lambda

Comparator<Integer> com = (x, y) -> {
    System.out.println("xxx");
    Integer.compare(x, y);
};
1
2
3
4

# Lambda 函数式接口

Lambda 表达式需要 函数式接口 的支持。

# 函数式接口是什么?

函数式接口,即一个接口中只有一个抽象方法,成为函数式接口。

我们通常会在函数式接口上使用注解 @FunctionalInterface 修饰,以规范并检查该类是否为函数式接口。

# 四大核心函数式接口

一、 消费型接口

public interface Consumer<T> {
    void accept(T t);
}
1
2
3

二、供给型接口

public interface Supplier<T> {
    T get();
}
1
2
3

三、函数型接口

public interface Function<T, R> {
	R apply(T t);
}
1
2
3

四、断言型接口

public interface Predicate<T> {
    boolean test(T t);
}
1
2
3

上述的四大核心接口是 Java 8 提供给我们的,也是日常当中使用最多的,可作用于各种应用场景。

比如消费型接口:

@Test
public void Test(){
    this.buyComputer("Mac Book Pro");
}
public void buyComputer(String computer){
    this.insertStr("我买了一台:", str -> System.out.println(str + computer));
}
public void insertStr(String string, Consumer<String> con){
    con.accept(string);
}
1
2
3
4
5
6
7
8
9
10

又比如供给型接口:

@Test
public void test(){
    List<Integer> numList = getNumList(10, () -> (int)(Math.random() * 100));
    for (Integer num : numList) {
        System.out.println(num);
    }
}
//需求:产生指定个数的整数,并放入集合中
public List<Integer> getNumList(int num, Supplier<Integer> sup){
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < num; i++) {
        Integer n = sup.get();
        list.add(n);
    }
    return list;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

至于函数型接口和断言型接口就不再举例了,请发挥你想象力。

# Lambda 方法引用

若 Lambda 体中的功能,已经有方法提供了实现,可以使用方法引用。

我们可以将方法引用理解为 Lambda 表达式的另外一种表现形式。

它可以为我们提供最为直观的方法调用。

需要注意的是:

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

# 语法

类名::MethodName
1

# 示例1

Consumer<String> con = System.out::println;
con.accept("Hello Java 8");
1
2

可以看出,这是一个最简单的例子,又比如这样:

public class TestService {
    public String show(){
        return "调用了 TestService 的 show 方法!";
    }
}
1
2
3
4
5
TestService testService = new TestService();
Supplier<String> sup = testService::show;
System.out.println(sup.get()); // 打印 调用了 TestService 的 show 方法!
1
2
3

# 示例2

首先我们有一个测试的service类。

public class TestService {
    public String show(){
        return "调用了 TestService 的 show 方法!";
    }
}
1
2
3
4
5

然后我们来一个方法的引用

@Test
public void test11(){
    System.out.println("step1--------------------------------");
    Function<TestService, String> fun = new Function<TestService, String>() {
        @Override
        public String apply(TestService testService) {
            return testService.show();
        }
    };
    String apply = fun.apply(new TestService());
    System.out.println(apply);

    System.out.println("step2--------------------------------");
    Function<TestService, String> fun2 = item -> item.show();
    String apply2 = fun2.apply(new TestService());
    System.out.println(apply2);


    System.out.println("step3--------------------------------");
    Function<TestService, String> fun3 = TestService::show;
    String apply3 = fun3.apply(new TestService());
    System.out.println(apply3);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

在上述的例子当中它们的执行结果是一样的,例子中从上到下做了三部分:

第一部分,就是我们最熟悉的匿名内部类了。

第二部分,使用了lambda表达式进行了优化。

重点是第三部分,可以看出省去了 item -> item.show(); 的写法,直接以类名TestService::show省去了书写参数、小括号、箭头等的代码,看上去更加的优雅。

简单的语句,蕴含着这么多东西,这就是为什么你曾经看别人写 Lambda 觉得特别难懂的原因!

# 示例3

最后再看另外一个例子:

BiPredicate<String, String> bp = (x, y) -> x.equals(y);
System.out.println(bp.test("123a", "123a"));

System.out.println("-----------------------------------------");

BiPredicate<String, String> bp2 = String::equals;
System.out.println(bp2.test("abc", "abc"));
1
2
3
4
5
6
7

在上述的例子当中,我已经能够预知你目前是一脸懵逼的状态,没关系,在看一下图解吧。

细心的你一定发现这其中的规律,当你的函数式接口提供的唯一方法的参数列表有2个,且有返回值时,并且你执行的方法体的方法参数只接收一个值时,可以使用第一个参数的参数类型::方法体来进行书写。

# 理解

通过上述的例子,你应该大致理解了什么是 Lambda 的方法引用。

这是一个慢慢习惯的过程,在方法引用上,大致可以分为如下几类:

  1. 对象的引用 :: 实例方法名
  2. 类名 :: 静态方法名
  3. 类名 :: 实力方法名

它其实只不过是一个语法糖,给予我们最优的解决了当流程比较复杂时,可以使用简洁的Lambda来实现这些流程。

其实最常见的也只不过是为了省略写方法时的小括号而已吧。

# 构造器引用

Lambda 也为我们提供了构造器的引用写法。

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

# 语法

类名::new
1

# 示例

还是以一个示例来的痛快。

public class TestService {
    public TestService(String val){
        System.out.println("Java8 " + val);
    }
}
1
2
3
4
5
@Test
public void test(){
    Function<String, TestService> fun =  TestService::new;
    fun.apply("我爱你"); // 打印 Java8 我爱你
}
1
2
3
4
5

上述例子证明,调用Function.apply方法后,它调用了TestService的构造方法,同时将Functionapply的参数传递给了构造器的val参数,从而达到这个效果。

本文由浅梦博客www.starmcc.com原创,转载请注明出处,请尊重别人的劳动成果,谢谢。

最近更新: 2019/12/28 下午8:31:51