1. Introduction
In this article, we’ll explain singleton classes and how we can create them in Kotlin. Singleton implementations in Java can be found here.
2. What Are Singleton Classes in Kotlin?
Singleton classes are a design pattern used to restrict the instantiation of a class to a single instance. In other words, a singleton class is a class that can only be instantiated once, and any subsequent attempts to instantiate the class will return the same instance that was created previously.
The need for singleton classes arises when it is necessary to maintain a single instance of a class for the entire lifetime of an application. For example, we might use a singleton class to represent a database connection, a configuration manager, or a logging service. By restricting the number of instances of these classes to one, we can ensure that the state of the application remains consistent and that there are no conflicts between multiple instances of the same class.
3. Implementing Singleton Classes in Kotlin
This section will cover different techniques to create a singleton class in Kotlin.
For the following examples, we’ll use the following unit test to verify that only one instance exists:
val instance1 = // Put instance creation here
val instance2 = // Put instance creation here
Assertions.assertSame(instance1, instance2)
Assertions.assertEquals("Doing something", instance1.doSomething())
First, we create two instances of a singleton. Then we verify that these instances are the same with a simple assertion. In the last step, we also verify that the doSomething method returns as expected.
3.1. Companion Object
We can implement a singleton in Kotlin using a companion object. It is a special type of object that is associated with a class and can be used to store static members and methods for the class. To create such a singleton, we store the single instance in the companion object of the singleton class:
class Singleton private constructor() {
companion object {
@Volatile
private var instance: Singleton? = null
fun getInstance() =
instance ?: synchronized(this) {
instance ?: Singleton().also { instance = it }
}
}
fun doSomething() = "Doing something"
}
The @Volatile annotation is needed to ensure that the instance property is updated atomically. This prevents other threads from creating more instances and breaking the singleton pattern. We need the synchronized keyword in the static getInstance method to prevent accessing the method from multiple threads simultaneously.
We can retrieve the instance with the following statement:
val instance = Singleton.getInstance()
3.2. Eager Initialization
The object keyword in Kotlin is used to declare a singleton class, also known as an object declaration. An object declaration is a concise way of creating a singleton class without the need to define a class and a companion object. It is an example of eager initialization, which creates the singleton when the class is first accessed. The instance remains in memory until the application is terminated:
object Singleton {
fun doSomething() = "Doing something"
}
We can retrieve the instance with the following statement:
val instance = Singleton
3.3. Lazy Initialization
To create a lazy initialization singleton in Kotlin, we can use a lazy delegate. A lazy delegate only allows us to initialize a property when it is first accessed. The instance remains in memory until the application terminates:
class Singleton private constructor() {
companion object {
val instance:Singleton by lazy {
Singleton()
}
}
fun doSomething() = "Doing something"
}
We can retrieve the instance with the following statement:
val instance = Singleton.instance
Note that this implementation is not thread-safe.
3.4. Double-Locking
Double-checked locking is a mechanism to reduce the overhead of synchronization by checking the lock only once and creating the instance only if the lock is not already held by another thread. Every time we want to retrieve the singleton instance, we perform a null check. If the singleton instance has already been created, we return it. If there is no instance available, we’ll create it.
First, we create a lock using the synchronized keyword. In a multithreaded application, it is possible that, in the meanwhile, another thread created the instance. Therefore, we need another null check, and only if the instance is still null we’ll create it:
class Singleton private constructor() {
companion object {
@Volatile
private var instance: Singleton? = null
fun getInstance(): Singleton {
if (instance == null) {
synchronized(this) {
if (instance == null) {
instance = Singleton()
}
}
}
return instance!!
}
}
fun doSomething() = "Doing something"
}
We can retrieve the instance with the following statement:
val instance = Singleton.getInstance()
The double-locking singleton is a powerful and efficient way of creating singleton classes in Kotlin, and it is similar to the double-locking singleton implementation in Java.
3.5. Enum
We can use enums to ensure that we only create one instance of a class in an application. Enum singletons are thread-safe by default, and they are easy to implement:
enum class Singleton {
INSTANCE;
fun doSomething() = "Doing something"
}
We can retrieve the instance with the following statement:
val instance = Singleton.INSTANCE
4. Advantages and Disadvantages of Singleton Classes
In this section, we’ll cover the main advantages and disadvantages of singleton classes.
4.1. Advantages
The advantages of singleton classes include the following:
- Guaranteed single instance: The most significant advantage of using a singleton class is that it guarantees that there will always be only one instance of the class in an application. This makes it easier to manage the state of an application and ensures that there are no conflicts between multiple instances of the same class.
- Easy to access: Singleton classes are easily accessible because the instance of the class can be obtained by calling a single method, regardless of where we call it from within an application. This makes it easier to reuse code and reduces the amount of code.
- Improved performance: Singleton classes can also improve the performance of an application because they ensure that we only create a single instance of them. We can reuse that single instance throughout the lifetime of the application. This reduces the overhead of creating and destroying multiple instances of the same class.
- Consistent state: With singletons, we can keep the state of an application consistent and predictable. This makes it easier to manage the state of an application and reduces the risk of bugs and errors.
4.2. Disadvantages
The disadvantages of singleton classes include the following:
- Difficult to test: Singleton classes can make testing more difficult because the single instance of the class is global, and it can be challenging to isolate tests from each other. This makes it harder to write tests that are independent and repeatable.
- Global state: The global nature of a singleton class can make it difficult to understand the state of an application and to track the source of bugs and errors. This can make it harder to maintain and debug an application.
- Tight coupling: Singleton classes can lead to tight coupling between different parts of an application, which makes it harder to modify or replace individual components. This can make it harder to maintain and evolve an application over time.
Singleton classes have both advantages and disadvantages. When deciding whether to use a singleton class, it is essential to weigh the advantage and disadvantages and consider the specific requirements of our application.
5. Conclusion
In this article, we learned that singleton classes are classes that ensure that we only create one instance.
We learned the different ways how we could implement singleton classes in Kotlin. There is, on the one hand, the object keyword, which is Kotlin-specific. On the other hand, we can also create a singleton using an enum or double-locking. In the end, we covered the advantages and disadvantages of singleton classes. Although singletons have several advantages, like easier state management or improved performance, they also have a few disadvantages, which make them more difficult to test.
As always, the implementation of all these examples can be found over on GitHub.