简体   繁体   中英

Get the enclosing class of a Java lambda expression

I have a method that takes a functional parameter, eg a Runnable . As it's a library method, I want it to use a logger derived from the functional parameter. Calling getClass on the functional parameter works fine for normal classes, and I can getEnclosingClass for nested or anonymous classes; but if it's a lambda expression, it returns some obscure name containing $$Lambda$ , which I could strip off manually like this:

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
    }
}

As you can see, that's not very elegant and probably not portable. I've tried getEnclosingClass , getEnclosingMethod , and getEnclosingConstructor , but they all return null .

Any ideas?

As already mentioned by Tassos Bassoukos it is by design.

The bytecode for the Lambda (class) is generated at runtime. So what you get is the actual name of the class. And the name is generated as target class name + "$$Lambda$" + a counter .

Find a small snippet for demonstration.

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();
    }
}

output

--- 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

I found a cool solution by benjiweber . It boils down to serializing the lamda to a java.lang.invoke.SerializedLambda and then get its declaring class:

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);
}

This is a lot of code, but the overhead is about 0.003 ms on my machine, which is okay for most use cases.

And you can do other cool stuff like:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM