5.8 java 11增强的Lambda表达式
Lambda表达式支持将代码块作为方法的参数,Lambda表达式允许使用更加简洁的代码来创建一个只有一个抽象方法的接口(这种接口被称为函数式接口)的实例。
一、Lambda表达式入门——为了避免匿名内部类的繁琐
我们前面介绍了Command表达式的例子:
定义一个处理数组元素的接口
package one;
public interface Command
{
//接口里定义的Process方法用于封装“处理行为”
void process(int element);
}
定义一个处理数组的类
package two;
import one.Command;
public class ProcessArray
{
public void process(int[] target,Command cmd)
{
for(var t:target)
{
cmd.process(t);
}
}
}
1、通过匿名类来调用Commad处理数组
import one.Command;
import two.ProcessArray;
class CommandTest1
{
public static void main(String[] args)
{
var pa=new ProcessArray();
int[] a={1,5,9,7};
pa.process(a,new Command(){
public void process(int element)
{
System.out.println("数组元素的平方:"+element*element);
}
});
}
}
---------- 运行Java捕获输出窗 ----------
数组元素的平方:1
数组元素的平方:25
数组元素的平方:81
数组元素的平方:49
输出完成 (耗时 0 秒) - 正常终止
2、Lambda表达式来简化匿名内部类对象
import one.Command;
import two.ProcessArray;
class CommandTest2
{
public static void main(String[] args)
{
var pa=new ProcessArray();
int[] a={1,5,9,7};
pa.process(a,(int element)->
System.out.println("数组元素的平方:"+element*element));
}
}
这段代码代码与创建匿名内部类时实现的process(int element)方法完全相同,只是不需要new Xxx(){}的繁琐形式,不需要指出重写方法的名字,也不需要指出重写方法的返回值类型,只需要给出重写方法括号以及括号里的形参列表即可。
3、Lambda语句的组成
Lambda表达式主要用于代替匿名内部类的繁琐语法。它由三部分组成:
1、形参列表。形参列表允许是省略类型。如果形参列表只有一个参数,甚至连形参列表的圆括号也可以省略。
2、箭头(->)
3、代码块。如果代码块只有一条语句允许省略代码块的花括号;如果只有一条return语句,甚至可以省略return关键字。Lambda表达式需要返回值,而他的代码块仅有一条省略了return 语句,Lambda表达式会自动返回这条语句的值。
Lambda表达式的集中简化形式:
interface Eatable
{
void taste();//public abstract
}
interface Flyable
{
void fly(String weather);
}
interface Addable
{
int add(int a,int b);
}
public class LambdaQs
{
//调用该方法需要Eatable对象
public void eat(Eatable e)
{
System.out.println(e);
e.taste();
}
//调用该方法需要Flyable对象
public void drive(Flyable f)
{
System.out.println("我正在驾驶:"+f);
f.fly("[碧空如洗的晴天]");
}
//调用该方法需要Addable对象
public void test(Addable add)
{
System.out.println("3加5的和为:"+add.add(3,5));
}
public static void main(String[] args)
{
var lq=new LambdaQs();
//Lambda语句只有一条语句,可以省略花括号
lq.eat(()->System.out.println("苹果味道不错!"));
//Lambda表达式形参列表只有一个形参,可以省略圆括号
lq.drive(weather->{
System.out.println("今天天气是"+weather);
System.out.println("直升机平稳飞行");});
//Lambda只有一条语句时,可以省略花括号
//代码块只有一条语句,即使该表达式需要返回值,也可以省略return关键字
lq.test((a,b)->{return (a+b);});
lq.test((a,b)->a+b);
}
}
---------- 运行Java捕获输出窗 ----------
LambdaQs$$Lambda$1/0x0000000801201040@72ea2f77
苹果味道不错!
我正在驾驶:LambdaQs$$Lambda$2/0x0000000801201840@eed1f14
今天天气是[碧空如洗的晴天]
直升机平稳飞行
3加5的和为:8
3加5的和为:8
输出完成 (耗时 0 秒) - 正常终止
lq.eat()使用不带形参列表的匿名方法,由于该Lambda表达式只有一条代码,因此可以省略花括号;
lq.drive()的Lambda表达式的形参列表只有一个形参,因此省略了形参列表的圆括号;
lq.test()的Lambda表达式的代码块只有一行语句,这行语句的返回值作为该代码块的返回值。
二、lambda表达式与函数式接口
Lambda表达式的类型,也成为“目标类型(target type)”,Lambda表达式的目标类型必须是“函数式接口(functional interface)”。函数式接口代表只包含一个一个抽象方法的接口。函数式接口可以包含多个默认方法、类方法,但只能声明一个抽象方法。
2.1 匿名内部类和Lambda表达式的适用情况
如果采用匿名内部类语法来创建函数式接口,且只需要实现一个抽象方法,在这种情况下,即可采用Lambda表达式来创建对象,该表示创建出来的对象的目标类型就是函数式接口。
注:
Java 8专门为函数式接口提供了@FunctionalInterface注解,该注解用于方法在接口定义前面,该注解对程序功能没有任何影响,它用于告诉编译器执行更严格的检查——检查该接口必须是函数式接口,否则编译就会出错。
下面程序使用匿名内部类:
/*@FunctionalInterface
*A 不是函数接口
* 在 接口 A 中找到多个非覆盖抽象方法
*/
interface A
{
public void test1();
public void test2();
default void test3()//接口中的默认方法
{
System.out.println("接口A中的默认方法");
}
}
public class 适用匿名内部类
{
public void test(A a)
{
System.out.println("接口A含有两个抽象方法和一个默认方法,此时适合用匿名内部类");
a.test1();
a.test2();
a.test3();
}
public static void main(String[] args)
{
var p=new 适用匿名内部类();
p.test(new A()
{
public void test1()
{
System.out.println("接口中的抽象方法1");
}
public void test2()
{
System.out.println("接口中的抽象方法2");
}
});
}
}
接口A含有两个抽象方法和一个默认方法,此时适合用匿名内部类
接口中的抽象方法1
接口中的抽象方法2
接口A中的默认方法
下面定义的接口B只有一个抽象方法,是函数式接口,此时适合用Lambda表达式:
@FunctionalInterface
interface B
{
void test1(String msg);//抽象方法,默认public abstract
default void test2()//接口中的默认方法
{
System.out.println("接口A中的默认方法");
}
}
public class LambdaFor
{
public void test(B b)
{
System.out.println("接口A含有1个抽象方法和一个默认方法,是函数式接口");
b.test1("函数式接口A中的抽象方法");
b.test2();
}
public static void main(String[] args)
{
var p=new LambdaFor();
p.test((msg)->
System.out.println(msg));
}
}
---------- 运行Java捕获输出窗 ----------
接口A含有1个抽象方法和一个默认方法,是函数式接口
函数式接口A中的抽象方法
接口A中的默认方法
输出完成 (耗时 0 秒) - 正常终止
2.2 使用Lambda表达式赋值给对象
用于Lambda表达式的结果就是被当成对象,因此程序中完全可以使用Lambda表达式进行赋值。我们知道接口不能创建实例,接口中只能定义常量,因此接口不存在构造器和初始化块。接口不能创建实例,但是通过Lambda表达式我们可以创建一个“目标类型”并把它赋值给函数式接口的对象。
例如:
@FunctionalInterface
interface Runnable
{
void printNum();
}
public class RunnableTest
{
public static void main(String[] args)
{
//Runnable接口中只包含一个无参数的构造器方法
//Lambda表达式代表的匿名方法实现了Runnable接口中唯一的无参数方法
//因此下面的方法创建了一个Runnable的对象
Runnable r=()->{
for(int i=0;i<10;i++)
System.out.print(" "+i);
};
r.printNum();
}
}
---------- 运行Java捕获输出窗 ----------
0 1 2 3 4 5 6 7 8 9
输出完成 (耗时 0 秒) - 正常终止
Lambda表达式实现的匿名方法——因此它只能实现特定函数式接口中唯一方法。这意味着Lambda表达式有两个限制:
1、Lambda表达式的目标类型必须是明确的函数式接口。
2、Lambda表达式只能为函数式接口创建对象。Lambda表达式只能实现一个方法,因此他只能为只有一个抽象方法的接口(函数式接口)创建对象。
关于第一点限制举例:
@FunctionalInterface
interface A
{
void test();
}
class LambdaLimit1
{
public static void main(String[] args)
{
//Object a=()->{System.out.println("This is a test!");};
//上面代码将报错: 不兼容的类型: Object 不是函数接口
//Lambda表达式的目标类型必须是明确的函数式接口
A a=()->{System.out.println("This is a test!");};
a.test();//This is a test!
}
}
从错误信息可以看出,Lambda表达式的目标类型必须是明确的函数式接口。上述表达式将Lambda表达式赋给Object变量,编译器只能确定该表达式的类型为Object,而Object并不是函数式接口。
为了保证Lambda表达式的目标类型是一个明确的函数式接口,常见有三种方式:
1、将Lambda表达式赋值给函数式接口的变量;
//参考上面的完整程序
A a=()->{System.out.println("This is a test!");};
2、将Lambda表达式作为函数接口类型的参数传给某个方法。
interface A
{
void test(String msg);
}
public class ATest
{
public static void med(A a)
{
System.out.println("主类的非静态方法");
a.test("我是传奇");
}
public static void main(String[] args)
{
ATest.med((msg)->System.out.println(msg));
}
}
---------- 运行Java捕获输出窗 ----------
主类的非静态方法
我是传奇
输出完成 (耗时 0 秒) - 正常终止
3、使用函数式接口类型对Lambda表达式进行强制转换。
Object a=(A)()->{System.out.println("This is a test!");};
三、在Lambda表达式中使用var
对与var声明变量,程序可以使用Lambda表达式进行赋值。但由于var代表需要由编译器推断的类型,因此使用Lambda表达式对var表达式定义的变量进行赋值时,必须指明Lambda表达式的目标类型。
例如:
var a=(A)()->{System.out.println("This is a test!");};
如果程序需要对Lambda表达式的形参列表添加注解,此时就不能省略Lambda表达式的形参类型——因为注解只能放在形参类型之前。在Java 11之前,程序必须严格声明Lambda表达式中的每个形参类型,但实际上编译器完全可以推断出lambda表达式中每个形参的类型。
例如:下面程序定义了一个Predator接口,该接口中的prey方法的形参使用了@NotNull注解修饰:
@interface NotNull{}
interface Predator
{
void prey(@NotNull String animal);
}
接下来程序打算使用Lambda表达式来实现一个Predator对象。如果Lambda表达式不需要对animal形参使用@NotNull注解,则完全可以省略animal形参注解;但如果希望为animal形参注解,则必须为形参声明类型,此时可直接使用var来声明形参类型。
@interface NotNull{}
interface Predator
{
void prey(@NotNull String animal);
}
public class PredatorTest
{
public static void main(String[] args)
{
//使用var声明lambda表达式的形参类型
//这样即可为Lambda表达式的形参添加注解
Predator p=(@NotNull var animal)->System.out.println("老鹰在抓"+animal);
p.prey("小鸡");
}
}
//老鹰在抓小鸡
四、方法引用和构造器引用
Lambda表达式的方法引用和构造器引用都需要两个英文冒号::。Lambda表达式支持如下几种引用方式:
种类 | 示例 | 说明 | 对应的Lambda表达式 |
---|---|---|---|
引用类方法 | 类名::类方法名 | 函数式接口中被实现的方法的参数全部传给该类方法作为参数 | (a,b...)->类名.类方法(a,b...) |
引用特定对象的实例方法 | 特定对象::示例方法名 | 函数式接口中被实现的方法的参数全部传给该实例方法作为参数 | (a,b...)->特定对象.实例方法(a,b...) |
引用某类对象的实例方法 | 类名::实例方法名 | 函数式接口中被实现的方法的第一个参数作为调用者,后面的参数传给该方法作为参数 | (a,b,c...)->a.实例方法(b,c...) |
引用构造器 | 类名::new | 函数式接口中被实现的方法的全部参数传给该构造器作为参数 | (a,b...)->new 类名(a,b...) |
4.1 引用类方法
@FunctionalInterface
interface Converter
{
Integer convert(String form);
}
public class ConverterTest
{
public static void main(String[] args)
{
//Lambda表达式只有一条语句,可以省略1花括号:Lambda表达式会把这条代码的值作为返回值
Converter c=(form)->Integer.parseInt(form);
System.out.println(c.convert("185"));
//下面通过引用类方法来实现相同的功能
Converter cPlus=Integer::valueOf;
System.out.println(cPlus.convert("140"));
}
}
4.2 引用特定对象的实例方法
@FunctionalInterface
interface Converter
{
Integer convert(String form);
}
public class ConverterTest1
{
public static void main(String[] args)
{
//先使用Lambda表达式来创建一个Converter对象
Converter c=form->"fkit.org".indexOf(form);//代码块只有一条语句,因此Lambda表达式会把这条代码的值作为返回值
System.out.println(c.convert("it"));//输出2
//引用特定对象的特定方法 "fkit.org"是一个String对象
Converter c1="fkit.org"::indexOf;
System.out.println(c1.convert("org"));//输出5
}
}
对于上面的示例方法引用,也就是说,调用"fkit.org"对象的indexOf()实例方法来实现Converter函数式接口中唯一的抽象方法,当调用Converter接口中的唯一抽象的方法时,调用参数会传给"fkit.org"对象的indexOf()实例方法。
4.3 引用某类对象的实例方法
先介绍一个函数:public String substring(int beginIndex, int endIndex)返回字符串索引范围[beginIndex,endIndex)的子字符串。
@FunctionalInterface
interface MyTest
{
String test(String a,int b, int c);
}
class substringTest
{
public static void main(String[] args)
{
MyTest m=(a,b,c)->a.substring(b,c);
System.out.println(m.test("fkjava",1,5));
//引用某类对象的实例方法
MyTest mPlus=String::substring;
System.out.println(mPlus.test("hello world",2,7));//相当于"hello world".substring(2,7)
}
}
---------- 运行Java捕获输出窗 ----------
kjav
llo w
输出完成 (耗时 0 秒) - 正常终止
4.4 引用构造器
JFrame屏幕上window的对象,能够最大化、最小化、关闭
import java.awt.*;
import javax.swing.*;
@FunctionalInterface
interface YourTest
{
JFrame win(String title);
}
public class MethodRefer
{
public static void main(String[] args)
{
// 下面代码使用Lambda表达式创建YourTest对象
// YourTest yt = (String a) -> new JFrame(a);
// 构造器引用代替Lambda表达式。
// 函数式接口中被实现方法的全部参数传给该构造器作为参数。
YourTest yt = JFrame::new;
JFrame jf = yt.win("我的窗口");
System.out.println(jf);
}
}
五、Lambda表达式和匿名内部类的联系和区别
Lambda表达式与匿名内部类之间存在如下相同点:
1、Lambda表达式与匿名内部类一样,都可以直接访问"effectively final"的局部变量,以及外部类的成员变量,包括实例变量和类变量。
2、Lambda表达式创建的对象与匿名内部类生成的对象一样,都可以直接从接口中继承的默认方法。
@FunctionalInterface
interface Displayable
{
void display();
default int add(int a,int b)
{
return a+b;
}
}
public class LambdaAndInner
{
private int age=12;
private static String name="fkit.org";
public void test()
{
var book="疯狂Java讲义";
Displayable dis=()->{
//访问"effictively final"的局部变量
System.out.println("book局部变量为:"+book);
//访问外部类的实例变量和类变量
System.out.println("外部类的age实例变量:"+age);
System.out.println("外部类的name类变量:"+name);
};
dis.display();
//调用方对从接口继承add()方法
System.out.println(dis.add(3,5));
}
public static void main(String[] args)
{
var lambda=new LambdaAndInner();
lambda.test();
}
}
---------- 运行Java捕获输出窗 ----------
book局部变量为:疯狂Java讲义
外部类的age实例变量:12
外部类的name类变量:fkit.org
8
输出完成 (耗时 0 秒) - 正常终止
与匿名函数相似的是,由于Lambda表达式访问了book局部变量,因此该局部变量相当于有一个隐式的final修饰,因此同样不允许对book局部变量重新赋值。当程序使用了Lambda表达式创建了Displayable对象之后,该对象不仅可调用接口的抽象方法,也可以调用接口中的默认方法,因此同样不允许对book局部变量重新赋值。
</font color=red>Lambda表达式与匿名内部类的区别:
1、匿名内部类可以为内部类可以为任意接口创建实例;但Lambda表达式只能为函数式创建实例。
2、匿名内部类可以为抽象类乃至普通类创建实例;但Lambda表达式只能为函数式接口创建实例。
3、匿名内部类是实现抽象方法的方法体允许调用接口中定义的默认方法;但Lambda表达式的代码块不允许不允许调用接口中的默认方法。
六、使用Lambda表达式调用Arrays的类方法
Arrays类的有些方法需要Comparator、XxxOperator、XxxFunction等接口的实例,这些接口都是函数式编程,因此可以使用Lambda表达式来调用Arrays的方法。
import java.util.Arrays;
public class LambdaArrays
{
public static void main(String[] args)
{
var arr1 = new String[] {"java", "fkava", "fkit", "ios", "android"};
Arrays.parallelSort(arr1, (o1, o2) -> o1.length() - o2.length());
//这行Lambda表达式的目标类型是Comparator,该Comparator指定判断字符串大小的标准:字符串越长,认为该字符串越大
System.out.println(Arrays.toString(arr1));
var arr2 = new int[] {3, -4, 25, 16, 30, 18};
// left代表数组中前一个所索引处的元素,计算第一个元素时,left为1
// right代表数组中当前索引处的元素
Arrays.parallelPrefix(arr2, (left, right)-> left * right);
//这行Lambda表达式的目标类型是IntBinaryOperator,该对象将会根据前后两个元素来计算当前元素
System.out.println(Arrays.toString(arr2));
var arr3 = new long[5];
// operand代表正在计算的元素索引
Arrays.parallelSetAll(arr3, operand -> operand * 5);
//这行Lambda表达式的目标类型是IntToLongFunction,该对象将会根据当前索引值计算当前元素的值。
System.out.println(Arrays.toString(arr3));
}
}
---------- 运行Java捕获输出窗 ----------
[ios, java, fkit, fkava, android]
[3, -12, -300, -4800, -144000, -2592000]
[0, 5, 10, 15, 20]
输出完成 (耗时 0 秒) - 正常终止