2017年参加工作以后一直都在使用JDK8的lambda风格方式进行Java编程。最直观的感受就是,代码更加简洁,清晰!其次在进行并发编程的时候,真的是太方便了!
本次开篇,借鉴了一本我特别喜欢,逢人必推荐的书籍《Java 8 函数式编程》(原版:《Java 8 Lambdas: Functional Programming For The Masses》)
本人打算围绕着这一本书,对自己这些年的一些Java8特性做一次完善的总结。写给自己看,再用自己的理解讲给别人听。
Java老矣,尚能饭否
随着硬件的发展,CPU从单核时代快进到多核时代,也意味着未来的编程语言的趋势一定是对多线程编程的支持愈发友好!这也是为什么Jdk8推出Lambda表达式的重要原因。Lambda表达式未出现以前,Java程序员在面对大数据集合的并行操作,编码上往往非常麻烦且不够高效,易用性低,可读性也不一定高。而Lambda的出现无疑是在Java语言层面上增加了支持。
我个人觉得,Java8的推出可以说挽回了很多准备弃坑Java的程序员的心。如果没有Lambda这种更加友好的编程风格的支持,我相信诸如Go,Scala这类的语言会挑战Java的王者地位。(可以去了解一下Go语言中的goroutine关键字对多线程的支持的友好性)
引用书籍中的一段文字
当然,这样做是有代价的,程序员必须学习如何编写和阅读使用Lambda表达式的代码。但是这不是一桩赔本的买卖。与手写一大段复杂,线程安全的代码相比,学习一点新语法和一些新习惯容易很多。开发企业级应用时,好的类库和框架极大降低了开发事件和成本,也为开发易用且高效的类库扫清了障碍
Lambda表达式
给出如下的实现Runnable的代码示例,直观感受一下lambda表达式的魅力。
public class DemoMain {
/**
* 编写Runnable的实现类
*/
private static class RunnableImpl implements Runnable{
public void run() {
System.out.println("Java7-编写Runnable方式");
}
}
public static void main(String[] args) {
Thread thread0=new Thread(new RunnableImpl());
Thread thread1=new Thread(new Runnable() {
public void run() {
System.out.println("Java7-匿名内部类方式");
}
});
Thread thread2=new Thread(()-> System.out.println("Java8的Lambda风格"));
thread0.start();
thread1.start();
thread2.start();
}
}
匿名内部类与匿名函数
thread0,编写一个Runnable实现类来实现多线程,不难看出这种方式的代码量更多(当然我们也也经常需要这样的一个实现类)
thread1,通过匿名内部类的方式实现多线程。匿名内部类的本质是一个继承该类或者实现该类接口的子类实例
thread2,通过lambda表达式实现多线程。
相信大家对thread0是十分了解的,这里不展开说。关注点在于thread1和thread2的比较。
从thread1的创建方式中,我们不难看出来,本质上我们是在Thread构造器中传递了一个对象作为参数,只不过这个对象是匿名内部类的实例。而thread2则看起来,更像是传递了一段可执行的代码作为“参数”。这样的一段定义了执行方式的代码,不就是Java中的函数嘛!只不过这个方法有些函数,“匿名函数”。
函数式编程和Lambda表达式
在面向对象的Java编程中,我们往往给定的参数是数据,而函数式编程则希望传入的是一个函数,即将函数“参数化”。相信看到这句话,你大致明白了函数式编程是怎么一回事。Lambda表达式是一个匿名函数,将行为像数据一样进行传递。
函数接口
在Java中,所有的方法参数都有固定的类型,入thread0的例子,参数类型是RunnableImpl。那么在thread2中,参数的类型是什么?
()->System.out.println(“Java8的Lambda风格”)。如果你用的是idea开发工具,你可以按住ctrl鼠标点“()”,跳转到Runnable接口类中,这个Runnable接口就是lambda表达式的类型。我们来看看Runnable的源码。
如下图所示:

不难看出有两个点。1:Runnable是一个接口类,2:只有一个抽象方法。我们把这样的一个接口类称为函数接口,lambda表达式的作为方法参数的时候,它的类型一定是函数接口。
@FunctionalInterface注解
Runnable接口采用了FunctionalInterface注解进行标注。该注解会强制Javac编译的时候检查当前的接口是否满足函数接口的标准。如果该注释标注给一个不符合条件的接口,枚举类等,编译的时候将会报错。相当于一种类型检查机制,所以当我们自己实现函数接口的时候,一定要规范的加上这个注解!它能够帮助我们提前发现风险。
Java中重要的函数接口
接口 | 参数 | 返回类型 | 常用场景 |
---|---|---|---|
Predicate | T | boolean | 用于条件判断 |
Consumer | T | void | 用于对一个给定参数进行处理 |
Function<T,R> | T | R | 用于将给定参数T处理成R类型返回 |
Supplier | None | T | 工厂方法获取一个T类型对象 |
UnaryOperator | T | T | 逻辑非 |
BinaryOperator | (T,T) | T | 对两个同类型对象进行合并操作,比如数值运算 |
类型推断
类型推断,它本质上是javac根据程序的上下文(方法签名,变量类型)推断出数据类型。所以我们在编写Java代码的时候,能够在一些不言而明的前提写,省掉对类型的显式指定。在JDK7的时候,我们可以通过菱形操作符来推断类型
public class DemoMain {
public static void main(String[] args) {
//显式指定类型
HashMap<String,String> testMap=new HashMap<String,String>();
//使用菱形操作符,根据变量类型进行推断
HashMap<String,String> testMap1=new HashMap<>();
//使用菱形操作符,根据方法签名进行推断。方法签名由方法名称+形参列表构成。重载的方法,方法签名是一样的,但是返回类型不同。
testMethod(new HashMap<>());
}
private static void testMethod(HashMap<String, String> objectObjectHashMap) {
System.out.println("根据方法签名推断");
}
}
Java8中的类型推断是对Java7类型推断机制的一个扩展。如下示例,Predicate只有一个泛型参数的函数接口,Lambda表达式实现了该接口。
javac会根据上下文中的参数推断出参数类型是Integer,也就是x是Integer类型的!
Predicate<Integer> atLeast5 = x -> x > 5;
public interface Predicate<T> {
boolean test(T t);
}
引用值,而非变量
我把threa1的代码修改成如下:
final String method="匿名内部类方式";
Thread thread1=new Thread(new Runnable() {
public void run() {
System.out.println("Java7-"+method);
}
});
如果是在内部类中,引用了一个外部对象,那么需要把是这个变量声明成final,这也意味着被引用的变量必须是一个不会再改变值。Java8中,已经不需要显式的指定final关键字,但是被引用的变量必须是“既定已成事实”的变量。我对“既定已成事实的变量”的理解就是,一旦定义且初始化,则后续都不会改变的变量。
lambda引用之前发生改动,无法编译

lambda引用时发生改动,无法编译

lambda引用之后发生改动,无法编译

使用了非“既定已成事实”的变量,则无法编译通过。这也解释了为什么Java中的Lambda表达式被称为闭包。因为闭包的本质是一个定义在函数中的函数。
