Lambda表达式支持将代码块作为方法参数,Lambda表达式允许使用更简洁的代码来创建只有一个抽象方法的接口(这种接口叫函数式接口)的实例

public interface Command
{
// 接口里定义的process()方法用于封装“处理行为”
void process(int[] origin);
}
public class ProcessArray
{
public void process(int[] target, Command cmd)
{
cmd.process(target);

}
}
public class CommandTest
{
public static void main(String[] args)
{
var pa = new ProcessArray();
int[] target = {3, -4, 6, 4};
// 处理数组,具体处理行为取决于匿名内部类
pa.process(target, new Command()
{
*public void process(int[] origin)
{
int sum = 0;
for (int tmp : origin)
{
sum += tmp;
}
System.out.println("数组元素的总和是:" + sum);
}*
});
}
}

ProcessArray类的process()方法处理数组时,希望传入一段代码作为具体的处理行为,从代码中可以看到,用于封装处理行为的关键就是代码中的粗体部分,但为了向process()方法传入这段粗体代码段,程序使用了匿名内部类的语法来创建对象

当使用Lambda表达式代替匿名内部类创建对象时,Lambda表达式的代码块将会代替实现抽象方法的方法体,Lambda表达式就相当于一个匿名方法,并且可以简化创建匿名内部类对象。

public class CommandTest2
{
public static void main(String[] args)
{
var 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);
});
}
}

另一组实例

public interface Command
{
// 接口里定义的process()方法用于封装“处理行为”
void process(int element);
}
public class ProcessArray
{
public void process(int[] target, Command cmd)
{
for (var t : target)
{
cmd.process(t);
}
}
}
public class CommandTest
{
public static void main(String[] args)
{
var pa = new ProcessArray();
int[] target = {3, -4, 6, 4};
// 处理数组,具体处理行为取决于匿名内部类
pa.process(target, new Command()
{
public void process(int element)
{
System.out.println("数组元素的平方是:" + element * element);
}
});
}
}
public class CommandTest2
{
public static void main(String[] args)
{
var pa = new ProcessArray();
int[] array = {3, -4, 6, 4};
// 处理数组,具体处理行为取决于匿名内部类
pa.process(array, (int element)->{
System.out.println("数组元素的平方是:" + element * element);
});
}
}

比较两段代码的不同,第二种实现不需要new Xxx(){}这种繁琐的代码,不需要指出重写的方法名字,不需要给出重写的方法的返回值类型,只要给出重写的方法括号以及括号里的形参列表即可

Lambda表达式组成

  • 形参列表,形参列表允许省略形参类型,如果形参列表中只有一个参数,甚至连形参列表的圆括号都可以省略
  • 箭头(->),必须使用英文中线和大于号
  • 代码块,如果代码块只包含一条语句,Lambda表达式允许省略代码块的花括号,那么这条语句就不要用花括号表示语句结束,如果Lambda代码块只有一条return语句,甚至连return关键都可以省略,Lambda表达式需要返回值,而他的代码块中仅有一条省略了return的语句,Lambda表达式会自动返回这条语句的值。
interface Eatable
{
void taste();
}
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("5与3的和为:" + add.add(5, 3));
}
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) -> a + b);
}
}
  • 代码​​lq.eat(*()** -> System.out.println("苹果的味道不错!"**)*);​​调用eat()方法,调用该方法需要一个Eatable类型的参数,但实际传入的是Lambda表达式
  • 代码​​lq.drive(*weather -> { System.out.println("今天天气是:" + weather); System.out.println("直升机飞行平稳"); **}*);​​调用drive()方法,调用该方法需要一个Flyable类型的参数,但实际传入的是Lambda表达式
  • 代码​​lq.test(*(**a, b) -> a + b*);​​调用test()方法,带哦用该方法需要一个Addable类型的参数,但实际传入的是Lambda表达式
  • 这说明Lambda表达式实际上将会被当成“任意类型”的对象,到底当成什么类型的对象,则取决于运行环境的需要

Lambda表达式与函数式接口

Lambda表达式的“类型”,也被称为“目标类型(target type)”,Lambda表达式的目标类型必须是“函数式接口”,函数式接口代表只包含一个抽象方法的接口,它可以包含多个默认方法、类方法但只能声明一个抽象方法。
如果采用匿名内部类语法来创建函数式接口的实例,则只需要实现一个抽象方法,在这种情况下即可采用Lambda表达式来创建对象,该表达式创建出来的对象的目标类型就是这个函数式接口。

Java8专门提供了函数式接口的注解​​@FunctionalInterface​​,该注解方法接口定义前面,虽然它对程序功能没有任何作用,但可以告诉编译器严格检查它标注的接口必须是函数式接口,否则报异常

