2

How can I check if 2 lambda's run the same code? Suppose I have the following code:

main() {
    Predicate<Person> a = (person -> person.getAge() > 18);
    Predicate<Person> b = (person -> person.getAge() > 18);
    boolean x = equalLamdbaCode(a, b)); // should return true

    Predicate<Person> c = (person -> person.getAge() > 40);
    boolean y = equalLamdbaCode(a, c)); // should return false
}

Is there a reliable way to implement that equalLamdbaCode(a, b) method?

4
  • what do you mean by comparing same code? equals or == is the comparisons I know ... can you clarify? Commented Mar 24, 2021 at 19:04
  • 1
    The questions in the duplicate list here state that this is not specified by JLS or JVMS, so implementations of Java have flexibility for lambdas. Commented Mar 24, 2021 at 19:31
  • 2
    Does this answer your question? Is there a way to compare lambdas? Commented Mar 24, 2021 at 19:40
  • As Christopher points out: "bytecode[s] being equal does not imply the lambda themselves are equal". Two lambdas with identical bytecodes can produce different results ... depending on the values of variables in scope that the point the lambdas were instantiated. Comparing lambdas that are not == is semantically difficult. Commented Sep 20, 2023 at 2:06

1 Answer 1

1

The answer depends on the functional interface the lambda implements.

If the functional interface the lambda implements does not extend Serializable, the answer is no, as there is no reliable way to read the lambda bytecode that will work across different compilers and JVM's.

If the functional interface the lambda implements does extend Serializable, the answer is yes. We can use the SerializedLambda approached described in this stack overflow answer. The gist of it is this: exploit the fact that any Serializable object that require an alternate form to be serialized MUST have a writeReplace method (as specified in Serializable JavaDoc), and we know for lambdas, the object returned by that method is SerializedLambda. From SerializedLambda, we know the lambda's implementer class and method names. From the class and method names, we can create a ClassVisitor to visit that class and read the bytecode corresponding to the lambda:

    private static SerializedLambda getSerializedLambda(Serializable lambda) throws Exception {
        final Method method = lambda.getClass().getDeclaredMethod("writeReplace");
        method.setAccessible(true);
        return (SerializedLambda) method.invoke(lambda);
    }

    private static List<Object> readBytecodeOf(Object lambdaObject) {
        if (lambdaObject instanceof Serializable serializable) {
            try {
                SerializedLambda serializedLambda = getSerializedLambda(serializable);
                ClassReader classReader = new ClassReader(serializedLambda.getImplClass());
                LambdaClassVisitor lambdaClassVisitor =
                        new LambdaClassVisitor(Opcodes.ASM9, serializedLambda.getImplMethodName());
                classReader.accept(lambdaClassVisitor,
                        ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
                return lambdaClassVisitor.getBytecode();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        } else {
            return List.of(new LambdaClassVisitor.UniqueObject(Float.NaN));
        }
    }

Where LambdaClassVisitor looks like:

public class LambdaClassVisitor extends ClassVisitor {
    final String lambdaMethodName;
    LambdaMethodVisitor methodVisitor;

    protected LambdaClassVisitor(int api, String lambdaMethodName) {
        super(api);
        this.lambdaMethodName = lambdaMethodName;
    }

    public record UniqueObject(float number) {
    }

    public List<Object> getBytecode() {
        if (methodVisitor.canBeCompared) {
            return methodVisitor.getMethodBytecode();
        } else {
            return List.of(new UniqueObject(Float.NaN));
        }
    }

    @Override
    public MethodVisitor visitMethod(int modifiers, String name, String descriptor, String signature, String[] exceptions) {
        if (lambdaMethodName.equals(name)) {
            this.methodVisitor =
                    new LambdaMethodVisitor(Modifier.isStatic(modifiers), api,
                            super.visitMethod(modifiers, name, descriptor, signature, exceptions));
            return this.methodVisitor;
        }
        return super.visitMethod(modifiers, name, descriptor, signature, exceptions);
    }
}

and LambdaMethodVisitor looks like

public class LambdaMethodVisitor extends MethodVisitor {
    final IdentityHashMap<Label, Integer> labelToId = new IdentityHashMap<>();
    final List<Object> methodBytecode = new ArrayList<>();
    boolean canBeCompared = true;
    boolean isStatic;

    protected LambdaMethodVisitor(boolean isStatic, int api, MethodVisitor methodVisitor) {
        super(api, methodVisitor);
        this.isStatic = isStatic;
    }

    public List<Object> getMethodBytecode() {
        return methodBytecode;
    }

    private record Instruction(int opcode) {
    }

    public void visitInsn(final int opcode) {
        super.visitInsn(opcode);
        methodBytecode.add(new Instruction(opcode));
    }

    record IntInstruction(int opcode, int operand) {
    }

    public void visitIntInsn(final int opcode, final int operand) {
        super.visitIntInsn(opcode, operand);
        methodBytecode.add(new IntInstruction(opcode, operand));
    }

    public void visitVarInsn(final int opcode, final int varIndex) {
        super.visitVarInsn(opcode, varIndex);
        if (!isStatic && opcode == Opcodes.ALOAD && varIndex == 0) {
            canBeCompared = false;
        }
        methodBytecode.add(new IntInstruction(opcode, varIndex));
    }
    
    // ... and so on for all visit... methods
}

That being said, bytecode being equal does not imply the lambda themselves are equal. For instance, consider the following lambdas:

public Supplier<Object> getObjectSupplier(final Object item) {
    return () -> item;
}

Supplier<Object> a = getObjectSupplier(1);
Supplier<Object> b = getObjectSupplier(2);

a and b will have the same bytecode, but they represent different functions. We want equalLambdaCode(a,b) == false, since they return different things. I attempt to detect this by marking all lambda whose bytecode either does INVOKEDYNAMIC (opcode used to create lambdas and other compiler specific things) or loads this as incomparable. Incomparable lambda gets List.of(UniqueObject(Float.NaN)) as their bytecode, which can never be equal to any other list since Float.NaN != Float.NaN.

With all those functions defined, we can now define equalLambdaCode as:

public static boolean equalLambdaCode(Object a, Object b)  {
    List<Object> aBytecode = readBytecodeOf(a);
    List<Object> bBytecode = readBytecodeOf(b);
    return aBytecode.equals(bBytecode);
}
Sign up to request clarification or add additional context in comments.

1 Comment

"That being said, bytecode being equal does not imply the lambda themselves are equal." - That should be in bold!!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.