为何String::compareToIgnoreCase无需实例即可使用?如何改写自定义方法?
问题场景
先看这段无法运行的代码:
@FunctionalInterface public interface MyConsumer<S> { void accept(S val); boolean equals(Object obj); // 多余定义,可删除 } public class LambdaClass { public void consumerUsage(String f, MyConsumer<String> printer){ printer.accept(f); } } public class FunctionalMethodAux { static List<String> stringList = new ArrayList<>(); public void lambdaAux (String s){ if (s.length() < 3) { System.out.println(s); } else { System.out.println(s.substring(0, 3)); } stringList.add(s); } public void fmTest(){ LambdaClass l = new LambdaClass(); String[] words = {"trololo", "foo", "bar"}; for(String src : words){ l.consumerUsage(src, FunctionalMethodAux::lambdaAux); // 报错行 } } }
将报错行改为l.consumerUsage(src, new FunctionalMethodAux()::lambdaAux)后即可正常运行。但对比下面这段代码,非静态方法String::compareToIgnoreCase无需创建实例就能正常工作:
String[] words = {"trololo", "foo", "bar"}; Arrays.sort(words, String::compareToIgnoreCase);
为什么String::compareToIgnoreCase无需实例就能用?
核心是区分两种非静态方法引用的类型:
- 绑定接收者的方法引用:比如
new FunctionalMethodAux()::lambdaAux,这种已经提前绑定了一个具体实例,调用时直接用该实例执行方法。 - 未绑定接收者的方法引用:就是
String::compareToIgnoreCase这种,它没有预先绑定实例,而是把函数式接口抽象方法的第一个参数作为该非静态方法的调用者(也就是this)。
具体看Arrays.sort的第二个参数,它需要Comparator<String>接口,其抽象方法签名是int compare(String o1, String o2)。而String的compareToIgnoreCase方法签名是int compareToIgnoreCase(String anotherString)。当用String::compareToIgnoreCase作为方法引用时,Java会自动把Comparator的第一个参数o1作为compareToIgnoreCase的调用者,第二个参数o2作为方法入参,相当于实现了(o1, o2) -> o1.compareToIgnoreCase(o2)。因为调用时的第一个参数就是实例,所以不需要提前创建对象。
而你的自定义场景中,MyConsumer<String>的抽象方法accept只有一个String参数。FunctionalMethodAux::lambdaAux作为非静态方法,需要一个FunctionalMethodAux实例作为调用者,同时需要一个String参数,但accept只提供了String参数,Java找不到对应的实例来调用lambdaAux,所以报错。改成new FunctionalMethodAux()::lambdaAux后,绑定了具体实例,此时方法引用的签名就和accept匹配(只需要String参数),因此能正常运行。
如何改写lambdaAux使其无需实例即可用方法引用?
有两种可行方案:
方案1:将lambdaAux改为静态方法
静态方法不需要依赖实例就能调用,此时FunctionalMethodAux::lambdaAux的签名void lambdaAux(String s)和MyConsumer<String>的accept方法完全匹配,可以直接使用:
public class FunctionalMethodAux { static List<String> stringList = new ArrayList<>(); // 修改为静态方法 public static void lambdaAux (String s){ if (s.length() < 3) { System.out.println(s); } else { System.out.println(s.substring(0, 3)); } stringList.add(s); } public void fmTest(){ LambdaClass l = new LambdaClass(); String[] words = {"trololo", "foo", "bar"}; for(String src : words){ l.consumerUsage(src, FunctionalMethodAux::lambdaAux); // 正常运行 } } }
方案2:调整函数式接口的参数(不推荐)
如果不想改成静态方法,可以修改MyConsumer的抽象方法,让它接收两个参数:FunctionalMethodAux实例和String,同时调整consumerUsage方法的参数。但这种方案会改变原有接口的设计,不符合最初的需求,因此更推荐方案1。
另外补充:MyConsumer接口里的equals方法定义是多余的,所有接口都会继承Object类的equals方法,函数式接口只要求有一个抽象方法(不包含Object的方法),可以直接删除该行。
内容的提问来源于stack exchange,提问作者Kifsif

