方法引用

方法引用

在使用 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()方法,传入参数得到计算结果。

 实现代码:

/**
函数式接口
*/
@FunctionalInterface
interface Calculate {
 int calc(int m, int n);
}

public class DemoMethod {
 public static void main(String[] args) {
     
 //使用匿名内部类实现
 Calculate c0 = new Calculate() {
 @Override
 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 调用当前类的静态方法 代码实现:

/**
函数式接口
*/
@FunctionalInterface
interface Calculate {
 int calc(int m, int n);
}

public class DemoMethod {
 public static void main(String[] args) {
     
 //使用匿名内部类实现
 Calculate c0 = new Calculate() {
 @Override
 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 的语法格式(尽管它已经相当简洁)呢?只要“引用”过去就好了:

 案例代码:

/**
函数式接口
*/
@FunctionalInterface
interface Calculate {
 int calc(int m, int n);
}

public class DemoMethod {
 public static void main(String[] args) {
     
 //使用匿名内部类实现
 Calculate c0 = new Calculate() {
 @Override
 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,输出计算结果 实现代码:

@FunctionalInterface
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 () {
 }

 @Override
 public String toString() {
 return band + "汽车";
 }
}

@FunctionalInterface
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;

//创建一个用于创建数组的接口
@FunctionalInterface
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


版权声明:本文为h294590501原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。