本章包含
- 代码的可读性
- 多核
- JAVA8特性的快速指南
JAVA8:为什么你需要关注?
JAVA已经更新了!在 2014 年 3 月,JAVA发布了新版本-JAVA8,JAVA8 引入的一些新特性可能会改变你日常中基本的编码方式。但不用担心,这本简洁的指南会带着你掌握一些要领,现在你就可以开始阅读。
第一章列举了 JAVA8 中增加的主要功能概况。接下来的两章则关注 JAVA8 的主要特性: lambda 表达式 和streams(流)。
驱动 JAVA8 改进的两大动机:
- 代码可读性
- 更加简化的多核支持
代码可读性
JAVA 是比较繁琐的,这导致了可读性的降低。换句话说,它需要很多代码才能表达一个简单的概念。
举个例子:给一个票据列表按数值递减排序。在 JAVA8 之前,你需要写这样写代码:
//java学习交流:737251827 进入可领取学习资源及对十年开发经验大佬提问,免费解答!Collections.sort(invoices,new Comparator<Invoice>(){
public int compare(Invoice inv1,Invoice inv2){
return Double.compare(inv2.getAmount(),inv1.getAmount());
}
});
像这种编码方式,你需要关注很多关于如何排序的小细节。换句话说,它很难对上面陈述的问题(票据排序问题)用一个简单的解决方案来描述。你需要创建一个 Comparator(比较器) 对象来定义如何对两个票据进行比较。为了实现比较,你需要提供一个 compare() 方法的实现。在阅读这段代码的时候,你必须花费较多而的时间来理解实现的细节而不是理解实际的问题描述。
在 JAVA8 中,你可以用下面这段代码来进行重构:
invoices.sort(comparingDouble(Invoice::getAmount).reversed());
现在,问题描述得清晰明了。(不要担心新的语法,我将会进行一个简短的介绍)。这就是你为什么应该关注 JAVA8 的原因,它带来了新的语言特色和API的更新,能让你写出更加简洁可读的代码。
此外,JAVA8 引入了一种新的API,叫做 Streams API。它可以让你写出可读性强的代码来处理数据。Streams API 支持几种内建的操作,以更简单的方式来处理数据。例如,在商业运营环境中,你可能希望产生一份日结报告用来过滤和聚合多个部门的票据信息。幸运的是,通过 Streams API,你可以不用去担心如何实现这种查询。这种方法和你使用SQL相似,事实上,在SQL中你可以制定一个查询而不用去关心它内部的实现。例如,假设你想要找出所有票据中数据大于1000的票据单号:
SELECT id FROM invoices WHERE amount > 1000
通常把这种查询的书写风格称作为声明式编程。这就是你将用 Streams API 解决这个问题的方式:
List<Integer> ids = invoices.stream()
.filter(inv->inv.getAmount > 1000 )
.map(Invoice::getId)
.collect(Collections.toList());
现在不要关注这些代码的细节,在第 3 章你会更深入地了解 Streams API。现在,把 Streams API 看作是一种新的抽象概念,以更好的可读性来处理数据的查询。
多核
JAVA8 中第二个大的改动就是多核处理时所不可缺少的。在过去,你的电脑只有一个处理单元。要想更快地运行一个应用程序通常意味着提升处理单元的性能。不幸的是,处理单元的处理速度已经不再提升。今天,绝大多数的电脑和移动设备都有多个处理单元(简称核)在并行工作。应用程序应该利用不同的处理单元来提升性能。JAVA 应用程序中经典的实现方式就是利用线程。不幸的是使用线程往往是困难和容易出错的,一般是为专家准备的。JAVA8 中的 Streams API 可以让你很简单地对数据查询进行并发处理。例如,你仅仅需要用 parallelStream() 替代 stream() 即可实现先前代码的并发运行:
List<Integer> ids = invoices
.parallelStream()
.filter(inv->inv.getAmount > 1000 )
.map(Invoice::getId)
.collect(Collections.toList());
在第 3 章,我会探讨使用 parallel streams 的细节及其最佳实现。
JAVA8特性的快速指南
这部分会提供 JAVA8 一些主要的新特性的概述,并附带一些代码例子,向你展示一些可用的概念。接下来的两章会重点描述 JAVA8 的两大重要特性:lambda 表达式 和 streams。
lambda 表达式
lambda 表达式让你用一种简洁的方式去避免一大块的代码。例如,你需要一个线程来执行一个任务。你可以创建一个 Runnable 对象,然后做为参数传递给 Thread:
Runnable runnable =new Runnable(){
@Override
public void run(){
System.out.println(“Hi”);
}
}
new Thread(runnable).start();
另一种办法,使用 lambda 表达式,你可以用一种更加易读的方式去重构先前的代码:
new Thread(()->System.out.println(“Hi”)).start();
在第 2 章,你将会学习关于 lambda 表达式的更多重要的细节。
方法引用
方法引用联合 lambda 表达式组成了一个新的特性。它可以让你快速的选择定义在类中的已经存在的方法。例如:你需要忽略大小写去比较一个字符串列表。一般地,你将会像这样写代码:
List<String> strs = Arrays.asList(“C”,”a”,”A”,”b”);
Collections.sort(strs,new Comparator<String>(){
@Override
public int compare(String s1,String s2){
return s1.compareToIgnoreCase(s2);
}
});
上面展示的这段代码可谓是极度详细。毕竟你需要的只是 compareToIgnoreCase 方法。利用方法引用,就可以明确地表明应该用 String 类中定义的 compareToIgnoreCase 方法来进行比较操作:
Collections.sort(strs,String::compareToIgnoreCase);
String::compareToIgnoreCase 这部分代码就是一个方法引用。它使用了一种特殊语法 :: (关于方法引用的更多细节会在接下来的章节中描述)。
Streams
几乎每一个 JAVA 应用程序都会创建和处理集合。它们是许多编程任务中的基石,可以让你聚合及处理数据。然而,处理集合过于繁琐而且难于并发。接下来的这段代码会说明处理集合会是多么的繁琐。从一个票据列表中找到训练相关的票据ID并按票据的数值排序:
List<Invoice> trainingInvoices = new ArraysList<>();
for(Invoice inv:invoices){
if(inv.getTitle().contains(“Training”)){
trainingInvoices.add(inv);
}
}
Collections.sort(trainingInvoices,new Comparator<Invoice>(){
public int compare(Invoice inv1,Invoice inv2){
return inv2.getAmount().compareTo(inv1.getAmount());
}
});
List<Integer> invoiceIds = new Arrays<>();
for(Invoice inv : trainingInvoices){
invoiceIds.add(inv.getId());
}
JAVA8 引进了一种新的抽象概念叫做 Stream ,可以让你以一种声明式的方式进行数据的处理。在 JAVA8 中你可以使用 streams 去重构之前的代码,就像这样:
List<Integer> invoiceIds = invoices.stream()
.filter(inv -> inv.getTitle().contains(“Training”))
.sort(comparingDouble(Invoice::getAmount).reversed())
.map(Invoice::getId)
.collect(Collections.toList());
另外,你可以通过使用集合中 parallelStream 方法取代 stream 方式来明确的并发执行一个 stream(现在不要关注这段代码的实现细节,你将会在第3 章中学到更多关于 Streams API 的知识)。
增强接口
JAVA8 中对接口进行了两大改造,使其可以在接口中声明具体的方法。
第一、JAVA8 引入了默认方法,它可以让你在接口声明的方法中增加实现体,作为一种将 JAVA API 演变为向后兼容的机制。例如,你会看到在 JAVA8 的 List 接口中现在支持一种排序方法,像下面这么定义的:
default void sort(Comparator<? super E> c){
Collections.sort(this,c);
}
默认方法也可以当做一种多重继承的机制来提供服务。事实上,在 JAVA8 之前,类已经可以实现多接口。现在,你可以从多个不同的接口中继承其默认方法。注意,为了防止出现类似 C++ 中的继承问题(例如钻石问题),JAVA8 定义了明确的规则。
第二、接口现在也可以拥有静态方法。它和定义一个接口,同时用一个内部类定义一个静态方法去进行接口的实例化是同一种机制。例如,JAVA 中有 Collection 接口和 定义了通用静态方法的 Collections 类,现在这些通用的静态也可以放在接口中。例如,JAVA8 中的 stream 接口是这样定义静态方法的:
public static <T> Stream<T> of (T…values){
return Arrays.stream(values);
}
新的日期时间 API
JAVA8 引入了一套新的日期时间 API ,修复了之前旧的 Date 和 Calendar 类的许多问题。这套新的日期时间 API 包含两大主要原则:
领域驱动设计
新的日期时间 API 采用新的类来精确地表达多种日期和时间的概念。例如,你可以用 Period 类去表达一个类似于 “2个月零3天(63天)”,用 ZonedDateTime 去表达一个带有时间区域的时间。每一个类提供特定领域的方法且采用流式风格。因此,你可以通过方法链写出可读性更强的代码。例如,接下来的这段代码会展示如何创建一个 LocalDateTime 对象而且增加 2小时30分:
LocalDateTime coffeeBreak = LocalDateTime.now()
.plusHours(2)
.plusMinutes(30);
不变性
Date(日期) 和 Calendar(日历)的其中一个问题就是他们是非线程安全的。此外,开发者使用 Dates (日期) 作为他们的API的一部分时,Dates(日期)的值可能会被意外的改变。为了避免这种潜在的BUG,在新的日期时间 API 中的所有类都是不可变的。
也就是说,在新的日期时间 API 中,你不能改变对象的状态,取而代之的是,你调用一个方法会返回一个带有更新的值的新对象。下面的这段代码列举了多种在新的日期时间 API 中可用的方法:
ZoneId london = ZoneId.of(“Europe/London”);
LocalDate july4 = LocalDate.of(2014,Month.JULY,4);
LocalTime early = LocalTime.parse(“08:05”);
ZonedDateTime flightDeparture = ZonedDateTime.if(july4,early,london);
System.out.println(flightDeparture);
LocalTime from = LocalTime.from(flightDeparture);
System.out.println(from)
ZonedDateTime touchDown = ZonedDateTime.of(july4,
LocalTime.of(11,35),
ZoneId.of(“Europe/Stockholm”));
Duration flightLength = Duration.between(flightDeparture,touchDown);
System.out.println(flightLength);
ZonedDateTime now = ZonedDateTime.now();
Duration timeHere = Duration.between(touchDown,now);
System.out.println(timeHere);
这段代码会产生一份类似于这样的输出:
2015-07-04T08+01:00[Europe/London]
08:45
PT1H50M
PT269H46M55.736S
CompletableFuture
JAVA8 中引入了一种新的方式来进行程序的异步操作,即使用一个新类 CompletableFuture 。它是旧的 Future 类的改进版,这种灵感来自于类似 Streams API 所选择的设计(也就是声明式的风格和流式方法链)。换句话说,你可以使用声明式的操作来组装多种异步任务。下面这个例子需要同时并发地查询两个独立(封闭)的任务。一个价格搜索服务与一个频率计算交织在一起。一但这两个服务返回了可用的结果,你就可以把他们的结果组合在一起,计算并输入在GBP中的价格:
findBestPrice(“iPhone6”)
.thenCombine(lookupExchangeRate(Currency.GBP),this::exchange)
.thenAccept(localAmount -> System.out.printf(“It will cost you %f GBP \n”,localAmount));
private CompletableFuture<Price> findBestPrice(String productName){
return CompletableFuture.supplyAsync(() -> priceFinder.findBestPrice(productName));
}
private CompletableFuture<Double> lookupExchangeRate(Currency localCurrency){
return CompletableFuture.supplyAsync(() -> exchangeService.lookupExchangeRate(Currency.USD,localCurrency));
}
Optional
JAVA8 中引入了一个新的类叫做 Optional。灵感来自于函数式编程语言,它的引入是为了当值为空或缺省时你的代码库能容许更好的模型。
把它当作是一种单值容器,这种情况下如果没有值则为空。Optional 已经在可供选择的集合框架(比如 Guava)中可用,但现在它作为 JAVA API 的一部分,可用于JAVA中。Optional 的另一个好处是它可以帮助你避免空指针异常。事实上,Optional 定义了方法强制你去明确地检查值存在还是缺省。下面这段代码就是一个例子:
getEventWithId(10).getLocation().getCity();
如果 getEventWithId(10) 返回 NULL,那么代码就会抛出 NullPointerException(空指针异常)。如果 getLocation() 返回 NULL,它也会抛出 NullPointerException(空指针异常)。换句话说,如果任何一个方法返回 NULL,就会抛出 NullPointerException(空指针异常)。你可以采用防御性的检查来避免这种异常,就像下面这样:
public String getCityForEvent(int id ){
Event event = getEventWithId(id);
if( event != null ){
Location location = event.getLocation();
if(location != null ){
return location.getCity();
}
}
return “ABC”;
}
在这段代码中,一个事件可能会有一个与之关联的地点。然而,一个地点总是会与一个与之关联的城市。不幸的是,它通常容易忘记去检查值是否为 NULL 。此外,这段代码太详细而且难于跟踪。使用 Optional 你可以用更加简洁清晰的方式去重构这段代码,就像这样:
public String getCityForEvent(int id){
Optional.ofNullable(getEventWithId(id))
.flatMap(this::getLocation)
.map(this::getCity)
.ofElse(“TBC”);
}
在任何时候,如果方法返回一个空的 Optional 对象,你就会得到默认值 TBC。