目录:
1. 函数式接口
Java 在 JDK1.8 之后引入了 lambda 表达式,在了解 lambda 表达式之前,我们首先需要了解下什么是函数式接口。所谓的函数式接口,就是指只有一个抽象方法的接口。例如 ActionListener 接口就是一个函数式接口:
public interface ActionListener extends EventListener {
/**
* Invoked when an action occurs.
*/
public void actionPerformed(ActionEvent e);
}
对于函数式接口,需要强调的是“只有一个抽象方法”,既不能没有抽象方法,也不能多于一个。为什么要强调是抽象方法呢?Java 接口的方法不都是抽象的吗?那是不是改成只有一个方法更好呢?
实际上,接口完全有可能重新申明 Object 类的方法,如 toString 或 clone,这些声明有可能会让方法不再是抽象的。更重要的是,JDK1.8 之后接口中已经可以声明非抽象默认方法了。如:
public interface Lambda {
default int method() {
return 0;
}
}
如果在设计自己的接口时,接口中只有一个抽象方法,可以用 @FunctionalInterface
注 解来标记这个接口。这样做有两个优点。如果你无意中增加了另一个非抽象方法,编译器会产生一个错误消息。另外javadoc 页里会指出你的接口是一个函数式接口。
2. 从匿名内部类到 lambda 表达式
假如我们在编程中需要用到某个方法,这个方法需要接收一个实现了某个函数式接口的对象参数,例如:
addActionListener(ActionListener l);
这时候我们该怎么实现此方法呢?这里比较容易想到的方法大概有四种,分别是:
- 编写一个普通的类,让这个类实现 ActionListener 接口,然后实例化一个对象传入方法中。
- 编写一个一般内部类或局部内部类(根据方法所在位置决定),让这个类实现 ActionListener 接口,然后实例化一个对象传入方法中。
- 使用匿名内部类传入一个实现了 ActionListener 接口的对象。
- 使用 lambda 表达式。
在 JDK1.8 之前,较为方便的方法是使用匿名内部类,但随着 lambda 表达式的出现,在函数式接口上,我们显然有了一个更好的选择。接下来将分别用上述四种方法进行具体的实现。
2.1 通过编写一个普通的类实现
// 编写一个实现了 ActionListener 接口的类
public class MyActionListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
}
}
addActionListener(new MyActionListener());
使用此方法实现时,我们需要编写一个实现了 ActionListener 的类,这样做显然比较繁琐,同时我们还需要为此再增加一个.java文件。
当然这么做也有其自己的优点——当在其它类中需要一个实现了 ActionListener 接口的对象,且接口中所需实现的方法的功能相同时(这种情况一般较少),就可以不必重新编写代码,直接实例化一个此类的对象即可。
2.2 通过内部类实现
public class MyClass {
// ...
// 内部类
public class MyActionListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
}
}
addActionListener(new MyActionListener());
}
通过内部类,可以不用新增一个 .java 文件,但显然我们还有更好的方法——匿名内部类。
2.3 通过匿名内部类实现
addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
}
});
在 JDK1.8 之前,使用匿名内部类是一种惯用的做法,就算是相比于 lambda 表达式,匿名内部类也有它的优点——不局限于函数式接口,可以有多个抽象方法,例如:
JButton button = new JButton();
button.addMouseListener(new MouseListener() {
@Override
public void mouseReleased(MouseEvent e) {
// TODO Auto-generated method stub
}
@Override
public void mousePressed(MouseEvent e) {
// TODO Auto-generated method stub
}
@Override
public void mouseExited(MouseEvent e) {
// TODO Auto-generated method stub
}
@Override
public void mouseEntered(MouseEvent e) {
// TODO Auto-generated method stub
}
@Override
public void mouseClicked(MouseEvent e) {
// TODO Auto-generated method stub
}
});
但对于函数式接口,在 JDK1.8 之后,利用 lambda 表达式实现无疑会更加方便。
2.4 通过lambda 表达式实现
addActionListener(e -> {
// TODO Auto-generated method stub
});
3. lambda 表达式的语法
前面的例子已经很好的展示了 lambda 表达式的优点——简洁、优雅。接下来我们将介绍如何使用 lambda表达式。继续拿 ActionListener 接口为例:
public interface ActionListener {
void actionPerformed(ActionEvent e);
}
// e 对应 ActionListener 接口里抽象方法中的参数
addActionListener(e -> {
// 抽象方法的具体实现
});
lambda 的基本结构是 (…)-> {…}。
(…)中的是接口中的抽象方法的参数,当没有参数时用,仍然要提供一个空括号();只有一个参数时可以不加括号,只给出参数,如上例所示;当有多个参数时用逗号分隔,如(int a, int b, …)。如果可以推导出一个 lambda 表达式的参数类型,则可以忽略其类型,如(a, b)。
{…} 中是抽象方法的主体,可以在大括号内部具体实现此方法。对于 lambda 表达式而言,它与匿名内部类相似,是可以访问其所在作用域范围内的外围数据的。具体访问细节与匿名内部类相似,即只能引用值不会改变的(final)变量。例如下面的做法就是不合法的:
// 在lambda表达式中被使用的变量,即使未被显示的定义为final,也会被视为是final的。
int start = 0;
ActionListener listener = e -> {
start++; // Error: Can't mutate captured variable
System.out.println(start);
};
另外如果在 lambda 表达式中引用变量,而这个变量可能在外部改变,这也是不合法的。例如:
for (int i = 1; i <= count; i++) {
ActionListener listener = e -> {
System.out.println(i);
// Error: Local variable i defined in an enclosing scope must be final or effectively final
};
}
4. 方法引用和构造器引用
1.方法引用
有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。例如,希望只要出现一个定时器事件就打印这个事件,当然利用 lambda 表达式我们可以轻松的完成此功能:
// Timer(int delay, ActionListener listener)
Timer t = new Timer(1000, e -> System.out.println(event));
但是,如果直接把 println 方法传递到 Timer 构造器就更好了。利用方法引用可以直接传递一个已有的方法,例如:
Timer t = new Timer(1000, Systei.out::println);
表达式 System.out::println 是一个方法引用(method reference )。
它等价于 lambda 表达式 x -> System.out.println(x)。
其中,:: 操作符分隔方法名与对象或类名,操作符前是对象或类名,操作符后是方法名。
2.构造器引用
构造器引用与方法引用很类似,只不过方法名为 new,例如,String::new 是 String 构造器的一个引用,具体引用哪一个构造器将取决于上下文。
可以用数组类型建立构造器引用。例如,int[]::new是一个构造器引用,它有一个参数—— 即数组的长度。这等价于 lambda 表达式 x -> new int[x]。
Java 有一个限制,无法构造泛型类型 T 的数组(表达式 new T[n] 会产生错误,因为这会被虚拟机擦除替换为 new Object[n]),数组构造器引用对于克服这个限制很有用。
Stream 接口有一个 toArray方法可以返回 Object 数组:
Object[] stringList = stream.toArray();
不过,这并不让人满意。这里我们希望得到一个 String 引用数组,而不是 Object 引用数组。流库利用构造器引用解决了这个问题。可以把 String[]::new 传入 toArray方法:
String[] stringList = stream.toArray(String[]::new);