Java Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Introduction

Design Patterns are common patterns that we use when writing our software. They represent established best practices developed over time. These can then help us to ensure that our code is well designed and well built.

Creational Patterns are design patterns that focus on how we obtain instances of objects. Typically, this means how we construct new instances of a class, but in some cases, it means obtaining an already constructed instance ready for us to use.

In this article, we're going to revisit some common creational design patterns. We'll see what they look like and where to find them within the JVM or other core libraries.

2. Factory Method

The Factory Method pattern is a way for us to separate out the construction of an instance from the class we are constructing. This is so we can abstract away the exact type, allowing our client code to instead work in terms of interfaces or abstract classes:

class SomeImplementation implements SomeInterface {
    // ...
}
public class SomeInterfaceFactory {
    public SomeInterface newInstance() {
        return new SomeImplementation();
    }
}

Here, our client code never needs to know about SomeImplementation, and instead, it works in terms of SomeInterface. Even more than this, though, we can change the type returned from our factory and the client code needn't change. This can even include dynamically selecting the type at runtime.

2.1. Examples in the JVM

Possibly the most well-known examples of this pattern the JVM are the collection building methods on the Collections class, like singleton(), singletonList(), and singletonMap(). These all return instances of the appropriate collection – Set, List, or Map – but the exact type is irrelevant. Additionally, the Stream.of() method and the new Set.of(), List.of(), and Map.ofEntries() methods allow us to do the same with larger collections.

There are plenty of other examples of this as well, including Charset.forName(), which will return a different instance of the Charset class depending on the name asked for, and ResourceBundle.getBundle(), which will load a different resource bundle depending on the name provided.

Not all of these need to provide different instances, either. Some are just abstractions to hide inner workings. For example, Calendar.getInstance() and NumberFormat.getInstance() always return the same instance, but the exact details are irrelevant to the client code.

3. Abstract Factory

The Abstract Factory pattern is a step beyond this, where the factory used also has an abstract base type. We can then write our code in terms of these abstract types, and select the concrete factory instance somehow at runtime.

First, we have an interface and some concrete implementations for the functionality we actually want to use:

interface FileSystem {
    // ...
}
class LocalFileSystem implements FileSystem {
    // ...
}
class NetworkFileSystem implements FileSystem {
    // ...
}

Next, we have an interface and some concrete implementations for the factory to obtain the above:

interface FileSystemFactory {
    FileSystem newInstance();
}
class LocalFileSystemFactory implements FileSystemFactory {
    // ...
}
class NetworkFileSystemFactory implements FileSystemFactory {
    // ...
}

We then have another factory method to obtain the abstract factory through which we can obtain the actual instance:

class Example {
    static FileSystemFactory getFactory(String fs) {
        FileSystemFactory factory;
        if ("local".equals(fs)) {
            factory = new LocalFileSystemFactory();
        else if ("network".equals(fs)) {
            factory = new NetworkFileSystemFactory();
        }
        return factory;
    }
}

Here, we have a FileSystemFactory interface that has two concrete implementations. We select the exact implementation at runtime, but the code that makes use of it doesn't need to care which instance is actually used. These then each return a different concrete instance of the FileSystem interface, but again, our code doesn't need to care exactly which instance of this we have.

Often, we obtain the factory itself using another factory method, as described above. In our example here, the getFactory() method is itself a factory method that returns an abstract FileSystemFactory that's then used to construct a FileSystem.

3.1. Examples in the JVM

There are plenty of examples of this design pattern used throughout the JVM. The most commonly seen are around the XML packages — for example, DocumentBuilderFactory, TransformerFactory, and XPathFactory. These all have a special newInstance() factory method to allow our code to obtain an instance of the abstract factory.

Internally, this method uses a number of different mechanisms – system properties, configuration files in the JVM, and the Service Provider Interface – to try and decide exactly which concrete instance to use. This then allows us to install alternative XML libraries in our application if we wish, but this is transparent to any code actually using them.

Once our code has called the newInstance() method, it will then have an instance of the factory from the appropriate XML library. This factory then constructs the actual classes we want to use from that same library.

For example, if we're using the JVM default Xerces implementation, we'll get an instance of com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl, but if we wanted to instead use a different implementation, then calling newInstance() would transparently return that instead.

4. Builder

The Builder pattern is useful when we want to construct a complicated object in a more flexible manner. It works by having a separate class that we use for building our complicated object and allowing the client to create this with a simpler interface:

class CarBuilder {
    private String make = "Ford";
    private String model = "Fiesta";
    private int doors = 4;
    private String color = "White";

    public Car build() {
        return new Car(make, model, doors, color);
    }
}

This allows us to individually provide values for make, model, doors, and color, and then when we build the Car, all of the constructor arguments get resolved to the stored values.

4.1. Examples in the JVM

There are some very key examples of this pattern within the JVM. The StringBuilder and StringBuffer classes are builders that allow us to construct a long String by providing many small parts. The more recent Stream.Builder class allows us to do exactly the same in order to construct a Stream:

Stream.Builder<Integer> builder = Stream.builder<Integer>();
builder.add(1);
builder.add(2);
if (condition) {
    builder.add(3);
    builder.add(4);
}
builder.add(5);
Stream<Integer> stream = builder.build();

5. Lazy Initialization

We use the Lazy Initialization pattern to defer the calculation of some value until it's needed. Sometimes, this can involve individual pieces of data, and other times, this can mean entire objects.

This is useful in a number of scenarios. For example, if fully constructing an object requires database or network access and we may never need to use it, then performing those calls may cause our application to under-perform. Alternatively, if we're computing a large number of values that we may never need, then this can cause unnecessary memory usage.

Typically, this works by having one object be the lazy wrapper around the data that we need, and having the data computed when accessed via a getter method:

class LazyPi {
    private Supplier<Double> calculator;
    private Double value;

