▌为什么需要lambda表示式?
举几个栗子:
sort
方法需要传入一个实现Comparator
接口的类的实例;Timer
需要传入一个实现ActionListener
接口的类的实例;Thread
需要传入一个实现Runnable
接口的类的实例…
这是为什么?它们需要的真的是这个实例对象吗?
透过现象看本质:它们真正需要的,是一个"函数",是一个告诉它们,根据什么去排序、被触发后执行什么、线程去执行什么任务的"函数"(compare、actionPerformed、run)。可是,Java不允许"函数"作为参数。
所以,才将这个"函数"(或者说是代码块),做成一个接口的一个抽象方法;我们需要"函数"作为参数时,就做出一个该接口的实现类的一个实例对象,作为参数———这个使命,被委托给了一个对象
Java8之后,这个作为参数的对象,可以被一个lambda表达式所取代了。
这无疑大大简化了代码,在某些情况下提升了效率——更重要的是,这是大势所趋的"函数式编程"思想的又一次胜利。
在没有计算机的数学时代,逻辑学家Church意识到他需要将一个函数符号化,他使用了希腊字母λ——λ的发音即为lambda
从那之后,带有参数的表达式就被称为lambda表达式
▌lambda表示式的语法
"箭头函数"是lambda表达式的其中一种。即()->{}
>>> lambda表达式是对对象的替代。下面我们演示一下这两种方式,并介绍lambda表达式的简单语法
// 使用匿名内部类,实现多线程
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程已被创建");
}
}).start();
// 使用Lambda表达式(代替Runable接口的具体实现类)
new Thread(() -> System.out.println(Thread.currentThread().getName() + "线程已被创建")).start();
// 使用匿名内部类,实现山的高度的排序
Arrays.sort(mountains, new Comparator<Mountain>() {
@Override
public int compare(Mountain m1, Mountain m2) {
return m1.height - m2.height;
}
]);
// 使用Lambda表达式(代替Comparator的具体实现类)
Arrays.sort(mountains, (Mountain m1, Mountain m2) -> {
return m1.height - m2.height;
});
// Lambda省略规则:
// 1.参数类型可省
// 2.一个参数时,()可省
// 3.函数体只有一句时,"{}" ";" "return"可以一起省
// 4.参数类型可省————可以【推导】出来时
// 5.其实,这个接口的名字也省了————因为可以【推导】出来
Arrays.sort(mountains, (m1, m2) -> m1.height - m2.height);
其实"作为参数"这样的说法并不完整,lambda表达式同样可以作为"返回值"
public class Demo {
public static Comparator<String> getComparator() {
return new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
};
}
public static Comparator2<String> getComparator2(){
return (s1, s2)-> s1.length() - s2.length();
}
public static void main(String[] args) {
String[] arr = {"loli", "hahaha", "x"};
Arrays.sort(arr, getComparator());
Arrays.sort(arr, getComparator2());
System.out.println(Arrays.toString(arr));
}
}
很容易发现,传入lambda表达式的形参都是接口,准确的说,是一个函数式接口
只要是函数式接口作为形参,就需要传入一个实现该接口的对象————lmabda表达式本身不是对象,但可以推导并生成出一个对象
▌ 函数式接口
有且只有一个抽象方法的接口。
@FunctionalInterface // 注解:检测接口是否是一个函数式接口
修饰符 interface 接口名称 {
public abstract void method(); // public abstract 还是推荐加上
}
函数式接口作为形参时,也就是需要传入该接口的对象时,就可以传入一个lambda表达式。
下面的思想一定要理解后,牢牢掌握 :
最好把lambda表达式看作一个函数——用这个函数覆盖接口的抽象方法,并生成一个该接口的实例对象。
这种做法比传入一个对象省略了不少代码(甚至省去了接口名称,因为可以推导出来)。
▌ 懒惰计算
懒惰计算也称为延迟求值计算。顾名思义。
>>> 举个例子:
public class Demo1 {
public static void printer(boolean isOk, String s){ // 打印机(只有在isOk允许时才打印)
if(isOk){
System.out.println(s);
}
}
public static void main(String[] args) {
String s1 = "loli";
String s2 = "suki";
String s3 = "saikou";
printer(false, s1 + s2 + s3);
}
}
>>> 看出这个"打印机"的弊端了吗 ?
>>> 当isOk为false时,字符串并不需要被打印,而"字符串的拼接"这个行为在传参时就实实在在的发生了————这无疑是一种浪费
>>> 下面看看我们如何使用 函数式接口+lambda表达式 优化这个问题
------- ↓ Builder.java ------------
@FunctionalInterface
public interface Builder {
public abstract String stringBilder();
}
-------- ↓ Demo2.java -------------
public class Demo2 {
public static void printer(boolean isOk, Builder bl){
if (isOk){
System.out.println(bl.stringBilder());
}
}
public static void main(String[] args) {
String s1 = "loli";
String s2 = "suki";
String s3 = "saikou";
printer(true, ()-> s1 + s2 + s3);
}
}
这的确需要理解 >_< :
优化后的方法传入的参数不是拼接的字符串,而是字符串拼接的函数
这样,字符串拼接这个行为就委托给了这个接口对象——在真正运行时,才会触发字符串拼接这个行为;而当isOk为false时,显然这段代码不会被运行到,拼接字符串的行为自然也就被避免了。
所以说:此时的"拼接字符串"是个懒惰计算——被延迟到了if块的内部
懒惰计算的特点是:原先的参数是个在调用时就会实实在在执行的动作,被替换成了一个描述该动作的函数
>>> 这个特点其实是比较明显的:
func(m, n, new Date(2077, 6, 21));
↓ 优化
func(m, n, ()->new Date(2077, 6, 21)); // (形参)函数式接口是个生产型接口:Supplier
▌ 常见的函数式接口
Supplier—get——生产工厂
Consumer—accept——消费者
Predicate—test——判断
Function—apply——类型转换
将函数式接口作为形参的语义是:
我要(生产、使用、判断、类型转换)一个数据;但是具体的行为(生产一个怎样的数据、怎样去使用数据、根据什么去判断、怎样去转换)就由之后传入的lambda表达式决定吧!
具体含义及其典例代码:戳这里,一定要戳 ! ! ! (★)
方法引用是lambda表达式的孪生兄弟———都作为函数式接口的实例
方法引用和lambda表达式一样,本身都不是一个对象。但作为参数时【推导并生成】了一个对象
方法引用的语法有三种情况(方法引用转化成lambda表达式):
❶ object::instanceMethod
(对象::普通实例方法)———直接补参数就行
❷ Class::instanceMethod
(类::普通实例方法)——— 类作为一个隐式参数,再补参数
❸ Class::staticMethod
(类::静态方法)——— 直接补参数就行
1.
System.out::println => x->System.out.println(x)
separator::equals => x->separator.equals(x)
2.
String::trim => x->x.trim()
String::concat => (x, y)->x.concat(y)
3.
Integer::valueOf => x->Integer.valueOf(x)
Integer::sum => (x, y)->Integer.sum(x, y)
理解:
s->s.length()==3
,需要完成"取长度"和"作比较"两个行为,你很难把它缩写为方法引用【补充知识点】
"构造器引用"————可以认为是一个"方法名为new的方法引用"
Integer::new 等价于lambda表达式: x->new Integer(x)
String[]::new 等价于lambda表达式: x->new String[x]
public class Demo {
public static String create(Supplier<String> sub){
return sub.get();
}
public static void main(String[] args) {
String str = "loli";
String s = create(()->{
return str;
});
System.out.println(s); // 输出"loli"
}
}
>>> 作为参数的lambda表达式中,使用到了一个未在lambda表达式中定义的变量————str
>>> 由此观之,lambda表达式的作用域,绝对不局限在它的()-{}的大括号中
实际上,lambda表达式的作用域,不是大括号,而是大括号的外围——和()->{}
所在的那一整句话在同一作用域
就本例而言,str不是在lambda表达式中定义的,而是被lambda表达式捕获(captured)的。
这种捕获,是一种更为严格的闭包(closure):
之所以是闭包,是因为lambda表达式从外部捕获了一个变量并据为己有,保存在自己的大括号中
之所以说更为严格,是因为Java要求捕获的这个变量,必须是事实最终变量(effectively final)——捕获之前,它在外围作用域不能"乱动";捕获之后,也不能对它进行修改
public class Demo {
public static int create(Supplier<Integer> sub) {
return sub.get();
}
public static void main(String[] args) { // 下面的lambda表达式都省略了一个大括号
for (int i = 0; i < 10; i++) {
create(() -> i); // Error:捕获之前,该变量不能"乱动"
}
int n = 3;
create(() -> n++); // Error:捕获之后,也不能去修改它
}
}
♪ End
♬ By a Lolicon