In answering this question about lambdas which capture local variables, I defined a simple lambda which captures a local variable, and showed that the lambda has a field with that variable's value. According to various sources (e.g. here, here), when a lambda captures a local variable, its value is stored in a "synthetic" field. This seems to be implied by the Java Virtual Machine Specification (§4.7.8), which says:
A class member that does not appear in the source code must be marked using a Synthetic attribute, or else it must have its ACC_SYNTHETIC flag set. The only exceptions to this requirement are compiler-generated methods which are not considered implementation artifacts, namely the instance initialization method representing a default constructor of the Java programming language (§2.9.1), the class or interface initialization method (§2.9.2), and the Enum.values() and Enum.valueOf() methods.
The lambda's field is not one of the defined exceptions, and the lambda's field is not declared in the source code, so by my understanding the field should be synthetic according to this rule.
The existence of the field can be easily demonstrated via reflection. However, when I check using the Field.isSynthetic method, it actually returns false. The documentation for this method says it:
Returns true if this field is a synthetic field; returns false otherwise.
I'm testing using JShell in Java 10.0.1:
> class A { static Runnable a(int x) { return () -> System.out.println(x); } }
| created class A
> Runnable r = A.a(5);
r ==> A$$Lambda$15/1413653265@548e7350
> import java.lang.reflect.Field;
> Field[] fields = r.getClass().getDeclaredFields();
fields ==> Field[1] { private final int A$$Lambda$15/1413653265.arg$1 }
> fields[0].isSynthetic()
$5 ==> false
The same behaviour occurs outside of JShell:
import java.lang.reflect.Field;
public class LambdaTest {
static Runnable a(int x) {
return () -> System.out.println(x);
}
public static void main(String[] args) {
Runnable r = a(5);
Field[] fields = r.getClass().getDeclaredFields();
boolean isSynthetic = fields[0].isSynthetic();
System.out.println("isSynthetic == " + isSynthetic); // false
}
}
What's the explanation for this discrepancy? Am I misinterpreting the JVMS, am I misinterpreting the Field.isSynthetic method documentation, are the spec and the docs using the word "synthetic" to mean different things, or is this a bug?
In general, your understanding about the synthetic nature of fields generated for captured variables is right.
When we use the following program
we get something like
Prior to JDK-11, you'll also find an entry like
in the outer class
CheckSynthetic.So for the anonymous inner class, the fields
this$0andval$bare marked as synthetic, as expected.For the lambda expression, the entire class has been marked as synthetic, but none of its members.
One interpretation could be that marking a class as synthetic is already sufficient here. Considering JVMS §4.7.8:
we could say that when the class does not appear in source code, there is no source code that could be checked for the presence of member declarations.
But more important is that this specification applies to class files and while those of us interested in more details know that under the hood, the reference implementation of
LambdaMetafactorywill generate byte code in the class file format to create an anonymous class, this is an unspecified implementation detail.As John Rose puts it:
So we shouldn’t reason about this class file structure and only focus on the visible behavior, which is the return value of
Field.isSynthetic(). While it’s reasonable to assume that under the hood, this implementation will just report whether the bytecode had the flag or attribute, we have to focus on the bytecode independent contract ofisSynthetic:Which brings us to JLS §13.1:
Not only is the possibility of a construct to be “declared … implicitly in source code” quiet fuzzy, the requirement to be marked as synthetic is limited to “a construct emitted by a Java compiler”. But the classes generated at runtime for lambda expressions are not emitted by a Java compiler, they are generated automatically by a bytecode factory. This is more than just quibbling, as the entire §13 is about Binary Compatibility, but the ephemeral classes generated within a single runtime are not subject to Binary Compatibility at all, as the current runtime is the only software which has to deal with them.
The requirements on the runtime class are specified in JLS §15.27.4:
So the specification does not cover many properties of the actual class and that’s intentional.
So when the result of
Field.isSynthetic()is only determined by the Java Language Specification, but the class of the inspected field is off specification, the result is unspecified.There’s room for interpretation whether, now that we can observe certain artifacts of a generated class, those artifacts should follow certain expectations regarding a similarity to ordinary classes, but there’s not enough information to discuss that. Most notably, there is not a single word in any of the cited specifications about why we have to mark constructs as synthetic and which consequences the presence or absence of the marker has.
Practical tests revealed that Java compilers, i.e.
javac, treat synthetic members as nonexistent when trying to access them on source level, but that has not been specified anywhere. Further, this behavior is not relevant for a runtime generated class which is never seen by a Java compiler. In contrast, for an access via Reflection, the synthetic flag seems to have no effect at all.