由于Lambda表达式的结果就是被当成对象,因此程序中完全可以使用Lambda表达式进行赋值

@FunctionalInterface
interface FkTest
{
void run();
}

public class LambdaTest
{
public static void main(String[] args)
{
// Runnable接口是Java本身提供的一个函数式接口,它只包含一个无参数的方法
// Lambda表达式代表的匿名方法实现了Runnable接口中唯一的、无参数的方法
// 因此下面的Lambda表达式创建了一个Runnable对象
Runnable r = () -> {
for (var i = 0; i < 100; i++)
{
System.out.println(i);
}
};
/* 下面代码报错: 不兼容的类型: Object不是函数接口
Object obj = () -> {
for (var i = 0; i < 100; i++)
{
System.out.println(i);
}
};*/
// 对Lambda表达式执行强制类型转换,这样可以确定该表达式的目标类型为Runnable函数式接口
Object obj1 = (Runnable)() -> {
for (var i = 0; i < 100; i++)
{
System.out.println(i);
}
};

// 同样的Lambda表达式可以被当成不同的目标类型,唯一的要求是:
// Lambda表达式的形参列表与函数式接口中唯一的抽象方法的形参列表相同
// 前面强制转型为Runnable的Lambda表达式也可强转为FkTest类型,因为FkTest接口中的
// 唯一抽象方法是不带参数的,而该Lambda表达式也是不带参数的
Object obj2 = (FkTest)() -> {
for (var i = 0; i < 100; i++)
{
System.out.println();
}
};

}
}

为了保证Lambda表达式的目标类型是一个明确的函数式接口,有三种方式:

  • 将Lambda表达式赋值给函数式接口类型的变量
  • 将Lambda表达式作为函数式接口类型的参数传给某个方法
  • 使用函数式接口对Lambda表达式进行强制类型转换

Java8在java.util.function包下预定义了大量函数式接口,例如XxxFunction、XxxConsumer、XxxxPredicate、XxxSupplier等等

方法引用和构造器引用

如果Lambda表达式的代码块只有一条代码,程序就可以省略Lambda表达式中代码块的花括号,不仅如此,如果Lambda表达式的代码块只有一条代码,还可以在代码块中使用方法引用和构造器引用,共支持4中引用

种类

示例

说明

对应的lambda表达式

引用类方法

类名 ::类方法

函数式接口中被实现方法的全部参数传给该类方法作为参数

(a,b…) -> 类名.类方法(a,b,…)

引用特定对象的实例方法

特定对象::实例方法

函数式接口中被实现方法的全部参数传给该方法作为参数

(a,b…) -> 特定对象.实例方法(a,b,…)

引用某类对象的实例方法

类名::实例方法

函数式接口中被实现方法的第一个参数作为调用者,后面的参数全部传给该方法作为参数

(a,b,c,…) -> a.实例方法(b,c,…)

引用构造器

类名::new

函数式接口中被实现方法的全部参数传给该构造器作为参数

(a,b,…) -> new 类名(a,b,…)

引用类方法

@FunctionalInterface
interface Converter{
Integer convert(String from);
}
public class MethodRefer
{
public static void main(String[] args)
{
// 下面代码使用Lambda表达式创建Converter对象
// Lambda表达式的代码块只有一条语句,因此省略了花括号
// 由于表达式所实现的convert()方法需要返回值,因此Lambda表达式将会把这条语句的结果作为返回值
Converter converter1 = from -> Integer.valueOf(from);
// 方法引用代替Lambda表达式:引用类方法
// 函数式接口中被实现方法的全部参数传给该类方法作为参数
Converter converter1 = Integer::valueOf;
Integer val = converter1.convert("99");
System.out.println(val); // 输出整数99
/* Lambda表达式的代码块只有一行调用类方法的代码,将其替换成了方法引用
      * 类方法引用,也就是调用Integer类的valueOf()类方法来实现Converter函数式接口中唯一的抽象方法
      * 当调用Converter接口中唯一的抽象方法时,调用参数将会传给Interger类的valueOf()类方法
 */
}
}

引用特定对象的实例方法

@FunctionalInterface
interface Converter{
Integer convert(String from);
}
public class MethodRefer
{
public static void main(String[] args)
{
// 下面代码使用Lambda表达式创建Converter对象
Converter converter2 = from -> "fkit.org".indexOf(from);
// 方法引用代替Lambda表达式:引用特定对象的实例方法。
// 函数式接口中被实现方法的全部参数传给该方法作为参数。
Converter converter2 = "fkit.org"::indexOf;
Integer value = converter2.convert("it");
System.out.println(value); // 输出2
}
}

引用某类对象的实例方法

@FunctionalInterface
interface MyTest 
{
String test(String a, int b, int c);
}

