Singleton design pattern in Java and Kotlin

Singleton design pattern in Java and Kotlin

What is the singleton design pattern?

With the singleton design pattern, you allow only one instance of a class to be created. If your class Person is singleton, then there can be at most one Person in your entire application. As the singleton design pattern is a quite straightforward pattern, there isn't much explanation needed. Though there are several ways in which to implement it ( especially in Java).

All code examples in this blog posts are available on GitHub.

Singletons in Java

The main idea on how to implement it, is by creating a class with properties and methods as you normally would do. However, now, you don't want the outside world to create instances of your class. Thus, you make your constructors private. This does leave you with a problem: how does the first instance gets created? This is where getInstance() chimes in. This is a public static function that you add to your class. This function should return the only instance of your class. There are two ways in which that first instance gets initialized.

You can initialize your singleton instance using lazy or eager initialization. Both have their advantages. But what's the difference? When you are using eager initialization, the only class instance will always be created. This is not the case when you are using lazy initialization. Because then the only instance will be created after calling the getInstance method.

Eager initialization

Let's create a singleton class using eager initialization. Recall that using eager initialization, the instance always gets created. In the following example, the class Person is singleton:

1public class Person {
2
3    private final static Person instance = new Person();
4    private final String name = "Frodo Baggings";
5
6    private Person() {
7    }
8
9    public static Person getInstance() {
10        return instance;
11    }
12
13    public String getName() {
14        return name;
15    }
16
17}

There are a few things that you should notice now. On line 3, private final static Person instance = new Person();, the only actual Person instance gets created. Then, on line 6-7, we have an empty private constructor. We had to explicitly add this constructor, because by making it private, others can't create instances. And finally, on line 9-11, we have our getInstance() method. As you can see, this method returns our already created instance of Person.

If we create a Main method and call getInstance twice, we can verify that both objects are actually the same. One way to do that is by checking out their hash codes, they will be the same:

1public static void main(final String[] args) {
2    final Person person1 = Person.getInstance();
3    final Person person2 = Person.getInstance();
4    
5    // Person 1 and 2 both have the same hash code:
6    System.out.println("Hash code person1: " + person1.hashCode());
7    System.out.println("Hash code person2: " + person2.hashCode());
8}

Lazy initialization

Now let's create an example using lazy initialization. Recall that using lazy initialization, you don't always have a created instance: there are always 0 or 1 instances. After calling getInstance, you first instance shall be created. The advantage of this, over eager initialization, is that if you do not use the singleton class, you don't have to hold on to resources that you don't use. In the following example, using lazy initialization, our Book class is singleton:

1public class Book {
2
3    private static Book instance;
4    private final String name;
5
6    private Book(final String name) {
7        this.name = name;
8    }
9
10    public static synchronized Book getInstance(final String name) {
11        if (instance == null) {
12            instance = new Book(name);
13        }
14
15        return instance;
16    }
17
18    public String getName() {
19        return name;
20    }
21
22}

Our book has one property: name. On line 3 you see our instance property, however, it hasn't been initialized yet. On the lines 6-8, you see, again, a private constructor. Then, on line 10-16, we have our getInstance method. You'll probably notice that it's bigger than our eager example. Within the if-statement, if (instance == null), we check if an instance has already been created. If not: create it or otherwise, return the already created instance.

In the following Main method, we create two books. Notice that we provide two different names: Name 1 and Name 2. We then print the names, and we see that "Name 1" gets printed both times. That's because the first call of getInstance creates our Book instance with the given name. The second time we called getInstance, that first instance actually gets returns. Thus, nothing actually happens with that second name:

1public static void main(final String[] args) {
2    final Book book1 = Book.getInstance("Name 1");
3    final Book book2 = Book.getInstance("Name 2");
4    
5    System.out.println("Name book1: " + book1.getName()); // Out: Name 1
6    System.out.println("Name book2: " + book2.getName()); // Out: Name 1
7}

Beware of multi-threads

When you use lazy initialization, you must be careful in multithreaded applications. In multithreaded applications, it may happen that different threads both create singleton instances, causing you application to have multiple instances of the same class, that should be singleton. That's why we make our getInstance method so-called thread-safe with concurrency control. One way to do that is by making the method synchronized, you can see that in the above lazy initialization example.

Singletons in Kotlin

See all that java code above? Check out how we make a singleton class Earth in Kotlin:

1object Earth {
2    const val greeting = "Hello!"
3}

That's it! And it's even thread safe. In Kotlin we use the object-keyword to create thread-safe singletons. You can add properties as you wish, in this case we added greeting, that we can access like this:

1fun main() {
2    println(Earth.greeting);
3}

Thus concludes the singleton design pattern Java and Kotlin. Hopefully you've learned something, and you can bring it into practice. All examples are available in our GitHub repository.