Java8:如何动态地获取一个 MethodReference 所引用的 Method

简介:Method References

Java8 的 lambda 表达式 可以很方便的用来创建一个匿名方法。在更多的情况下,可以通过一种新的 方法引用 的语法来基于一个现有的方法创建一个新的 lambda 表达式。

例如:某个方法需要传入一个 java.util.function.Supplier<Long>

setTimeStampSupplier(Supplier<Long> supplier);

假如我们打算为这个方法提供一个总是返回系统当前时间戳的 supplier,那么在按照传统的方法书写:

obj.setTimeStampSupplier(new Supplier<Long>() {
    @Override
    public Long get() {
        return System.currentTimeMillis();
    }
});

由于 java.util.function.Supplier 是一个 FunctionalInterface,所以可以使用 Lambda 表达式的方式来简化书写:

obj.setTimeStampSupplier(() -> System.currentTimeMillis());

借由 Method References 语法的帮助,这种写法可以更加简化为:

obj.setTimeStampSupplier(System::currentTimeMillis);

上文中的 System::currentTimeMillis 就是一个方法引用。

获取 Method Reference 所引用的 Method 实例

理论分析

那么是否能在运行时获取某个给定的 Method Reference 所引用的 java.lang.reflect.Method 实例呢?

答案是:没有可靠的、完美的方法(来自:Mike Strobel 的回答

Java 实现 Lambda Expression 的方式并不是引入了一个新的数据类型,而可以理解成 JVM 在运行时动态生成匿名类(实际上是通过 invokedynamic 指令实现的,具体可以参阅 Java 8 Lambdas - A Peek Under the Hood
这篇文章,这样效率比匿名类要高很多,不过不妨碍理解),因此在这一机制下,在语言层面上 Method Reference 和 Method 并没有一一对应的关系,因此也没有可靠的方法能够完美获取 Method Reference 所引用的 Method。

“没有可靠的方法” 并不等于 “没有方法”

如题,没有可靠的完美的方法 并不等于 没有方法。在满足某些特定的条件下,是可以的通过 Method Reference 获取到其引用的方法的。

Method Reference 可以分成下面四种类型:

种类 例子
引用给定类型上的静态方法 ContainingClass::staticMethodName,例如:java.lang.Thread::currentThread
引用给定对象上的实例方法 containingObject::instanceMethodName,例如:java.lang.Thread.currentThread::getName
引用给定类型上的实例方法 ContainingType::methodName,例如:java.lang.Thread::getId
引用构造函数 ClassName::new,例如:java.lang.Object::new

上表中第一类和第三类的区别在于(以例子中的方法为例):

  • 第一类 Method Reference 将匹配形如 Supplier<Thread> 的函数式接口
  • 第三类 Method Reference 将匹配形如 Function<Thread, Long> 或者 Consumer<Thread> 的函数式接口

如果给定的 MethodReference 是上表中的第二类或者第三类,并且满足:

  1. 给定的类或者给定对象的类不是 final 类,并且最好需要有一个有效的无参数构造函数,或者给定的类为接口
  2. 指定的实例方法不是 final 方法

在这种情况下,可以借助 cglib 来创建一个给定类的实例,并拦截给定的方法,然后在此实例上调用给定的 MethodReference,于是就可以在拦截器中获取 Method 实例了。(这也就解释了为什么会有上面的限制,因为 cglib 的 Enhancer 无法创建一个 final 类的子类,也无法拦截一个 final 方法)

代码实例

假定我们要获取 Thread::getId 引用的 Method,那么可以使用如下的代码:

// 定义一个 MethodReference
Function<Thread, Long> methodRef = Thread::getId;

// 创建一个 Enhancer,并配置拦截器
AtomicReference<Method> ref = new AtomicReference<Method>();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Thread.class);
enhancer.setCallback(new MethodInterceptor() {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        ref.set(method);
        return null;
    }
});

// 创建一个实例
Thread phantom = (Thread) enhancer.create();

// 在实例上调用 MethodReference
methodRef.apply(phantom);

Method method = ref.get();
System.out.println(method);

运行结果:

public long java.lang.Thread.getId()

优化

重构为库函数

有了上面的原型以后,我们可以重构出一个获取 带返回值但是无参数的实例方法 的 Method Reference 的 Method 的库函数:

public static <T, R> Method getReferencedMethod(Class<T> clazz, Function<T, R> methodRef) {
    // 创建一个 Enhancer,并配置拦截器
    AtomicReference<Method> ref = new AtomicReference<Method>();
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(clazz);
    enhancer.setCallback(new MethodInterceptor() {
        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            ref.set(method);
            return null;
        }
    });
    
    // 创建一个实例
    @SuppressWarnings("unchecked")
    T phantom = (T) enhancer.create();
    
    // 在实例上调用 MethodReference
    methodRef.apply(phantom);
    
    Method method = ref.get();
    if (method == null) {
        // 如果传入的不是方法引用,而是直接 new 出来的 Function 实例,那么 method 就会是 null
        throw new IllegalArgumentException(String.format("Invalid method reference on class [%s]", clazz));
    }
    return method;
}

