# 11. Recap: the Java 26 changes you skipped Prev: [10. Other 27 changes](10-other-27-changes.md) · Next: [12. Version lanes 25 - 29](12-lanes-25-to-29.md) Java 26 (GA 2026-03-17) is also a non-LTS release. If you are on 25 and going to 27 you meet all of it at once. Script: [`scripts/recap26.sh`](../scripts/recap26.sh). ## JEP 500: final means final Reflection has always let `Field.setAccessible(true)` followed by `Field.setInt(...)` overwrite a `final` field. Source: [`FinalFieldMutation`](../recap26/src/FinalFieldMutation.java), transcript [70](output/70-final-field-mutation.txt). | Run | Result | |---|---| | JDK 25 | silent | | JDK 26 default | three `WARNING:` lines, then it proceeds | | JDK 26 `--enable-final-field-mutation=ALL-UNNAMED` | silent (the opt-in the warning names) | | JDK 26 `--illegal-final-field-mutation=deny` | `IllegalAccessException`: "... is not allowed to mutate final fields" | | JDK 26 `--illegal-final-field-mutation=debug` | the warning plus the stack trace of the offending call | `--illegal-final-field-mutation` takes `allow`, `warn`, `debug` or `deny`. A JFR event, `jdk.FinalFieldMutation`, is recorded with default settings whenever a mutation is *permitted* (one event, with the declaring class and field name); under `deny` there is nothing to record. Even when it works it misleads: the field reads 9090 but `port()` still returns 8080, because `final int port = 8080` is a constant variable and javac copied the literal into the method. Who this hits: serialisation libraries, mocking frameworks, dependency-injection containers and test code that set `final` fields. Run your test suite once with `deny` on 26 or later. ## JEP 504: the Applet API is gone `java.applet.*`, `javax.swing.JApplet` and the `AppletInitializer` overload of `java.beans.Beans.instantiate` were removed ([52](output/52-api-diff-25-to-26.txt)). [`AppletGone.java`](../recap26/src/broken/AppletGone.java) compiles on 25 with a `[removal]` warning and fails on 26 with `package java.applet does not exist` ([73](output/73-removed-in-26.txt)). In the same transcript, [`ThreadStopGone.java`](../recap26/src/broken/ThreadStopGone.java): `Thread.stop()` (which has thrown `UnsupportedOperationException` since 20) no longer exists at all: `cannot find symbol`. ## JEP 516: AOT object caching with any GC The AOT cache (JEP 483 / 514 / 515 in 24 and 25) stores pre-parsed and pre-linked classes and, in some configurations, pre-built heap objects such as the module graph. Until 26 the heap-object part needed G1. Workflow used here: `-XX:AOTMode=record -XX:AOTConfiguration=...`, then `-XX:AOTMode=create -XX:AOTCache=...`, then run with `-XX:AOTCache=...`. What the JVM's own log says ([71](output/71-aot-cache-matrix.txt)): | Cache built under | Run under | JDK 25 | JDK 26 / 27 | |---|---|---|---| | G1 | G1 or Serial | heap objects used | heap objects used | | G1 | ZGC | cache refused | cache refused | | ZGC | ZGC | cache used, **heap objects not used** | **heap objects used** | | ZGC | G1 or Serial | cache refused | cache refused | So "any GC" means *each* GC can now benefit, not that one cache serves every GC: the cache records whether compressed oops were on (`The saved state of UseCompressedOops (1) is different from runtime (0), CDS will be disabled`), and ZGC does not use them. Build the cache under the collector you deploy with. Startup, median of 9 runs, indicative only ([72](output/72-aot-startup.txt)): with ZGC, JDK 25 barely moves (about 83 -> 81 ms, because no heap objects are used), while JDK 26 and 27 go from about 62 to about 40 ms; with G1 all three go from about 60 to about 35-37 ms. A 27 JVM has a new diagnostic flag `AOTCompatibleOopCompression`; setting it at cache creation did not make a G1-built cache load under ZGC in one attempt, and nothing more was investigated. ## JEP 517: HTTP/3 in the HTTP client `HttpClient.Version.HTTP_3`, `HttpOption.H3_DISCOVERY` and `HttpOption.Http3DiscoveryMode` (`ANY`, `ALT_SVC`, `HTTP_3_URI_ONLY`) are the API surface ([75](output/75-http3-fallback.txt), from `javap`). The sandbox has no UDP egress, so no real HTTP/3 exchange was run. What [`Http3Fallback`](../recap26/src/Http3Fallback.java) shows instead: a client that *prefers* HTTP/3 against a TLS server that only speaks HTTP/1.1 falls back and `response.version()` says `HTTP_1_1`; a request pinned to `HTTP_3_URI_ONLY` fails with `HttpConnectTimeoutException: quic handshake timeout`. ## JEP 522: G1 throughput "Improve throughput by reducing synchronization between application and GC threads." Not benchmarked here; it is a claim from the JEP, and it is the reason 26 is a fairer G1 baseline than 25 in [chapter 2](02-g1-default.md). ## New public API without a JEP Verified by javap diff ([52](output/52-api-diff-25-to-26.txt)) and asserted in [`Api26`](../recap26/src/Api26.java) ([74](output/74-api26.txt)): `String.equalsFoldCase`, `compareToFoldCase` and `UNICODE_CASEFOLD_ORDER` (`"STRASSE".equalsFoldCase("straße")` is true; `equalsIgnoreCase` says false), `UUID.ofEpochMillis` (a version-7 UUID for a given instant), `Process` implementing `Closeable`, `MemoryMXBean.getTotalGcCpuTime()`, `ByteOrder` becoming an enum, `HPKEParameterSpec` (Hybrid Public Key Encryption), and the Unicode 17 blocks in `Character.UnicodeBlock`. Prev: [10. Other 27 changes](10-other-27-changes.md) · Next: [12. Version lanes 25 - 29](12-lanes-25-to-29.md)