Records and annotations
Brian Goetz
brian.goetz at oracle.com
Thu Jun 13 16:46:09 UTC 2019
There's a sub-option of B that has been suggested, call it B-, which we
can also consider: only push down annotations from record components
into _implicit_ declarations of mandated members, rather than all
declarations (implicit or explicit.)
The benefit of this (compared to B) is transparency: you get what is
present in the source file.
The downside is that if you explicitly declare a member, you have to
explicitly replicate its annotations, and it's easy to forget to do so.
On 6/6/2019 4:48 PM, Brian Goetz wrote:
> Recall that some time ago, we were discussing some different
> directions for how to handle annotations on record components.
>
> Approach A: Record components are a new kind of annotation target
> location; if an annotation is meta-annotated with this target kind, it
> can be applied to record components. Expose reflection over
> annotations on record components as with other features.
>
> Approach B: Annotations on record components are merely “pushed down”
> to the corresponding JLS-mandated API elements (constructor
> parameters, accessor methods, fields), according to the allowed target
> kinds of the annotation (if the annotation is only valid on fields, it
> is only pushed down to fields.).
>
> Approach B+: Like B, except that we continue to reify the provenance
> of the annotations, and expose them through reflection as annotations
> on the record component _in addition to_ annotations on the mandated
> API elements.
>
> In an alternate universe where we had done records first, and were now
> adding annotations, we’d surely pick A. However, in the current
> universe, picking A would put us in an adoption bind; we have to wait
> for specific annotations to acquire knowledge of the new target kinds
> (through the @Target meta-annotation), and for frameworks to be aware
> of annotations on record components, before we can migrate classes
> dependent on those annotations/frameworks to be records. Further,
> library authors suffer a familiar problem: if @Foo is meta-annotated
> with a target kind of RECORD_COMPONENT, then that means it must have
> been compiled against a Java 14+ JDK, which means that the resulting
> classes are dependent on JDK 14+, unless they use something like MR
> Jars to have two versions in one JAR. This would further impede
> adoption.
>
> For guidance in our A/B choice, we can look to enums. Enum constants
> are surely a first-class language element, and can be annotated, but
> they do not have their own annotation target kind; instead, the
> compiler pushes down the annotations onto the fields that carry the
> enum constants. While this might be an uneasy dependence on the
> translation strategy, in fact this translation strategy is mandated
> (because we want migrating between a class with static constant fields
> and an enum to be a binary-compatible migration.).
>
> Records are in a similar boat as enums; while there is a translation
> strategy going on here, the elements of it are mandated by the
> language specification. So I think the trick that enums use is a
> reasonable one to carry forward to records, allowing us to seriously
> consider B/B+. (Strategy A also has a lot of accidental detail;
> class file attributes for various kinds of options and bookkeeping to
> manage exactly what is being annotated, reflection API surface, etc.).
>
> The following type-checking strategy applies to B and B+:
>
> - A record component may be annotated by a declaration annotation
> with no target kind meta annotation, or whose target kind includes one
> or more of PARAMETER, FIELD, or METHOD
> - The type of a record component may be annotated by a type annotation
>
> Strategy B then entails pushing down annotations through tree
> manipulation to the right places. For PARAMETER annotations, they are
> pushed down to the parameters of the implicit constructor; for FIELD
> annotations, to the fields; for METHOD annotations, to the accessor.
> And for type annotations, to the corresponding type use in
> constructor parameters, field declarations, and accessor methods.
> (And if the annotation is applicable to more than one of these, it is
> pushed down to all applicable targets.)
>
> But wait! What if the author also explicitly declares, say, the
> accessor method?
>
> record R(int a) {
> int a() { return a; }
> }
>
> No problem, we can still push the annotation down, and there is
> precedent for annotations being “inherited” in this way.
>
> But wait! What if the author explicitly declares the same annotation,
> but with conflicting values?
>
> record R(@Foo(1) int a) {
> @Foo(2) int a() { return a; }
> }
>
> We can still push down @Foo(1), and then look to see if @Foo is a
> repeating annotation. If it is, great; if not, then a() has two @Foo
> annotations, which results in a compilation error. So we always push
> down, and then enforce arity rules.
>
> By pushing annotations down in this manner, existing reflection can
> pick up the annotations on the various class members with no
> additional work or reflection API surface. Are we done?
>
> We might be done, or we might want to do more (strategy B+). In B+,
> we _additionally_ reify which annotations were present on the
> component, and (possibly) expose additional reflection API surface to
> query annotations on record components. Why would we want to do this?
> Well, one reason that occurs to me is that we’ve been holding the
> move of “abstract records” and records extending abstract records in
> our back pocket. In this case, we might wish to copy annotations down
> from a record component in a superclass to the corresponding
> pseudo-component in the subclass, for example. But, I’m not
> particularly compelled by this — I think the strategy we took for
> enums is mostly good enough. So I’m voting for pure B.
>
More information about the amber-spec-experts
mailing list