    public synchronized Double getValue() {
        if (value == null) {
            value = calculator.get();
        }
        return value;
    }
}

Computing pi is an expensive operation and one that we may not need to perform. The above will do so on the first time that we call getValue() and not before.

5.1. Examples in the JVM

Examples of this in the JVM are relatively rare. However, the Streams API introduced in Java 8 is a great example. All of the operations performed on a stream are lazy, so we can perform expensive calculations here and know they are only called if needed.

However, the actual generation of the stream itself can be lazy as well. Stream.generate() takes a function to call whenever the next value is needed and is only ever called when needed. We can use this to load expensive values – for example, by making HTTP API calls – and we only pay the cost whenever a new element is actually needed:

Stream.generate(new BaeldungArticlesLoader())
  .filter(article -> article.getTags().contains("java-streams"))
  .map(article -> article.getTitle())
  .findFirst();

Here, we have a Supplier that will make HTTP calls to load articles, filter them based on the associated tags, and then return the first matching title. If the very first article loaded matches this filter, then only a single network call needs to be made, regardless of how many articles are actually present.

6. Object Pool

We'll use the Object Pool pattern when constructing a new instance of an object that may be expensive to create, but re-using an existing instance is an acceptable alternative. Instead of constructing a new instance every time, we can instead construct a set of these up-front and then use them as needed.

The actual object pool exists to manage these shared objects. It also tracks them so that each one is only used in one place at the same time. In some cases, the entire set of objects gets constructed only at the start. In other cases, the pool may create new instances on demand if it's necessary

6.1. Examples in the JVM

The main example of this pattern in the JVM is the use of thread pools. An ExecutorService will manage a set of threads and will allow us to use them when a task needs to execute on one. Using this means that we don't need to create new threads, with all of the cost involved, whenever we need to spawn an asynchronous task:

ExecutorService pool = Executors.newFixedThreadPool(10);

pool.execute(new SomeTask()); // Runs on a thread from the pool
pool.execute(new AnotherTask()); // Runs on a thread from the pool

These two tasks get allocated a thread on which to run from the thread pool. It might be the same thread or a totally different one, and it doesn't matter to our code which threads are used.

7. Prototype

We use the Prototype pattern when we need to create new instances of an object that are identical to the original. The original instance acts as our prototype and gets used to construct new instances that are then completely independent of the original. We can then use these however is necessary.

Java has a level of support for this by implementing the Cloneable marker interface and then using Object.clone(). This will produce a shallow clone of the object, creating a new instance, and copying the fields directly.

This is cheaper but has the downside that any fields inside our object that have structured themselves will be the same instance. This, then, means changes to those fields also happen across all instances. However, we can always override this ourselves if necessary:

public class Prototype implements Cloneable {
    private Map<String, String> contents = new HashMap<>();

    public void setValue(String key, String value) {
        // ...
    }
    public String getValue(String key) {
        // ...
    }

    @Override
    public Prototype clone() {
        Prototype result = new Prototype();
        this.contents.entrySet().forEach(entry -> result.setValue(entry.getKey(), entry.getValue()));
        return result;
    }
}

7.1. Examples in the JVM

The JVM has a few examples of this. We can see these by following the classes that implement the Cloneable interface. For example, PKIXCertPathBuilderResult, PKIXBuilderParameters, PKIXParameters, PKIXCertPathBuilderResult, and PKIXCertPathValidatorResult are all Cloneable.

Another example is the java.util.Date class. Notably, this overrides the Object.clone() method to copy across an additional transient field as well.

8. Singleton

The Singleton pattern is often used when we have a class that should only ever have one instance, and this instance should be accessible from throughout the application. Typically, we manage this with a static instance that we access via a static method:

public class Singleton {
    private static Singleton instance = null;

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

There are several variations to this depending on the exact needs — for example, whether the instance is created at startup or on first use, whether accessing it needs to be threadsafe, and whether or not there needs to be a different instance per thread.

8.1. Examples in the JVM

The JVM has some examples of this with classes that represent core parts of the JVM itselfRuntime, Desktop, and SecurityManager. These all have accessor methods that return the single instance of the respective class.

Additionally, much of the Java Reflection API works with singleton instances. The same actual class always returns the same instance of Class, regardless of whether it's accessed using Class.forName(), String.class, or through other reflection methods.

In a similar manner, we might consider the Thread instance representing the current thread to be a singleton. There are often going to be many instances of this, but by definition, there is a single instance per thread. Calling Thread.currentThread() from anywhere executing in the same thread will always return the same instance.

9. Summary

In this article, we've had a look at various different design patterns used for creating and obtaining instances of objects. We've also looked at examples of these patterns as used within the core JVM as well, so we can see them in use in a way that many applications already benefit from.

Java bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
guest
0 Comments
Inline Feedbacks
View all comments