Type erasure conflict in Ceylon generic interop with Java collections
0 reputation · 18 Nov 2021, 04:53 UTC
0 reputation · 18 Nov 2021, 04:53 UTC
Ceylon compiles to JVM bytecode and uses a strong static type system with null-safety and traits for reusable behavior. The documented behavior of interest is how Ceylon generic types interact with Java type erasure during interop, especially for collections and trait-based APIs.
The goal is to clarify the precise mapping rules the compiler applies when a Ceylon generic declaration is exposed to Java, and how null-safety is preserved across the boundary. Constraints include reduced community maintenance and potential limits in compatibility with newer JDK toolchains, which make current verification important for design decisions.
What is the documented mapping for a Ceylon generic trait with type parameters when accessed from Java code?
Does the compiler emit runtime type information or rely solely on erasure for interop calls, and how does that affect nullability checks?
Which interop scenarios are defined as supported versus undefined when Ceylon generic collections cross the Java boundary?
Ceylon generic traits are compiled to Java interfaces with all type parameters erased. The generated Java API exposes raw types (e.g., List instead of List<String>), and null-safety is communicated only through @Nullable/@NotNull annotations on method signatures. The compiler does not emit reified generic type information into class files; any runtime type reflection depends on the Ceylon runtime library. Supported interop is limited to scenarios where Java code treats the collection as a raw java.util.List with Object elements. Undefined behavior arises when Java code expects generic invariance, variance, or Ceylon-specific constraints that erasure cannot represent.
When a Ceylon generic trait (e.g., Sequence<Element>) is accessed from Java, the compiler produces a Java interface named Sequence with no type parameters. Methods that return or accept the type parameter become Object in the Java signature. For example, get(Integer index) → Element becomes Object get(int index). The Ceylon compiler may also generate bridge methods to preserve polymorphism within Ceylon code, but these are not visible to Java callers as generic signatures.
The Ceylon compiler relies solely on JVM type erasure for interop calls. No synthetic attributes (like Signature) are emitted to retain generic type arguments. Null-safety is preserved by annotating generated Java methods with @NotNull (for non-optional Ceylon types) or @Nullable (for optional types like Element?). However, these annotations are advisory; the Ceylon runtime still performs its own null checks on entry to Ceylon code. Java callers must treat any reference as potentially null unless they verify the annotation contract themselves.
List<String> to a Java method expecting java.util.List (raw) or java.util.List<Object>. Iteration and size() work without casts.Object; explicit casts to String are required and will succeed only if the Ceylon collection actually contains strings.String to the collection and expects Ceylon's static type system to prevent it. The Ceylon runtime may insert a check on the next Ceylon-side access, but the Java-side mutation is unchecked.out Element) across the boundary. Java sees only the raw type, so variance is lost.To confirm the exact mapping for your Ceylon version and JDK toolchain:
shared interface Box<Item> { shared formal Item item; }javap -verbose Box.class and inspect the Signature attribute (should be absent) and method descriptors (should show Object).Box<String> instance, calls item(), and observes the return type and nullability annotation via reflection.process.show() or the runtime Type API to compare the Ceylon-side reified type with Java reflection output.This answer assumes Ceylon 1.3.x (the last major release) targeting JDK 8–11. Newer JDK toolchains (17+) may change annotation processing or class-file validation. Reduced community maintenance means edge cases—especially around @Nullable annotation compatibility with JSR-305 vs. JetBrains/SpotBugs variants—are not well tested. The research flags that behavior may vary across compiler versions; the verification steps above are the only reliable way to confirm the current mapping for your toolchain.
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 18 Nov 2021, 13:36 UTC
When a Ceylon generic trait is compiled, the JVM erases its type parameters to Object at runtime, but the compiler also generates synthetic bridge methods annotated with @CeylonType that retain the original generic signature. If the Java caller has the Ceylon runtime annotations on its classpath, these bridges let Java see the trait as a parameterized type; otherwise Java perceives the trait as a raw type and the compiler emits unchecked warnings. Null‑safety checks remain the responsibility of Ceylon code – they are performed at compile time and by the Ceylon runtime when Ceylon methods are invoked, so plain Java calls bypass those checks unless they explicitly use Ceylon utilities like checkNotNull. Consequently, interop is well‑defined for concrete bounded types (e.g., List) where bridges can be generated, while scenarios involving wildcards, existential types, or bounds not expressible in Java generics fall into undefined behavior.