获取Java lambda表达式的封闭类

14

我有一个接受函数参数 (例如 Runnable) 的方法。由于这是一个库方法,我希望它使用从函数参数派生的记录器。调用函数参数的 getClass 对于普通类可以正常工作,并且我可以使用 getEnclosingClass 来获取嵌套或匿名类;但如果是 lambda 表达式,则返回一些包含 $$Lambda$ 的模糊名称,我可以手动去掉它:

Class<?> type = runnable.getClass();
String canonical = type.getCanonicalName();
int lambdaOffset = canonical.indexOf("$$Lambda$");
if (lambdaOffset > 0) {
    try {
        type = Class.forName(canonical.substring(0, lambdaOffset));
    } catch (ClassNotFoundException e) {
        // strange, but we can stick to the type we already have
    }
}

如您所见,这并不是非常优雅的,可能也不太可移植。我已经尝试了getEnclosingClassgetEnclosingMethodgetEnclosingConstructor,但它们都返回null

有什么想法吗?


1
我相信这是有意为之的设计。 - Tassos Bassoukos
2个回答

9

正如Tassos Bassoukos所提到的那样,这是设计上的问题。

Lambda(类)的字节码是在运行时生成的。因此,您得到的是类的实际名称。该名称生成为目标类名称 + "$$Lambda$" + 计数器

以下是演示的小片段。

package sub.optimal;
import static java.lang.System.out;

public class EnclosingClass {

    static class InnerRunnable implements Runnable {

        @Override
        public void run() {
            out.println("--- inner class");
        }
    }

    public static void main(String... args) {
        showIdentity(() -> System.out.println("--- lambda 1"));
        showIdentity(() -> System.out.println("--- lambda 2"));
        showIdentity(new InnerRunnable());
        showIdentity(new Runnable() {
            @Override
            public void run() {
                out.println("--- anonymous class");
            }
        });
    }

    private static void showIdentity(Runnable runnable) {
        runnable.run();
        Class<? extends Runnable> clazz = runnable.getClass();
        out.printf("class name     : %s%n", clazz.getName());
        out.printf("class hashcode : %s%n", clazz.hashCode());
        out.printf("canonical name : %s%n", clazz.getCanonicalName());
        out.printf("enclosing class: %s%n", clazz.getEnclosingClass());
        out.println();
    }
}

输出

--- lambda 1
class name     : sub.optimal.EnclosingClass$$Lambda$1/2147972
class hashcode : 2147972
canonical name : sub.optimal.EnclosingClass$$Lambda$1/2147972
enclosing class: null

--- lambda 2
class name     : sub.optimal.EnclosingClass$$Lambda$2/10376386
class hashcode : 10376386
canonical name : sub.optimal.EnclosingClass$$Lambda$2/10376386
enclosing class: null

--- inner class
class name     : sub.optimal.EnclosingClass$InnerRunnable
class hashcode : 28014437
canonical name : sub.optimal.EnclosingClass.InnerRunnable
enclosing class: class sub.optimal.EnclosingClass

--- anonymous class
class name     : sub.optimal.EnclosingClass$1
class hashcode : 19451386
canonical name : null
enclosing class: class sub.optimal.EnclosingClass

1
这对于jdk 1.8是正确的,但在jdk 11中行为已经改变。 - samabcde
名称模式可能会随着每个版本的发布而改变;它没有在任何地方指定。 - rü-
@rü 我同意。我们在2017年讨论过这一点。无论如何,应该避免构建依赖于实现内部的代码。如果确实需要依赖,那么这种情况需要通过单元测试来覆盖,以便在行为发生变化时失败。 - SubOptimal

2

我发现了一个很棒的解决方案,来自benjiweber。它的核心是将lambda表达式序列化为java.lang.invoke.SerializedLambda,然后获取其声明类:

private static final int COUNT = 1_000_000;
private static boolean first = true;

public static void main(String[] args) {
    long t = System.currentTimeMillis();
    for (int i = 0; i < COUNT; i++) {
        showIdentity(() -> {
        });
    }
    String time = NumberFormat.getNumberInstance().format((double) (System.currentTimeMillis() - t) / COUNT);
    System.out.println("time per call: " + time + "ms");
}

public interface MethodAwareRunnable extends Runnable, Serializable {}

private static void showIdentity(MethodAwareRunnable consumer) {
    consumer.run();
    String name = name(consumer);
    if (first) {
        first = false;
        Class<?> clazz = consumer.getClass();
        System.out.printf("class name     : %s%n", clazz.getName());
        System.out.printf("class hashcode : %s%n", clazz.hashCode());
        System.out.printf("canonical name : %s%n", clazz.getCanonicalName());
        System.out.printf("enclosing class: %s%n", clazz.getEnclosingClass());
        System.out.printf("lambda name    : %s%n", name);
    }
}

private static String name(Object consumer) {
    return method(consumer).getDeclaringClass().getName();
}

private static SerializedLambda serialized(Object lambda) {
    try {
        Method writeMethod = lambda.getClass().getDeclaredMethod("writeReplace");
        writeMethod.setAccessible(true);
        return (SerializedLambda) writeMethod.invoke(lambda);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

private static Class<?> getContainingClass(SerializedLambda lambda) {
    try {
        String className = lambda.getImplClass().replaceAll("/", ".");
        return Class.forName(className);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

private static Method method(Object lambda) {
    SerializedLambda serialized = serialized(lambda);
    Class<?> containingClass = getContainingClass(serialized);
    return Arrays.stream(containingClass.getDeclaredMethods())
                 .filter(method -> Objects.equals(method.getName(), serialized.getImplMethodName()))
                 .findFirst()
                 .orElseThrow(RuntimeException::new);
}

这是一大段代码,但在我的设备上开销约为0.003毫秒,对于大多数使用情况来说还可以接受。

你还可以做其他很酷的事情,比如:

Map<String, String> hash = hash(
    hello -> "world",
    bob -> "was here"
);

你为什么只对lambda表达式所在的封闭“Class”执行所有这些操作?很可能你的日志记录器会执行类似的操作getLogger(clazz.getName())。那么为什么不使用你最初的方法,将类名部分取到$$Lambda* - SubOptimal
@SubOptimal:你说得对。这就是我已经决定的;我只是加上了这个,因为它太酷了;-) - rü-
对我来说,只有在Lambda类名无法简单推导时使用它才有用。但是它遵循这个简单的模式:the.package.EnclosingClass$$Lambda$xx/yyyy,其中xx是每个类的序列号,yyy是Lambda的哈希码。与往常一样,解决方案取决于您想要实现什么目标。 - SubOptimal
类名的模式是实现细节,JDK可以随时更改。上述方法仅使用官方文档API,因此更加稳定。但你是对的:这主要是一次理论讨论;-) - rü-
我同意。这种改变应该由单元测试发现。在这种情况下,应该有一个单元测试。;-) 但你是对的,这更像是一次理论讨论。 - SubOptimal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接