RFR: 8374654: Inconsistent handling of lambda deserialization for Object method references on interfaces
Jan Lahoda
jlahoda at openjdk.org
Wed Jan 7 18:47:57 UTC 2026
On Tue, 6 Jan 2026 23:26:35 GMT, Liam Miller-Cushon <cushon at openjdk.org> wrote:
> See [JDK-8374654](https://bugs.openjdk.org/browse/JDK-8374654). This fixes an inconsistency between generated Lambda `$deserialization$` methods and the runtime behaviour of `SerializedLambda` instances, for Object method references on interfaces. At runtime the methods are resolved to declared methods, so Object method references on interfaces will always have the `getImplMethodKind`, `getImplClass`, and `getImplMethodSignature` of the method declared in `Object`, not of an override in the interface.
(I am sorry for a longish post, but I can't write this short.)
There's a lot of history in this, and even though I tried to page some of it back, I may still be missing some pieces.
In short, as far as I know, there are quite a few cases where (de)serialization of method references misbehaves. And changes around this area tend to cause regressions, often regressions that are found quite late.
In this specific case, I think the simple example that could be used as a motivation for this PR is (based on your code and code from PR #28943):
$ cat Main.java; jdk-25/bin/java Main.java
/**
* @test
* @compile Main.java
* @run main Main
*/
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class Main {
public static void main(String... args) throws Exception {
F<I2, Integer> r2 = (F<I2, Integer>) I2::hashCode;
((F<I2, Integer>) serialDeserial(r2)).apply(new I2() {});
}
@SuppressWarnings("unchecked")
static <T> T serialDeserial(T object) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(object);
oos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
T result = (T) ois.readObject();
ois.close();
return result;
}
interface I1 extends Serializable {}
interface I2 extends I1 {
@Override
public int hashCode();
}
interface F<T, R> extends Serializable {
R apply(T t);
}
} Exception in thread "main" java.io.InvalidObjectException: ReflectiveOperationException during deserialization
at java.base/java.lang.invoke.SerializedLambda.readResolve(SerializedLambda.java:269)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at java.base/java.io.ObjectStreamClass.invokeReadResolve(ObjectStreamClass.java:1066)
at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2142)
at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1620)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:487)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:445)
at Main.serialDeserial(Main.java:26)
at Main.main(Main.java:15)
Caused by: java.lang.reflect.InvocationTargetException
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:119)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at java.base/java.lang.invoke.SerializedLambda.readResolve(SerializedLambda.java:267)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at java.base/java.io.ObjectStreamClass.invokeReadResolve(ObjectStreamClass.java:1066)
at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2142)
at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1620)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:487)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:445)
at Main.serialDeserial(Main.java:26)
at Main.main(Main.java:15)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at jdk.compiler/com.sun.tools.javac.launcher.SourceLauncher.execute(SourceLauncher.java:254)
at jdk.compiler/com.sun.tools.javac.launcher.SourceLauncher.run(SourceLauncher.java:138)
at jdk.compiler/com.sun.tools.javac.launcher.SourceLauncher.main(SourceLauncher.java:76)
Caused by: java.lang.IllegalArgumentException: Invalid lambda deserialization
at Main.$deserializeLambda$(Main.java:12)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
... 16 more
which, I think, is caused directly by not mapping the methods correctly. And you patch fixes that.
However, a different case works without your patch and fails with your patch (for me):
$ cat Main.java && echo "Running on JDK 25:" && jdk-25/bin/java Main.java && echo "Running on patched JDK:" && ./build/linux-x86_64-server-release/jdk/bin/java Main.java
/**
* @test
* @compile Main.java
* @run main Main
*/
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class Main {
public static void main(String... args) throws Exception {
F<I2, Integer> r2 = (F<I2, Integer>) I2::hashCode;
F<I1, Integer> r1 = (F<I1, Integer>) I1::hashCode;
((F<I1, Integer>) serialDeserial(r1)).apply(new I1() {});
((F<I2, Integer>) serialDeserial(r2)).apply(new I2() {});
}
@SuppressWarnings("unchecked")
static <T> T serialDeserial(T object) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(object);
oos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
T result = (T) ois.readObject();
ois.close();
return result;
}
interface I1 extends Serializable {}
interface I2 extends I1 {
@Override
public int hashCode();
}
interface F<T, R> extends Serializable {
R apply(T t);
}
} Running on JDK 25:
Running on patched JDK:
Exception in thread "main" java.lang.ClassCastException: class Main$1 cannot be cast to class Main$I2 (Main$1 and Main$I2 are in unnamed module of loader com.sun.tools.javac.launcher.MemoryClassLoader @5b1669c0)
at Main.main(Main.java:16)
But, it "works" on JDK 25 only by "accident": the `$deserializeLambda$` code does not disambiguate between the two method references in neither in JDK 25 nor after the patch, but in JDK 25 the method reference that is being deserialized is always the `I1::hashCode`, and that one "works" in both cases due to the class hierarchy. After the patch, the one that is used is always `I2::hashCode`, and that one does not work for `I1`.
I think there is one underlying problem, and that is even though javac will (now) use a sharp method reference in the bootstrap's arguments (note the `REF_invokeInterface Main$I2.hashCode:()I`):
0: #142 REF_invokeStatic java/lang/invoke/LambdaMetafactory.altMetafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
Method arguments:
#131 (Ljava/lang/Object;)Ljava/lang/Object;
#132 REF_invokeInterface Main$I2.hashCode:()I
#134 (LMain$I2;)Ljava/lang/Integer;
#136 5
#137 0
the metafactory here:
https://github.com/openjdk/jdk/blob/dd20e9150666f247af61dfa524a170ef7dd96c03/src/java.base/share/classes/java/lang/invoke/AbstractValidatingLambdaMetafactory.java#L138
will always see:
caller.revealDirect(implementation): invokeVirtual java.lang.Object.hashCode:()int
making it more difficult to disambiguate the two references. This is https://bugs.openjdk.org/browse/JDK-8068253 I believe.
Note the `instantiatedMethodType` (in `SerializedLambda`) here is different between the two method references used in this testcase, so one possibility would be to also check the value of that to disambiguate between the references.
Alternatives include:
- wait for https://bugs.openjdk.org/browse/JDK-8068253, javac will then need to be adjusted, partly or completely dropping this trickery around second-guessing which supertype's method should be used while deserializing (where the current trickery is not completely correct in case of separate compilation)
- replace the complex checks in `$deserializeLambda$` with a "hash" - this would be sent to the `altMetafactory` when instantiating the reference, passed through `SerializedLambda`, and then used while deserializing (we already have a "hash" for serializable lambdas)
- simply expand all serializable method references to lambdas
(where each of these has its own set of compatibility issues, at least for pre-existing streams, of course)
-------------
PR Comment: https://git.openjdk.org/jdk/pull/29075#issuecomment-3720244831
More information about the compiler-dev
mailing list