Java - 方法引用(Java 8+)

在使用Lambda表达式的时候,我们实际上传递进去的代码就是一种解决方案:拿什么参数做什么操作。那么考虑一种情况:如果我们在Lambda中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复逻辑?

1 冗余的Lambda场景

来看一个简单的函数式接口以应用Lambda表达式:

package top.onefine.demo.methodReference;

// 定义打印的函数式接口
@FunctionalInterface
public interface Printable {
    
    
    // 定义字符串的抽象方法
    void print(String s);
}

在 Printable 接口当中唯一的抽象方法 print 接收一个字符串参数,目的就是为了打印显示它。那么通过Lambda来使用它的代码很简单:

package top.onefine.demo.methodReference;

public class Demo01Printable {
    
    
    // 定义一个方法,参数传递Printable接口,对字符串进行打印
    public static void printString(Printable p) {
    
    
        p.print("Hello world!");
    }

    public static void main(String[] args) {
    
    
        // 调用printString方法,方法的参数是一个函数式接口,可以传递一个lambda表达式
//        printString((String s) -> {
    
    
//            System.out.println(s);
//        });

        /*
         * 分析:
         *      Lambda表达式的目的:打印参数传递的字符串:
         *          把参数s,传递给System.out对象,调用此对象中的方法println对字符串进行输出
         *      注意:
         *          1. System.out对象是已经存在的
         *          2. println方法也是已经存在的
         *      所以可以使用方法引用来优化Lambda表达式——直接使用System.out方法直接引用(调用)println方法
         * 
         */

        // 方法引用
        printString(System.out::println);  // 方法的参数可以省略
    }
}

其中 printString 方法只管调用 Printable 接口的 print 方法,而并不管 print 方法的具体实现逻辑会将字符串打印到什么地方去。而 main 方法通过Lambda表达式指定了函数式接口 Printable 的具体操作方案为:拿到String(类型可推导,所以可省略)数据后,在控制台中输出它。

2 问题分析

这段代码的问题在于,对字符串进行控制台打印输出的操作方案,明明已经有了现成的实现,那就是 System.out对象中的 println(String) 方法。既然Lambda希望做的事情就是调用 println(String) 方法,那何必自己手动调用呢?

3 用方法引用改进代码

能否省去Lambda的语法格式(尽管它已经相当简洁)呢?只要“引用”过去就好了:

// 方法引用
printString(System.out::println);  // 方法的参数可以省略

请注意其中的双冒号 :: 写法,这被称为“方法引用”,而双冒号是一种新的语法。

4 方法引用符

双冒号 :: 为引用运算符,而它所在的表达式被称为方法引用。如果Lambda要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者。

语义分析

例如上例中, System.out 对象中有一个重载的 println(String) 方法恰好就是我们所需要的。那么对于printString 方法的函数式接口参数,对比下面两种写法,完全等效:

  • Lambda表达式写法: s -> System.out.println(s);
  • 方法引用写法: System.out::println

第一种语义是指:拿到参数之后经Lambda之手,继而传递给 System.out.println 方法去处理。

第二种等效写法的语义是指:直接让 System.out 中的 println 方法来取代Lambda。两种写法的执行效果完全一样,而第二种方法引用的写法复用了已有方案,更加简洁。

注:Lambda 中传递的参数一定是方法引用中的那个方法可以接收的类型,否则会抛出异常。

推导与省略

如果使用Lambda,那么根据“可推导就是可省略”的原则,无需指定参数类型,也无需指定的重载形式——它们都将被自动推导。而如果使用方法引用,也是同样可以根据上下文进行推导。

函数式接口是Lambda的基础,而方法引用是Lambda的孪生兄弟。

下面这段代码将会调用 println 方法的不同重载形式,将函数式接口改为int类型的参数:

@FunctionalInterface
public interface PrintableInteger {
    
    
	void print(int str);
}

由于上下文变了之后可以自动推导出唯一对应的匹配重载,所以方法引用没有任何变化:

public class Demo03PrintOverload {
    
    
	private static void printInteger(PrintableInteger data) {
    
    
		data.print(1024);
	}
	
	public static void main(String[] args) {
    
    
		printInteger(System.out::println);
	}
}

这次方法引用将会自动匹配到 println(int) 的重载形式。

5 通过对象名引用成员方法

