Java8部分新特性的学习
Posted 行歌
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java8部分新特性的学习相关的知识,希望对你有一定的参考价值。
Java8中的新特性
一、Lambda表达式
Lambda表达式可以理解为一种可传递的匿名函数:它没有名称,但又参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
匿名:和匿名类类似的,它没有明确的名字
函数:Lambda函数不属于某个特定的类,但和方法一样都具有参数列表、函数主体、返回类型,还可以有抛出的异常列表
传递:Lambda表达式可以作为参数传递给方法或者存储在变量中。
Lambda表达式的基本形式: (parameters)-> expression 或(parameters)-> {statements;}
怎么使用Lambda表达式?
哪里可以使用Lambda表达式?你可以在函数式接口上使用Lambda表达式。
函数式接口也就是只定义了一个抽象方法的接口。Lambda表达式允许直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。
上面既然提到了Lambda表达式是函数式接口的抽象方法的具体实现,那这两者之间的函数描述符应该是一致的,才可以编译成功。
如
public void process(Runnable r){ r.run(); } process(()->System.out.println(“this is awesome”));
Runnable接口的函数描述符为()->void,而其对应的Lambda表达式实现也为()->void。签名一致的情况下才能通过类型检查。
函数式接口:
在Java8以前,常用的函数式接口Comparable、Runnable、Callable等。而在Java8中又引入了一系列的函数式接口。
常用的函数式接口
函数式接口 |
函数描述符 |
原始类型特化 |
Predicate<T> |
T->boolean |
由于Java中存在将原始类型自动转化为对应包装类型的机制以及对应的逆过程(装箱和拆箱),但进行这一操作会影响性能(装箱后的值是将基本类型包裹起来,并保存在堆中,这样一来就需要更多的内存,并需要额外的内存搜索来获取被包裹的值。故而Java8为函数式接口提供了原始类型特化的版本,以便在输入和输出都是原始类型时避免自动装箱,针对转梦的输入参数类型的函数式接口的名称都要加上对应的基本类型前缀。如DoublePredicate,LongPredicate,IntPredicate) |
Consumer<T> |
T->void |
|
Function<T> |
T->R |
|
Supplier<T> |
()->T |
|
UnaryOperator<T> |
T->T |
|
BinaryOperator<T> |
(T,T)->T |
|
BiPredicate<L,R> |
(L,R)->boolean |
|
BiConsumer<T,U> |
(T,U)->void |
|
BiFunction<T,U,R> |
(T,U)->R |
类型检查:
Lambda的类型是从使用Lambda的上下文(比如接受它传递的方法的参数或者接受它的值的局部变量)中推断出来的,而我们且将上下文要求Lambda表达式需要的类型称为目标类型。当目标类型的抽象方法的函数描述符和Lambda表达式的函数描述符一致时,即类型检查无误。这样我们就可以将一个Lambda表达式应用到不同的函数式接口中,只要二者的函数描述符一致即可。
注意点:特出的void兼容规则
如果一个Lambda的主体是一个语句表达式,它就和一个返回void的函数描述符兼容(参数列表兼容的前提下)
类型推断:Java编译器可以从上下文推断出用什么函数式接口来配合Lambda表达式,也能推断出使用Lambda的前面,这样就可以在Lambbda语法中省去标注参数类型。
Lambda与闭包:
闭包:闭包是一个函数的实例,可以无限制地范围那个函数的非本地变量。如闭包可以作为参数传递给另一个函数,也可以访问和修改器作用域之外的变量。
而Java8中Lambda表达式和匿名类可以做类似于闭包的事情,可以作为参数传递给方法,并且可以访问其作用域之外的变量,但存在一个限制,它们不能修改定义Lambda的方法的局部变量的内容,这些变量都必须是隐式最终的。而匿名类则为显式最终的(可以理解为是对值封闭,而不是对变量封闭)
二、方法引用:
方法引用可以被看做仅仅调用特定方法情形下的Lambda的一种快捷写法。它表示,如果一个Lambda表达式代表的只是直接调用这个方法,那最好还是用名称来调用它,而不是去描述如何调用它。
示例:
(Apple a)->a.getWeight() 等价于方法引用的 Apple::getWeight
(str,i)->str.substring(i) 等价于string::substring
如何构建方法引用:
指向静态方法的方法引用:
指向任意类型的实例方法的方法引用
指向现有对象的实例方法的方法引用
构造函数引用
对于一个现有构造函数,可以利用它的名称和关键字new来创建它的一个引用ClassName::new
Supplier<Apple> c1= Apple::new; Apple a1=c1.get(); Function<Integer,Apple> c2=Apple::new; Apple a2=c2.apply(110); BiFunction<String,Integer,Apple> c3=Apple::new; Apple a3=c3.apply(“green”,110);
三、Stream
1.Stream的概念简析
流(Stream)是Java API的新成员,允许你以声明性方法处理数据集合(在一定程度上有点类似于SQL语句),它表示的是要做什么,而不是怎么做。此外,它还可以透明的并行处理。
下面先就展现一个流的示例写法:
List<String> threeHighCaloricDishNames = menu.stream()
//筛选
.filter(dish -> dish.getCalories()>500)
//排序,降序
.sorted(comparing(Dish::getCalories).reversed())
//转换
.map(Dish::getName)
//限制输出为3个
.limit(3)
//将流转换为集合
.collect(Collectors.toList());
System.out.println(threeHighCaloricDishNames);
上述代码完成了下述操作:
获取一个数据源(source)→ 数据转换→执行操作获取想要的结果
这里我们将数据转换中的操作称为中间操作,最后的执行操作获得想要的结果称作终端操作。
2.Stream的基本操作
以下是一些常见的中间操作和终端操作
中间操作
操作 |
返回类型 |
操作参数 |
函数描述符 |
filter |
Stream<T> |
Predicate<T> |
T->boolean |
map |
Stream<R> |
Function<T,R> |
T->R |
faltmap |
Stream<R> |
Function<T,R> |
T->Stream<R> |
limit |
Stream<T> |
|
|
sorted |
Stream<T> |
Comparator<T> |
(T,T)->int |
distinct |
Stream<T> |
|
|
终端操作
操作 |
返回类型 |
目的 |
forEach |
void |
消费流中的每个元素并对其应用Lambda。 |
count |
long |
返回流中的元素个数 |
collect |
根据目的决定 |
把流规约成一个集合,如List、Map、Set甚至是Integer。 |
anyMatch |
boolean |
检查流中是否有所有元素能匹配给定的谓词 |
anyMatch |
boolean |
检查流中是否有一个元素能匹配给定的谓词 |
findAny |
Optional<T> |
查找到任一个匹配给定谓词的元素 |
findFirst |
Optional<T> |
查找到第一个匹配给定谓词的元素 |
reduce |
Optional<T> |
将流中所有元素组合起来,完成指定的操作,如求和、求积、求最大值、最小值等 |
注意点:
每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道
在对一个Stream进行多次中间操作时,由于转换操作具有延迟特性(多个转换操作只会在终端操作的时候融合起来,然后一次循环完成)。我们可以简单的这样理解,在Stream中有个操作函数的集合,每次转换操作的时候都会把转换函数放到这个集合中,在终端操作的时候循环Stream对应的集合,然后对每个元素执行所有的函数。
此外,有些中间操作和终端操作具有短路的特性。
即指对于一个中间操作而言,如果它接收的是一个无限大的流,但返回的是有限的新流。(如limt)
对于一个终端操作,如果它接受的是一个无限大的流,但能在有限的时间内计算出结果(如findFirst、findAny等)
3.Stream的使用
Stream的使用涉及到三点:
Stream的构建、Stream的转换、Stream的归约
1)Stream的构建:
最常用的创建Stream有两种途径:
a) 通过Stream接口的静态工厂方法(注意:Java8里接口可以带静态方法);
b) 通过Collection接口的默认方法--stream(),把一个Collection对象转换成Stream
Stream接口的静态工程方法
- of方法
- generate方法
- iterate方法
Collection接口的默认方法:
stream()
2)Stream的转换:
Stream中转换需要了解的就是刚才前面提到的Stream的中间操作。
3)Stream的归约:
Stream中的归约的最基本的方法即前面提到的Stream的终端操作。
这边需要重点强调的就是collect方法。它可以接受各种做法作为参数,将流中的元素累积成一个汇总结构。具体的做法是通过定义一个新的Collector接口来定义的。
Collector类中具有很多的工厂方法,如前面使用过的toList方法,或是toSet、toCollection、counting、summingInt等。
根据不同的工厂方法,可以实现不同的归约操作。此外你还可以实现自定义收集器。
4.Stream的并行化
在前面提到过流为并行化处理数据提供了便利,这边特别要强调的是这种流并行更加适用于计算密集型并且不涉及到IO操作的。
一个最基本的代码如下:
public static void parallelSum(long n){ Stream.iterate(1L,i->i+1) .limit(n) .parallel() .reduce(0L,Long::sum); }
这里通过parallel方法将一个顺序流转换成了并行流。
为了显式控制在遍历流的过程中哪些操作要并行执行,那些操作到顺序执行,可以这样做
Stream.iterate(1L,i->i+1)
.filter(…)
.sequential()
.map(…)
.parallel()
.reduce()
需要注意的一点是共享可变状态会影响到并行流以及并行计算。
四、接口的默认方法
在Java8可以在接口中用default关键字实现接口的默认方法。这个方法可以在实现该接口的所有子类中使用。
为什么要引入这个?主要是出于API改进过程中,如果需要添加新的接口,会对使用这个API的用户产生比较大的影响。(用户代码中实现了这个接口的类都需要实现这个新添加的接口)
现在,我们可以在Java8中使用default实现在Java API中进行改进并且不影响到用户使用这个API,那在哪里添加这个default代码最好呢?自然是在有这个需求的所有子类的共同父接口中去实现这个default方法,如前面提到的在Collection接口中实现的Stream方法,这个将集合转变为流的方法即为一个默认方法。
在这种程度上,接口和抽象类的区别又小了一点。
a) 一个类只能继承一个抽象类,但可以实现多个接口
b) 一个抽象类可以通过实例变量保存一个通用状态,而接口不能有实例变量
那可能有人会提出这样的疑问,因为子类最多只能继承一个父类,但能实现多个接口。那么是否意味着这是在某种程度上实现多重继承?如果子类也实现了相同的方法,这时应该怎么选择调用哪个方法?
解决冲突的三条原则:
a) 类中的方法优先级最高。类或弗雷中声明的方法的优先级高于任何声明为默认方法的优先级。
b) 如果第一条无法判断,那么子接口的优先级更高,函数签名相同时,有限选择拥有最具体实现的默认方法的接口,即如B继承了A,那么B就比A更加具体。
c) 最后,如果还是无法判断,继承了多个接口的类必须显式覆盖并调用期望的方法。
四、Optional取代null
在Java开发中最常见的bug就应该是空值异常,为了避免这种异常,我们一般在需要的地方添加对值是否为null进行检查。
当变量存在时,Optional类只是对值的简单封装,而当变量不存在时,缺失的值会被建模成一个空的Optional对象,由方法Optional.empty()返回。(Optional.empty是一个静态工厂方法,返回Optional类的特定单一实例)
此外,对Optional进行的操作在一定程度上和Stream的操作有点类似
创建Optional:
- Optional.empty:声明一个空的Optional
- Optional.of(car)根据一个非空值创建Optional
- Optional.ofNullable(car)可接受null的Optional
提取和转换:
- 使用map
- 使用flatMap链接Optional对象
- 使用filter剔除特定的值
- 读取Optional的值
其中重点讲一下读取Optional的几种方法:
get |
如果值存在,将该值用Optional封装返回,否则抛出一个NoSuchElementException |
ifPresent |
如果值存在,就执行使用该值的方法调用,否则什么也不做 |
isPresent |
如果值存在就返回true,否则返回false |
OrElse |
如果有值则将其返回,否则返回一个默认值 |
OrElseGet |
如果有值则将其返回,否则返回一个由指定的Supplier接口生成的值 |
OrElseThrow |
如果有值则将其返回,否则抛出一个由指定的Supplier接口生成的异常 |
一个示例:
public String getCarInsuranceName(Optional<Person> person){ return person.flatMap(Person::getCar) .flatMap(Car::getInsuurance) .map(Insurance::getName) .orElse("UnKnown"); }
五、CompletableFuture
Future接口—Java5引入:
它实现了一种异步的计算,返回一个执行该计算后的结果引用,当运行结束后将引用返回调用方,当主线程已经运行到没有异步操作的结果无法在继续任何有意义的工作时,调用它的get方法获取操作的结结果,如果操作完成了,该方法会立即返回操作的结果,否则会阻塞线程直至操作完成。为了避免这种情况,Java提供了一个有超时参数的get方法,如future.get(1,TimeUnit.SECONDS),表示如果1s后没有结果的话,则退出get。
Future接口的局限性:回调无法放到与任务不同的线程中执行。不能讲控制流分离到不同的事件处理器中。例如当主线程等待各个异步执行的线程返回的结果来做下一步操作的时候,就必须阻塞在future.get方法的地方等待方法返回,这时候就又变成了同步;此外,我们也很难表述Future结果之间的依赖性。
出于以下这些新特性的要求,Java8引入了CompletableFuture接口:
- 将两个异步计算的结果合为一个,并且这两个异步计算任务运行在不同的线程,并且第二个计算依赖于第一个计算的结构。
- 等待所有异步任务的完成
- 等待所有异步任务中任意一个完成并获得计算结果
- 编程式地完成异步任务(手动给出一个结果)
- 异步任务完成响应事件
下面我们结合实际使用中的代码来讲讲CompeltableFuture的使用
public Future<Double> getPriceAsync(String product){ //手动创建 CompletableFuture<Double> futurePrice = new CompletableFuture<>(); new Thread(()->{ double price = calculatePrice(product); //当长时间计算的任务结束并得出结果时,设置Future的返回值 futurePrice.complete(price); }).start(); //无需等待还没有结束的计算,直接返回Future对象 return futurePrice; }
在这里,我们创建了一个代表异步计算的CompletableFuture对象实例,并建立子线程去计算price,不等计算结束,直接返回一个Future实例,当刚才的耗时计算完成后它会调用complete方法,结束CompletableFuture对象的运行,并设置变量的值。
然而上述的代码存在着一些问题,当在价格计算过程中出现了一些错误,而用于提示错误的异常被限制了计算线程中,最终会杀死该线程,从而导致等待get方法返回结果的客户端被阻塞。
当然,在这种情况下,我们一般都推荐使用重载版本的get方法,在其中使用一个超时参数来避免这种情况的发生(正如前面提到的),而这样的话,我们没有机会知道计算线程里发生的异常,所以CompletableFuture提供了一个completeExceptionally方法将导致CompletableFuture内发生问题的异常抛出。
修改后的代码如下
public Future<Double> getPriceAsync(String product){ //手动创建 CompletableFuture<Double> futurePrice = new CompletableFuture<>(); new Thread(()->{ try{ double price = calculatePrice(product); //当长时间计算的任务结束并得出结果时,设置Future的返回值 futurePrice.complete(price); }catch (Exception ex){ //抛出异常 futurePrice.completeExceptionally(ex); } }).start(); //无需等待还没有结束的计算,直接返回Future对象 return futurePrice; }
这里我们已经了解了如何显式创建CompletableFuture对象以及获得返回值,但其实CompletableFuture类自身提供了大量的工厂方法简化整个过程。
使用工厂方法supplyAsync创建CompletableFuture对象
public Future<Double> getPriceAsync(String product){ return CompletableFuture.supplyAsync(()->calculatePrice(product)); }
或者使用supplyAsync方法的重载版本,传入第二个参数指定不同的执行现场执行生产者方法。(后文会提到)
前面提到过我们可以使用并行流的方式来解决顺序计算导致的阻塞,这里需要指出的是,在当偏重于计算集中型的并行操作时,使用并行流的方式的效果不错,而当并行单元涉及到IO操作或者进行的并行都不算计算密集型的,那么久不推荐使用并行流,其中的原因主要有并行流使用的线程默认为CPU的核心核心数(包括虚拟核心)以及流操作具有延迟特性(在处理流的流水线时如果发生了IO等待,由于流的延迟特性我们很难判断是什么时候引发了等待)
所以,这里我们需要讨论一下如何利用CompletableFuture建立高效的并发。
在默认情况下,CompletableFuture使用的线程池和并发流一样,默认都采用固定数目的线程。
这里我们就需要借助于并发库中的Executor为我们定制执行器,从而根据并发任务的不同选择合适的线程池来执行,(这里就用到了刚才提到的supplyAsync的重载版本。)
private final Executor executor = Executors.newFixedThreadPool(Math.min(shops.size(),100)),new ThreadFactory(){ public Thread newThread(Runnable r){ Thread t = new Thread(r); //使用守护线程,不会阻止程序的关闭 t.setDaemon(true); return t; } }
以重载版本的supplyAsync创建CompletableFuture对象。
CompletableFuture.supplyAsync(()->shop.getName()+"price is"+shop.getPrice(product),executor);
下面就要研究如何使用CompletableFuture实现流水线操作了
这里我们不讨论使用并发流实现流水线操作,因为刚才提到的并发流的线程池问题,其在计算个体变多时扩展性不好,而自定义CompletableFuture调度任务执行的执行器则更能充分利用cpu。
这里我们需要了解CompletableFuture的thenApply和thenCompose方法
public static List<String> findPrice4(String product){ List<CompletableFuture<String>> priceFuture = shops.stream() // 异步获取价格 .map(shop -> CompletableFuture.supplyAsync( () -> shop.getPrice2(product), executor)) // 获取到价格后对价格解析 .map(future -> future.thenApply(Quote::parse)) // 另一个异步任务构造异步应用报价 .map(future -> future.thenCompose(quote -> CompletableFuture.supplyAsync( () -> Discount.applyDiscount(quote), executor))) .collect(toList()); return priceFuture.stream() //join 操作和get操作有相同的含义,等待所有异步操作的结果。 .map(CompletableFuture::join) .collect(toList()); }
在上述代码中,使用thenApply方法应用了第一个异步任务获得的家国后对其进行解析,而thenCompose引用第一个异步任务解析后的结果展开第二个异步任务,并实现两个异步任务结果的整合。
上面操作红使用了thenCompose来完成了不同异步任务结果的整合,其实,thenCompose一般应用于后面的异步任务依赖于前面任务的情况下的整合。更多的时候两个异步任务并没有依赖关系,这个时候我们应该使用thenCombine方法进行整合,其操作方法和前面的类似,此处略去。
七、新的日期和时间API
在此前Java原生的API对日期和时间这一部分的支持非常不理想,比如说在Java1中退出的java.util.Date类,其年费选择起始是1900,月份起始是0,使得创建日期很不直观。所以Java8推出了一套的新的日期和时间的API。这里就不细谈了,在实际使用中查阅API文档即可。
以上是关于Java8部分新特性的学习的主要内容,如果未能解决你的问题,请参考以下文章