Noargsconstructors and setters are outdated, 90's style old school Java. They needlessly allow entire categories of defects that are easily avoided by using only allargsconstructors and no setters. Please stop writing code like that.
How many times have you come across (or written) code like this
public class User {
private String email;
private String username;
//either no constructor (= Java automatically adds default noargsconstructor)
//OR explicit noargsconstructor public User (){}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
//maybe some builder too, who knows
}
or using Lombok or whatever
@Data
//or
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@Builder
//or whatever other hodgepodge of annotations
public class User {
private String email;
private String username;
}
(NOTE the User example class is a "data" class, but that's besides the point - it doesn't matter if it's a data class or a Service class or a Resource class or a Controller class or DAO class or what have you)
You've probably come across this more times than you can count, even in fresh code, right?
Briefly, it 1) needlessly allows instantiating objects in an invalid state, and 2) needlessly allows mutation.
Consider
User user = new User();
What does this even mean? This object doesn't even have its most fundamental attributes. What possible use could any part of any system have for such an object? Why even instantiate and pass such an object around, anywhere? It's completely nonsensical, let alone that it's begging to cause problems like NullPointerException etc. It doesn't matter if you immediately after set the attributes - just instantiate the object in a complete, valid state to begin with instead.
Unless you have extremely good reason to make objects of a class mutable, by default, your classes should be written such that objects are immutable. This is especially true for this User example - once you get users from whatever is the source of truth for users, you MUST NOT MUTATE THEM, or even allow the POSSIBILITY of mutating them, in ANY way. Otherwise, your code could somehow end up changing (or removing) a user object's email or whatever before passing it on somewhere else. That would be extremely dangerous!
Even when you're dealing with less "important" objects, making them immutable is still beneficial. It keeps your classes shorter and less complex and it makes it a lot easier to debug and reason about the code when you know an object in a given state won't suddenly do a rug pull on you and change state while being passed through the system. It also enables safe parallelization, thread safety and any number of other more "technical" advantages.
These are just some of the most common ones I see, but I'm sure more could be thought of.
The most common style of builder makes use of and relies on noargsconstructors and setters. So if we want to use a builder, that makes it OK, right?
No! A builder doesn't make the problems just go away, nor does it justify allowing them.
Taking a step back, why do you need a builder? What's wrong with using the allargsconstructor? If your class is well designed, intantiating objects is not complicated.
User user = new User("foo@bar.com", "foobar");
// ^^^ this is not complicated, and there is no world
// in which adding a builder to do this is necessary
If you need a builder because instantiating objects is complicated because your class has a lot of attributes and can be in any number of different states depending on which attributes are set and which are not etc etc, that is a smell that your class is poorly designed - fix that first before adding a builder.
I happily admit builders do have one advantage even in well-designed classes: they name the attributes you're sending in, so it's less likely you'll send in the email as the username and vice versa. For that reason, I'm actually not entirely opposed to builders, but I will point out that 1) our IDE's kind of do this for us (but that doesn't help when we're in GitHub etc) and 2) overuse of classes like String for things like email and username etc is actually a problem all of it's own - primitive obsession, which is beyond the scope of this post, but a big source of errors all of its own. (briefly, rather than using String for things like email and username, make actual classes instead, such that new User(new Email("foo@bar.com"), new Username("foobar"))
)
Anyway, if you really, really need or want a builder, there are builder implementations that don't make use of or rely on noargsconstructors and setters - use them instead. (see eg immutables/immutables#438)
Jackson or some other tool requiring a noargsconstructor doesn't justify adding one - there is almost always a way to enable the tool to use the allargs, here's an example of a class from one of our repos
@JsonIgnoreProperties(ignoreUnknown = true)
public class User {
private final String userId;
private final String username;
//...
@JsonCreator
public User(
@JsonProperty("userId") final String userId,
@JsonProperty("username") final String username
//...
) {
this.userId = userId;
this.username = username;
//...
}
public String getUserId() {
return userId;
}
public String getUsername() {
return username;
}
//...
}
Maybe you carefully code review to make sure there are no bugs resulting from objects being in invalid states, or objects not mutating in undesirable ways. But... why not just not code in a way that the compiler will not allow them, and you won't have to (hopefully) catch them in code review? You're not being paid to be a compiler! Code review is supposed to focus on more important things, not trivial bugs resulting from needlessly error allowing implementation details. Code review can never be guaranteed to catch all resulting bugs, but the compiler can.
I don't mean to be uncharitable, but frankly, if your coding standards require you to, by default, allow instantiating objects in states that are demonstrably invalid, and allow mutating objects that must not be mutated, then your coding standards and how you’ve always done things are wrong, and you can and should change them.
Sometimes, objects have to be instantiated in an incomplete stage at one point in the system so they can be passed around to other parts that add more values before reaching some end complete state. That's fine! But then, make that explicit - make the end methods accept only complete objects of a complete class, and make methods that deal with the objects before they're complete deal with objects that are clearly explicitly incomplete.
public class IncompleteUser {
private String email;
//doesn't have username yet
}
public class User {
private String email;
private String username;
}
Sure! Despite what it may seem, I’m actually not making some fundamentalist argument that absolutely everything must always be immutable and mutability is never desirable or permissible. I'm simply saying, rather than by default always allowing instantiating objects in an invalid state and mutation, instead, by default, your classes should not allow mutation and instantiating objects in an invalid state.
No, it's not. People like Josh Bloch and Brian Goetz have been talking about this for decades, and there's a reason why not only Java but other languages like Kotlin, Rust etc have moved in this direction with data classes, records etc. Get with the times.
Endless articles, books etc bring up this very basic thing, here are just a few:
- Effective Java by Josh Bloch https://www.oracle.com/java/technologies/effectivejava.html
- Josh Bloch QA https://www.oracle.com/technical-resources/articles/javase/bloch-effective-08-qa.html
- Brian Goetz QA https://www.youtube.com/watch?v=ZyTH8uCziI4
- Another Brian Goetz QA https://blogs.oracle.com/javamagazine/post/what-are-they-buildingand-why-6-questions-for-the-top-java-architects
- Kotlin avoids entire categories of Java defects https://archive.md/kUA0f
Yes! Should mentioned that the world's most popular language Python has added data class in its latest version. Looking forward to starting using that!
Edit: No it's in 3.7? Why is my team not using it already? (Just switched over to Python) :D