什么是lambda表达式?
- lambda 表达式是一个可传递的代码块,可以在以后执行一次或者多次 ----->《Java核心技术》
- lambda 表达式是一个匿名函数
为什么使用lambda表达式?
先看一个例子,这是一个典型的比较器,使用这个比较器可以对任意字符串按长度进行排序。
Comparator<String> com = new Comparator<String>() {
@Override
public int compare(String first, String second) {
return first.length()-second.length();
}
};
String[] str = {"saddsa", "sdadcsa","rgeavrr", "csfdcwe"};//如果使用Arrays类中的sort()方法,只能按照字典顺序排序。假如想要对其
//按照长度排序,需要传入比较器
Arrays.sort(str, com);
分析这段代码,其实我们不难发现,在执行排序方法的时候。compare() 方法并非只执行一次,在数组完成排序之前,sort() 方法会一直调用compare() 方法,只要元素的顺序不正确就会重新排列元素,将比较所需的 代码块(s1.length()-s2.length()) 放在 sort() 方法中,与其他的排序逻辑集成然后完成排序。也就是是说,这里存在一个不断回调的过程。
这么一分析,我们发现,真正参与排序逻辑的只是 代码块(s1.length()-s2.length()) 罢了。也许我们会想,为什么不直接给 sort() 方法传递这个代码块,而需要给它传递一个 Comparator 实例呢?这样不会显得过于繁琐吗?实际上,在Java 8 之前,我们也只能这样去做,Java是面向对象的语言,假若我们需要传递某段代码块,必须构造一个对象,这个对象的类需要有一个方法包含所需的代码。难道就没有解决的办法了?当然不是!
Java 8 推出了 lambda 表达式,能非常完美的解决这个问题,可以使代码变得更加简洁,灵活,优雅。(这时你可以思考一下Java语言为什么美?)
废话不多说,直接上代码
//第一种方式
Comparator<String> com = (first, second) -> first.length() - second.length();
String[] str = {"saddsa", "sdadcsa","rgeavrr", "csfdcwe"};
Arrays.sort(str, com);
//第二种方式
Arrays.sort(str, (first, second) -> first.length() - second.length());
看到没有,看到没有,这就是差距!你那么多废话,我一句就搞定,而且我效率还比你高,这就是强!
现在,让我们一起进入 lambda表达式 的世界,请在开始之前想一想刚才我所说的:lambda表达式传递的是代码块(函数),请暂时忘掉对象
如何使用lambda表达式
-
-> :lambda 操作符,它将 lambda表达式 分为两个部分
左侧:指定 lambda表达式 所需要的所有参数
右侧:lambda表达式的主体,即 lambda表达式 要执行的功能 -
lambda表达式语法
//方式一
Runnable r1 = () -> System.out.println("Hello World");//即使没有参数,也要提供括号
//方式二
Consumer<String> fun = (args) -> System.out.println(args);//方法带参数,需要传入一个参数
Consumer<String> fun = args -> System.out.println(args);//lambda 只需要一个参数时,参数的小括号可以省略
//方式三
BinaryOperator<long> bo = (x, y) -> { //lambda 需要两个参数,并且有返回值
System.out.println("实现接口");
return x + y;
};
BinaryOperator<long> bo = (long x, long y) -> x + y;//当lambda体只有一条语句时,return和大括号可以省略
BinaryOperator<long> bo = ( x, y) -> x + y;//类型可以省略,Java可以通过上下文推断得出,称为类型推断
函数式接口——理解 lambda表达式的关键
任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口
注意:这里说函数式接口必须有一个抽象方法也许会使大家疑惑。我们会想接口中所有的方法不都是抽象的吗?
实际上,接口完全有可能重新 声明Object类的 toString, clone 等方法,这些声明有可能会让方法不再是抽象的。
不过,默认方法和静态方法不会破坏函数式接口的定义
@FunctionalInterface//该注解会强制检查该接口是不是一个函数式接口
public interface FunctionalDefaultMethods {
void method();
default void defaultMethod() { //允许默认方法
}
}
lambda表达式只支持函数式接口,因为lambda表达式到最后只能转换为函数式接口。其实就我个人理解而言,这是一种必要的转换,lambda表达式本来就是为了方便某个重复性的功能,使对象的概念弱化,凸显逻辑代码的地位(也就是代码块)。如果不是函数式接口,你弄他好几个方法,lambda表达式总不能一次性全部去执行吧,这在逻辑上也是行不通的。这纯属是我对函数式接口的个人理解,也许会有误,欢迎讨论。
方法引用与构造器引用
前面说过,如果 lambda表达式 的代码块只有一条代码,可以省略花括号。同样的,你也可以使用方法引用和构造器引用。使用方法引用和构造器引用可以使代码阅读性更强,更简洁。
- object::InstanceMethodName 引用对象的实例方法
- ClassName::staticMethodName 引用类的静态方法(普通类)
- Class::instanceMethodName 引用类型对象的实例方法(比如String)
- ClassName::new 引用构造方法
Timer t = new Timer(1000, ele -> System.out.println(ele));//添加一个定时器,每隔1秒打印一次 ele
//使用方法引用可以写成(与上面的等价)
Timer t = new Timer(1000, System.out::println);//直接将 println 方法传递给构造器
对字符串排序
Arrays.sort(str, String::compareToIgnoreCase);//等价于 (x, y) -> x.compareToIgnoreCase(y)
//类似的 Math::pow 等价于 (x, y) -> Math.pow(x, y);
假如有多个重载的同名方法,编译器会尝试从上下文找出你指的那一个方法。不知不觉,Java又强势装逼一波。
- 构造器引用——主要与Stream结合使用:与方法引用类似,将方法名换为new。
//有一个字符串列表,将其转换为Person对象数组,需要在字符串上调用构造器
ArrayList<String> names = .....;
Stream<Person> stream = names.stream().map(Person::new);//Person::new 是 Person构造器 的一个引用
List<Person> people = stream.collect(Collection.toList);
- 数组也可以建立构造器引用
int[]::new 等价于 x -> new int[]
//将流中的对象转换为对象数组
Person[] person = stream.toArray(Person[]::new);
处理lambda表达式
通过上面的介绍,我们或许对lambda表达式有了进一步的理解,现在我们将学会怎样去使用lambda表达式。
使用lambda表达式的重点是为了延迟执行,下面是使用lambda表达式的几个场景
1.在一个单独的线程中运行代码
2.多次运行代码
3.在算法的适当位置运行代码(排序中的比较操作)
4.发生某种情况时执行代码(点击按钮数据到达)
其实,我们可以想一想,这些情况大多在哪里出现呢?想到排序,想到比较,这很容易让我们联想到集合和数组。嗯,就我现阶段的水平而言,或许我只能理解lambda表达式在集合和数组中给我带来的便利,但我想这也就足够了。
//这就是一个比较常规的用法,可以把lambda表达式作为参数传递给方法,而且需要注意,lambda表达式的参数必须是一个函数式接口
public static void repeat(int n , Runnable action){
for(int i = 0 ; i < n; i++)
action.run();
}
repeat(10, () -> System.out.println("Hello, world"));