如果我们想要起一个线程来打印一串字符串,我们之前的写法通常是这样:
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的类型是从使用Lambda的上下文推断出来的,上下文中Lambda表达式所需要代表的类型称为 目标类型 ,如上示例中“() -> System.out.println("hello world!")” 代表的是Runnable类型的实例,所以相同的lambda表达式在不同的上下文中可能代表不同类型的函数式接口
假设上面的示例中,如果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);
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();