Lambda表达式支持将代码块作为方法参数,Lambda表达式允许使用更简单的代码来创建只有一个抽象方法的接口(这种接口被称为函数式接口)的实例。
5.8.1 Lambda 表达式入门
Command.java
package code;
public interface Command
{
// 接口里定义的process()方法用于封装“处理行为”
void process(int[] target);
}
ProcessArray.java
package code;
public class ProcessArray
{
public void process(int[] target , Command cmd)
{
cmd.process(target);
}
}
CommandTest.java
package code;
public class CommandTest{
public static void main(String [] args){
ProcessArray pa = new ProcessArray();
int [] target = {3,-4,6,4};
pa.process(target,new Command(){
public void process(int [] target){
int sum = 0;
for(int tmp : target){
sum += tmp;
}
System.out.println("数组元素的总和是"+ sum);
}
});
}
}
数组元素的总和是9
CommandTest2.java
package code;
public class CommandTest2{
public static void main(String[] args){
ProcessArray pa = new ProcessArray();
int [] array = {3,-4,6,7};
pa.process(array, (int[] target)->{
int sum = 0;
for(int tmp : target){
sum += tmp;
}
System.out.println("数组元素的总和是" + sum);
});
}
}
Lambda表达式可以简化匿名内部类对象,不需要new Xxx(){}这种繁琐的代码,不需要指出重写的方法名字,也不需要给出重写的方法的返回值类型———只要给出重写的方法括号以及括号里的形参列表即可。
Lambda表达式的代码块会代替实现抽象方法的方法体,Lambda表达式就相当一个匿名方法。
Lambda表达式由三个部分组成:
- 形参列表。允许省略形参类型,如果形参列表中只有一个参数,甚至连形参列表的圆括号也可以省略。
- 箭头(->)
- 代码块。如果代码块只包含一条语句,Lambda表达式允许省略代码块的花括号,那么这条语句就不要用花括号表示语句结束。Lambda代码块只有一条return语句,甚至可以省略return关键字,Lambda表达式需要返回值,而他的代码块中仅有一条省略了return的语句,Lambda表达式会自动返回这条语句的值。
示例:LambdaQs.JAVA
package code;
interface Eatable{
void taste();
}
interface Flyable{
void fly(String weather);
}
interface Addable{
int add(int a ,int b);
}
public class LambdaQs{
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();
//Lambda表达式的代码块只有一条语句,可以省略花括号
lq.eat(()->System.out.println("苹果的味道不错"));
//Lambda表达式的形参列表只有一个形参,可以省略圆括号
lq.drive(weather->{
System.out.println("今天天气是"+ weather);
System.out.println("直升机飞行平稳");
});
//Lambda表达式的代码块只有一条语句,可以省略花括号
//代码块中只有一条语句,即使该表达式需要返回值,也可以省略return关键字
lq.test((a,b)->a+b);
}
}
code.LambdaQs
Lambda$1/1175962212@24d46ca6
苹果的味道不错
我正在驾驶code.LambdaQs$$Lambda$2/925858445@2f92e0f4
今天天气是碧空如洗的晴日
直升机飞行平稳
5+3的和为8
5.8.2 Lambda 表达式与函数式接口
Lambda表达式的类型,也被称为“目标类型(target type)”,Lambda表达式的目标类型必须是“函数式接口(functional interface)”。函数式接口代表只包含一个抽象方法的接口,函数式接口可以包含多个默认方法,类方法,但只能声明一个抽象方法。
如果采用 匿名内部类语法来创建函数式接口的实例,则只需要实现一个抽象方法,在这种情况下即可采用Lambda表达式来创建对象。
java8专门为函数式接口提供了@FunctionalInterface注解,该注解通常放在接口定义前面,该注解对程序功能没有任何作用,它用于告诉编译器执行更严格的检查—检查该接口必须是函数式接口,否则编译器就会报错。
由于Lambda表达式的结果就是别当成对象,因此程序中完全可以使用Lambda表达式进行赋值。
Runnable r =()->{
for(int i = 0;i<100;i++)
System.out.println();
};
Lambda表达式有两个限制。
- Lambda表达式的目标类型必须是明确的函数式接口
- Lambda表达式只能为函数式接口创建对象,Lambda表达式只能实现一个方法,因此它只能为只有一个抽象方法的接口创建对象。
Obj obj = ()->{
for(int i=0;i<100;i++)
System.out.println();
};
上面程序直接赋值给Object变量,编译上面程序会报错。
LambdaTest.java:33: 错误: 不兼容的类型: Object 不是函数接口
所以Lambda表达式的目标类型必须是明确的函数式接口,而Lambda表达式的类型为Object,Object并不是函数式接口,因此上面代码报错。
可以用上面三种常见的方式来保证代码正确:
- 将Lambda表达式赋值给函数式接口类型的参数传给某个方法
- 将Lambda表达式作为函数式接口类型的参数传给某个方法
- 使用函数式接口对Lambda表达式进行强制类型转换
Object obj = (Runnable)->{
for(int i = 0;i<100;i++)
System.out.println();
}
同样的Lambda表达式的目标类型完全可能是变化的—–唯一的要求是要与目标类型中唯一的抽象方法有相同的形参列表。
@FunctionalInterface
interface FKTest{
void run();
}
Object obj2 = (FKTest)()->{
for(int i= 0;i<100;i++)
System.out.println();
};
Java8在java.util.function包下预定义了大量函数式接口。
- XxxFunction
- 包含一个apply()抽象方法,返回一个新值
- XxxConsumer
- 包含一个accept()抽象方法,不返回处理结果
- XxxPredicate
- 包含一个test()抽象方法,返回一个boolean值
- XxxSupplier
- 包含一个getAsXxx()抽象方法,返回一个数据
5.8.3 方法引用与构造器引用
方法引用和构造器引用可以让Lambda表达式的代码更加简洁。
种类 | 示例 | 对应的Lambda表达式 |
---|---|---|
引用类方法 | 类名::类方法 | (a,b…)->类名.类方法(a,b…) |
引用特定对象的实例方法 | 特定对象::实例方法 | (a,b…)->特定对象.实例方法(a,b…) |
引用某类对象的实例方法 | 类名::实例方法 | (a,b…)->a.实例方法(b…) |
引用构造器 | 类名::new | (a,b…)->类名(a,b…) |
详细:
- 引用类方法
@functionalInterface
interface Converter{
Integer convert(String from);
}
public class MethodRefer{
public static void main(String []args){
//Converter con1 = from->Integer.valueOf(from);
Converter con1 = Integer::valueOf;
Integer val = con1.convert("22");
System.out.println(val);
}
}
对于上面的类方法引用,也就是调用Integer类的valueOf()类方法来实现Converter函数式接口中唯一的抽象方法,当调用Converter接口中的唯一的抽象方法时,调用参数将会传给Integer类的valueOf()类方法。
2. 引用特定对象的实例方法
先用Lambda表达式来创建一个Converter对象
Converter converter2 = from->"fkit.org".indexOf(from);
Integer value = converter2.convert("it");
System.out.println(value);
调用converter1对象的convert()方法将字符串转换为整数,
Converter converter2 = "fkit.org"::indexOf;
Integer value = converter2.convert("it");
对于上面的实例方法,也就是调用“fkit.org”对象的indexOf()实例方法,来实现,Converter函数式接口中唯一的抽象方法,调用参数将会传给”fkit.org”对象的indexOf()实例方法。
3. 引用某类对象的实例方法
@FunctionalInterface
interface MyTest{
String test(String a,int b,int c);
}
该函数式接口中包含一个test()抽象方法,该方法负责根据Stirng,int,int三个参数生成一个String返回值。
//MyTest mt = (a,b,c)->a.substring(b,c);
MyTest mt = String::substring;
String str = mt.test("Java I Love You",2,9);
System.out.println(str);
创建Lambda表达式,
接着可以调用mt对象的test()—–由于mt对象是Lambda表达式创建的,test()方法执行体就是Lambda表达式的代码块部分,因此上面程序输出
va I Lo
对于上面的实例方法引用,也就是调用某个String对象的substring()实例方法来实现MyTest函数式接口中唯一的抽象方法,当调用MyTest接口中的唯一的抽象方法时,第一个调用参数将作为substring()方法的调用者,剩下的调用参数会作为substring()实例方法的调用参数。
4. 引用构造器
import javax.swing.*;
@FunctionalInterface
interface YourTest{
//JFrame需要导入javax.swing.*;
JFrame win(String title);
}
该函数接口包含一个win()抽象方法, 该方法负责根据String参数生成一个JFrame返回值。
//YourTest yt = (String a) -> new JFrame(a);
YourTest yt = JFrame::new;
JFrame jf = yt.win("我的窗口");
//System.out.println(jf);
jf.setVisible(true);
使用Lambda表达式创建一个YourTest对象,
接着是调用yt对象的win()方法—-由于yt对象是Lambda表达式创建的,因此win()方法执行体就是Lambda表达式的代码块部分,即执行体就是执行new JFrame(a);语句,并将这条语句的值作为方法的返回值。
调用YourTest对象的win()抽象方法时,实际只传入了一个String类型的参数,这个String类型的参数会被传给JFrame构造器—这就确定了是调用JFrame类的、带一个String参数的构造器。
5.8.4 Lambda表达式与匿名内部类的联系和区别
Lambda表达式是匿名内部类的一种简化,因此它可以部分取代匿名内部类的作用,Lambda和匿名内部类有以下的相同点
- 都可以直接访问“effectively final”的局部变量,以及外部类的成员变量。
- 两个生成的对象一样,都可以直接调用从接口中继承的默认方法。
LambdaAndInner.java
package code;
@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 = "疯狂软件中心";
public void test(){
String book = "疯狂Java讲义";
Displayable dis = ()->{
System.out.println("book局部变量为" + book);
System.out.println("外部类的age实例变量为" + age);
System.out.println("外部类的name类变量为" + name);
};
dis.display();
System.out.println(dis.add(3,5));
}
public static void main(String [] args){
LambdaAndInner lambda = new LambdaAndInner();
lambda.test();
}
}
book局部变量为疯狂Java讲义
外部类的age实例变量为12
外部类的name类变量为疯狂软件中心
8
当程序使用Lambda表达式创建了Displayable的对象之后,该对象不仅可调用接口中唯一的抽象方法,也可调用接口中的默认方法。
主要存在如下区别:
- 匿名内部类可以为任何接口创建实例—-不管接口包含多少个抽象方法,但是Lambda表达式只能为函数式接口创建实例。
- 匿名内部类可以为抽象类甚至普通类创建实例;但Lambda表达式只能为函数式接口创建实例;
- 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法,但Lambda表达式不允许调用接口中定义的默认方法
//尝试调用接口中的默认方法,编译器会报错
System.out.println(add(3,5));
虽然Lambda比大师的目标类型:Displayable中包含了add()方法,但是Lambda表达式的代码块不允许调用这个方法, 而改用匿名内部类的时候就可以调用这个add()方法。
5.8.5 使用Lambda表达式调用Arrays的类方法
Arrays类的有些方法需要Comparator、XxxOperato、XxxFunction等接口的实例,这些接口都是函数式接口,因此可以使用Lambda表达式来调用Arrays的方法。
package code;
import java.util.Arrays;
public class LambdaArrays{
public static void main(String [] args){
String [] arr1 = new String[]{
"java","fkava","fkit","ios","android"};
Arrays.parallelSort(arr1,(o1,o2)->o1.length()-o2.length());
System.out.println(Arrays.toString(arr1));
int[] arr2 = new int[]{3,-4,25,16,30,18};
Arrays.parallelPrefix(arr2,(left,right)->left*right);
System.out.println(Arrays.toString(arr2));
long[] arr3 = new long[5];
Arrays.parallelSetAll(arr3,operand->operand*5);
System.out.println(Arrays.toString(arr3));
}
}
[ios, java, fkit, fkava, android]
[3, -12, -300, -4800, -144000, -2592000]
[0, 5, 10, 15, 20]
- 第一段arr1的Lambda表达式的目标类型是Comparator,该Comparator指定了判断字符串大小的标准,字符串越长,即可认为该字符串越大;
- 第二段arr2的Lambda表达式的目标类型是IntBinaryOperator,该对象将会根据前后两个元素来计算当前元素的值;
- 第三段arr3的Lambda表达式的目标类型是IntToLongFunction,该对象将会根据元素的索引来计算当前元素的值。