Java基础汇总(十一)——Java8新特性

一、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 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值