一、Lambda表达式概述
Lambda表达式是Java 8 引入的一个重要特性,它是函数式编程的基础。Lambda表达式本质上是一种匿名函数(Anonymous Function),是代码块。
Lambda表达式允许将函数作为方法的参数或者将代码块作为数据进行传递。
匿名内部类和Lambda表达式
匿名内部类和Lambda表达式都是在Java中可把代码块作为数据进行传递的容器,
它们可以用来传递行为(函数),可作为输入参数或返回值。
- 匿名内部类
匿名内部类最常见的用法是图形用户界面(GUI)编程中组件的事件处理中。例如:为了响应用户的操作,需要注册一个事件监听器。用户单击按钮时程序的响应动作。
匿名内部类的版本:
/***GUI事件处理匿名内部类的版本***/
button.addActionListener(new ActionListener()){
public void actionPerformed(ActionEvent event){
System.out.println("你点击了按键!");
}
};
在这个例子中,我们创建了一个匿名内部类,它实现了ActionListener()接口。当用户点击按钮时,按钮会调用方法actionPerformed(ActionEvent event)来响应,会在屏幕上显示“你点击了按键!”提示信息。
另一个示例:
/***线程实例的匿名内部类的版本***/
Thread thread = new Thread(new Runnable() {
public void run() {
System.out.println("我是匿名内部类的线程!");
}
});
thread.start();
匿名内部类线程版本,用匿名类实现了Runnable()接口的一个实例,并以此为参数来构建一个线程。
- Lambda表达式:
Lambda表达式是Java 8中引入的一种更简洁、更直观的方式来表示函数式接口的实例,使得代码更紧凑。
下面这行是GUI事件处理Lambda表达式的版本,这是一个带参数的Lambda表达式:
/***GUI事件处理Lambda表达式的版本***/
button.addActionListener(event->System.out.println("你点击了按键!"));
再看一个用无参数的Lambda表达式创建线程的版本:
/***线程实例的Lambda表达式的版本***/
Thread thread = new Thread(() -> {
System.out.println("我是Lambda表达式实现的线程!");
});
thread.start();
Lambda表达式甚至不需要标识名,这一点与匿名内部类相似。
Lambda表达式的引入让Java代码更加简洁明了,提高了代码的可读性和可维护性。
Lambda表达式的输入参数应当是值,不能是变量
Lambda表达式的输入参数不能是变量,尤其是在Lambda表达式内是不可变的。
在Lambda表达式中,如果引用了外部的变量,则这些变量必须是final修饰的或 事实上的final(effectively final)变量。final变量表示它的值不可变,而 事实上的final(effectively final)变量在Lambda表达式中被隐式视为final,即它的值在Lambda表达式中不可再被修改。这个限制是为了避免在Lambda表达式中修改外部变量引发线程安全问题。Lambda表达式是一种闭包,它可以访问外部作用域的变量,如果允许在Lambda表达式内部修改这些变量,可能导致多个线程同时修改同一个变量,引发线程安全问题。
在匿名内部类中,需要引用方法中变量作为参数时,需要把变量用final修饰,这意味着这参数不能在匿名内部类中随意改变。
还是以按钮的监听器为例来说明:
匿名内部类的版本2:
final String name = getUserName();
button.addActionListener(new ActionListener()){
public void actionPerformed(ActionEvent event){
System.out.println(name + ":你好!");
}
};
Java 8 虽然放松了这一限制,可以引用 非final变量,但是作为Lambda表达式输入参数,
在Lambda表达式中引用的外部变量必须是final修饰的或 事实上的final(effectively final)变量。
Lambda表达式的版本2:
String name = getUserName();
button.addActionListener(event->System.out.println(name + ":你好!"));
如果你试图给该变量多次赋值,然后在Lambda表达式中引用它,编译器就会报错,无法通过编译。
Lambda表达式是什么类型?
Lambda表达式是函数接口的实例,因此它的类型是函数接口。
但是Lambda表达式本身不包含实现的函数接口的信息;该信息可从其上下文环境中推导出来的。例如,下面这个Lambda表达式:
x -> 2 * x
这是二个操作符的函数接口:
interface IntOperation { int operate(int i); } //IntOperation函数接口
interface DoubleOperation { double operate(double i); } //DoubleOperation函数接口
下面这二行代码的写法都是合法的
IntOperation iOp = x -> x * 2; //把Lambda表达式赋值给IntOperation函数接口
DoubleOperation dOp = x -> x * 2; //把Lambda表达式赋值给DoubleOperation函数接口
Lambda表达式是对象吗?
Lambda表达式是函数接口的实例,因此它是Object子类的实例。Object也是Lambda表达式超类。下面这些都是合法的赋值:
Runnable r = () -> {}; // 创建一个Runnable类型的Lambda表达式,并赋值给句柄r
Object obj = r; // 向上转型为Object
Lambda表达式的作用域规则
虽然有的情景,Lambda表达式可以代替匿名内部类,但是Lambda表达式和匿名内部类的作用域规则是不同的。下面我们从this关键字和super关键字来进行说明。
在Java中,lambda表达式中的this关键字和super关键字具有特定的含义和使用场景。
Lambda表达式中的this关键字指向的是当前类的实例(包含Lambda表达式的类实例),Lambda表达式则只是当前类的一个属性或行为(方法、函数)。
这与匿名内部类中的this关键字不同,匿名内部类中的this指向的是匿名类本身。
我们来看一个测试验证例程LambdaScopeRules,例如:
public class LambdaScopeRules {
private String name = "LambdaScopeRules主类";
public void test() {
// 匿名类实现
new Thread(new Runnable() {
String name = "匿名类A";
@Override
public void run() {
System.out.println("匿名类的this指向的类:" + this.name);
}
}).start();
// lambda表达式实现
new Thread(() -> {
String name = "lambda表达式";
System.out.println("Lambda的this指向的类:" + this.name);
}).start();
}
public static void main(String[] args) {
LambdaScopeRules rulesTest = new LambdaScopeRules();
rulesTest.test();
}
}
测试结果如下所示:
从测试结果可得出结论
- 匿名类的this指针表示的就是匿名类自身,因此匿名类(与其他类一样)有自己的命名空间,它有类级别的作用域。
- Lambda 表达式的this指针表示的则是其主类(宿主类)。Lambda表达式没有引入新的命名空间,Lambda表达式只有代码块级别的作用域。
同样道理,对于super关键字也有与this关键字类似的不同。有兴趣的读者可以自行增加测试代码进行验证。
Lambda 表达式应用场景
Lambda 表达式主要用于实现只有一个抽象方法的接口,即函数式接口。例如,Java中的ActionListener、Runnable、Callable、Comparator和Consumer等都是函数式接口。
下面这些都是Java中库中定义的一部分函数接口:
public interface Consumer<T> { void accept(T t); }
public interface Runnable { void run(); }
public interface Callable<V> { V call() throws Exception; }
public interface ActionListener { void actionPerformed(ActionEvent e); }
public interface Comparator<T> { int compare(T o1, T o2); boolean equals(Object obj); }
类似的,如果一个方法接受声明于java.util.function包内的接口,例如Predicate、Function、Comparable、Consumer或 Supplier,那么就可以向其传递Lambda表达式。
函数接口是只有一个抽象方法的接口。
例如,集合上的forEach方法可以具有以下签名:
public void forEach(Consumer<? super T> consumer);
方法forEach()需要一个实现Consumer接口的实例作为参数。这里我们就可以使用一个Lambda表达式作为方法forEach()的入口参数。
我们来看一个实例,假设pointList是一个列表List
我们使用forEach来处理java.awt.Point列表中每个元素的x和y坐标,实现坐标的移动功能。如果使用Consumer的匿名内部类来实现,如下是匿名内部类实现版本:
pointList.forEach(new Consumer<Point>() {
public void accept(Point p) {
p.move(p.y, p.x);
}
});
Lambda表达式实现版本,则如下所示:
pointList.forEach(p -> p.move(p.y, p.x));
这是一个代码演示版本,无法真正在屏幕上演示。其完整的代码如下:
import java.awt.Point;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public class MovePoint {
public static void main(String[] args) {
List<Point> pointList = new ArrayList<>();
pointList.add(new Point(12, 68));
pointList.add(new Point(65, 26));
pointList.add(new Point(252, 18));
//版本一,匿名内部类实现版本
pointList.forEach(new Consumer<Point>() {
public void accept(Point p) {
p.move(p.y, p.x);
}
});
//版本二,Lambda表达式实现版本
pointList.forEach(p -> p.move(p.y, p.x));
}
}
Lambda 表达式能否完全替代匿名内部类
既然Lambda表达式这么好,那么它是否可以完全替代匿名内部类呢?
当然不行,因为Lambda表达式只能用于实现只有一个抽象方法的接口(即函数式接口)。如果一个接口或抽象类有多个抽象方法,那么它就不是一个函数式接口,就不能使用Lambda表达式来实现,这种情形还是需要使用传统的匿名类或具体的类来实现。
二、Lambda表达式的语法
Lambda表达式使用箭头->作为标志,将参数和函数体分隔开,参数可以有零个或多个,函数体可以是一个表达式、一段代码块或一个自定义的函数(方法)。如果主体是一个表达式,它将直接返回该表达式的结果;如果主体是一个代码块或自定义函数,它将按照常规的Java语法执行,并且还可以使用return语句来返回值。
Lambda表达式的基本语法 如下:
(parameters) -> expression /***格式一***/
(parameters) -> { statements; } /***格式二***/
其中:parameters是输入参数,参数可以有零个或多个。
expression是表示一个表达式;它可以是一个表达式、一段代码块或一个自定义的函数(方法)。
statements; 表示多条语句。
通常情况函数体的前后要由大括号括起来{ statements; };单行语句可省略大括号和最后的分号。
Lambda表达式的示例:
- 无参数的Lambda表达式:
() -> System.out.println("Lambda表达式无参数示例!");
- 带参数的Lambda表达式:
/***单行Lambda表达式***/
x -> 2 * x; //单个参数可省略括号
(int x, int y) -> x + y; //参数列表中的参数可以是显式类型声明
(x, y) -> x + y; //如果编译器可以根据上下文推断出参数类型,则可以省略类型声明
- 带参数的多行Lambda表达式:
c -> {
int s = c.size();
c.clear();
return s;
}
示例:
我们最后再来看一个简单的例程,本例用Lambda表达式定义了一个线程:
Runnable r = ()->new Timer(1000, e->System.out.println("日期时间:"+new Date())).start();
这行代码使用无参数的Lambda表达式定义了一个可运行的实例(实现了Runnable接口)。需要说明的是最后的“.start()”不是启动线程,它启动的是定时器Timer。
这个例子中共有两个Lambda表达式,其中的“e->System.out.println(“日期时间:”+new Date())”,是一个单参数的Lambda表达式,作为定时器的回调方法(函数)参数。这实现
的是ActionListener函数接口。
下面是完整的例程代码:
import java.util.Date;
import javax.swing.Timer;
public class Lambda {
public static void main(String[] args) {
Runnable r=()->new Timer(1000, e->System.out.println("日期时间:"+new Date())).start();
r.run(); //启动线程
while(true); //主线程无限循环
}
}
例程的运行时每隔1秒打印一次日期时间,其运行结果如下所示:
参考文献
标签:Java,接口,析解,匿名,new,public,表达式,Lambda From: https://blog.csdn.net/weixin_42369079/article/details/143272414