eclipse-jdt / eclipse-jdt/eclipse.jdt.core
ECJ synthesizes bridge method referencing java.util.SequencedMap when compiling -8/-source 8 on a JDK 21+ host, unlike javac
- Dominant language
- Java
- Stars
- 237
- Forks
- 195
- Avg merge
- 1d 10h
- Merged PRs (30d)
- 49
Description
When compiling a class that implements `java.util.SortedMap` (without overriding `reversed()`) at `-source 8 -target 8` (or `-8`) while the compiler itself is hosted on a JDK 21+ runtime, **without** `--release`/an explicit bootclasspath restriction, ECJ 3.46.200 synthesizes an extra bridge method:
```
public java.util.SequencedMap reversed();
aload_0
invokeinterface java/util/SortedMap.reversed:()Ljava/util/SortedMap;
areturn
```
`java.util.SequencedMap` does not exist before JDK 21, so any class file emitted in Java-8 format (major version 52) containing this method fails with `ClassNotFoundException`/`NoClassDefFoundError: java/util/SequencedMap` the moment `reversed()` is invoked (or at verification time, depending on the VM) on any JDK 8-20 runtime.
**Importantly, `javac` given the exact same flags on the exact same JDK 21 host does *not* synthesize this bridge.** It correctly omits it and produces a clean Java-8-compatible class. This shows the behavior is an ECJ-specific inconsistency, not merely an unavoidable consequence of compiling `-source 8` without `--release` on a newer JDK.
### Real-world impact
This was discovered via `org.apache.felix.resolver.util.OpenHashMap` (vendored in Eclipse Equinox's `org.eclipse.osgi` bundle), which implements `SortedMap` without overriding `reversed()`. Built by Tycho's `tycho-compiler-plugin` with `useJDK=BREE` (falls back to running JVM's API when no explicit `--release`/bootclasspath is set) on a JDK 21 build host, the resulting `org.eclipse.osgi` jar throws `ClassNotFoundException: java.util.SequencedMap` at runtime under JDK 17, breaking OSGi TCK test container startup. See https://github.com/eclipse-equinox/equinox/pull/295 for context.
### Standalone reproducer (no Maven/Eclipse needed)
A copy of the source / script is inline below; you can also download `org.eclipse.jdt.core.compiler.batch` from Maven Central (`org.eclipse.jdt:ecj`, matching build version) to compile MyMap.java with ECJ directly.
`MyMap.java`:
```java
import java.util.AbstractMap;
import java.util.Comparator;
import java.util.SortedMap;
import java.util.Set;
public class MyMap extends AbstractMap implements SortedMap {
public Comparator comparator() { return null; }
public SortedMap subMap(K fromKey, K toKey) { return null; }
public SortedMap headMap(K toKey) { return null; }
public SortedMap tailMap(K fromKey) { return null; }
public K firstKey() { return null; }
public K lastKey() { return null; }
public Set> entrySet() { return null; }
}
```
Compile with javac (JDK 21, no `--release`):
```
javac -source 8 -target 8 -nowarn -d out-javac MyMap.java
javap -p out-javac/MyMap.class
```
→ no `reversed()` method present at all, clean.
Compile with ECJ (same version Tycho uses, `org.eclipse.jdt.core.compiler.batch-3.46.200`):
```
java -jar org.eclipse.jdt.core.compiler.batch-3.46.200.v20260907-1115.jar -8 -nowarn -d out-ecj MyMap.java
javap -p -c out-ecj/MyMap.class
```
→
```
public java.util.SequencedMap reversed();
Code:
0: aload_0
1: invokeinterface #33, 1 // InterfaceMethod java/util/SortedMap.reversed:()Ljava/util/SortedMap;
6: areturn
```
### Expected behavior
ECJ should either:
- not synthesize this bridge at all when the effective target/source level is below 21 (matching javac's behavior), or
- refuse to compile / emit a clear error/warning if it cannot guarantee `-source`/`-target 8` compatibility without a bootclasspath restriction.
### Proposed test
I've drafted a regression test (`SortedMapReversedBridgeTest`) for `org.eclipse.jdt.core.tests.compiler`, see PR (linked below).
### Environment
- ECJ version: `3.46.200.v20260907-1115`
- Host JDK: 21 (Temurin)
- Reproduced with plain command-line ECJ, no Maven/Eclipse/Tycho involved.
Contributor guide
Assessment
This issue has not been assessed yet.