In The Amber Room: Even More Java Plans for 2025 - JVM Weekly vol. 117
Today - Project Amber. And Kotlin - a lot of Kotlin.
1. Glancing into the Future Part 2: Project Amber
And here we have a sequel to the release from two editions ago. Nicolai Parlog, Java Developer Advocate, has published the promised video outlining the changes in Project Amber.
Similar to its predecessor, the video highlights the future directions of Java development for 2025, but this time it focuses on features introduced under Project Amber. Four main features currently in preview phase are discussed: Flexible Constructor Bodies, Simple Source Files and Instance Main Methods (with a twist we’ll get to shortly), Module Imports, and Primitive Patterns. Nicolai's video explores how these improvements will impact developer productivity, particularly by offering greater flexibility in creating constructors and simplifying project imports management.
The second part of the video covers upcoming explorations of new features that may be formalized through future JEP proposals. A key element is decomposition—extending current pattern-matching capabilities beyond records. The video also introduces the concept of "withers," enabling easy assignment of new values when creating object copies ( Jonathan Vila 🥑 ☕️ once described this concept brilliantly in his article Builders, Withers, and Records - Java’s Path to Immutability). Additional topics include Deconstruction on Assignment, Custom Patterns, the potential return of String Templates, and Stable Values - lazy initialization treated as final by the JVM (more on that in a bit).
As you see, a lot. It will be a great use of your 8 minutes of life, trust me.
As I mentioned we’d return to Simple Source Files and Instance Main Methods — on the mailing list of the Amber project, Gavin Bierman announced plans to finalize the "on-ramp" initiative in JDK 25.
Regular readers may be familiar with this term—it refers to initiatives aimed at simplifying the learning process for beginners by removing unnecessary syntax and conceptual barriers that can be overwhelming in the early stages of learning the language. As part of this initiative, a class named IO has been introduced, providing basic input and output methods like println, print, readln, and read.
Initially, these methods were planned to be automatically imported in simple programs, allowing their direct use without qualified names. However, to ensure a smoother transition from simple to more advanced programs, automatic static import was abandoned. Instead, the IO class was moved to the java.lang package, so its methods can be accessed via qualified names, like IO.println("Hello, world");.
But that’s not all - IO is set for further modifications. Currently, its methods are thin wrappers around the Console class, but the developers plan to base them on System.out and System.in to better align with developer habits. They do not intend, however, to add more complex functions like readInt in JDK 25, reasoning that data conversion and error handling are separate concerns that could be addressed in future releases but aren’t essential right away.
Additionally, the specification plans to replace the term "simple compilation unit" with "compact compilation unit" for more precise terminology. I have some reservations about this — "compact" doesn’t evoke the image of "writing first programs" and instead implies knowledge of a "non-compact" mode. But I understand the aversion to the term "simple," which my experience tells me is often seen as value-laden: if we like technologies we call them "simple, but it does not bring any true information about the properties of the given thing.
Speaking of future projects, a lot of love has recently gone to JEP 502: Stable Values (Preview), whose proposal was updated in January. But before delving into details, first, let’s dive into a bit of context.
In the modern Java ecosystem, developers often face a choice between early initialization of fields marked as final (which can slow down application startup or force unnecessary object creation) and dropping final in favor of mutable fields (which complicates code, hinders thread safety, and prevents compiler optimizations tied to immutability).
Traditional solutions, like the "class-holder idiom":
class OrderController {
public static Logger getLogger() {
class Holder {
private static final Logger LOGGER = Logger.create(...);
}
return Holder.LOGGER;
}
}
or double-checked locking:
class OrderController {
private volatile Logger logger;
public Logger getLogger() {
Logger v = logger;
if (v == null) {
synchronized (this) {
v = logger;
if (v == null) {
logger = v = Logger.create(...);
}
}
}
return v;
}
}
are often cumbersome, complex, and prone to errors. I think that just looking at the above examples makes my brain cry a little.
JEP 502 addresses this problem by introducing the concept of Stable Values, which combines the benefits of lazy initialization with immutability and the optimizations associated with final fields. The new API allows for creating a StableValue<T> object that initially remains "unset" (uninitialized) and is assigned a value exactly once.
According to the JEP:
class OrderController {
// OLD:
// private Logger logger = null;
// NEW:
private final StableValue<Logger> logger = StableValue.ofUnset();
Logger getLogger() {
return logger.orElseSet(() -> Logger.create(OrderController.class));
}
void submitOrder(User user, List<Product> products) {
getLogger().info("order started");
...
getLogger().info("order submitted");
}
}
Once set, it becomes immutable, allowing the JVM to treat it as final and apply optimizations like eliminating unnecessary reads or inlining expressions. The key benefit is that initialization can occur at any chosen moment—when the value is actually needed rather than during object construction or class initialization—leading to faster application startup and more flexible object creation.
Kotlin users may immediately recall the lateinit mechanism, but it does not offer lazy initialization or thread safety. Instead, Stable Values are more akin to Kotlin’s less popular by lazy delegation:
val message: String by lazy {
println("Initializing 'message'")
"Hello, world!"
}
The main difference is that Stable Values in Java are introduced as part of the standard library and are tightly integrated with the JVM, allowing for the aforementioned compiler and runtime optimizations. In contrast, Kotlin’s by lazy mechanism is part of the language and offers similar functionality but may not take advantage of the same JVM optimizations available through JEP 502 — at least until it is potentially internally refactored to do so under the hood.
2. SOLID Kotlin - and Its Implementations
Speaking of Kotlin, I was surprised by the simultaneous publication of several articles about SOLID principles in this language. SOLID is a set of five design principles aimed at creating clean, maintainable, and scalable object-oriented code by properly distributing responsibilities among classes. It’s a term that seems to have lost some of its luster - often most memorized (though typically in a shallow, uncritical manner) by those attending job interviews. Thus, the emergence of articles devoted to it naturally caught my attention.
The topic was taken up by the publication ITNext.io, where Ioannis Anifantakis first covered LSP (Liskov Substitution Principle), followed by SRP (Single Responsibility Principle) just three days later.
There’s already been a lot written about SOLID (some speakers have built entire literary and training careers around it), but the Ioannis articles offer an interesting perspective on applying these principles in Kotlin, illustrating them with practical examples and language-specific insights to provide readers with actionable guidance and correct implementation examples.
For example, the SRP article highlights that each class should have only one reason to change, using the example of an OTPSender class that shouldn’t handle both sending OTP codes and their validation. The author suggests splitting it into two classes: OTPSender for sending and OTPValidator for validation, resulting in more modular and maintainable code. While this advice might sound trivial when described this way, Ioannis Anifantakis shows how Kotlin’s specific mechanisms support this design.
The LSP article discusses the principle that subclass objects should be able to replace base class objects without affecting the program’s correctness (you can thank me for this definition during your next job interview). The author uses the example of a Bird class with a fly() method and a problematic Penguin subclass that shouldn’t inherit flying capabilities. The solution involves introducing a Flyable interface, implemented only by birds that can actually fly.
Interestingly, this isn’t the only recent series of articles about SOLID (including Kotlin) that appeared in late January.
Christian Ekrem published a series dedicated to SOLID, with each article using a different programming language to demonstrate one of the SOLID principles. The Kotlin-focused episode, Liskov Substitution: The Real Meaning of Inheritance, illustrates the classic rectangle-square problem: mathematically, a square is a rectangle, but in implementation, inheriting a Square class from a Rectangle can lead to unexpected behavior when width and height setters violate the expectations for rectangles. Instead of inheritance, the author suggests the more LSP-compliant approach of using composition and interfaces.
After this introductory discussion, the article presents a practical example of payment processing, where different payment processors (CreditCardProcessor, DebitCardProcessor) implement the same PaymentProcessor interface. The author highlights common but subtle LSP violations, such as throwing unexpected exceptions or returning null where the base type does not expect it. To ensure LSP compliance, he recommends using contract tests and documenting preconditions and postconditions for methods.
But that’s not the only Kotlin-related publication from this author. In the article A Use Case for UseCases in Kotlin published on January 31, 2025, he shares his experience with the UseCase class during a job interview (BTW: congratulations on landing the new position, Christian! I hope this won’t affect your writing productivity). Initially, Christian considered this construct overly complex and reminiscent of outdated Java practices. However, upon further analysis, he realized that UseCase classes play a crucial role in Clean Architecture in the codebase he was working on, representing application-specific business rules and promoting clear separation of concerns. It’s a short but insightful analysis of a specific code fragment on an isolated use case.
3. On Performing a Java-to-Kotlin Migration at the Facebook Scale
Finally, since this edition turned out to be quite Java/Kotlin-heavy, it’s time for the last topic, which has been sitting in my archive for a while, waiting for the right moment to share it with you.
In December 2024, Meta announced in the article Translating Java to Kotlin at Scale by Jocelyn Luizzi , Jingbo Yang , Eve Matthaey that they had reached an advanced stage in migrating their Android codebase from Java to Kotlin, with over half of approximately ten million lines of code already converted. The decision to pursue full conversion, rather than just writing new code in Kotlin, stemmed from the desire to maximize developer productivity benefits and improve null safety. Leaving a significant portion of the code in Java could have caused null-related issues and required maintaining parallel tooling for both languages, further slowing down compilation processes, especially at Facebook’s scale.
To tackle the challenges of such a large migration, Meta developed a tool called Kotlinator (love that name), which automates the conversion process. The tool’s workflow, as described in the article, consists of six stages: deep compilation for symbol resolution, pre-processing with the Editus tool (kill me, couldn't find anything about this one), running JetBrains J2K (Java-to-Kotlin Converter) in headless mode, post-processing, compilation to detect errors, and automatic commit of changes.
The article also outlines challenges the team faced during the migration, such as handling complex code transformation cases and integration with custom DI frameworks. To address these problems, Meta collaborated with other companies to identify and resolve numerous edge cases. Thanks to these efforts, Meta not only accelerated its migration process but also contributed to improving tools and practices related to code conversion to Kotlin across the industry — thanks to publications like this, we all feel a bit of that BigTech envy.
If you don’t have Facebook-scale projects but do have cases requiring large-scale refactoring, I currently face similar challenges and would like to recommend two tools. One is OpenRewrite by Moderne , which offers a wide variety of recipes for automated refactoring. The other is Renovate, which handles automatic dependency updates quite well. They might come in handy for you too.
PS: I couldn't resist: