Java 8 之方法引用 - Method References

什么是方法引用

简单地说,就是一个 Lambda 表达式。在 Java 8 中,我们会使用 Lambda 表达式创建匿名方法,但是有时候,我们的 Lambda 表达式可能仅仅调用一个已存在的方法,而不做任何其它事,对于这种情况,通过一个方法名字来引用这个已存在的方法会更加清晰,Java 8 的方法引用允许我们这样做。方法引用是一个更加紧凑,易读的 Lambda 表达式,注意方法引用是一个 Lambda 表达式,其中方法引用的操作符是双冒号 "::"。

方法引用例子

首先定义一个 Person 类,如下:

public class Person { 
    String name;
    LocalDate birthday; 

    public Person(String name, LocalDate birthday) { 
        this.name = name; 
        this.birthday = birthday;
    }

    public LocalDate getBirthday() { 
        return birthday;
    } 

    public static int compareByAge(Person a, Person b) { 
        return a.birthday.compareTo(b.birthday);
    }

    @Override 
    public String toString() { 
        return this.name;
    }
}

假设我们有一个 Person 数组,并且想对它进行排序,这时候,我们可能会这样写:

原始写法

public class Main { 
    static class PersonAgeComparator implements Comparator<Person> { 
        public int compare(Person a, Person b) { 
            return a.getBirthday().compareTo(b.getBirthday());
        }
    } 

    public static void main(String[] args) {
        Person[] pArr = new Person[]{ 
            new Person("003", LocalDate.of(2016, 9, 1)), 
            new Person("001", LocalDate.of(2016, 2, 1)), 
            new Person("002", LocalDate.of(2016, 3, 1)), 
            new Person("004", LocalDate.of(2016, 12, 1))};

        Arrays.sort(pArr, new PersonAgeComparator());

        System.out.println(Arrays.asList(pArr));
    }
}

其中,Arrays类的sort方法定义如下:

public static <T> void sort(T[] a, Comparator<? super T> c)

Comparator接口是一个函数式接口,因此可以使用 Lambda 表达式,而不需要定义一个实现Comparator接口的类,并创建它的实例对象,传给 sort 方法。

使用 Lambda 表达式,我们可以这样写:

改进一,使用 Lambda 表达式,未调用已存在的方法

public class Main { 
    public static void main(String[] args) {
        Person[] pArr = new Person[]{ 
            new Person("003", LocalDate.of(2016, 9, 1)), 
            new Person("001", LocalDate.of(2016, 2, 1)), 
            new Person("002", LocalDate.of(2016, 3, 1)), 
            new Person("004", LocalDate.of(2016, 12, 1))};

        Arrays.sort(pArr, (Person a, Person b) -> { 
            return a.getBirthday().compareTo(b.getBirthday());
        });

        System.out.println(Arrays.asList(pArr));
    }
}

然而,在以上代码中,关于两个人生日的比较方法在 Person 类中已经定义了,因此,我们可以直接使用已存在的 Person.compareByAge 方法。

改进二,使用 Lambda 表达式,调用已存在的方法

public class Main { 
    public static void main(String[] args) {
        Person[] pArr = new Person[]{ 
            new Person("003", LocalDate.of(2016, 9, 1)), 
            new Person("001", LocalDate.of(2016, 2, 1)), 
            new Person("002", LocalDate.of(2016, 3, 1)), 
            new Person("004", LocalDate.of(2016, 12, 1))};

        Arrays.sort(pArr, (a, b) -> Person.compareByAge(a, b));

        System.out.println(Arrays.asList(pArr));
    }
}

因为这个 Lambda 表达式调用了一个已存在的方法,因此,我们可以直接使用方法引用来替代这个 Lambda 表达式

改进三,使用方法引用

public class Main { 
    public static void main(String[] args) {
        Person[] pArr = new Person[]{ 
            new Person("003", LocalDate.of(2016, 9, 1)), 
            new Person("001", LocalDate.of(2016, 2, 1)), 
            new Person("002", LocalDate.of(2016, 3, 1)), 
            new Person("004", LocalDate.of(2016, 12, 1))};

        Arrays.sort(pArr, Person::compareByAge);

        System.out.println(Arrays.asList(pArr));
    }
}

在以上代码中,方法引用 Person::compareByAge 在语义上与 Lambda 表达式 (a, b) -> Person.compareByAge(a, b) 是等同的

四种方法引用类型

静态方法引用

ContainingClass::staticMethodName 

比较容易理解,和静态方法调用相比,只是把 . 换为 ::

例子:

  1. String::valueOf,等价于 Lambda:s -> String.valueOf(s)
  2. Math::pow 等价于lambda表达式 (x, y) -> Math.pow(x, y);
  3. 前面举的例子 Person::compareByAge 就是一个静态方法引用
  4. 从一个数字列表中找出最大的一个数字,方法引用方式:
Function<List<Integer>, Integer> maxFn = Collections::max;
// 等价于 Lambda 表达式:
// Function<List<Integer>, Integer> maxFn = (numbers) -> Collections.max(numbers);
maxFn.apply(Arrays.asList(1, 10, 3, 5))。
  1. 字符串反转
// 函数式接口  
interface StringFunc {  
    String func(String n);  
}  

class MyStringOps {  
    // 静态方法:反转字符串  
    public static String strReverse(String str) {  
        String result = "";  
        for (int i = str.length() - 1; i >= 0; i--) {  
            result += str.charAt(i);  
        }  
        return result;  
    }  
}  

