1.什么是lambda?

如果我们想要起一个线程来打印一串字符串,我们之前的写法通常是这样:

ExecutorService executorService = Executors.newFixedThreadPool(3);
executorService.execute(new Runnable() {
    @Override
    public void run() {
        System.out.println("hello world!");
    }
});
executorService.shutdown();

使用lambda表达式后,可以改写为这个样:

ExecutorService executorService = Executors.newFixedThreadPool(3);
executorService.execute(() -> System.out.println("hello world!"));
executorService.shutdown();

我们可以看到使用lambda表达式后,代码变得更加简洁,这里的"() -> System.out.println("hello word!")"其实就相当于Runnable接口的匿名实现,你会发现Runnable的抽象方法run()的签名与() -> System.out.println("hello word!")的签名是一致的(lambda表达式的签名下面会讲到)。简而言之,可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式,它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。

lambda表达式由三部分构成:参数列表、->(分割符)、主体,基本语法如下:
(parameters) -> expression 或者 (parameters) -> { statements; }

  • 参数列表:多个参数用逗号隔开,类型可以省略不写,要保证与函数式接口抽象方法的参数列表一致,如果只有一个参数"()"可以省略;
  • 箭头(->): 用于分割参数列表和主体;
  • 主体:可以直接是表达式,有多条语句要用花括号括起来,如果lambda需要返回一个值,那么返回值即是expression本身所表示的值,如有使用的“{}”,需要用return关键字返回具体值。

Lambda的类型是从使用Lambda的上下文推断出来的,上下文中Lambda表达式所需要代表的类型称为目标类型,如上示例中“() -> System.out.println("hello world!")” 代表的是Runnable类型的实例,所以相同的lambda表达式在不同的上下文中可能代表不同类型的函数式接口

2.函数式接口

假设上面的示例中,如果Runnable接口有两个抽象方法run()和run2(),那么lambda表达式该怎么表示呢,相当于重写了哪个方法呢?这种情况是不能使用lambda表达式的,只有在使用了函数式接口的地方才能使用lambda表达式,所以这里要说一下函数式接口的定义。所谓函数式接口,即:只有一个抽象方法的接口。 Java8已经为我们提供了一些常用的函数式接口,如下表:

函数式接口 函数描述符 原始类型特化
Predicate<T> T->boolean IntPredicate,LongPredicate, DoublePredicate
Consumer<T> T->void IntConsumer,LongConsumer, DoubleConsumer
Function<T,R> T->R IntFunction<R>, IntToDoubleFunction, IntToLongFunction, LongFunction<R>, LongToDoubleFunction, LongToIntFunction, DoubleFunction<R>, ToIntFunction<T>, ToDoubleFunction<T>, ToLongFunction<T>
Supplier<T> ()->T BooleanSupplier,IntSupplier, LongSupplier, DoubleSupplier
UnaryOperator<T> T->T IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator
BinaryOperator<T> (T,T)->T IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator
BiPredicate<L,R> (L,R)->boolean
BiConsumer<T,U> (T,U)->void ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T>
BiFunction<T,U,R> (T,U)->R ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U>

函数式接口的抽象方法的签名基本上就是Lambda表达式的签名,我们将这种抽象方法叫作函数描述符,比如T->boolean表示传入一个T类型的参数并返回boolean类型的值。

原始类型特化是在某个函数是接口上,把输入或输出参数特化为原始类型,这样就避免了拆装箱操作,以提高性能。例如IntPredicate把输入参数特化为int类型,ToLongFunction把返回值特化为long类型。

查看上表函数式接口的源码,会发现它们都有一个@FunctionalInterface注解,这是Java8提供的用来表示接口是否为函数式接口,但它不是必须的,只要接口只包含一个抽象方法就是函数式接口,只是如果接口上加上了@FunctionalInterface注解,那么往接口中添加其他抽象方法时编译就会报错,起到一个限定作用;
特殊的void兼容规则
如果一个Lambda的主体是一个语句表达式(expression),它就和一个返回void的函数描述符兼容(当然需要参数列表一致)。例如,以下两行都是合法的,尽管List的add方法返回了一个boolean,而不是Consumer上下文(T -> void)所要求的void:
// Predicate返回了一个boolean
Predicate<String> p = s -> list.add(s);
// Consumer返回了一个void
Consumer<String> b = s -> list.add(s);

3.lambda使用局部变量的限制

lambda可以没有限制的在主体中引用实例变量和静态变量,但是引用的局部变量必须声明为final或者事实上是final。因为实例变量存储在堆中,而局部变量保存在栈上。如果Lambda直接访问局部变量,而且Lambda是在另一个线程中使用的,当使用Lambda的线程时,可能会在分配该局部变量的线程将这个变量收回之后去访问该变量。因此,Java在访问局部变量时,实际上是在访问它的副本,而不是访问原始变量,如果局部变量仅仅赋值一次那么副本和原始变量就没有什么区别了——因此就有了这个限制,要保证副本和原始值保持一致。
<br/>例如下面的代码,如果把 "//name = "jack";" 注释去掉,就会报错

ExecutorService executorService = Executors.newFixedThreadPool(3);
String name = "tome";
executorService.execute(() -> System.out.println("hello " + name));
//name = "jack";
executorService.shutdown();

小飞侠
388 声望80 粉丝

一切都在进行着,唯一可能停止的只有自己!!!


引用和评论

0 条评论