这是最常见的一种用法,与上例相同。如果一个类中已经存在了一个成员方法:

package top.onefine.demo.methodReference;

public class MethodRerObject {
    
    
    // 传递字符串,把字符串按照大写输出
    public void printUpperCaseString(String str) {
    
    
        System.out.println(str.toUpperCase());
    }
}

函数式接口仍然定义为:

package top.onefine.demo.methodReference;

// 定义打印的函数式接口
@FunctionalInterface
public interface Printable {
    
    
    // 定义字符串的抽象方法
    void print(String s);
}

那么当需要使用这个 printUpperCase 成员方法来替代 Printable 接口的Lambda的时候,已经具有了MethodRefObject 类的对象实例,则可以通过对象名引用成员方法,代码为:

package top.onefine.demo.methodReference;

/**
 * 通过对象名引用成员方法:
 *      使用前提:对象名已经存在,成员方法也是已经存在
 */
public class Demo02ObjectMethodReference {
    
    

    // 定义一个方法,方法的参数传递Printable接口
    public static void printString(Printable p) {
    
    
        p.print("Hello");
    }

    public static void main(String[] args) {
    
    
        printString((String s) -> {
    
    
            // 创建MethodRerObject对象
            MethodRerObject o = new MethodRerObject();
            // 调用MethodRerObject对象中的成员方法printUpperCaseString,把字符串按照大写输出
            o.printUpperCaseString(s);
        });

        /* 使用方法引用来优化lambda表达式
                对象MethodRerObject是已经存在的
                成员方法printUpperCaseString也是已经存在的
           因此可以使用 对象名 来引用成员方法
         */
        MethodRerObject o = new MethodRerObject();  // 对象名o
        printString(o::printUpperCaseString);

        printString(new MethodRerObject()::printUpperCaseString);

    }
}

6 通过类名称引用静态方法

由于在 java.lang.Math 类中已经存在了静态方法 abs ,所以当我们需要通过Lambda来调用该方法时,有两种写法。

定义函数式接口:

package top.onefine.demo.methodReference;

@FunctionalInterface
public interface Calcable {
    
    
    // 定义一个抽象方法,传递一个整数,对整数进行绝对值计算并返回
    int calaAbs(int number);
}

计算绝对值:

package top.onefine.demo.methodReference;

/**
 * 通过类名引用静态成员方法
 *      使用前提:类已经存在,静态成员方法也已经存在
 */
public class Demo03StaticMethodReference {
    
    

    // 定义一个方法,方法的参数传递要计算绝对值的整数和函数式接口Calcable
    public static int method(int number, Calcable calcable) {
    
    
        return calcable.calaAbs(number);
    }

    public static void main(String[] args) {
    
    
        int absValue = method(-10, (int n) -> Math.abs(n));  // 对参数进行绝对值计算并返回结果
        System.out.println(absValue);

        /*
            使用方法引用优化
                Math类是已经存在的
                abs计算绝对值的静态方法也是已经存在的
         */
        System.out.println(method(-20, Math::abs));
    }
}

在这个例子中,下面两种写法是等效的:

  • Lambda表达式: n -> Math.abs(n)
  • 方法引用: Math::abs

7 通过super引用成员方法

如果存在继承关系,当Lambda中需要出现super调用时,也可以使用方法引用进行替代。

package top.onefine.demo.methodReference;

/**
 * 定义见面的函数式接口
 */

interface Greetable {
    
    
    // 定义一个见面的方法
    void greet();
}

/**
 * 定义父类
 */
class Human {
    
    
    // 定义一个sayHello的方法
    public void sayHello() {
    
    
        System.out.println("Hello, human");
    }
}

/**
 * 定义子类
 */
public class Man extends Human{
    
    
    // 子类重写父类sayHello的方法
    @Override
    public void sayHello() {
    
    
        System.out.println("Hello, man");
    }

    // 定义一个方法参数传递Greetable函数式接口
    public static void method(Greetable g) {
    
    
        g.greet();
    }

    public void show() {
    
    
        method(() -> {
    
    
            // 创建父类Human对象
            Human human = new Human();
            // 调用父类的sayHello方法
            human.sayHello();
        });
        // 因为有子父类关系,所以存在一个关键字super,代表父类,可以直接使用super调用父类的成员方法
        method(() -> super.sayHello());
        /* 使用super引用父类的成员方法
                super是已经存在的
                父类的成员方法sayHello也是已经存在的
         */
        method(super::sayHello);
    }

