共计 5514 个字符,预计需要花费 14 分钟才能阅读完成。
开篇
在很久之前粗略的看了一遍《Java8 实战》。客观的来,说这是一本写的非常好的书,它由浅入深的讲解了 JAVA8 的新特性以及这些新特性所解决的问题。最近重新拾起这本书并且对书中的内容进行深入的挖掘和沉淀。接下来的一段时间将会结合这本书,以及我自己阅读 JDK8 源码的心路历程,来深入的分析 JAVA8 是如何支持这么多新的特性的,以及这些特性是如何让 Java8 成为 JAVA 历史上一个具有里程碑性质的版本。
Java8 的新特性概览
在这个系列博客的开篇,结合 Java8 实战中的内容,先简单列举一下 JAVA8 中比较重要的几个新特性:
- 函数式编程与 Lambda 表达式
- Stram 流处理
- Optional 解决空指针噩梦
- 异步问题解决方案 CompletableFuture
- 颠覆 Date 的时间解决方案
后面将针对每个专题发博进行详细的说明。
简单说一说函数式编程
函数式编程的概念并非这两年才涌现出来,这篇文章用一种通俗易懂的方式对函数式编程的理念进行讲解。顾名思义,函数式编程的核心是函数。函数在编程语言中的映射为方法,函数中的参数被映射为传入方法的参数,函数的返回结果被映射为方法的返回值。但是函数式编程的思想中,对函数的定义更加严苛,比如参数只能被赋值一次,即参数必须为 final 类型,在整个函数的声明周期中不能对参数进行修改。这个思想在如今看来是不可理喻的,因为这意味着任何参数的状态都不能发生变更。
那么函数式编程是如何解决状态变更的问题呢?它是通过函数来实现的。下面给了一个例子:
String reverse(String arg) {if(arg.length == 0) {return arg;}
else {return reverse(arg.substring(1, arg.length)) + arg.substring(0, 1);
}
}
对字符串 arg 进行倒置并不会修改 arg 本身,而是会返回一个全新的值。它完全符合函数式编程的思想,因为在整个函数的生命周期中,函数中的每一个变量都没有发生修改。这种不变行在如今称为Immutable 思想
,它极大的减少了函数的副作用。这一特性使得它对单元测试,调试以及编发编程极度友好。因此在面向对象思想已经成为共识的时代,被重新提上历史的舞台。
但是,编程式思想并不只是局限于此,它强调的不是将所有的变量声明为 final,而是将这种可重入的代码块在整个程序中自由的传递和复用。JAVA 中是通过对象的传递来实现的。举个例子,假如现在有一个筛选订单的功能,需要对订单从不同的维度进行筛选,比如选出所有已经支付完成的订单,或是选出所有实付金额大于 100 的订单。
简化的订单模型如下所示:
public class Order{
private String orderId;
// 实付金额
private long actualFee;
// 订单创建时间
private Date createTime;
private boolean isPaid
}
接着写两段过滤逻辑分别实现选出已经支付完成的订单,和所有实付金额大于 100 的订单
// 选出已经支付完成的订单
public List<Order> filterPaidOrder(List<Order> orders) {List<Order> paidOrders = new ArrayList<>();
for(Order order : orders) {if(order.isPaid()) {paidOrders.add(order);
}
}
return paidOrdres;
}
// 选出实付金额大于 100 的订单
public List<Order> filterByFee(List<Order> orders) {List<Order> resultOrders = new ArrayList<>();
for(Order order : orders) {if(order.getActualFee()>100) {resultOrders.add(order);
}
}
return resultOrders;
}
可以看到,上面出现了大量的重复代码,明显的违背了 DRY(Dont Repeat Yourself)原则,可以先通过 模板模式 将判断逻辑用抽象方法的形式抽取出来,交给具体的子类来实现。代码如下:
public abstract class OrderFilter{public List<Order> filter(List<Order> orders) {List<Order> resultOrders = new ArrayList<>();
for(Order order : orders) {
// 调用抽象方法
if(isWantedOrder(order)) {resultOrders.add(order);
}
}
return resultOrders;
}
abstract boolean isWantedOrder(Order o);
}
public abstract class PaidOrderFilter extends OrderFilter{
// 重写过滤的判断逻辑
boolean isWantedOrder(Order o){return o.isPaid();
}
}
public abstract class FeeOrderFilter extends OrderFilter{
// 重写过滤的判断逻辑
boolean isWantedOrder(Order o){return o.getActualFee() > 100;
}
}
但是,继承本身会带来类和类之间比较重的耦合,而可重入函数的传递则解决了这个问题。代码如下:
public interface OrderFilter{boolean isWantedOrder(Order o);
}
public List<Order> filter(List<Order> orders, OrderFilter orderFilter) {List<Order> resultOrders = new ArrayList<>();
for(Order order : orders) {if(orderFilter.isWantedOrder(o)) {resultOrders.add(order);
}
}
return resultOrders;
}
// 过滤出已经支付的订单
filter(orders, new OrderFilter(){
@Override
public boolean isWantedOrder(Order o){return o.isPaid();
}
})
通过这种方式,filter 方法基本上处于稳定,只需要自定义传入的订单过滤器即可。但是,在当代对可读性和减少重复代码的极致追求下,重构到这种程度依然不能让具有代码洁癖的程序员们满意,于是 Lambda 表达式应运而生。
Lambda 表达式
Java8 中的 Lambda 表达式和 Lambda Calculus 并不是一个概念,因此所有被 Lambda 计算伤害过的小伙伴千万不要恐惧。在 Java8 中,它更加类似于匿名类的代码糖,从而极大的提高代码的可读性(大部分场景),灵活性和简洁性。Lambda 表达式的基本结构如下:
(parameters) -> expression
(parameters) -> {expression}
它其实就是函数的一个简化版本,括号中的 parameters 会填入这个函数的参数类型,在 expression 中会填入具体执行的语句。如果没有大括号,则 expression 只允许填入一条语句,且会根据 Lambda 表达是的上下文,自动补全 return 语句。举几个具体的例子:
() -> "hello world" 类似于 String methodName(){return "hello world";}
(int i, int j) -> i > j 类似于 Boolean compare(){ return i > j;}
因此 Lambda 表达式本质上就是对匿名函数的一种快捷展示。而上面的代码使用 lambda 表达式还可以继续重构如下:
// 标记该接口为函数式接口,要求只能有一个待实现的函数声明
@FuncationalInterface
public interface OrderFilter{boolean isWantedOrder(Order o);
}
public List<Order> filter(List<Order> orders, OrderFilter orderFilter) {List<Order> resultOrders = new ArrayList<>();
for(Order order : orders) {if(orderFilter.isWantedOrder(o)) {resultOrders.add(order);
}
}
return resultOrders;
}
// 过滤出已经支付的订单
filter(orders, (Order o) -> o.isPaid());
filter(orders, (Order o) -> o.getActualFee() > 100);
Lambda 表达式本身还有一些约定,以及进一步简化的空间,这点各位笔者可以通过这篇文章自行再去了解。
Lambda 的灵活性还体现在同样的 Lambda 表达式可以赋值给不同的函数式接口,代码如下:
@FuncationalInterface
public interface Runnable{void run();
}
@FuncationalInterface
public interface AnotherInterface{void doSomething();
}
Runnable r = () -> System.out.println("hello world");
AnotherInterface a = () -> System.out.println("hello world");
那么编译器是如何解析 Lambda 表达式的呢?它其实是根据上下文推断该 Lambda 表达式该映射到什么函数式接口上的。就以上文的 filter 方法为例子,它传入的函数式接口为 OrderFilter,其中函数的定义为传入 Order 并返回 Boolean 值。编译器就会根据这个上下文来判断 Lambda 表达式是否符合函数式接口的要求,如果符合,则将其映射到该函数式接口上。
Lambda 表达式中的局部变量和异常
Lambda 表达式作为匿名类的语法糖,它的特性和匿名类保持一致。即如果 Lambda 表达式要抛出一个非检查性异常(Unchecked Error), 则需要在函数式接口中显示的声明出来。如下:
@FuncationalInterface
public interface AnotherInterface{void doSomething() throws UncheckedException;
}
除此以外,还有一个场景是需要在 Lambda 表达式中引用外部的变量。外部的变量包括局部变量,实例变量和静态变量。其中,只允许对实例变量和静态变量进行修改,所有的被引用的局部变量都必须显性的或是隐形的声明为 final。代码如下:
// 实例变量
int fieldVariable;
public void someMethod() {
// 局部变量
int localVariable = 0;
// 不允许修改局部变量
Runnable r1 = () -> localVariable++;
// 可以修改实例变量
Runnable r2 = () -> fieldVarialbe++;
// 不允许,因为被 Lambda 表达式引用的局部变量必须显式或隐式的声明为局部变量
Runnable r3 = () -> System.out.println(localVariable);
localVariable++;
}
之所以有这样的约定,是因为局部变量是保存于栈上的,保存于栈上意味着一旦该方法执行完毕,栈中的局部变量就会被弹出并回收。这里也隐式的表明局部变量其实是约束于当前线程使用的 。此时如果 Lambda 表达式是传递到其它线程中执行的,比如上文中创建的 Runnable 对象传递给线程池执行,则会出现访问的局部变量已经被回收的异常场景。 而实例变量和静态变量则不同,二者是保存在堆中的,本身就具有多线程共享的特性。
方法的引用
方法的引用证明程序员对代码的洁癖已经到了无法抢救的程度。JAVA8 中提出的方法引用的思想允许我们将方法定义传递给各个函数。比如如果要使用 System.out.print 方法,则可以传入System.out::println
。方法的引用主要有三种场景:
- 指向静态的方法的引用。如 Integer 中的静态方法 parseInt,可以通过 Integer::parseInt 来引用
- 指向任意类型实例方法的方法引用。如
list.sort((s1, s2)->s1.compareToIgnoreCase(s2));
, 可以修改为list.sort(String::compareToIgnorecase)
,即满足arg0.someMethod(restArgs)
语法 - 指向现有对象实例的方法引用,如类 ClassA 有一个实例 classA,并且有一个方法 someMethod,则可以通过
classA::someMethod
进行方法引用。 - 构造函数引用
ClassName::new
。对于有参数的构造函数,则需要结合已有的函数式接口进行引用。
下期预告
下一篇文章将会结合 JAVA8 中预定义的一些 FunctionalInterface 的源码来介绍如何使用这些函数式接口帮助我们编程。
- Consumer
- Supplier
- Predicate
- Function
并且会以 JAVA8 的 comparing 方法为例子,详细解释方法引用的使用