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();
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。