什么是Lambda表达式?
Lambda是Java8的重要更新,Lambda表达式支持将代码块作为方法参数,Lambda表达式允许使用更简洁的代码来创建只有一个抽象方法的接口的实例。
入门
案例:对数组进行操作
定义操作数组的命令的接口
package org.westos.demo8;
public interface Command {
//接口里定义的process方法用于封装"处理行为"
void process(int[] target);
}
定义数组操作,传入实现接口的行为和要操作的数组
package org.westos.demo8;
public class ProcessArray {
public void process(int[] target,Command cmd) {
cmd.process(target);
}
}
先使用匿名内部类
package org.westos.demo8;
public class CommandTest {
public static void main(String[] args) {
ProcessArray pa=new ProcessArray();
int[] target={3,-4,6,4};
//处理数组,具体处理方式取决于匿名内部类
pa.process(target, new Command() {
@Override
public void process(int[] target) {
int sum=0;
for (int tmp:target){
sum+=tmp;
}
System.out.println("数组的元素和"+sum);
}
});
}
}
使用Lambda表达式
package org.westos.demo8;
public class CommandTest2 {
public static void main(String[] args) {
ProcessArray pa=new ProcessArray();
int[] array={3,-4,6,4};
//处理数组,具体处理方式取决于匿名内部类
pa.process(array, (int[] target)->{
int sum=0;
for (int tmp:target){
sum+=tmp;
}
System.out.println("数组的元素和"+sum);
});
}
}
Lambda与创建匿名内部类时需要实现的process(int[] target)方法完全相同,只是不需要new Xxx{}这种烦琐的代码,不需要指出重写方法的方法名字,也不需要给出重写方法的返回值类型——只要给出重写的方法括号以及括号里的形参列表即可。
Lambda表达式其实就相当于一个匿名方法。主要作用就是代替匿名内部类的烦琐语法。
由三部分组成:
- 形参列表。形参列表允许省略形参类型,如果形参列表中只有一个参数,甚至连形参列表的圆括号也可以省略。
- 箭头(->)。
- 代码块:如果代码块只包含一条语句,Lambda表达式允许省略代码块的花括号。Lambda代码块只有一条return语句甚至可以省略return关键字。
简写写法:
interface Eatable {
void taste();
}
interface Addable {
int add(int a,int b);
}
interface Flyable {
void fly(String weather);
}
package org.westos.LambdaDemo;
public class LambdaQs {
//调用该方法需要Eatable对象
public void eat(Eatable e){
System.out.println(e);
e.taste();
}
public void drive(Flyable f){
System.out.println("我正在驾驶:"+f);
f.fly("【碧空如洗的晴日】");
}
public void test(Addable add){
System.out.println("5和3的和为:"+add.add(5,3));
}
public static void main(String[] args) {
LambdaQs lq=new LambdaQs();
//使用匿名内部类
lq.eat(new Eatable() {
@Override
public void taste() {
System.out.println("苹果味道不错");
}
});
//不省略花括号的Lambda表达式
lq.eat(()->{
System.out.println("苹果味道不错");
});
//使用Lambda表达式,表达式的代码块只有一句,可以省略花括号
lq.eat(()-> System.out.println("苹果味道不错"));
//Lambda表达式的形参只有一个形参,可以省略圆括号
lq.drive(weather -> {
System.out.println("今天的天气是"+weather);
System.out.println("直升机飞行平稳");
});
//代码块中只有一句,可以省略花括号,如果这时候有return返回也可以省略
lq.test((a,b)->a+b);
}
}
从上面的代码中,如果一个方法需要一个类作为参数时,传入Lambda表达式将会被当成任意类型对象。
Lambda表达式与函数式接口
Lambda表达式的类型被称为“目标类型”,Lambda表达式的目标类型必须是函数式接口。
函数式接口代表只包含一个抽象方法接口。函数式类型接口可以包含多个默认方法、类方法,但只能声明一个抽象方法。
//Runnable是Java本身提供的一个函数式接口
//其中只包含一个无参的抽象方法,因此下面的Lambda创建了一个Runnable对象
Runnable r=()->{
for (int i=0;i<100;i++){
System.out.println();
}
};
- Lambda表达式的目标类型必须是明确的函数式接口;
- Lambda表达式只能为函数式接口创建对象。因为Lambda表达式只能实现一个方法。
Object ob=()->{
for (int i=0;i<100;i++){
System.out.println();
}
};
在使用Lambda表达式会报下面的错误。因为Object不是函数式接口,所以不能用Lambda表达式来创建对象。
Error:(40, 19) java: 不兼容的类型: java.lang.Object 不是函数接口
为了保证Lambda表达式的目标类型是一个明确的函数式接口,可以有以下三种常见的方式:
- 将Lambda表达式赋值给函数式接口类型的变量;
- 将Lambda表达式作为函数式接口类型的参数传给某个方法;
- 使用函数式接口对Lambda表达式进行强制类型转换;
比如将上面代码改为:
Object ob=(Runnable)()->{
for (int i=0;i<100;i++){
System.out.println();
}
};
@FunctionalInterface注释,通常放在接口定义前面,检查该接口必须是函数式接口,否则编译器会报错。
同样的Lambda表达式的目标类型完全可能是变化的,唯一要求是,Lambda表达式实现的匿名方法与目标类型中唯一的抽象方法有相同的形参列表。
@FunctionalInterface
interface FKtest {
void run();
}
因为FKtest现在也是函数式接口类型,所以前面的类型转换也可以用FKtest来转换。
Java8中常见的函数式接口
- XxxFunction:这类接口通常只包含一个apply()抽象方法,该方法对参数进行处理、转换,然后返回一个新的值,该函数式接口通常用于对指定数据进行转换处理。
- XxxConsumer:这类接口通常包含一个accept()抽象方法,该方法也是对参数进行处理、转换。只是不返回处理结果。
- XxxxPredicate:这类接口中通常包含一个test()抽方法,该方法通常用来处理对参数进行某种判断,然后返回一个boolean值。该接口通常用于判断参数是否满足特定条件,经常用于进行筛虑数据。
- XxxSupplier:这类接口通常包含一个getAsXxx()抽象方法,该方法不需要输入参数,会按某种了逻辑算法返回一个数据。
方法引用与构造器引用
如果Lambda表达式的代码块只有一条代码,还可以在代码块中使用方法引用和构造器引用。
Lambda表达式支持的方法引用和构造器引用
种类 | 示例 | 说明 | 对Lambda表达式 |
---|---|---|---|
引用类方法 | 类名::类方法 | 函数式接口中被实现方法的全部参数传给给类方法作为参数 | (a,b…)->类名.类方法(a,b…) |
引用特定对象的实例方法 | 特定对象::实例方法 | 函数式接口中被实现方法的全部参数传给该方法作为参数 | (a,b…)->特定对象.实例方法(a,b…) |
引用某类对象的实例方法 | 类名::实例方法 | 函数式接口中被实现方发的第一个参数作为调度者,后面参数全部传给该方法作为参数 | |
引用构造器 | 类名::new | 函数式接口中被实现方法的全部参数传给该构造器作为参数 | (a,b…)->new 类名(a,b,…) |
引用类方法
interface Converter {
//返回一个字符类型
Integer converter(String from);
}
//将字符类型转换成整数型
//用Lambda表达式创建一个Converter对象
Converter converter1=from -> Integer.valueOf(from);
//由于converter1对象是Lambda表达式创建的,所以convert()方法执行体就是Lambda表达式的代码块部分。
Integer va1=converter1.converter("99");
System.out.println(va1);
由于上面的Lambda表达式代码块只有一行代码,所以可以用如下方法替换:
Converter converter2=Integer::valueOf;
当调用Converter接口中的唯一的抽象方法时,调用参数将会传给Interger类的valueOf()方法。
引用特定对象的实例方法
先使用Lambda方法来创建一个Converter对象:
Converter converter3=from -> "fkit.org".indexOf(from);
Integer value=converter3.converter("it");
System.out.println(value);
//“fkit.org”是一个String对象,调用这个对象的indexOf实例方法
//因为代码块只有一行,所以可以用下面的代码替换
Converter converter4="fkit.org"::indexOf;
Integer value1=converter3.converter("it");
System.out.println(value1);
对于上面的实例方法引用,也就是调用"fkit.org"对象的indexOf()实例方法来实现Converter函数式接口中唯一的抽象方法,当调用Converter接口中唯一的抽象爱那个方法时,调用参数将会传给"fkit.org"对象的indexOf()实例方法。
引用某类对象的实例方法
定义如下函数式接口:
@FunctionalInterface
interface MyTest {
String test(String a,int b,int c);
}
MyTest mt=(a,b,c)->a.substring(b,c);
//可以用以下代码替换
MyTest mt1=String::substring;
String str=mt.test("I love Java",3,5);
String str1=mt.test("Java I love you",3,5);
System.out.println(str);
System.out.println(str1);
对于上面的实例方法,也就是调用某个String对象的substring()实例方法来实现MyTest函数式接口中唯一的抽象方法,当调用Mytest接口中唯一的抽象方法时,第一个调用参数将作为substring()方法的调度者,剩下的调用参数将会作为substring()方法的调用参数。
引用构造器
定义如下函数式接口:
//下面代码使用Lambda表达式创建YourTest对象
//引用了JFrame的构造器
YourTest yt=(String a)->new JFrame(a);
//可以用下面的代码替换
YourTest yt1=JFrame::new;
//这里的win方法相当于执行了new JFrame(a)语句,并作为这个方法的返回值
JFrame jf=yt.win("我的窗口");
JFrame jf1=yt1.win("我的窗口");
System.out.println(jf);
System.out.println(jf1);
调用某个JFrame类的构造器来实现YourTest函数式接口中唯一的抽象方法,当调用这个抽象方法时,String类型的调用参数将会传给JFrame构造器。这就确定以时调用JFrame类的,一个带参数的构造器。
总结:
个人理解,使用Lamabda表达式时,也就是创建函数式接口对象时(其实可以理解为时匿名的子类对象,因为后面发生了重写),用你定义的引用类方法、引用对象方法或者引用构造器方法来重写了这个类中的抽象方法。
Lambda表达式与匿名内部类的联系和区别
相同点
- Lambda表达式与匿名内部类一样,都可以直接访问“effectively final”的局部变量,以及外部类的成员变量;
- Lambda表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法。
package org.westos.LambdaDemo;
@FunctionalInterface
interface Displayable {
void display();
default int add(int a,int b){
return a+b;
}
}
package org.westos.LambdaDemo;
public class DisplayTest {
private int age=12;
private static String name="教育中心";
public void test(){
String book="Java从入门到放弃";
//代码块相当于重写了接口中的抽象方法display()
//所以左右边Lambda表达式其实是子类的对象,而且继承了父类的默认方法
Displayable dis=()->{
System.out.println("局部变量"+book);
System.out.println("外部类变量"+name);
System.out.println("外部实例变量"+age);
};
dis.display();
System.out.println(dis.add(3,5));
}
public static void main(String[] args) {
DisplayTest dt=new DisplayTest();
dt.test();
}
}
- 与匿名内部类相似的是,Lambda表达式只能放完最终变量book,被匿名内部类访问的局部变量都隐式添加了final修饰符在前面。
匿名内部类和Lambda的区别
- 匿名内部类可以为任意窗口创建实例,但Lambda表达式只能为函数式接口创建实例;
- 匿名内部类甚至可以为抽象方法和普通方法创建实例,而Lambda表达式只能为函数式接口创建实例;
- 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法,而Lambda表达式的代码块不允许调用接口中定义的默认方法。