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);
}
equalsor==is the comparisons I know ... can you clarify?==is semantically difficult.