Lambda

记录下个人见解:

从一个常见的例子认识Lambda表达式

启动一个线程执行一个任务: 原始写法:

public class Lambda {
    public static void main(String[] args) {
        new Thread(new MyTask());

    }
}

class MyTask implements Runnable {
    @Override
    public void run() {
        // do something
        System.out.println("这是任务1");
    }
}

步骤: - 创建一个实现类,实现Runable接口。 - 并重写run方法,里面的代码也即需要新开线程实现的功能。 - 使用new Thread(),创建线程,并将任务MyTask创建一个对象,放入Thread对象中。

那么问题来了,如果我有很多task,例如task1、task2、task3…,那么我们就要创建很多类,MyTask1…MyTaskN,然而这些MyTaskN,很多时候是无意义的,我们的目标只是执行一段代码而已,并不想创建类,增加维护复杂度。

总结下,我(代码)需要一个对象,并调用对象的指定方法。我不管这个对象的实现,我只需要定义一个接口,并定义好我需要调用的方法,你只负责实现这个接口,然后传递给我,我来执行调用。

再抽象下,这里创建新线程,执行新任务,本质上就是执行一段代码,但是java不支持代码段参数,所以需要将这段代码放在一个对象中,并协议好调用方法,自己手动创建一个Runable对象,run方法便是调用的方法。

如果不创建Runable的实现类,有什么其他办法实现呢?有,使用匿名类。

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("只是一个任务!");
            }
        });

不够简洁,简写,定义新的符号 ->

new Thread(() -> System.out.println("只是一个任务!"));

可以看出,Lambda表达式实际就是创建了一个匿名函数。下面分析下构造。

  • () 常规java参数,这里无参(实际参数,不需要类型,只需要值即可,类型可由上下文推断出来。(a, b)→ xxx , a, b都是参数值,不用定义类型)

  • -> 推导这是一个Lambda表达式,需要根据Thead类,推断出是哪一个接口的匿名实现类,这里是Runable。

  • System.out.println("只是一个任务!") 执行代码段,只有一行代码,可以省略`{}和分号(;), 如果多行,就用{}`包裹起来,并且使用分号(;) 。

至此,基本已表示清楚,需要注意一点,如何推导出类型的:

// 1
Runnable runnable = () -> System.out.println("只是一个任务!");
// 正确
new Thread(runnable);

// 2
VoidFunc0 other = () -> System.out.println("只是一个任务!");
// 错误
new Thread(other);

// 3 正确
new Thread(() -> {
    System.out.println("只是一个任务!");
});

从1、2可以看出,同一Lambda表达式,类型可以不同。也从侧面印证了Lambda表达式只是符合特定规律的一类表达式,返回类型可以自己定义,只要符合这个规则。这里不能推导出类型是Runable还是VoidFun0,说以需要指定类型。

从3可以知道,这里使用Thread的一个有参构造器,参数类型是Runable,故Lambda可以根据上下问推断出这个表达式类型是Runable的。

分析下Lambda表达式创建条件

有前面可知Lambda表达式是为了消除创建实现类,然后在进行创建对象当做参数。Runable以及VoidFunc0均为接口,由Lambda表达式进行实现。并且同一个表达式,可以有不同的类型接收。现在来看一看为何。

源码:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}


package cn.hutool.core.lang.func;

/**
 * 函数对象<br>
 * 接口灵感来自于<a href="http://actframework.org/">ActFramework</a><br>
 * 一个函数接口代表一个一个函数,用于包装一个函数为对象<br>
 * 在JDK8之前,Java的函数并不能作为参数传递,也不能作为返回值存在,此接口用于将一个函数包装成为一个对象,从而传递对象
 *
 * @author Looly
 *
 * @since 3.2.3
 */
@FunctionalInterface
public interface VoidFunc0 {
    void call() throws Exception;
}

共同点,都是有一个@FunctionalInterface注解,该注解的作用是标识此接口可用于Lambda表达式。那么什么样的接口可以用此注解标识呢?

通过JDK8源码javadoc,可以知道这个注解有以下特点: - 该注解只能标记在”有且仅有一个抽象方法”的接口上。 - JDK8接口中的静态方法和默认方法,都不算是抽象方法。 - 接口默认继承java.lang.Object,所以如果接口显示声明覆盖了Object中方法,那么也不算抽象方法。 - 该注解不是必须的,如果一个接口符合”函数式接口”定义,那么加不加该注解都没有影响。加上该注解能够更好地让编译器进行检查。如果编写的不是函数式接口,但是加上了@FunctionInterface,那么编译器会报错。

注:符合这4点的接口,即使不使用 @FunctionalInterface 注解,也可用于创建Lambda表达式,加上这个只是为了编译器检测,如果错误,会给出提示。类似于 @Override 。

为何只能有一个抽象方法?

() -> System.out.println("只是一个任务!");

可以近似理解,将这段代码视为抽象方法的实现。

扩展,通过创建自己的函数式接口,来体会Lambda表达式。

无参的形式

@FunctionalInterface
public interface MyNoArgsLambda {

    void doSomething();

}

public class MyNoArgsLambdaTest {
    public static void main(String[] args) {
        MyNoArgsLambda lambda = () -> System.out.println("You are ...");
        lambda.doSomething();
    }
}

控制台,会输出 You are ... Lambda表达式会生成一个对象,其实现了MyNoArgsLambda接口,并重写了`doSomething`方法,方法内容即为表达式内容。

单个参数

@FunctionalInterface
public interface SingleArgLambda {
    void doSomething(String arg);
}
public class SingleArgLambdaTest {
    public static void main(String[] args) {
        SingleArgLambda lambda = e -> System.out.println("you are " + e);
        lambda.doSomething("big");
    }
}

控制台输出:you are big。 这里有一个参数e,不需要加括号(e)→,不需要指定类型,会从SingleArgLambda推断出。

多个参数

@FunctionalInterface
public interface MultiArgsLambda {
    void doSomething(String a, int b, int c);
}
public class MultiArgsLambdaTest {
    public static void main(String[] args) {
        MultiArgsLambda lambda = (a, b, c) -> System.out.println(a + (b/c));
        lambda.doSomething("多参数除法:", 10, 2);
    }
}

控制台输出:多参数除法:5。 这里abc随便定义,也可以是xyz,类型对应MultiArgsLambda#doSomething的参数类型。