public class MethodRefer
{
public static void main(String[] args)
{
// 下面代码使用Lambda表达式创建MyTest对象
MyTest mt = (a, b, c) -> a.substring(b, c);
// 方法引用代替Lambda表达式:引用某类对象的实例方法。
// 函数式接口中被实现方法的第一个参数作为调用者,
// 后面的参数全部传给该方法作为参数。
MyTest mt = String::substring;
String str = mt.test("Java I Love you", 2, 9);
System.out.println(str); // 输出:va I Lo
}
}

引用构造器

@FunctionalInterface
interface YourTest
{
JFrame win(String title);
}
public class MethodRefer
{
public static void main(String[] args)
{
// 下面代码使用Lambda表达式创建YourTest对象
YourTest yt = a -> new JFrame(a);
// 构造器引用代替Lambda表达式。
// 函数式接口中被实现方法的全部参数传给该构造器作为参数。
YourTest yt = JFrame::new;
JFrame jf = yt.win("我的窗口");
System.out.println(jf);
}
}
  • 调用JFrame类的构造器来实现YourTest函数式接口中唯一的抽象方法,当调用YourTest接口中唯一的抽象方法时,调用参数将会传给JFrame构造器
  • 调用YourTest对象的win()抽象方法时,实际传入了一个String类型的参数,这个参数将会被传给JFrame构造器,因此是调用了JFrame类的带一个String参数的构造器

Lambda表达式与匿名函数内部类的联系【可以忽略】

Lambda表达式是匿名内部类的一种简化,因此它可以部分取代匿名内部类的作用,Lambda表达式与匿名内部类的共同点:

  • Lambda表达式与匿名内部类一样,都可以直接访问“effectively final”的局部变量,以及外部类的成员变量(包括实例变量和类变量)
  • 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 = "我欲乘风归去";
public void test()
{
var book = "又恐琼楼玉宇";
Displayable dis = ()->{
// 访问“effectively final”的局部变量
System.out.println("book局部变量为:" + book);
// 访问外部类的实例变量和类变量
System.out.println("外部类的age实例变量为:" + age);
System.out.println("外部类的name类变量为:" + name);
// 尝试调用接口中的默认方法,编译器会报错
// System.out.println(add(3, 5));
};
dis.display();
// 调用dis对象从接口中继承的add()方法
System.out.println(dis.add(3, 5));
}
public static void main(String[] args)
{
var lambda = new LambdaAndInner();
lambda.test();
}
}
  • Lambda表达式创建了一个Displayable的对象,Lambda表达式的代码块中的​​System.out.println("book局部变量为:" + book); System.out.println("外部类的age实例变量为:" + age); System.out.println("外部类的name类变量为:" + name);​​代码分别示范了访问“effectively final”的局部变量、外部类的实例变量和类变量
  • Lambda表达式访问了book局部变量,因此该局部变量相当于有一个隐式的final修饰,就不允许对book局部变量重新赋值
  • 代码​​System.out.println(dis.add(3, 5));​​表明,Lambda表达式创建了Displayable的对象后,该对象不仅可以调用接口中唯一的抽象方法,也可以调用接口中的默认方法

Lambda表达式与匿名函数内部类的区别【可以忽略】

  • 匿名内部类可以为任意接口创建实例,无论接口里有多少个抽象方法,只要匿名内部类实现所有的抽象方法即可,但Lambda表达式只能为函数式接口创建实例
  • 匿名内部类可以为抽象类甚至普通类创建实例,但Lambda表达式不幸
  • 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法,但Lambda表达式的代码块不允许调用接口中定义的默认方法,因此 System.out.println(add(3, 5));会报错,如果将Lambda表达式改为匿名内部类的写法,当匿名内部类实现display()抽象方法时,则完全可以调用add()方法

使用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());
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);
System.out.println(Arrays.toString(arr2));
var arr3 = new long[5];
// operand代表正在计算的元素索引
Arrays.parallelSetAll(arr3, operand -> operand * 5);
System.out.println(Arrays.toString(arr3));
}
}

Lambda表达式​​(o1, o2) -> o1.length() - o2.length()​​​的目标类型是Comparator, 该函数式接口指定了判断字符串大小的标准,字符串越长,即认为该字符串越大
Lambda表达式​​​(left, right)-> left * right​​​的目标类型是IntBinaryOperator,该函数式接口根据前后两个元素来计算当前元素的值
Lambda表达式​​​operand -> operand * 5​​​的目标类型是IntToLongFunction,该函数式接口根据元素的索引来计算当前元素的值
执行结果如下:

[ios, java, fkit, fkava, android]
[3, -12, -300, -4800, -144000, -2592000]
[0, 5, 10, 15, 20]