There is a parallel thread going on which touches on similar things - there people are wanting "record component references."
I adapted the code from https://github.com/rilindbicaj/fluentmapper/blob/main/fluentmapper-provider/src/main/java/dev/bici/fluentmapper/provider/expression/parser/ExpressionParser.java to use the new classfile api + work for a quick demo of the approach. Basically, you can get at the code in the body of a lambda via some serialization hackery. The degree to which this is actually supported is to me unknown. import java.io.IOException; import java.io.Serializable; import java.io.UncheckedIOException; import java.lang.classfile.ClassFile; import java.lang.classfile.ClassModel; import java.lang.classfile.instruction.FieldInstruction; import java.lang.invoke.SerializedLambda; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; public final class FieldReference { private final Class<?> root; private final List<Field> fields; private FieldReference(Class<?> root, List<Field> fields) { this.root = root; this.fields = fields; } public Class<?> root() { return root; } public List<Field> fields() { return fields; } private static SerializedLambda toSerializedLambda(Expression<?, ?> expression) { try { var writeReplaceMethod = expression.getClass().getDeclaredMethod("writeReplace"); writeReplaceMethod.setAccessible(true); return (SerializedLambda) writeReplaceMethod.invoke(expression); } catch (ReflectiveOperationException e) { throw new IllegalArgumentException("Could not extract SerializedLambda from the provided expression;", e); } } public static <T> FieldReference of(Class<T> klass, Expression<T, ?> expression) { // TODO: This work can likely be cached. var serializedLambda = toSerializedLambda(expression); var containingClass = serializedLambda.getImplClass(); var lambdaSignature = serializedLambda.getImplMethodName(); // TODO: Unsure if this is the right class loader to use var classLoader = klass.getClassLoader(); byte[] classBytes; try (var classResource = classLoader.getResourceAsStream( containingClass.replace('.', '/') + ".class" )) { if (classResource == null) { throw new IllegalArgumentException("Could not find class-file resource in class loader"); } classBytes = classResource.readAllBytes(); } catch (IOException e) { throw new UncheckedIOException(e); } ClassModel classModel = ClassFile.of().parse(classBytes); var methodModel = classModel.methods().stream() .filter(method -> lambdaSignature.equals(method.methodName().toString())) .findFirst() .orElseThrow(() -> new IllegalArgumentException("Could not find the implementing method for the expression lambda")); var code = methodModel.code().orElseThrow(); // TODO: This could use a lot of prechecks var fields = new ArrayList<Field>(); Class<?> lastType = klass; for (var element : code.elementList()) { if (element instanceof FieldInstruction fieldInstruction) { try { fields.add( lastType.getDeclaredField(fieldInstruction.field().name().toString()) ); // TODO: This is almost certainly not sound if (fieldInstruction.typeSymbol().packageName().isEmpty()) { lastType = classLoader.loadClass( fieldInstruction.typeSymbol().displayName() ); } else { lastType = classLoader.loadClass( fieldInstruction.typeSymbol().packageName() + "." + fieldInstruction.typeSymbol().displayName() ); } } catch (NoSuchFieldException e) { throw new IllegalArgumentException("Inaccessable field", e); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } } return new FieldReference(klass, List.copyOf(fields)); } @FunctionalInterface public interface Expression<A, B> extends Serializable { B get(A a); } @Override public String toString() { return "FieldReference[" + "root=" + root.getName() + ", fields=" + fields.stream().map(Field::getName).toList() + ']'; } } And you can use all of that with a lambda that only accesses fields to emulate "field references." class A { String aa; B b; } class B { C c; } class C { String v; D d; } class D { E e; } class E { F f; } class F { G g; } class G { String v; } public class Test { public static void main(String[] args) { var refA = FieldReference.of(A.class, a -> a.aa); IO.println(refA); var refB = FieldReference.of(A.class, a -> a.b.c.d); IO.println(refB); } } FieldReference[root=A, fields=[aa]] FieldReference[root=A, fields=[b, c, d]] So that is one approach to look into. On Mon, Dec 1, 2025, 8:38 PM Bruno Eberhard <[email protected]> wrote: > Am 01.12.2025 um 22:00 schrieb Ethan McCue: > > On that note, i'd say you have somewhat of a bigger problem w.r.t. enums > > [..] > > As with all things in jdk.unsupported, I can't help but wonder if there > > are any VM level invariants you run afoul of by making extra enum > > instances. > > I don't know about the VM internals. I did never run into problems with > these extra enums. > > > My first stab at #2 is to add something like this record [..] > Thank you for your input. I have to try this. > > > I think it's also worth asking - do you have any usage statistics on > > your library? (Maven central used to offer download stats, etc.) If you > > are going to be broken anyways it is probably useful to know the blast > > radius. > > Very good point. As far as I know minimal-j is only used by me for a > smaller and one bigger App. In the bigger App ( An ERP called > lisheane.ch , currently only available in german ) I invested quite some > time. I would have to rewrite parts of it. > > So the world will not be standing still if minimal-j doesn't work > anymore or has to be changed. > > I would still like the idea of field references ( Person::name , > Person::address::city ) in the java language as equivalent for the $ > constant. It is really nice to use. But I don't have the knowledge how > to add it to the language or how to propose the feature. >