class MethodRefDemo {  
    public static String stringOp(StringFunc sf, String s) {  
        return sf.func(s);  
    }  
    public static void main(String[] args) {  
        String inStr = "lambda add power to Java";  
        // MyStringOps::strReverse 相当于实现了接口方法 func() ,并在接口方法 func() 中作了 MyStringOps.strReverse() 操作  
        String outStr = stringOp(MyStringOps::strReverse, inStr);  
        System.out.println("Original string: " + inStr);  
        System.out.println("String reserved: " + outStr);  
    }  
}  

引用特定对象的实例方法

实例上的实例方法引用
instanceReference::instanceMethodName 

例子:x::toString,对应的 Lambda:() -> this.toString()
与引用静态方法相比,都换为实例对象而已

如下示例,引用的方法是 myComparisonProvider 对象的 compareByName 方法:

class ComparisonProvider { 
    public int compareByName(Person a, Person b) { 
        return a.getName().compareTo(b.getName());
    } 

    public int compareByAge(Person a, Person b) { 
        return a.getBirthday().compareTo(b.getBirthday());
    }
}

ComparisonProvider myComparisonProvider = new ComparisonProvider();
Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);
超类上的实例方法引用
super::methodName

通过使用 super,可以引用方法的超类版本。除此以外,还可以捕获 this 指针

  • this::equals 等价于 Lambda 表达式 x -> this.equals(x)

引用特定类型的任意对象的实例方法 (较少用)

ClassName::methodName 

若类型的实例方法是泛型的,就需要在::分隔符前提供类型参数,或者(多数情况下)利用目标类型推导出其类型。
静态方法引用和引用特定类型的任意对象的实例方法拥有一样的语法。编译器会根据实际情况做出决定。
一般我们不需要指定方法引用中的参数类型,因为编译器往往可以推导出结果,但如果需要我们也可以显式在::分隔符之前提供参数类型信息。

例子:

  1. String::toString,对应的 Lambda:(s) -> s.toString()
    这里不太容易理解,实例方法要通过对象来调用,方法引用对应 Lambda,Lambda 的第一个参数会成为调用实例方法的对象。
  2. 字符串数组中任意一个对象的 compareToIgnoreCase 方法:
String[] stringArray = { "Barbara", "James", "Mary" };
Arrays.sort(stringArray, String::compareToIgnoreCase);
  1. 在泛型类或泛型方法中,也可以使用方法引用
interface MyFunc<T> {  
    int func(T[] als, T v);  
}  
class MyArrayOps {  
    public static <T> int countMatching(T[] vals, T v) {  
        int count = 0;  
        for (int i = 0; i < vals.length; i++) {  
            if (vals[i] == v) count++;  
        }  
        return count;  
    }  
}  
class GenericMethodRefDemo {  
    public static <T> int myOp(MyFunc<T> f, T[] vals, T v) {  
        return f.func(vals, v);  
    }  
    public static void main(String[] args){  
        Integer[] vals = {1, 2, 3, 4, 2, 3, 4, 4, 5};  
        String[] strs = {"One", "Two", "Three", "Two"};  
        int count;  
        count = myOp(MyArrayOps::<Integer>countMatching, vals, 4);  
        System.out.println("vals contains " + count + " 4s");  
        count = myOp(MyArrayOps::<String>countMatching, strs, "Two");  
        System.out.println("strs contains " + count + " Twos");  
    }  
}  

当把泛型方法指定为方法引用时,类型参数出现在 :: 之后、方法名之前。在这种情况下,并非必须显示指定类型参数,因为类型参数会被自动推断得出。对于指定泛型类的情况,类型参数位于类名的后面::的前面。

构造方法引用

构造方法引用又分构造方法引用和数组构造方法引用

构造方法引用 (也可以称作构造器引用)
ClassName::new 

构造函数本质上是静态方法,只是方法名字比较特殊,使用前提是该类必须有无参构造函数

例子:

  1. String::new,对应的 Lambda:() -> new String()
  2. Supplier
class PersonFactory {
    private Supplier<Person> supplier;

    public PersonFactory(Supplier<Person> supplier) {
        this.supplier = supplier;
    }

    public Person getPerson() {
        return supplier.get();
    }
}

PersonFactory factory = new PersonFactory(Person::new);
Person p1 = factory.getPerson();
  1. Stream
List<String> strings = new ArrayList<String>();  
strings.add("a");  
strings.add("b");  
Stream<Button> stream = strings.stream().map(Button::new);  
List<Button> buttons = stream.collect(Collectors.toList());  
数组构造方法引用
TypeName[]::new

int[]::new 是一个含有一个参数的构造器引用,这个参数就是数组的长度。等价于 lambda 表达式 x -> new int[x]

IntFunction<int[]> arrayMaker = int[]::new;
int[] array = arrayMaker.apply(10)     // 创建数组 int[10]

引用特定对象的实例方法 与 引用特定类型的任意对象的实例方法 的区别

class Person {
    private String name;     // 省略 getter、setter

    public int compare(Person p1, Person p2) {
        return p1.getName().compareTo(p2.getName());
    }

    public int compareTo(Person p) {
        return this.getName().compareTo(p.getName());
    }
}

// 用特定对象的实例方法
Arrays.sort(persons, p1::compare);

// 引用特定类型的任意对象的实例方法
Arrays.sort(persons, Person::compareTo);
// 相当于 (p1, p2) -> p1.compareTo(p2)

什么场景适合使用方法引用

当一个 Lambda 表达式调用了一个已存在的方法

什么场景不适合使用方法引用

需要往引用的方法传参数的时候不适合:

IsReferable demo = () -> ReferenceDemo.commonMethod("Argument in method.");
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容