45. Exemplify the initialization-on-demand holder design pattern
Before we tackle the solution of implementing the initialization-on-demand holder design pattern, let’s quickly recap a few ingredients of this solution.
Static vs. non-static blocks
In Java, we can have initialization non-static blocks and static blocks. An initialization non-static block (or simply, a non-static block) is automatically called every single time we instantiate the class. On the other hand, an initialization static block (or simply, a static block) is called a single time when the class itself is initialized. No matter how many subsequent instances of that class we create, the static block will never get executed again. In code lines:
public class A {
{
System.out.println("Non-static initializer ...");
}
static {
System.out.println("Static initializer ...");
}
}
Next, let’s run the following test code to create three instances of A
:
A a1 = new A();
A a2 = new A();
A a3 = new A();
The output reveals that the static initializer is called only once, while the non-static initializer is called three times:
Static initializer ...
Non-static initializer ...
Non-static initializer ...
Non-static initializer ...
Moreover, the static initializer is called before the non-static one. Next, let’s talk about nested classes.
Nested classes
Let’s look at a quick example:
public class A {
private static class B { ... }
}
Nested classes can be static or non-static. A non-static nested class is referred to as an inner class; further, it can be a local inner class (declared in a method) or an anonymous inner class (class with no name). On the other hand, a nested class that is declared static is referred to as a static nested class. The following figure clarifies these statements:
Figure 2.28: Java nested classes
Since B
is a static class declared in A
, we say that B
is a static nested class.
Tackling the initialization-on-demand holder design pattern
The initialization-on-demand holder design pattern refers to a thread-safe lazy-loaded singleton (single instance) implementation. Before JDK 16, we can exemplify this design pattern in code as follows (we want a single thread-safe instance of Connection
):
public class Connection { // singleton
private Connection() {
}
private static class LazyConnection { // holder
static final Connection INSTANCE = new Connection();
static {
System.out.println("Initializing connection ..."
+ INSTANCE);
}
}
public static Connection get() {
return LazyConnection.INSTANCE;
}
}
No matter how many times a thread (multiple threads) calls Connection.get()
, we always get the same instance of Connection
. This is the instance created when we called get()
for the first time (first thread), and Java has initialized the LazyConnection
class and its statics. In other words, if we never call get()
, then the LazyConnection
class and its statics are never initialized (this is why we name it lazy initialization). And, this is thread-safe because static initializers can be constructed (here, INSTANCE
) and referenced without explicit synchronization since they are run before any thread can use the class (here, LazyConnection
).
JDK 16+
Until JDK 16, an inner class could contain static members as constant variables but it couldn’t contain static initializers. In other words, the following code would not compile because of the static initializer:
public class A {
public class B {
{
System.out.println("Non-static initializer ...");
}
static {
System.out.println("Static initializer ...");
}
}
}
But, starting with JDK 16, the previous code is compiled without issues. In other words, starting with JDK 16, Java inner classes can have static members and static initializers.
This allows us to tackle the initialization-on-demand holder design pattern from another angle. We can replace the static nested class, LazyConnection
, with a local inner class as follows:
public class Connection { // singleton
private Connection() {
}
public static Connection get() {
class LazyConnection { // holder
static final Connection INSTANCE = new Connection();
static {
System.out.println("Initializing connection ..."
+ INSTANCE);
}
}
return LazyConnection.INSTANCE;
}
}
Now, the LazyConnection
is visible only in its containing method, get()
. As long as we don’t call the get()
method, the connection will not be initialized.