JAVA8新特性介绍
特性介绍
Lambda表达式(函数式编程)
lambda 表达式让你用一种简洁的方式去避免一大块的代码。例如,你需要一个线程来执行一个任务。需要创建一个 Runnable 对象,然后做为参数传递给 Thread。
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello World");
}
}).start();
使用lambda表达式的话,可以简化很多
new Thread(() -> System.out.println("Hello World")).start();
方法引用
方法引用可以理解为一个特殊的 lambda 表达式,也是一个新的特性。它可以快速的选择定义在类中的已经存在的方法。例如:输出一个字符串,通常可以这样写(JAVA8):
Arrays.asList(1, 2, 3, 4, 5).forEach(new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
System.out.println(integer);
}
});
使用lambda表达式的话:
Arrays.asList(1, 2, 3, 4, 5).forEach(integer -> System.out.println(integer));
改用方法引用会更加简洁:
Arrays.asList(1, 2, 3, 4, 5).forEach(System.out::println);
符号::
是方法引用时特有的。
Streams
几乎每一个 JAVA 应用程序都会创建和处理集合。它们是许多程序处理任务的基石,集合可以用来聚合及处理数据。然而,处理集合过于繁琐而且难以处理并发。比如,从一个发票列表中找到“聚餐”相关,且金额在200元以上的发票ID,并按按发票金额的数值排序:
List<Invoice> invoices = new ArrayList<>();
List<Invoice> selectedInvoices = new ArrayList<>();
for (Invoice inv : invoices) {
if (inv.getTitle().contains("聚餐") && inv.getAmount() > 200) {
selectedInvoices.add(inv);
}
}
Collections.sort(selectedInvoices, new Comparator<Invoice>() {
public int compare(Invoice inv1, Invoice inv2) {
// 倒序
return inv2.getAmount().compareTo(inv1.getAmount());
}
});
List<Integer> selectedInvoicesIds = new ArrayList<>();
for (Invoice inv : selectedInvoices) {
selectedInvoicesIds.add(inv.getId());
}
上面冗长的判断,是计算机应该执行的命令。而使用JAVA8中引入的流来处理的话,则不太一样了,可以很直观的看到每一步做的处理。
List<Invoice> invoices = new ArrayList<>();
List<Integer> selectedInvoicesIds = invoices.stream()
.filter(inv -> inv.getTitle().contains("聚餐") && inv.getAmount() > 200)
.sorted(Comparator.comparingDouble(Invoice::getAmount).reversed())
.map(Invoice::getId)
.collect(Collectors.toList());
甚至在Streams中,可以简单的通过将invoices.stream()
替换成invoices.parallelStream()
就实现了集合的并发处理。当然,并不是所有情况都适合用并发处理。
另外,上面没有考虑空指针的情况,请看Optional
。
Optional
JAVA8 中引入了一个新的类叫做 Optional。很多编程语言都可以自动处理空值,而JAVA没有,这就导致了空指针异常这个几乎每个人都遇到过,也深感邪恶的异常。Optional灵感来自于函数式编程语言,它的引入是为了当值为空时代码可以争取地执行。
Optional是一种单值容器,这种情况下如果没有值则为空。Optional 很久以前已经在第三方集合框架(比如 Guava)中可用,但现在它作为 JAVA API 的一部分,可用于JAVA中。事实上,Optional 定义了方法强制你去明确地检查值存在还是缺省。
上述代码中,inv.getTitle()
和inv.getAmount()
都没有判断空,这显然在很有可能的情况下导致空指针异常,修改如下:
List<Invoice> invoices = new ArrayList<>();
List<Integer> selectedInvoicesIds = invoices.stream()
.filter(inv -> Optional.ofNullable(inv.getTitle()).orElse("").contains("聚餐") && Optional.ofNullable(inv.getAmount()).orElse(0d) > 200)
.sorted(Comparator.comparingDouble((ToDoubleFunction<Invoice>) item -> Optional.ofNullable(item.getAmount()).orElse(0d)).reversed())
.map(Invoice::getId)
.collect(Collectors.toList());
显得有一些杂乱,但是比起写很多if else判断来避免空指针,这种方式显然更加方便。
简单示例
再看一个简单的例子,如果有一个门店信息,要获取它所在的省份和城市:
City city = store.getProvince().getCity()
如果省份没有设置,那不可避免会带来空指针异常,但是如果需要判断的话,你得像下面这样:
City city = null;
if (store != null) {
Province province = store.getProvince();
if (province != null) {
city = province.getCity();
}
}
而使用 Optional 包一层的话,简单,且可以完全避免异常:
City city = Optional.ofNullable(store).map(Store::getProvince).map(Province::getCity).orElse(null);
Optional 的map
方法会判断值是否为空,所以可以很好的避免空带来的异常。
CompletableFuture
在并发编程中,我们通常会用到一组非阻塞的模型:Promise,Future 和 Callback。其中的 Future 表示一个可能还没有实际完成的异步任务的结果,针对这个结果可以添加 Callback 以便在任务执行成功或失败后做出对应的操作,而 Promise 交由任务执行者,任务执行者通过 Promise 可以标记任务完成或者失败。 可以说这一套模型是很多异步非阻塞架构的基础。
这一套经典的模型在 Scala、C# 中得到了原生的支持,但 JAVA8 之前并没有支持 Callback 的 Future 可用,当然也并非在 JAVA 界就没有发展了,比如 Guava 就提供了ListenableFuture 接口,而 Netty 4+ 更是提供了完整的 Promise、Future 和 Listener 机制。
JAVA8中提供 CompletableFuture 作为支持回调的 Future,在异步场景下,不再需要手动调用future.get()
来等待结果,只需要提供complete,exceptionally,thenApply,thenAccept,thenRun
就可以使用回调机制,轻松处理“完成后回调”、“异常时回调”、“继续执行”、“接受结果”、“继续运行(此处多为依赖的异步调用)”。
CompletableFuture 可以使用的场景足以独立成文,而且文章不少呢,不再赘述。CompletableFuture 在异步/回调上和 RxJava 是类似的,但是 RxJava 可以处理的场景更加丰富。
接口default方法
JAVA8 中对接口进行了两大改造,使其可以在接口中声明具体的方法。
引入默认方法
它可以在接口声明的方法中增加实现体,作为一种将 JAVA API 演变为向后兼容的机制。例如,可以看到在 JAVA8 的 List 接口中现在支持一种排序方法,像下面这么定义的:
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
接口现在也可以拥有静态方法
它和定义一个接口,同时用一个内部类定义一个静态方法去进行接口的实例化是同一种机制。例如,JAVA 中有 Collection 接口和定义了通用静态方法的 Collections 类,现在这些通用的静态也可以放在接口中。例如,JAVA8 中的 stream 接口是这样定义静态方法的:
public static<T> Stream<T> of(T... values) {
return Arrays.stream(values);
}
新的Date和Time接口(类似JodaTime)
JAVA8 引入了一套新的日期时间 API ,修复了之前旧的 Date 和 Calendar 类的许多问题。这一套新的Date和Time的时间API风格,基本和Joda-Time一致,毕竟Joda-Time框架的作者正是JSR-310的规范的倡导者。
这套新的日期时间 API 包含两大主要原则:
领域驱动设计
新的日期时间 API 采用新的类来精确地表达多种日期和时间的概念。例如,可以用 Period 类去表达一个类似于 “2个月零3天(63天)”,用 ZonedDateTime 去表达一个带有时间区域的时间。每一个类提供特定领域的方法且采用流式风格。因此,可以通过方法链写出可读性更强的代码。
不变性
Date(日期) 和 Calendar(日历)的其中一个问题就是他们是非线程安全的。此外,当使用 Date 作为API的一部分时,Date 的值可能会被意外的改变。为了避免这种潜在的BUG,在新的日期时间 API 中的所有类都是不可变的。
也就是说,在新的日期时间 API 中,你不能改变对象的状态,取而代之的是,你调用一个方法会返回一个带有更新的值的新对象。
带来的好处
代码可读性
JAVA 写出来的代码,大多是比较繁琐的,这导致了可读性的降低。换句话说,它需要很多代码才能表达一个简单的概念。
举个例子:简单的递减排序
List<Integer> integers = Arrays.asList(1, 34, 771, 14, 3, 5, 299);
Collections.sort(integers, new Comparator<Integer>() {
public int compare(Integer a, Integer b) {
return Integer.compare(b, a);
}
});
System.out.println(integers);
你知道什么时候是a-b
,什么时候是b-a
吗?
排序的时候,要关注具体哪个值减哪个值,学 C 语言的时候就知道的,根据返回值是大于零、等于零、小于零来判断。但是可读性差,而且每次排序都要关注这个细节,非常繁琐。
而使用JAVA8 的流、以及新的接口方法,可以简单的排序,并且可以不关注排序细节。
List<Integer> integers = Arrays.asList(1, 34, 771, 14, 3, 5, 299);
integers.sort(Comparator.comparingInt(Integer::intValue).reversed());
System.out.println(integers);
提升多核心处理能力
这里的最佳体现就是并行流。我们平时的代码中有大量的集合处理,而通常情况下,都是串行处理的,毕竟写一个并行处理集合的程序太过于复杂了,提升的速度可能不及带来BUG的风险。并行流的存在,非常简单的解决了这个问题。
当然,还有CompletableFuture,它的出现也是为了能够在更多需要使用异步编程的时候,可以简单地实现,而不是写出可能带有一堆BUG的异步程序。
总结
总之,一切都在向好的方向发展,更专业的人做更专业的事。复杂的并行、异步,交给更深入底层的人来做。我们做好业务上需要的并发和异步。
函数式的编程,带来了非常强的代码可读性,又可以避免空指针异常,非常适合在流程性业务逻辑中使用起来。易读的代码,也会给日后的维护带来遍历。
我建议,大家都用JAVA8,将这些新特性都用上,很明显,它们比之前的版本好太多了。
参考文献: