Constructors in Java classes work well when they are simple. With growing complexity, the code becomes less readable. It gets worse and worse until it reaches a point when Java itself says enough! and rejects it. Can you put complex logic to a constructor? Is a static factory method an alternative?
Basic constructor
The most simple and natural way of creating an object is to use a constructor like in the code below.
public class City {
private final Integer id;
private final String name;
public City(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
}
It only initializes fields. Usually, it is all that is expected from a constructor.
A little bit of some logic is not a problem either.
public class City {
private final Integer id;
private final String name;
public City(Integer id, String name) {
this.id = id;
this.name = name == null ? "" : name;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
}
You can even create other objects and call their methods. Multiple options are possible.
Inheritance and constructors
Even when one class extends to another, apparently everything is fine. Java provides us with a way to call a constructor from the parent class using the super()
method.
public class AppException extends Exception {
public AppException(int errorCode) {
super("Application Exception: errorCode=" + errorCode);
}
}
Unfortunately, the super(
) method comes with limitations.
For example, I would like to build the exception message depending on the errorCode
value. Like in the below code:
public class AppException extends Exception {
public AppException(int errorCode) {
String msg;
switch (errorCode) {
case 1:
msg = "User not found";
case 2:
msg = "Malformed request";
default:
msg = "Unknown error";
}
super(msg);
}
}
Although it seems pretty straight forward, it does not work. The compiler protests saying Call to 'super()' must be first statement in constructor body. The thing is that Java syntax does not allow any statements before calling the super()
method. Of course, in this particular case, a workaround is simple:
public class AppException extends Exception {
public AppException(int errorCode) {
super(buildMessage(errorCode));
}
private static String buildMessage(int errorCode) {
switch (errorCode) {
case 1:
return "User not found";
case 2:
return "Malformed request";
default:
return "Unknown error";
}
}
}
A private static method solves the problem. This problem. The next one will not be that easy.
Choosing the right parent constructor
Imagine AppException
from the above examples, but this time cause
must be set only when error code is 1. The cause is handled in the parent class - Exception, so a proper Exception constructor should be used. If there were no constructor limitations, the following code would do the job.
public class AppException extends Exception {
public AppException(int errorCode, Throwable cause) {
if (errorCode == 1) {
super(buildMessage(errorCode));
} else {
super(buildMessage(errorCode), cause);
}
}
private static String buildMessage(int errorCode) {
switch (errorCode) {
case 1:
return "User not found";
case 2:
return "Malformed request";
default:
return "Unknown error";
}
}
}
However, for the already known reasons, the compiler will not allow it - the super()
method must be the first statement in the constructor, so even a simple if
statement is not allowed.
Static factory method instead of constructor
Such a case is a good candidate to use a static factory method instead of a limited constructor. First, the constructor can be hidden (a private access modifier). Second, to avoid the if
statement in the constructor, we can have two constructors: one with an error code as a parameter and another one with an error code and a cause. And the whole if
statement with calling the constructors can be moved to a static method that creates an AppException
object based on the provided parameters.
public class AppException extends Exception {
public static AppException create(int errorCode, Throwable cause) {
if (errorCode == 1) {
return new AppException(errorCode);
} else {
return new AppException(errorCode, cause);
}
}
private AppException(int errorCode) {
super(buildMessage(errorCode));
}
private AppException(int errorCode, Throwable cause) {
super(buildMessage(errorCode), cause);
}
private static String buildMessage(int errorCode) {
switch (errorCode) {
case 1:
return "User not found";
case 2:
return "Malformed request";
default:
return "Unknown error";
}
}
}
As the new method is just a static method, not a constructor, the constructor limitations do not apply to it. It may contain a complex logic before any object is instantiated.
Instead of calling a constructor with the new keyword, the object should be created by calling the static method:
AppException.create(2, exc);
Even though the constructors are best solutions in most cases, remember that static factory methods can help you out in more complex situations.