2014年3月,Java8作为Java历史上变化最大的一个版本发布了。这里为什么说Java8是变化最大的版本呢,比如说它增加了Lambda表达式特性,它增加了Stream API等等,虽然这些新的特性能让我们编写的程序代码更加的简洁更加具有语义化,但是这些都只是表象。在我看来,之所以说Java8是Java历史上变化最大的版本的本质原因只有一个——它填补了Java在函数式编程领域一直以来的空白。
我们在学习Java这门语言的时候就知道了,在Java中万物皆对象,对象在Java编程模型中始终是“一等公民”的存在。现在同样,在函数式编程中,函数被视为了“一等公民”(维基百科原文 In functional programming, functions are treated as first-class citizens.)。
好的,让我们回到本篇文章的主题,让我们认识下什么是lambda表达式。lambda表达式,维基百科上的解释是一种用于表示匿名函数和闭包的运算符,看到这个解释还是觉得很抽象,接下来我们看一个例子:
public class SwingTest {
public static void main(String[] args) {
JFrame jFrame = new JFrame("My JFrame");
JButton jButton = new JButton("My JButton");
jButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Button Pressed!");
}
});
jFrame.add(jButton); jFrame.pack();
jFrame.setVisible(true);
jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
}
这是一段Swing编程中的代码,对Swing不熟悉的朋友也没关系,整体逻辑是给Button绑定一个监听事件,当点击Button时会在控制台输出"Button Pressed!"内容。这里创建了一个匿名内部类的实例来绑定到监听器,这也是以往比较常规的代码组织形式。但是仔细看一下我们会发现,实际上我们真正关注的就是一个ActionEvent类型的参数e和向控制台输出的语句:
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Button Pressed!");
}
下面我们将上段程序中以匿名内部类创建接口实例的方式替换成Lambda表达式后,代码如下:
public static void main(String[] args) {
JFrame jFrame = new JFrame("My JFrame");
JButton jButton = new JButton("My JButton");
jButton.addActionListener(e -> System.out.println("Button Pressed!"));//lambda
jFrame.add(jButton);
jFrame.pack();
jFrame.setVisible(true);
jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
关注最中间部分代码的变化,由原来的6行代码,现在1行就可以实现了。这就是Lambda表达式的一种简单形式。
我们以lambda表达式的方式替换了原来由匿名内部类实现的形式,其中lambda表达式中的参数e就代表原来匿名内部类中actionPerformed方法的入参e,入参e是ActionEvent类型,lambda表达式中之所以可以省略参数类型是因为编译器可以通过上下文来推断出e是ActionEvent类型,并且当lambda表达式只有一个入参时,参数外面的括号可以省略。然后是方法实现部分,就是向控制台输出"Button Pressed!",同样,如果方法体中只有一行代码时,方法体外面的大括号也可以省略。
所以,我们可以看出Lambda表达式的语法是:
(param1, param2, param3) -> {
//todo
}
到这里我们停一下,思考下这个问题,除了代码简洁了,Lambda表达式还给我们带来了什么变化吗?
我们回忆一下,在Java中,我们是否无法将函数作为参数传递给一个方法,也无法声明返回值是一个函数的方法。在Java8之前,答案是肯定的。
那么,在上面的例子中我们居然可以将一段代码逻辑作为参数传递给了监听器,告诉监听器事件触发时你可以这么做,而不再需要以匿名内部类的方式作为参数。这就是Java8带来的最大的变化:函数式编程。
支持函数式编程的语言有很多,在JavaScript中,把函数作为参数传递,或者返回值是一个函数的情况非常常见,JavaScript是一门非常常见的函数式语言。
在函数式编程语言中,lambda表达式的类型是函数。而在Java中,lambda表达式是对象,它们必须依附于一类特别的对象类型——函数式接口(Functional Interface)。
函数式接口的定义:如果一个接口中,有且只有一个抽象的方法(Object类中的方法不包括在内),那这个接口就可以被看做是函数式接口。我们看下Runnable接口的声明:
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
在Java8后,Runnable接口多了一个FunctionalInterface注解,表示该接口是一个函数式接口。但是如果我们不添加FunctionalInterface注解的话,如果接口中有且只有一个抽象方法时,编译器也会把该接口当做函数式接口看待。
同时,下面这个自定义的接口,MyInterface也是一个函数式接口,因为toString()是Object类中的方法,只是在这里进行了复写,不会增加接口中抽象方法的数量。
@FunctionalInterface
public interface MyInterface {
void test();
String toString();
}
上面提到lambda表达式属于Java对象,那么这个对象的类型是什么呢?我们回顾下SwingTest程序,这里以匿名内部类的方式创建了一个ActionListener接口实例:
jButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Button Pressed!");
}
});
使用Lambda表达式改进后变成了:
jButton.addActionListener(e -> System.out.println("Button Pressed!"));
可以看出我们使用Lambda表达式创建了一个ActionListener接口的实例,并提供了具体的内部实现。我们看下ActionListener接口的定义:
public interface ActionListener extends EventListener {
/**
* Invoked when an action occurs.
*/
public void actionPerformed(ActionEvent e);
}
只有一个抽象方法,虽然没添加FunctionalInterface注解,但是也符合函数式接口的定义,编译器会认为这是一个函数式接口。所以,通过lambda表达式,我们可以创建函数式接口的实例,即Lambda表达式返回的是函数式接口类型。
我们除了可以通过lambda表达式创建函数式接口的实例之外,还可以有其他两种方式,参考FunctionalInterface注解的Java Doc:
Note that instances of functional interfaces can be created with lambda expressions,
method references, or constructor references
创建函数式接口实例的方式有三种:
* lambda表达式
* 方法引用
* 构造方法引用
先不着急学习其它两种创建函数式接口实例的方式,接下来再通过一个例子加深下通过lambda表达式来创建函数式接口实例:
public class Test1 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
list.forEach(new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
System.out.println(integer);
}
});
}
}
这段程序很简单,首先初始化一个Integer类型的集合然后向控制台输出每个元素。其中我们注意到forEach方法,它是Java8中新增加的默认方法。接口中的默认方法可以有具体的实现:
public interface Iterable<T> {
.
.省略
.
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
}
forEach方法被声明在Iterable接口中,并被关键字default修饰。这样任何一个该接口的子类型都可以继承forEach方法的实现,所以List接口因为是Iterable的间接子接口,所以也继承了该默认方法。Java8采用这种巧妙的方式既扩展了接口的功能,又兼容了老版本。
接下来分析下forEach的实现,首先接收了一个Consumer类型的参数action,进行非空判断,然后遍历当前所有元素交由action的accept方法进行处理。那么Consumer又是什么鬼,看源码:
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);
.
.省略
.
}
一个接口,有且仅有一个抽象方法,被@FunctionalInterface修饰,典型的函数式接口。好的,现在我们知道forEach接收的Consumer类型的参数是一个函数式接口,接口里唯一的accept抽象方法接收一个参数,不返回值。那通过上一篇文章我们知道,创建函数式接口类型的实例其中一种方式是使用lambda表达式,所以可以将最上面的程序改造一下:
public class Test1 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
//Lambda表达式 接收一个参数 不返回值
list.forEach(item -> System.out.println(item));
}
}
该lambda表达式:
item -> System.out.println(item)
接收一个参数 不返回值,符合accept方法定义,编译通过。也就是说如果使用lambda表达式来创建一个函数式接口实例,那这个lambda表达式的入参和返回必须符合这个函数式接口中唯一的抽象方法的定义。
上面我们提到创建函数式接口实例的另外两种方式,方法引用和构造方法引用,接下来我们继续改造程序:
public class Test1 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
//方法引用
list.forEach(System.out::println);
}
}
看到out后面有两个冒号,反正当时我是凌乱了。。。这个就是函数式接口实例第二种创建方式:方法引用。方法引用的语法是 对象::方法名(只是其中一种)。同样,使用方法引用方式去创建函数式接口实例也必须遵守函数式接口中抽象方法的定义,看下此处println方法源码:
public void println(Object x) {
String s = String.valueOf(x);
synchronized (this) {
print(s);
newLine();
}
}
接收一个参数,并不返回值,编译通过。
最后我们来看下创建函数式接口实例的最后一种,第三种方式:构造方法引用 ,继续改程序:
public class Test1 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
//构造方法引用
list.forEach(Test1::new);
}
Test1(Integer i){
System.out.println(i);
}
}
构造方法引用的语法是:类名::new
我们给Test1新添加了一个构造方法,该构造方法接收一个参数,不返回值,符合Consumer函数式接口中的抽象方法accept定义,编译通过。实际工作中我们不会如此实现构造方法的,此处只是为了演示使用构造方法引用创建函数式接口实例的语法使用。
除了Consumer之外,Java8还内置了很多函数式接口方便我们直接使用,常用的如Function、BiFunction、Predicate等。后续包括Stream API都会介绍到。
如果觉得本篇文章对你有一点点帮助的话,帮忙点个赞哈