一、Lambda表达式(闭包)
Lambda允许把函数作为一个方法的参数,即函数可以作为参数传递进方法中
Lambda表达式格式:(逗号分隔的参数列表、->符号与函数体)
例1:
Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) );
//参数e的类型是由编译器推测出来的
Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.println( e ) );
//把参数类型与参数包括在括号中的形式直接给出参数的类型
Arrays.asList( "a", "b", "d" ).forEach( e -> {
System.out.print( e );
System.out.print( e );
} );
//复杂函数体可以放在一对花括号中
Lambda可以引用类的成员变量与局部变量,但如果如果这些变量不是类型final,它们会被隐含的转为final类型
例2:
String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach(
( String e ) -> System.out.print( e + separator ) );
//与下面代码等价
final String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach(
( String e ) -> System.out.print( e + separator ) );
Lambda可以返回一个值,但若Lambda的函数体只有一行的话,没必要显式使用return语句
例3:
Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> e1.compareTo( e2 ) );
//与下面代码等价
//Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> {
int result = e1.compareTo( e2 );
return result;
} );
二、Lambda表达式与匿名内部类的区别
在Java8增加Lambda特性之前,Java程序员不得不使用匿名类来实现函数参数化并传入其他方法中
匿名内部类:
其仍是一个类,无需显式指定类名,编译器会自动为该类取名
public class LambdaTest {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello World");
}
}).start();
}
}
编译之后将会产生两个 class 文件
LambdaTest.class
LambdaTest$1.class
使用 javap -c LambdaTest.class 进一步分析 LambdaTest.class 的字节码,部分结果如下
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Thread
3: dup
4: new #3 // class com/example/myapplication/lambda/LambdaTest$1
7: dup
8: invokespecial #4 // Method com/example/myapplication/lambda/LambdaTest$1."<init>":()V
11: invokespecial #5 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
14: invokevirtual #6 // Method java/lang/Thread.start:()V
17: return
在 4: new #3 这一行创建了匿名内部类的对象
Lambda表达式:
//上述代码变为lambda形式
public class LambdaTest {
public static void main(String[] args) {
new Thread(() -> System.out.println("Hello World")).start();
}
}
编译后只会产生一个文件 LambdaTest.class(只产生一个class文件)
javap 对该文件反编译后的结果
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Thread
3: dup
4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
12: invokevirtual #5 // Method java/lang/Thread.start:()V
15: return
从上面的结果我们发现 Lambda 表达式被封装成了主类的一个私有方法,并通过 invokedynamic 指令进行调用,即:
- Lambda 表达式是通过 invokedynamic 指令实现的
- Lambda 表达式不会产生新的类
- Lambda 表达式中使用 this 关键字时,其指向的是外部类的引用(因为其不会创建匿名内部类)
三、函数式接口
函数式接口是为使现有的函数友好地支持Lambda表达式而提出的概念
函数式接口只定义了唯一的抽象方法的接口(除了隐含的Object对象的公共方法),若增加新的方法,就非函数式接口,编译会失败
函数式接口里还允许定义 java.lang.Object 里的 public 方法,因为任何一个函数式接口的实现,默认都继承了 Object 类,包含了来自 java.lang.Object 里对这些抽象方法的实现
但若是用@FunctionalInterface注解,可以在接口中添加默认方法与静态方法,无论几个(Java8中所有类库的已有接口都添加了@FunctionalInterface注解)
例:
//增加默认方法(default)
@FunctionalInterface
public interface XttblogService{
void sayMessage(String message);
default void doSomeMoreWork1(){
}
default void doSomeMoreWork2(){
}
}
//增加静态方法(static)
@FunctionalInterface
public interface XttblogService {
void sayMessage(String message);
static void printHello(){
System.out.println("Hello");
}
static void xttblogHello(){
System.out.println("Xttblog Hello");
}
}
//Object类中的public方法
@FunctionalInterface
public interface XttblogService {
void sayMessage(String message);
@Override
boolean equals(Object obj);
@Override
String toString();
@Override
int hashCode();
}
典型的函数式接口:
- java.lang.Runnable
- java.util.concurrent.Callable
Lambda表达式是如何符合 Java 类型系统的?
每个Lambda对应于一个给定的类型,用一个接口来说明。而这个被称为函数式接口(functional interface)的接口必须仅仅包含一个抽象方法声明。每个那个类型的Lambda表达式都将会被匹配到这个抽象方法上。因此默认的方法并不是抽象的,你可以给你的函数式接口自由地增加默认的方法。
例(函数式接口使用):
public class 函数式接口使用 {
@FunctionalInterface
interface A {
void say();
default void talk() {
}
}
@Test
public void test1() {
A a = () -> System.out.println("hello");
a.say();
}
@FunctionalInterface
interface B {
void say(String i);
}
public void test2() {
//下面两个是等价的,都是通过B接口来引用一个方法,而方法可以直接使用::来作为方法引用
B b = System.out::println;
B b1 = a -> Integer.parseInt("s");//这里的a其实换成别的也行,只是将方法传给接口作为其方法实现
B b2 = Integer::valueOf;//i与方法传入参数的变量类型一直时,可以直接替换
B b3 = String::valueOf;
//B b4 = Integer::parseInt;类型不符,无法使用
}
@FunctionalInterface
interface C {
int say(String i);
}
public void test3() {
C c = Integer::parseInt;//方法参数和接口方法的参数一样,可以替换。
int i = c.say("1");
//当我把C接口的int替换为void时就会报错,因为返回类型不一致。
System.out.println(i);
//综上所述,lambda表达式提供了一种简便的表达方式,可以将一个方法传到接口中。
//函数式接口是只提供一个抽象方法的接口,其方法由lambda表达式注入,不需要写实现类,
//也不需要写匿名内部类,可以省去很多代码,比如实现runnable接口。
//函数式编程就是指把方法当做一个参数或引用来进行操作。除了普通方法以外,静态方法,构造方法也是可以这样操作的。
}
}
四、方法引用
详细使用方法参考:
Java双冒号(::)运算符详解_会飞的鱼干干的博客-CSDN博客_java 双冒号 多参数
方法引用可以看作Lambda表达式的更简洁的一种表达形式,使用::操作符
方法引用分类:
- 指向静态方法的方法引用(例:Integer的parseInt方法,写作Integer::parseInt)
- 指向任意类型实例方法的方法引用(例:String的length方法,写作String::length)
- 指向现有对象的实例方法的方法引用(例:假设有一个本地变量localVariable用于存放Variable类型的对象,它支持实例方法getValue,那么可以写成localVariable::getValue)
例:
Function<String, Integer> stringToInteger = (String s) -> Integer.parseInt(s);
//使用方法引用
Function<String, Integer> stringToInteger = Integer::parseInt;
//构造函数引用(方法引用中的一种特殊的形式)
Supplier<SomeClass> c1 = SomeClass::new;
SomeClass s1 = c1.get();
//等价于
Supplier<SomeClass> c1 = () -> new SomeClass();
SomeClass s1 = c1.get();
//构造函数有一个参数的情况
Function<Integer, SomeClass> c1 = SomeClass::new;
SomeClass s1 = c1.apply(100);
//等价于
Function<Integer, SomeClass> c1 = i -> new SomeClass(i);
SomeClass s1 = c1.apply(100);
五、函数式接口中的默认方法:
使用default 关键字给接口增加非抽象的方法实现,这个特性也被叫做扩展方法(Extension Methods)
例:
- static和default不能同时使用
- 可以有多个默认方法
- 实现类方法不能重名
public class 接口的默认方法 {
class B implements A {
// void a(){}实现类方法不能重名
}
interface A {
//可以有多个默认方法
public default void a(){
System.out.println("a");
}
public default void b(){
System.out.println("b");
}
//报错static和default不能同时使用
// public static default void c(){
// System.out.println("c");
// }
}
public void test() {
B b = new B();
b.a();
}
}
在Java8中,Map接口也增加了一些默认方法
默认方法的使用场景:
- 可选方法
- 行为的多继承
处理默认方法的冲突规则:
- 类中的方法优先级最高:类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。
- 子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口(如果B继承了A,那么B就比A更具体)
- 如果上述方法还是无法判断默认方法的优先级,则继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现
//显式选择默认方法公式
//X.super.m(..)显式地调用希望调用的方法
public class C implements B, A {
public void hello() {
B.super().hello();
}
}
默认方法与抽象方法不同之处在于抽象方法必须要求实现,但是默认方法则没有这个要求
每个接口都必须提供一个所谓的默认实现,这样所有的接口实现者将会默认继承它(如果有必要的话,可以覆盖这个默认实现)
private interface Defaulable {
// Interfaces now allow default methods, the implementer may or
// may not implement (override) them.
default String notRequired() {
return "Default implementation";
}
}
private static class DefaultableImpl implements Defaulable {
}
private static class OverridableImpl implements Defaulable {
@Override
public String notRequired() {
return "Overridden implementation";
}
}
//Defaulable接口用关键字default声明了一个默认方法notRequired()
//Defaulable接口的实现者之一DefaultableImpl实现了这个接口,并且让默认方法保持原样
//Defaulable接口的另一个实现者OverridableImpl用自己的方法覆盖了默认方法
//Java 8带来的另一个有趣的特性是接口可以声明(并且可以提供实现)静态方法
private interface DefaulableFactory {
// Interfaces now allow static methods
static Defaulable create( Supplier< Defaulable > supplier ) {
return supplier.get();
}
}
public static void main( String[] args ) {
Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::new );
System.out.println( defaulable.notRequired() );
defaulable = DefaulableFactory.create( OverridableImpl::new );
System.out.println( defaulable.notRequired() );
}
//输出
//Default implementation
//Overridden implementation
//在JVM中,默认方法的实现是非常高效的,并且通过字节码指令为方法调用提供了支持
//默认方法允许继续使用现有的Java接口,而同时能够保障正常的编译过程
在声明一个默认方法前,请仔细思考是不是真的有必要使用默认方法,因为默认方法会带给程序歧义,并且在复杂的继承体系中容易产生编译错误.
六、重复注解
在Java8之前,相同的注解在同一位置只能声明一次,不能声明多次。Java 8打破了这条规则,引入了重复注解机制,这样相同的注解可以在同一地方声明多次。
重复注解机制本身必须用@Repeatable注解
例:
package com.javacodegeeks.java8.repeatable.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
public class RepeatingAnnotations {
@Target( ElementType.TYPE )
@Retention( RetentionPolicy.RUNTIME )
public @interface Filters {
Filter[] value();
}
@Target( ElementType.TYPE )
@Retention( RetentionPolicy.RUNTIME )
@Repeatable( Filters.class )
public @interface Filter {
String value();
};
@Filter( "filter1" )
@Filter( "filter2" )
public interface Filterable {
}
public static void main(String[] args) {
for( Filter filter: Filterable.class.getAnnotationsByType( Filter.class ) ) {
System.out.println( filter.value() );
}
}
}
//输出结果
//filter1 filter2
正如我们看到的,这里有个使用@Repeatable( Filters.class )注解的注解类Filter,Filters仅仅是Filter注解的数组,但Java编译器并不想让程序员意识到Filters的存在。这样,接口Filterable就拥有了两次Filter(并没有提到Filter)注解。
同时,反射相关的API提供了新的函数getAnnotationsByType()来返回重复注解的类型(请注意Filterable.class.getAnnotation( Filters.class )经编译器处理后将会返回Filters的实例)。
七、Java库新特性
Optional
到目前为止,臭名昭著的空指针异常是导致Java应用程序失败的最常见原因。以前,为了解决空指针异常,Google公司著名的Guava项目引入了Optional类,Guava通过使用检查空值的方式来防止代码污染,它鼓励程序员写更干净的代码。受到Google Guava的启发,Optional类已经成为Java 8类库的一部分。
Optional实际上是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。更多详情请参考官方文档。
我们下面用两个小例子来演示如何使用Optional类:一个允许为空值,一个不允许为空值。
public class 空指针Optional {
public static void main(String[] args) {
//使用of方法,仍然会报空指针异常
// Optional optional = Optional.of(null);
// System.out.println(optional.get());
//抛出没有该元素的异常
//Exception in thread "main" java.util.NoSuchElementException: No value present
// at java.util.Optional.get(Optional.java:135)
// at com.javase.Java8.空指针Optional.main(空指针Optional.java:14)
// Optional optional1 = Optional.ofNullable(null);
// System.out.println(optional1.get());
Optional optional = Optional.ofNullable(null);
System.out.println(optional.isPresent());
System.out.println(optional.orElse(0));//当值为空时给与初始值
System.out.println(optional.orElseGet(() -> new String[]{"a"}));//使用回调函数设置默认值
//即使传入Optional容器的元素为空,使用optional.isPresent()方法也不会报空指针异常
//所以通过optional.orElse这种方式就可以写出避免空指针异常的代码了
//输出Optional.empty。
}
}
如果Optional类的实例为非空值的话,isPresent()返回true,否从返回false。为了防止Optional为空值,orElseGet()方法通过回调函数来产生一个默认值。map()函数对当前Optional的值进行转化,然后返回一个新的Optional实例。orElse()方法和orElseGet()方法类似,但是orElse接受一个默认值而不是一个回调函数。
Optional< String > firstName = Optional.of( "Tom" );
System.out.println( "First Name is set? " + firstName.isPresent() );
System.out.println( "First Name: " + firstName.orElseGet( () -> "[none]" ) );
System.out.println( firstName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
System.out.println();
//输出
//First Name is set? true First Name: Tom Hey Tom!
Stream
最新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。这是目前为止对Java类库最好的补充,因为Stream API可以极大提供Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。
Stream API极大简化了集合框架的处理(但它的处理的范围不仅仅限于集合框架的处理,这点后面我们会看到)。让我们以一个简单的Task类为例进行介绍:
Task类有一个分数的概念(或者说是伪复杂度),其次是还有一个值可以为OPEN或CLOSED的状态.让我们引入一个Task的小集合作为演示例子:
final Collection< Task > tasks = Arrays.asList(
new Task( Status.OPEN, 5 ),
new Task( Status.OPEN, 13 ),
new Task( Status.CLOSED, 8 )
);
我们下面要讨论的第一个问题是所有状态为OPEN的任务一共有多少分数?在Java 8以前,一般的解决方式用foreach循环,但是在Java 8里面我们可以使用stream:一串支持连续、并行聚集操作的元素。
// Calculate total points of all active tasks using sum()
final long totalPointsOfOpenTasks = tasks
.stream()
.filter( task -> task.getStatus() == Status.OPEN )
.mapToInt( Task::getPoints )
.sum();
System.out.println( "Total points: " + totalPointsOfOpenTasks );
//输出
//Total points: 18
这里有几个注意事项。
第一,task集合被转换化为其相应的stream表示。然后,filter操作过滤掉状态为CLOSED的task。
下一步,mapToInt操作通过Task::getPoints这种方式调用每个task实例的getPoints方法把Task的stream转化为Integer的stream。最后,用sum函数把所有的分数加起来,得到最终的结果。
在继续讲解下面的例子之前,关于stream有一些需要注意的地方(详情在这里).stream操作被分成了中间操作与最终操作这两种。
中间操作返回一个新的stream对象。中间操作总是采用惰性求值方式,运行一个像filter这样的中间操作实际上没有进行任何过滤,相反它在遍历元素时会产生了一个新的stream对象,这个新的stream对象包含原始stream 中符合给定谓词的所有元素。
像forEach、sum这样的最终操作可能直接遍历stream,产生一个结果或副作用。当最终操作执行结束之后,stream管道被认为已经被消耗了,没有可能再被使用了。在大多数情况下,最终操作都是采用及早求值方式,及早完成底层数据源的遍历。
stream另一个有价值的地方是能够原生支持并行处理。让我们来看看这个算task分数和的例子。
stream另一个有价值的地方是能够原生支持并行处理。让我们来看看这个算task分数和的例子。
// Calculate total points of all tasks
final double totalPoints = tasks
.stream()
.parallel()
.map( task -> task.getPoints() ) // or map( Task::getPoints )
.reduce( 0, Integer::sum );
System.out.println( "Total points (all tasks): " + totalPoints );
//Total points (all tasks): 26.0
这个例子和第一个例子很相似,但这个例子的不同之处在于这个程序是并行运行的,其次使用reduce方法来算最终的结果。
经常会有这个一个需求:我们需要按照某种准则来对集合中的元素进行分组。Stream也可以处理这样的需求,下面是一个例子:
// Group tasks by their status
final Map< Status, List< Task > > map = tasks
.stream()
.collect( Collectors.groupingBy( Task::getStatus ) );
System.out.println( map );
//{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}
让我们来计算整个集合中每个task分数(或权重)的平均值来结束task的例子。
// Calculate the weight of each tasks (as percent of total points)
final Collection< String > result = tasks
.stream() // Stream< String >
.mapToInt( Task::getPoints ) // IntStream
.asLongStream() // LongStream
.mapToDouble( points -> points / totalPoints ) // DoubleStream
.boxed() // Stream< Double >
.mapToLong( weigth -> ( long )( weigth * 100 ) ) // LongStream
.mapToObj( percentage -> percentage + "%" ) // Stream< String>
.collect( Collectors.toList() ); // List< String >
System.out.println( result );
//[19%, 50%, 30%]
最后,就像前面提到的,Stream API不仅仅处理Java集合框架。像从文本文件中逐行读取数据这样典型的I/O操作也很适合用Stream API来处理。下面用一个例子来应证这一点。
final Path path = new File( filename ).toPath();
try( Stream< String > lines = Files.lines( path, StandardCharsets.UTF_8 ) ) {
lines.onClose( () -> System.out.println("Done!") ).forEach( System.out::println );
}
对一个stream对象调用onClose方法会返回一个在原有功能基础上新增了关闭功能的stream对象,当对stream对象调用close()方法时,与关闭相关的处理器就会执行。
Stream API、Lambda表达式与方法引用在接口默认方法与静态方法的配合下是Java 8对现代软件开发范式的回应。更多详情请参考官方文档。
Date/Time API (JSR 310)
Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。对日期与时间的操作一直是Java程序员最痛苦的地方之一。标准的 java.util.Date以及后来的java.util.Calendar一点没有改善这种情况(可以这么说,它们一定程度上更加复杂)。
这种情况直接导致了Joda-Time——一个可替换标准日期/时间处理且功能非常强大的Java API的诞生。Java 8新的Date-Time API (JSR 310)在很大程度上受到Joda-Time的影响,并且吸取了其精髓。新的java.time包涵盖了所有处理日期,时间,日期/时间,时区,时刻(instants),过程(during)与时钟(clock)的操作。在设计新版API时,十分注重与旧版API的兼容性:不允许有任何的改变(从java.util.Calendar中得到的深刻教训)。如果需要修改,会返回这个类的一个新实例。
让我们用例子来看一下新版API主要类的使用方法。第一个是Clock类,它通过指定一个时区,然后就可以获取到当前的时刻,日期与时间。Clock可以替换System.currentTimeMillis()与TimeZone.getDefault()。
// Get the system clock as UTC offset
final Clock clock = Clock.systemUTC();
System.out.println( clock.instant() );
System.out.println( clock.millis() );
//2014-04-12T15:19:29.282Z
//1397315969360
我们需要关注的其他类是LocaleDate与LocalTime。LocaleDate只持有ISO-8601格式且无时区信息的日期部分。相应的,LocaleTime只持有ISO-8601格式且无时区信息的时间部分。LocaleDate与LocalTime都可以从Clock中得到。
// Get the local date and local time
final LocalDate date = LocalDate.now();
final LocalDate dateFromClock = LocalDate.now( clock );
System.out.println( date );
System.out.println( dateFromClock );
// Get the local date and local time
final LocalTime time = LocalTime.now();
final LocalTime timeFromClock = LocalTime.now( clock );
System.out.println( time );
System.out.println( timeFromClock );
//2014-04-12
//2014-04-12
//11:25:54.568
//15:25:54.568
最后,让我们看一下Duration类:在秒与纳秒级别上的一段时间。Duration使计算两个日期间的不同变的十分简单。下面让我们看一个这方面的例子。
// Get duration between two dates
final LocalDateTime from = LocalDateTime.of( 2014, Month.APRIL, 16, 0, 0, 0 );
final LocalDateTime to = LocalDateTime.of( 2015, Month.APRIL, 16, 23, 59, 59 );
final Duration duration = Duration.between( from, to );
System.out.println( "Duration in days: " + duration.toDays() );
System.out.println( "Duration in hours: " + duration.toHours() );
上面的例子计算了两个日期2014年4月16号与2014年4月16号之间的过程。下面是程序在控制台上的输出:
Duration in days: 365 Duration in hours: 8783 对Java 8在日期/时间API的改进整体印象是非常非常好的。一部分原因是因为它建立在“久战杀场”的Joda-Time基础上,另一方面是因为用来大量的时间来设计它,并且这次程序员的声音得到了认可。更多详情请参考官方文档。
并行(parallel)数组
Java 8增加了大量的新方法来对数组进行并行处理。可以说,最重要的是parallelSort()方法,因为它可以在多核机器上极大提高数组排序的速度。下面的例子展示了新方法(parallelXxx)的使用。
package com.javacodegeeks.java8.parallel.arrays;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
public class ParallelArrays {
public static void main( String[] args ) {
long[] arrayOfLong = new long [ 20000 ];
Arrays.parallelSetAll( arrayOfLong,
index -> ThreadLocalRandom.current().nextInt( 1000000 ) );
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
i -> System.out.print( i + " " ) );
System.out.println();
Arrays.parallelSort( arrayOfLong );
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
i -> System.out.print( i + " " ) );
System.out.println();
}
}
//这个程序在控制台上的输出如下(请注意数组元素是随机生产的):
//Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378 Sorted: 39 220 263 268 325 607 655 678 723 793
上面的代码片段使用了parallelSetAll()方法来对一个有20000个元素的数组进行随机赋值。然后,调用parallelSort方法。这个程序首先打印出前10个元素的值,之后对整个数组排序。
CompletableFuture
在Java8之前,我们会使用JDK提供的Future接口来进行一些异步的操作,其实CompletableFuture也是实现了Future接口, 并且基于ForkJoinPool来执行任务,因此本质上来讲,CompletableFuture只是对原有API的封装, 而使用CompletableFuture与原来的Future的不同之处在于可以将两个Future组合起来,或者如果两个Future是有依赖关系的,可以等第一个执行完毕后再实行第二个等特性。
先来看看基本的使用方式:
public Future<Double> getPriceAsync(final String product) {
final CompletableFuture<Double> futurePrice = new CompletableFuture<>();
new Thread(() -> {
double price = calculatePrice(product);
futurePrice.complete(price); //完成后使用complete方法,设置future的返回值
}).start();
return futurePrice;
}
得到Future之后就可以使用get方法来获取结果,CompletableFuture提供了一些工厂方法来简化这些API,并且使用函数式编程的方式来使用这些API,例如:
Fufure price = CompletableFuture.supplyAsync(() -> calculatePrice(product));
代码是不是一下子简洁了许多呢。之前说了,CompletableFuture可以组合多个Future,不管是Future之间有依赖的,还是没有依赖的。
如果第二个请求依赖于第一个请求的结果,那么可以使用thenCompose方法来组合两个Future
public List<String> findPriceAsync(String product) {
List<CompletableFutute<String>> priceFutures = tasks.stream()
.map(task -> CompletableFuture.supplyAsync(() -> task.getPrice(product),executor))
.map(future -> future.thenApply(Work::parse))
.map(future -> future.thenCompose(work -> CompletableFuture.supplyAsync(() -> Count.applyCount(work), executor)))
.collect(Collectors.toList());
return priceFutures.stream().map(CompletableFuture::join).collect(Collectors.toList());
}
上面这段代码使用了thenCompose来组合两个CompletableFuture。supplyAsync方法第二个参数接受一个自定义的Executor。 首先使用CompletableFuture执行一个任务,调用getPrice方法,得到一个Future,之后使用thenApply方法,将Future的结果应用parse方法, 之后再使用执行完parse之后的结果作为参数再执行一个applyCount方法,然后收集成一个CompletableFuture的List, 最后再使用一个流,调用CompletableFuture的join方法,这是为了等待所有的异步任务执行完毕,获得最后的结果。
注意,这里必须使用两个流,如果在一个流里调用join方法,那么由于Stream的延迟特性,所有的操作还是会串行的执行,并不是异步的。
再来看一个两个Future之间没有依赖关系的例子:
Future<String> futurePriceInUsd = CompletableFuture.supplyAsync(() -> shop.getPrice(“price1”))
.thenCombine(CompletableFuture.supplyAsync(() -> shop.getPrice(“price2”)), (s1, s2) -> s1 + s2);
这里有两个异步的任务,使用thenCombine方法来组合两个Future,thenCombine方法的第二个参数就是用来合并两个Future方法返回值的操作函数。
有时候,我们并不需要等待所有的异步任务结束,只需要其中的一个完成就可以了,CompletableFuture也提供了这样的方法:
//假设getStream方法返回一个Stream<CompletableFuture<String>>
CompletableFuture[] futures = getStream(“listen”).map(f -> f.thenAccept(System.out::println)).toArray(CompletableFuture[]::new);
//等待其中的一个执行完毕
CompletableFuture.anyOf(futures).join();
使用anyOf方法来响应CompletableFuture的completion事件。
参考文章:
Java-Tutorial/21、Java8新特性终极指南.md at master · h2pl/Java-Tutorial · GitHub