只需要如此调用即可:getReferencedMethod(Thread.class, Thread::getId)

再进一步,适配满足条件的任何情况

上述的库函数只能用于获取带返回值但是无参数的实例方法,如果遇到其它情况的实例方法的时候应该怎么办呢?

这个时候我们需要再将上面的库函数进一步抽象,然后通过为目标实例方法来定义函数式接口来实现。

首先抽象 getReferencedMethod 函数:

public static <T> Method getReferencedMethod(Class<T> clazz, Consumer<? super T> invoker) {
    // 创建一个 Enhancer,并配置拦截器
    AtomicReference<Method> ref = new AtomicReference<Method>();
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(clazz);
    enhancer.setCallback(new MethodInterceptor() {
        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            ref.set(method);
            return null;
        }
    });
    
    // 创建一个实例
    @SuppressWarnings("unchecked")
    T phantom = (T) enhancer.create();
    
    // invoker 需要在实例上调用 MethodReference
    invoker.accept(phantom);
    
    Method method = ref.get();
    if (method == null) {
        // 如果传入的不是方法引用,而是直接 new 出来的 Function 实例,那么 method 就会是 null
        throw new IllegalArgumentException(String.format("Invalid method reference on class [%s]", clazz));
    }
    return method;
}

接下来,我们需要为特定的目标实例方法创建一个函数式接口,使其能够接纳这个实例方法的 MethodReference。假定我们需要适配 NavigableMap::subMap(K fromKey, K toKey),那么我们可以定义如下的函数式接口:

/**
 * 一个可以匹配带有两个参数的实例方法引用的函数式接口
 */
@FunctionalInterface
public interface MethodRefWith2Args<T, A1, A2> {
    void accept(T instance, A1 arg1, A2 arg2) throws Exception;
}

然后我们创建一个 Consumer 用来调用 Method Reference,最后将他们封装在一起,一个崭新的、用来匹配带两个参数的方法的 MethodReference 的 getReferencedMethod 诞生了:

public static <T, A1, A2> Method getReferencedMethod(
        Class<T> clazz, 
        MethodRefWith2Args<? super T, A1, A2> methodRef) {
    return getReferencedMethod(clazz, phantom -> {
        try {
            // 后面参数传 null 没关系,因为实际被调用的是我们自己的 MethodInterceptor
            // 不会去处理参数
            // 不过如果参数类型是 primitive 的话,这里会抛出 NullPointerException
            methodRef.accept(phantom, null, null);
        } catch (Exception e) {
            // 正常情况下,不会跑到这里来
        }
    });
}

举一反三

如果工程中需要用此方法获取方法实例的目标实例方法并没有太多参数,那么其实可以预先定义好一堆的适配用的函数式接口,譬如:MethodRefWith3ArgsMethodRefWith4ArgsMethodRefWith5ArgsMethodRefWith6Args……,在大多数情况下就够用了。

不过仍然不能大意。如果目标实例方法的某个参数是 primitive 类型(例如 int 而非 Integer),在撰写用来实际调用 methodReference 的 invoker (即基础的 getReferencedMethod 的第二个参数)的时候需要特殊处理。不能一股脑儿传 null 了。

例如需要匹配的实例方法是 NavigableMap.headMap(K toKey, boolean inclusive),那么用前面的例子就不行,需要修改成:

public static <T, A1, A2> Method getReferencedMethod(
        Class<T> clazz, 
        MethodRefWith2Args<? super T, A1, A2> methodRef) {
    return getReferencedMethod(clazz, phantom -> {
        try {
            // 注意第三个参数,必须是一个非 null 的值
            // 否则在 unboxing 的时候会抛出 NullPointerException
            methodRef.accept(phantom, null, new Boolean());
        } catch (Exception e) {
            // 正常情况下,不会跑到这里来
        }
    });
}

后记

有兴趣的可以看看 StackOverflow 上的问题:How to get the MethodInfo of a Java 8 method reference? 在这个问题中我也有简略作答。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,495评论 18 139
  • 1.在C/C++中实现本地方法 生成C/C++头文件之后,你就需要写头文件对应的本地方法。注意:所有的本地方法的第...
    JayQiu阅读 2,296评论 0 3
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,670评论 0 9
  • //Clojure入门教程: Clojure – Functional Programming for the J...
    葡萄喃喃呓语阅读 3,604评论 0 7
  • 夏天过去的第一天晚上,中原中也坐在天台上喝酒,把脸喝得红扑扑。他很少喝这么廉价的罐装啤酒,那味道和猫尿并无二致,低...
    三字成书阅读 537评论 0 1