    public static void main(String[] args) {
    
    
        new Man().show();
    }
}

在这个例子中,下面两种写法是等效的:

  • Lambda表达式: () -> super.sayHello()
  • 方法引用: super::sayHello

8 通过this引用成员方法

this代表当前对象,如果需要引用的方法就是当前类中的成员方法,那么可以使用“this::成员方法”的格式来使用方法引用。

package top.onefine.demo.methodReference;

/**
 * 定义一个富有的函数式接口
 */
@FunctionalInterface
interface Richable {
    
    
    // 定义一个买方法,想买啥买啥
    void buy();
}

/**
 * 通过this引用本类的成员方法
 */
public class Husband {
    
    
    // 定义一个买房子的方法
    public void buyHouse() {
    
    
        System.out.println("北京二环内买一套四合院");
    }

    // 定义一个结婚的方法,参数传递Richable接口
    public void marry(Richable r) {
    
    
        r.buy();
    }

    // 定义一个非常开心的方法
    public void soHappy() {
    
    
        // 调用结婚的方法,方法的参数Richable是一个函数式接口
        marry(() -> this.buyHouse());

        /*
        使用方法引用优化Lambda表达式
            this是已经存在的
            本类的成员方法buyHouse也是存在的
         */
        marry(this::buyHouse);
    }

    public static void main(String[] args) {
    
    
        new Husband().soHappy();
    }
}

在这个例子中,下面两种写法是等效的:

  • Lambda表达式: () -> this.buyHouse()
  • 方法引用: this::buyHouse

9 类的构造器引用

由于构造器的名称与类名完全一样,并不固定。所以构造器引用使用 类名称::new 的格式表示。

package top.onefine.demo.methodReference;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
class Person {
    
    
    private String name;
}

/**
 * 定义一个创建Person对象的函数式接口
 */
@FunctionalInterface
interface PersonBuilder {
    
    
    // 根据传递的姓名创建Person对象返回
    Person builderPerson(String name);
}

/**
 * 类的构造器(构造方法)引用
 */
public class Demo {
    
    
    // 定义一个方法,参数传递姓名和PersonBuilder接口,
    // 方法中通过姓名创建Person对象
    public static void printName(String name, PersonBuilder pb) {
    
    
        Person person = pb.builderPerson(name);
        System.out.println(person.getName());
    }

    public static void main(String[] args) {
    
    
        printName("one fine", (String name) -> new Person(name));
        /*
        使用方法引用优化Lambda表达式
            构造方法Person(name)已知
            创建对象已知 new
         */
        printName("one fine", Person::new);
    }
}

在这个例子中,下面两种写法是等效的:

  • Lambda表达式: name -> new Person(name)
  • 方法引用: Person::new

10 数组的构造器引用

数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。

package top.onefine.demo.methodReference;

/**
 * 定义一个创建数组的函数式接口
 */
@FunctionalInterface
interface ArrayBuilder {
    
    
    // 定义一个创建int类型数组的方法,参数传递数组的长度,返回创建好的int类型数组
    int [] builderArray(int length);
}

/**
 * 数组的构造器引用
 */
public class Demo2 {
    
    
    // 方法的参数传递创建数组的长度和ArrayBuilder接口
    // 方法的内部根据传递的长度使用ArrayBuilder中的方法创建数组并返回
    public static int [] createArray(int length, ArrayBuilder ab) {
    
    
        return ab.builderArray(length);
    }

    public static void main(String[] args) {
    
    
        // 根据参数,创建指定长度的数组并返回
        int[] array = createArray(6, (int length) -> new int[length]);
        System.out.println(array.length);
        /*
         * 使用方法引用优化lambda表达式
         *      已知创建的是int [] 数组
         *      数组的长度也是已知的
         */
        array = createArray(7, int[]::new);  // int[] 引用 new
        System.out.println(array.length);
    }
}

在这个例子中,下面两种写法是等效的:

  • Lambda表达式: length -> new int[length]
  • 方法引用: int[]::new

猜你喜欢

转载自blog.csdn.net/jiduochou963/article/details/105377538
今日推荐