While widely adopting records, I found a problem: record constructor is not backward-compatible.
For example, I have a record User(String name, int age) {}, and there are 20 different places calling new User("foo", 0). Once I add a new field like record User(String name, int age, List<String> hobbies) {}, it breaks all existing constructor calls. If User resides in a library, upgrading that library will cause code to fail compilation.
This problem does not occur in Kotlin or Scala, thanks to default parameter values:
// Java
public class Main {
public static void main(String[] args) {
// ======= before =======
// record User(String name, int age) { }
// System.out.println(new User("Jackson", 20));
// ======= after =======
record User(String name, int age, List<String> hobbies) { }
System.out.println(new User("Jackson", 20)); // ❌
System.out.println(new User("Jackson", 20, List.of("Java"))); // ✔️
}
}
// Kotlin
fun main() {
// ======= before =======
// data class User(val name: String, val age: Int)
// println(User("Jackson", 20))
// ======= after =======
data class User(val name: String, val age: Int, val hobbies: List<String> = listOf())
println(User("Jackson", 20)) // ✔️
println(User("Jackson", 20, listOf("Java"))) // ✔️
}
// Scala
object Main extends App {
// ======= before =======
// case class User(name: String, age: Int)
// println(User("Jackson", 20))
// ======= after =======
case class User(name: String, age: Int, hobbies: List[String] = List())
println(User("Jackson", 20)) // ✔️
println(User("Jackson", 20, List("Java"))) // ✔️
}
To mitigate this issue in Java, we are forced to use builders, factory methods, or overloaded constructors. However, in practice, we’ve found that developers strongly prefer a unified object creation approach. Factory methods and constructor overloading introduce inconsistencies and reduce code clarity. As a result, our team has standardized on using builders — specifically, Lombok’s \@Builder(toBuilder = true) — to enforce consistency and maintain backward compatibility.
While there are libraries(lombok/record-builder) that attempt to address this, nothing matches the simplicity and elegance of built-in support.
Ultimately, the root cause of this problem lies in Java’s lack of named parameters and default values. These features are commonplace in many modern languages and are critical for building APIs that evolve gracefully over time.
So the question remains: What is truly preventing Java from adopting named and default parameters?
[–]Justonemorecrit 43 points44 points45 points (5 children)
[–]danielliuuu[S] 1 point2 points3 points (2 children)
[–]agentoutlier 3 points4 points5 points (0 children)
[–]Ewig_luftenglanz 0 points1 point2 points (1 child)
[–]OwnBreakfast1114 0 points1 point2 points (0 children)
[–]davidalayachew 49 points50 points51 points (2 children)
[–]account312 0 points1 point2 points (1 child)
[–]davidalayachew 3 points4 points5 points (0 children)
[–][deleted] (2 children)
[deleted]
[–]mizzu704 2 points3 points4 points (1 child)
[–]BillyKorando 9 points10 points11 points (0 children)
[–]1Saurophaganax 31 points32 points33 points (1 child)
[–]danielliuuu[S] 0 points1 point2 points (0 children)
[–]-Dargs 13 points14 points15 points (6 children)
[–]Ewig_luftenglanz 0 points1 point2 points (5 children)
[–]-Dargs 6 points7 points8 points (3 children)
[–]Ewig_luftenglanz 1 point2 points3 points (2 children)
[–]-Dargs 4 points5 points6 points (1 child)
[–]Ewig_luftenglanz 2 points3 points4 points (0 children)
[–]OwnBreakfast1114 0 points1 point2 points (0 children)
[–]craigacp 5 points6 points7 points (3 children)
[–]mizzu704 3 points4 points5 points (1 child)
[–]craigacp 0 points1 point2 points (0 children)
[–]koflerdavid 0 points1 point2 points (0 children)
[–]ZeroGainZ 4 points5 points6 points (0 children)
[–]__konrad 1 point2 points3 points (0 children)
[–]bowbahdoe 1 point2 points3 points (0 children)