Re: Proposal: Static Service Traits—Enhancing Java’s Static Polymorphism and ServiceLoader Facilities
Olexandr Rotan
rotanolexandr842 at gmail.com
Thu Jan 29 15:06:38 UTC 2026
And on the different note, annotations may not even be able to be put or AP
may not be able to process some code, if you are integrating
libraries/frameworks into your project.
For example, if someone erroneously or not, wrote code with two
constructors marked with @Inject/@Autowired, and you then register this
type as bean in your DI container - you are bound to fail, no AP will even
save you, as you cant "check" already compiled code with AP. You may try to
use use-sote validation, but traversing over library class hierarchies in
compile time may be a terrible experience, and some classes that library is
compiled against may not be present in compilation process by be reachable
in runtime, which is common case for libraries. So effectively, you just
rely on the fact that lib dev obeyed contract, and cant force them to use
the same static analysis as you, so your failure ultimately deferred to the
runtime
On Thu, Jan 29, 2026, 16:59 Olexandr Rotan <rotanolexandr842 at gmail.com>
wrote:
> Presicely, you can, and this is what I said in the last few paragraphs.
> But, there are disadvantages, as I also wrote, and every time I write
> something like this I can't help but feel like I am monkey-patching.
>
> Strictly speaking, AP may not even start (as far as i know), if there are
> not annotations at all in the project, and doing things like discovering
> implantations in compile-time is the territory when work becomes a true
> monkey-patching, unless you require user to put annotations at least
> somewhere in the project, like on each package, so that their CU to check
> are discoverable. But this is a HUGE discoverability barrier, from new
> dependency to new annotation EVERYWHERE.
>
> One serious limitation of AP is that javac is single-threaded so there
> will always be synchronization points. You can get around this by creating
> static analyzer checks, but this is another lot of pain, as I'm sure you
> know: the only point when you see the feedback is the build, which often
> only happens in CI, and the best you may hope for in the meantime is the
> sanity of you static analyzer plugin's IDE support, but practice shows that
> even for something as widely adopted as spring modulith or nullaway, it
> takes years for major IDEs to adapt.
>
> And once again - static analyzer is not a part of the language. And,
> frankly speaking, AP also really feels like lurking in escape hatch. AP
> clearly may be the answer, but in it's current state, it lacks capabilities
> (remember Archie's checker framework adventure for example), UX and
> performance. But still, I don't think that interface contract validation
> should rely on something that is called ANNOTATION processor, as there are
> no annotation in this process, and you cant force your user to obey this
> contract without "gently asking" them to use you static analysis tools. But
> they very well may not
>
> On Thu, Jan 29, 2026, 14:54 Josiah Noel <josiahnoel at gmail.com> wrote:
>
>> Can't you write an annotation processor to do all three of those checks
>> during compilation?
>>
>> On Thu, Jan 29, 2026, 2:01 AM Olexandr Rotan <rotanolexandr842 at gmail.com>
>> wrote:
>>
>>> I may be not exactly at the same topic as Steffen, but I think that,
>>> more vaguely speaking, the issue can be more generally described as "lack
>>> of compile time metaprogramming for contract verification", which manifests
>>> in, for example, such scenarios:
>>>
>>> 1. No way to verify jackson-using class has exactly one @JsonCreator
>>> 2. No way to assert there is exactly one static method that is invokable
>>> with given arguments (static traits specifically, for example ensure there
>>> is some unambigous metadata-producing method for runtime discovery
>>> mechanisms)
>>> 3. Or, more generally, no way to ensure that class A implements a
>>> word-phrased (on the contrary to method-phrased, which is statically
>>> enforced) contract of interface B, such as for SequencedCollection. Of
>>> course, this particular example is largely beyond the limits of
>>> metaprogramming, but for something simpler such as potential interface
>>> "UnmodifiableList" that would have declared that all modifying methods of
>>> List in class implementing UnmodifiableList should ultimately throw an
>>> exception or at least not mutate backing state fields, static checks are
>>> doable.
>>>
>>> The other thing is that this is what static analysis tools are made for.
>>> Unfortunately, not everyone uses even developer-provided analysis profiles
>>> when using said developer`s library, so the fact that there is no langauge
>>> level contract enforcement capabilities for more complex scenarios surely
>>> leads to a lot of unnecessary runtime errors.
>>>
>>> Though, of course, I admit that what I have described (metaprogrammatic
>>> contract enforcement) is a far larger scope then just static methods.
>>> Ultimately, even just static-method-contract enforcement would solve a lot
>>> of reflection-related issues for me when writing things like runtime
>>> discovery
>>>
>>> On Thu, Jan 29, 2026 at 2:31 AM Ron Pressler <ron.pressler at oracle.com>
>>> wrote:
>>>
>>>> The first two paragraphs boil down to “it’s difficult to work with
>>>> user-defined numeric types, especially with a tight memory layout”. As you
>>>> probably know, this is a problem we’re already working on solving.
>>>>
>>>> Aside from that, I think the problem you’re reporting is:
>>>>
>>>> "The java.util.ServiceLoader ... requires explicit custom code to
>>>> verify bindings at startup for fail-fast behavior. The API remains an extra
>>>> invocation hurdle with its lookup and instantiation requirements.”
>>>>
>>>> What you’re saying about “right-sizing” the language is absolutely
>>>> right, but for the JDK team to propose a solution to a problem you’ve
>>>> identified, the problem first needs to be understood. Assuming that what I
>>>> quoted is the problem you’ve tried expressing, can you elaborate more on:
>>>>
>>>> 1. Requires explicit custom code to verify bindings at startup for
>>>> fail-fast behavior.
>>>>
>>>> 2. The API remains an extra invocation hurdle with its lookup and
>>>> instantiation requirements.
>>>>
>>>> The more concrete you are, as in “I tried to do X and this is the code
>>>> I wrote”, the easier it would be to understand.
>>>> I think I know what you mean in #1, but it would be helpful to show
>>>> what code you had to write. In #2, what is exactly the hurdle? E.g. let’s
>>>> say that the hurdle is that the API is verbose. That is only a problem if
>>>> it has to be used lots of times in a program, so what was your program that
>>>> required so many uses of ServiceLoader?
>>>>
>>>> (P.S. “right-sizing” the language doesn’t mean that anything that could
>>>> be improved with a language feature should be because every addition to the
>>>> language complicates it. We like it when a non-trivial language feature can
>>>> solve a big problem or multiple small ones.)
>>>>
>>>> — Ron
>>>>
>>>>
>>>> > On 28 Jan 2026, at 23:20, Steffen Yount <steffenyount at gmail.com>
>>>> wrote:
>>>> >
>>>> > Hi Ron,
>>>> >
>>>> > Thank you for the feedback. It's totally reasonable to push for the
>>>> > "why" before getting into the "how."
>>>> >
>>>> > The personal problem I encounter is the severe and inconvenient
>>>> > extensibility costs of the current language model. When pursuing
>>>> > data-oriented design or domain-specific numeric types in Java, I find
>>>> > the language's current facilities to be a significant design obstacle.
>>>> > These are not just issues I find while building, but constraints that
>>>> > fundamentally alter how I consider a project's architecture before a
>>>> > single line of code is written.
>>>> >
>>>> > I have encountered the following specific friction points:
>>>> >
>>>> > The Instantiation Tax: On several occasions, I have turned to Java for
>>>> > intensive math calculations. These efforts typically start small but
>>>> > eventually outgrow Java's supported numeric abstractions. To move
>>>> > beyond small memory footprints and 64-bit representational limits, I
>>>> > find I must refactor to contiguous arrays, reusable Flyweight objects,
>>>> > and custom math methods just to manage the memory pressure and data
>>>> > type limitations. Because I cannot define polymorphic static contracts
>>>> > for these types, I am forced to pay an Instantiation Tax—maintaining
>>>> > "witness" objects just to access static logic. The ergonomic noise and
>>>> > heap-inefficiency of the unwanted object headers are so high that I am
>>>> > often discouraged from pursuing my original abstractions entirely.
>>>> > While I have used primitive long types as witnesses for static
>>>> > overloaded method binding, using the NewType pattern for type-safe
>>>> > bindings remains prohibitively expensive; a new NewType class's
>>>> > implementation immediately excludes it from participation within the
>>>> > language's built-in expression operators. In the aforementioned
>>>> > efforts, I have ultimately abandoned Java for C/C++ simply because
>>>> > they allowed me to shape the data layout and its static behavior
>>>> > without this "abstraction tax."
>>>> >
>>>> > The Expression Problem (Post-hoc Abstraction): When I find it
>>>> > necessary to treat third-party classes as part of a common
>>>> > abstraction—for instance, when attempting to resolve Guice-based
>>>> > injection or AOP design issues without the "magic" of runtime
>>>> > reflection—the traditional path is the Adapter Pattern. I find this
>>>> > route unsustainable; creating a new wrapper class for every instance
>>>> > not only fragments object identity but generates significant GC churn.
>>>> > I have seen production code with wrapper classes nested eight levels
>>>> > deep just to satisfy disparate abstractions. The ability to implement
>>>> > type level contracts rather than just instance level contracts, along
>>>> > with type level extension methods, would allow us to side-step the
>>>> > wrapper classes with implementations that bind to existing types
>>>> > without modifying their source code. The lack of them serves as a wall
>>>> > preventing me from designing the clean, type-safe, and AOT-friendly
>>>> > systems I know are possible elsewhere.
>>>> >
>>>> > The ServiceLoader Ceremony: The java.util.ServiceLoader acts like more
>>>> > of a library than a language feature. It requires explicit custom code
>>>> > to verify bindings at startup for fail-fast behavior. The API remains
>>>> > an extra invocation hurdle with its lookup and instantiation
>>>> > requirements. A coherent language-integrated, static service interface
>>>> > method dispatch and binding would dramatically reduce this ceremony
>>>> > and increase utility by moving it from a manual runtime search to a
>>>> > link-time certainty.
>>>> >
>>>> > My "big picture" problem is that Java’s evolution model currently
>>>> > makes it difficult to "grow the language" via libraries that feel
>>>> > native and are performan, such as the recent prototype exploration of
>>>> > Float16. I believe the language should provide the infrastructure for
>>>> > _Static Service Traits_ or otherwise make that kind of library-driven
>>>> > growth a standard capability for all developers.
>>>> >
>>>> > I feel "corralled" into 1990s instance-based OOP. When I explore
>>>> > data-oriented design or high-performance numeric abstractions, the
>>>> > features found in my competitors' language tool belts would be
>>>> > incredibly useful; without them, I find myself looking at alternate
>>>> > language implementations just to avoid Java's structural obstacles.
>>>> >
>>>> > Given that Project Amber’s stated mission is to "right-size language
>>>> > ceremony" and improve developer productivity, doesn't a proposal that
>>>> > eliminates this Instantiation Tax and link-time service ceremony seem
>>>> > like a relevant and worthy pursuit?
>>>> >
>>>> > -Steffen
>>>> >
>>>> >
>>>> > On Wed, Jan 28, 2026 at 7:45 AM Ron Pressler <ron.pressler at oracle.com>
>>>> wrote:
>>>> >>
>>>> >> The hardest part in designing and evolving a language is deciding
>>>> which problems are important enough to merit a solution in the language and
>>>> how their priorities compares to other problems. It’s the hardest part
>>>> because the language team are expert at coming up with solutions, but they
>>>> may not always know what problems people enoucnter in the field, how
>>>> frequently they encounter them, and how they work around them today.
>>>> >>
>>>> >> I’m sure there is some problem hidden here and in your previous
>>>> post, but it is not articulated well and is hidden in a poposed solution,
>>>> even though no solution is even worth exploring before understanding the
>>>> problem. And so the best way to get to a solution is for you to focus on
>>>> the problem and only on the problem.
>>>> >>
>>>> >> What was the problem you *personally* ran into? How bad were its
>>>> implications? How did you work around it?
>>>> >>
>>>> >> With the hard part done, the JDK team will then be able to assess
>>>> its severity and think whether it merits a solution in the JDK, if so,
>>>> where (language, libraries, or VM), and how to prioritise it against other
>>>> problems worth tackling. Then they’ll be able to propose a solution, and
>>>> that’s would be the time to try it out and discuss it.
>>>> >>
>>>> >> — Ron
>>>> >>
>>>> >>
>>>> >>
>>>> >>> On 28 Jan 2026, at 00:28, Steffen Yount <steffenyount at gmail.com>
>>>> wrote:
>>>> >>>
>>>> >>> The recent thread "Java Language Enhancement: Disallow access to
>>>> static members via object references" highlights a long-standing tension in
>>>> Java's handling of static members. While that thread seeks to further
>>>> decouple instance state from static logic, I would like to propose moving
>>>> in the opposite direction: "doubling down" on Java’s compile-time and
>>>> link-time static polymorphism.
>>>> >>>
>>>> >>> By beefing up java.util.ServiceLoader facilities and integrating
>>>> its discovery mechanism directly into the language via Static Service
>>>> Traits, we can facilitate the "Witness Object" paradigm discussed by Brian
>>>> Goetz's "growing the java language" presentation and the algebraic
>>>> "well-known interface" model for custom numeric types (like Float16)
>>>> proposed in Joe Darcy's "Paths to Support Additional Numeric Types on the
>>>> Java Platform" presentation.
>>>> >>>
>>>> >>> == Static Service Traits for Java ==
>>>> >>>
>>>> >>> I propose a system of Static Service Traits. I use the term "Trait"
>>>> advisedly: this feature adopts a rigorous Coherence Model (inspired by
>>>> systems like Rust) to ensure that service resolution is not merely a
>>>> dynamic search, but a type-safe, deterministic binding of static
>>>> capabilities to types.
>>>> >>> 1. The service Contextual Keyword
>>>> >>> We introduce service as a contextual modifier for interface
>>>> declarations. Marking an interface as a service identifies it as a "service
>>>> type" with a contract for static capabilities and a high-performance
>>>> service provider registry.
>>>> >>>
>>>> >>> 2. Static Implementations and Extension Methods
>>>> >>> • Static Implementations:
>>>> >>> • In Interface Headers: interface MyTrait implements
>>>> ServiceX<T>. Methods are fulfilled as static.
>>>> >>> • In Class Headers: class MyClass implements static
>>>> Numeric<Float16>. Methods are implemented as static on the class. Existing
>>>> signature rules prevent a method from being both a static and an instance
>>>> implementation simultaneously.
>>>> >>> • Static Extension Methods: Desugared at the call site.
>>>> myInstance.method() becomes MyClass.method(myInstance). Notably, if
>>>> myInstance is null, it desugars to MyClass.method(null) without an
>>>> immediate NullPointerException.
>>>> >>> • Ergonomic Aliases: To simplify signatures, we introduce
>>>> private nested static type aliases This and Super (e.g., static This
>>>> add(This a, This b)).
>>>> >>>
>>>> >>> 3. Operational Mechanics & Link-Time Integration
>>>> >>> A ServiceLoader Controller is integrated into the JVM’s
>>>> class-loading pipeline. During class definition, the Controller eagerly
>>>> extracts all relevant metadata to populate the Static Service Provider
>>>> Registry, including:
>>>> >>> • Header-level static implements and implements declarations.
>>>> >>> • Service binding descriptors from module-info.class.
>>>> >>> • META-INF/services/ provider-configuration files.
>>>> >>> Hierarchical Precedence Resolution: To ensure deterministic
>>>> binding, the Controller resolves call sites to their most specific service
>>>> provider via a waterfall dispatch model:
>>>> >>> • Tier 1: Type Specialization: Most specific generic match wins,
>>>> applying the same scrutiny and rules currently used for existing static
>>>> overloaded method resolution.
>>>> >>> • Tier 2: Physical Locality: Provider in the same file
>>>> (.jar/.class) as the caller wins.
>>>> >>> • Tier 3: Loader Proximity: Nearest ClassLoader in the
>>>> delegation path wins.
>>>> >>> • Tier 4: Modular Topology: Internal > Explicit > java.base >
>>>> Transitive > Automatic.
>>>> >>> • Tier 5: Sequential Order: Final tie-breaker via Classpath
>>>> order.
>>>> >>>
>>>> >>> 4. Coherence, The Orphan Rule, and Quarantining
>>>> >>> To achieve the type-safety of a trait system, we enforce an adapted
>>>> Orphan Rule: A module (or package on the classpath) must own either the
>>>> service interface or the target type to define an implementation.
>>>> >>> • Coherence Enforcement: Violations in modular code trigger a
>>>> LinkageError.
>>>> >>> • Behavioral Continuity: Violations in classpath code trigger a
>>>> load-time warning and the provider is quarantined from the Static Registry.
>>>> To ensure continuity, quarantined providers remain accessible via existing
>>>> java.util.ServiceLoader API calls, protecting legacy iteration-based
>>>> discovery while ensuring the integrity of the new link-time dispatch.
>>>> >>> 5. Performance and AOT Considerations
>>>> >>> This model transforms ServiceLoader into a link-time resolver. JIT
>>>> compilers can treat service calls as direct invokestatic instructions,
>>>> enabling aggressive optimization. This is highly compatible with Project
>>>> Leyden and GraalVM, as precedence can be "baked" into the binary during AOT
>>>> compilation.
>>>> >>> Conclusion
>>>> >>> By transitioning ServiceLoader to a link-time resolver, we provide
>>>> a type-safe, high-performance path for algebraic types and witness-based
>>>> generics. This allows Java to "grow" through libraries—fulfilling the goals
>>>> of both Darcy and Goetz—while maintaining the performance and stability
>>>> characteristics of the modern JVM.
>>>> >>>
>>>> >>>
>>>> >>> Thoughts?
>>>> >>
>>>>
>>>>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20260129/68d47085/attachment-0001.htm>
More information about the amber-dev
mailing list