在使用 Lambda 表达式的时候,我们实际上传递进去的代码就是一种解决方案: 拿什么参数做什么操作。 那么考虑一种情况: 如果我们在 Lambda 中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复的功能代码?
什么是方法引用
方法引用就是一个 Lambda 表达式的一种形式,功能与 Lambda 相同。 在 Java 8 中,我们会使用 Lambda 表达式创建匿名方法。 但是有时候,//Lambda 表达式可能仅仅调用一个已存在的方法,而不做任何其它事, 对于这种情况,通过一个方法名字来引用这个已存在的方法会更加清晰。 //方法引用是一个更加紧凑,易读的 Lambda 表达式。 方法引用的操作符是双冒号"::"。
冗余的 Lambda 场景
需求说明:1) 创建一个函数式接口 Calculate,包含抽象方法 int calc(int m,int n),用于实现对两个数的计算。2) 创建主类,使用匿名内部类实现 Calculate 接口,并且实现计算的功能。3) 调用 calc()方法,传入参数得到计算结果。4) 使用 Lambda 实例化 Calculate 对象,并且自己写代码实现计算功能。5) 调用 calc()方法,传入参数得到计算结果。
实现代码:
/** 函数式接口 */ interface Calculate { int calc(int m, int n); } public class DemoMethod { public static void main(String[] args) { //使用匿名内部类实现 Calculate c0 = new Calculate() { public int calc(int m, int n) { return m + n; } }; System.out.println("计算结果:" + c0.calc(3, 5)); //使用 Lambda 表达式提供解决方案,相当于自己实现这个方法体 Calculate c1 = (int m, int n) -> m + n; System.out.println("计算结果:" + c1.calc(3, 5)); } }
问题分析
假设在 Demo01Method 这个类中已经有了计算两个数的静态方法的实现, 我们可以在 Lambda 表达式中直接调用这个方法, 而不需要自己在 Lambda 表达式中去实现这个功能。
代码步骤:1) 在主类中创建一个静态方法 int sum(int a, int b),实现两个数的相加。2) 使用 Lambda 调用当前类的静态方法 代码实现:
/** 函数式接口 */ interface Calculate { int calc(int m, int n); } public class DemoMethod { public static void main(String[] args) { //使用匿名内部类实现 Calculate c0 = new Calculate() { public int calc(int m, int n) { return m + n; } }; System.out.println("计算结果:" + c0.calc(3, 5)); //使用 Lambda 表达式提供解决方案,相当于自己实现这个方法体 Calculate c1 = (int m, int n) -> m + n; System.out.println("计算结果:" + c1.calc(3, 5)); //使用 Lambda 调用当前类的静态方法 Calculate c2 = (m, n) -> DemoMethod.sum(3, 5); System.out.println("计算结果:" + c2.calc(3, 5)); } //已经有了实现功能的方法 private static int sum(int a, int b) { return a + b; } }
用方法引用改进代码
能否省去 Lambda 的语法格式(尽管它已经相当简洁)呢?只要“引用”过去就好了:
案例代码:
/** 函数式接口 */ interface Calculate { int calc(int m, int n); } public class DemoMethod { public static void main(String[] args) { //使用匿名内部类实现 Calculate c0 = new Calculate() { public int calc(int m, int n) { return m + n; } }; System.out.println("计算结果:" + c0.calc(3, 5)); //使用 Lambda 表达式提供解决方案,相当于自己实现这个方法体 Calculate c1 = (int m, int n) -> m + n; System.out.println("计算结果:" + c1.calc(3, 5)); //使用 Lambda 调用当前类的静态方法 Calculate c2 = (m, n) -> DemoMethod.sum(3, 5); System.out.println("计算结果:" + c2.calc(3, 5)); //使用方法引用 Calculate c3 = DemoMethod::sum; System.out.println("计算结果:" + c3.calc(3, 5)); } //已经有了实现功能的方法 private static int sum(int a, int b) { return a + b; } }
请注意其中的双冒号::写法,这被称为“方法引用”,而双冒号是一种新的语法。
方法引用符
双冒号::为引用运算符,而它所在的表达式被称为方法引用。 如果 Lambda 要表达的函数方案已经存在于某个方法的实现中, 那么则可以通过双冒号来引用该方法作为 Lambda 的替代者。 要注意,这里的方法引用功能与 Lambda 是一样的, //代替了 Lambda 表达式,也代替了以前的匿名内部类。 //可以理解为这个方法引用创建了一个匿名内部类,并且实现了接口中的方法。
语义分析
对比下面两种写法,完全等效: //Lambda 表达式写法 (m, n) -> DemoMethod.sum(3, 5) //方法引用写法 DemoMethod::sum 第一种语义是指:拿到参数之后经 Lambda 之手,继而传递给 sum()方法去处理。 第二种语义是指:直接让 DemoMethod 类来引用 sum()方法来取代 Lambda。 两种写法的执行效果完全一样,而第二种方法引用的写法更加简洁。
方法引用的过程
推导与省略
如果使用 Lambda,那么根据“可推导就是可省略”的原则, 无需指定参数类型和返回值——它们都将被自动推导。 而如果使用方法引用,也是同样可以根据具体传入的参数值和参数个数进行推导。 //函数式接口是 Lambda 的基础, //而方法引用是可以代替 Lambda,让 Lambda 更加简化,但在功能上是一样的。
方法引用的原则:
1) 如果 Lambda 表达式的方法体中/*只有一句话*/,而这句话就是调用另一个方法,可以使用方法引用代替。 2) 被引用的方法与函数式接口中的抽象方法://参数类型相同,参数个数相同,返回值类型相同,与方法名无关。 建议被引用的方法与接口中的抽象方法参数类型、返回值类型相同。
四种方法引用类型
//静态方法引用 类名::静态方法 //对象方法引用 对象名::成员方法 //类构造器引用 类名::new //数组构造器引用 类型名[]::new
类名称引用静态方法的语法
类名::静态方法(不能使用对象名引用静态方法 ) 由于在 java.lang.Math 类中已经存在了静态方法 abs(),用于求一个数的绝对值。 所以当我们需要通过Lambda 来调用该方法时,有两种写法。 //Lambda 表达式 num -> Math.abs(num) //方法引用 Math::abs
案例说明:1) 有一个函数式接口 Calcable,包含抽象方法 int calc(int num)2) 在 Lambda 中调用 Math.abs()方法实现求绝对值3) 直接通过 Math 类方法引用实现求绝对值 实现步骤:1) 创建函数式接口 Calcable,包含抽象方法 int calc(int num),用于计算传入整数,返回计算结果。2) 创建主类,创建主函数,使用 Lambda 表达式创建 Calcable 对象,计算传入整数的绝对值。3) 调用 calc()方法传入-10,输出计算结果4) 使用类方法引用创建 Calcable 对象,直接引用类方法 Math::abs 方法5) 调用 calc()方法传入-10,输出计算结果 实现代码:
interface Calcable { int calc(int num); } public class DemoStaticMethodRef { public static void main(String[] args) { //使用 Lambda 表达式实现 Calcable c1 = num -> Math.abs(num); System.out.println("-10 的绝对值是:" + c1.calc(-10)); //使用类方法引用 Calcable c2 = Math::abs; System.out.println("-10 的绝对值是:" + c2.calc(-10)); } } //在上面的案例中接口中的 int calc(int num)与被引用的 int Math.abs(int num), //具有相同的行为,参数类型和返回值类型相同。 //在这个例子中,下面两种写法是等效的: //下面两种写法是等效的 //Lambda 表达式 num -> Math.abs(num) //方法引用 Math::abs
通过对象引用成员方法
对象方法引用又分三种类型: //实例上的对象方法引用、父类上的对象方法引用、类型上的对象方法引用 实例上的对象方法引用语法 对象名::对象方法 this::本类对象方法 super::对象方法
提问:System.out 是一个对象还是一个类?
答:查看 System 类的源代码可以得知它是一个 PrintStream 类型的静态成员变量,是一个对象。 所以我们调用System.out.println()其实是调用 out 这个对象的 println()方法。 public final class System { public static final PrintStream out = null; }
案例需求:使用 Consumer 接口,调用 accept(字符串),将提供的字符串直接打印出来。 案例步骤:1) 创建主类和主函数2) 创建 Consumer 对象,这是一个函数式接口,有一个抽象方法 void accept(T t)3) 使用 Lambda 表达式,实现方法体,在方法体中调用 System.out.println()方法打印字符串。4) 调用 accept()方法提供要打印的字符串5) 创建 Consumer 对象,使用对象方法引用,引用 out 对象的 println 方法6) 调用 accept()方法提供要打印的字符串 案例代码:
public class DemoObjMethodRef { public static void main(String[] args) { // 使用 Lambda 表达式,打印字符串 Consumer c1 = s -> System.out.println(s); c1.accept("Hello Java"); //方法引用,打印字符串 Consumer c2 = System.out::println; c2.accept("Hello World"); } } //在这个例子中,下面两种写法是等效的: //下面两种写法是等效的 //Lambda 表达式 s -> System.out.println(s) //方法引用 System.out::println
类的构造器引用语法:
类名称::new 由于构造器的名称与类名完全一样,但一个类可以有多个构造方法,参数不同。
案例效果: 案例步骤:1) 创建汽车类,有一个 String 属性品牌2) 创建有参的构造方法和无参的构造方法3) 重写 toString()方法,返回 band+"汽车"4) 创建函数式接口 Factory,包含抽象方法 Car makeCar(String name),用于创建汽车对象。5) 创建主类和主函数6) 使用 lambda 表达式直接调用有参的构造方法实例化汽车。7) 调用 makeCar()方法,输出汽车对象。8) 使用构造器引用,调用有参的构造方法,因为接口中的 makeCar 方法是有参数的9) 调用 makeCar()方法,输出汽车对象。 案例代码:
class Car { private String band; public Car(String band) { this.band = band; } public Car () { } public String toString() { return band + "汽车"; } } interface Factory { //创建一辆汽车 Car makeCar(String name); } public class DemoConstructorRef { public static void main(String[] args) { //使用 lambda 表达式直接实例化汽车返回 Factory f1 = (name) -> new Car(name); Car c1 = f1.makeCar("BMW"); System.out.println("制造:" + c1); //使用构造器引用,调用有参的构造方法,因为接口中的 makeCar 方法是有一个参数 Factory f2 = Car::new; Car c2 = f2.makeCar("Audi"); System.out.println("制造:" + c2); } } // 代码分析: //下面两种写法是等效的 //Lambda 表达式 (name) -> new Car(name) //方法引用 Car::new
数组的构造器引用
数组构造器语法: 数组类型[]::new 数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。 如果对应到 Lambda 的使用场景中时,需要一个创建数组的函数式接口。
案例需求:分别使用 Lambda 表达式和数组构造器引用创建 2 个长度各为 5 的整数数组。 案例步骤:1) 创建一个用于创建数组的接口 ArrayBuilder,包含一个抽象方法 int[] buildArray(int length), 提供数组的长度,返回一个创建好的数组。2) 创建主类主函数3) 使用 Lambda 表达式创建上面的接口对象,调用方法创建一个长度为 5 的数组,并且输出数组。4) 使用数组构造器创建上面的接口对象,调用方法创建一个长度为 5 的数组,并且输出数组。 案例代码:
import java.util.Arrays; //创建一个用于创建数组的接口 interface ArrayBuilder { //提供数组的长度,返回一个创建好的数组 int[] buildArray(int length); } public class DemoArrayRef { public static void main(String[] args) { //使用 Lambda 表达式创建数组 ArrayBuilder ab1 = length -> new int[length]; int [] arr1 = ab1.buildArray(5); System.out.println("创建的数组 1:" + Arrays.toString(arr1)); //使用数组构造器创建数组 ArrayBuilder ab2 = int[]::new; int[] arr2 = ab2.buildArray(5); System.out.println("创建的数组 2:" + Arrays.toString(arr2)); } } // 代码分析: //在这个例子中,下面两种写法是等效的: //下面两种写法是等效的 //Lambda 表达式 length -> new int[length] //方法引用 int[]::new
方法引用小结
方法引用语法
方法引用类型 语法 静态方法引用 类名::静态方法 对象方法引用 对象名::成员方法 类构造器引用 类名::new 数组构造器引用 类名[]::new
int calc(int num) int Math.abs(int x) x -> Math.abs(x) Math::abs void accept(String s) void println(s) () -> System.out.println(s) System.out::println Car makeCar(String name) new Car(String name) name-> new Car(name) Car::new int[] buildArray(int length) new int[length] x -> new int[x] int[]::new