1. Overview

In Java, private constructors effectively prevent external code from creating instances of the class using that constructor. Private constructors are versatile and can be utilized for various purposes, including implementing design patterns like the singleton and builder patterns, as well as static factory methods.

In this quick tutorial, let’s explore how to declare private constructors in Kotlin.

2. Private Constructors in Kotlin

In Kotlin, a private constructor is inaccessible outside of the class where it’s declared, regardless of whether the declaring class is a top-level class or an inner/nested class. In other words, a private constructor cannot be invoked by code external to the declaring class.

Private constructors are denoted by the private modifier preceding the constructor declaration. An example may address it clearly:

class Student private constructor(val name: String, val age: Int) {
    companion object {
        fun createInstance(pair: Pair<String, Int>): Student {
            return Student(pair.first.uppercase(), pair.second)
        }
    }
}

As we can see in the above Student class, we’ve marked the primary constructor as private. Therefore, a Student instance can only be instantiated within the class. Further, to enable external code to acquire a Student instance, we’ve included the createInstance() function within a companion object, functioning like a static method.

The createInstance() function also accepts a Pair object as the parameter to return a Student. Moreover, we invoke the private constructor while converting the name String to uppercase. This ensures that all Student instances are consistently initialized with uppercase names.

Now, let’s attempt to create a Student instance outside the class using the private constructor:

val kai = Student("Kai", 18)

The code won’t compile, and the compiler complains that it cannot access the private constructor:

Kotlin: Cannot access '<init>': it is private in 'Student'

However, we can get the expected Student instance by passing a Pair to createInstance():

val kai = Student.createInstance("Kai" to 18)

assertEquals("KAI", kai.name)
assertEquals(18, kai.age)

3. A Few Words About Private Constructors in Kotlin data Classes

So far, we’ve learned how to declare private constructors in regular Kotlin classes, and it’s pretty straightforward. However, when implementing private constructors within Kotlin data classes, especially in library development contexts, it’s crucial to be aware that the private constructor may inadvertently be exposed through the automatically generated copy() function.

Next, let’s understand it through an example:

data class StudentData private constructor(val name: String, val age: Int) {

    companion object {
        fun createInstance(pair: Pair<String, Int>): StudentData {
            return StudentData(pair.first.uppercase(), pair.second)
        }
    }
}

As the code shows, we created a data class, StudentData, with a private constructor. Essentially, this class resembles the Student class in structure, with the distinction that it’s implemented as a data class.

So, we still cannot create an instance by invoking the constructor, for example:

val kaiData = StudentData("Kai", 18)

If we do this, the compiler complains:

Kotlin: Cannot access '<init>': it is private in 'StudentData'

Similarly, we can get an instance through the createInstance() function:

val kaiData = StudentData.createInstance("Kai" to 18)
assertEquals("KAI", kaiData.name)
assertEquals(18, kaiData.age)

We know that data classes generate the copy() function automatically so that we can conveniently get a copy from an existing instance. Furthermore, the generated copy() function invokes the constructor internally to create a new instance. In other words, the copy() function exposes the private constructor to external code. 

Next, let’s understand this quickly through an example:

val liam = kaiData.copy(name = "Liam", age = 20)
assertEquals("Liam", liam.name)
assertEquals(20, liam.age)

In this scenario, we generated a new StudentData object named liam from the pre-existing kaiData object using copy(). Also, this function enables us to supply new property values, thus overwriting the corresponding values of kaiData.

Consequently, liam was effectively instantiated through the private constructor, and as a result, its name doesn’t appear in uppercase.

4. Conclusion

In this article, we explored how to declare private constructors in Kotlin classes. Additionally, it’s essential to note that in data classes, the private constructor may be inadvertently exposed by the automatically generated copy() function.

As always, the complete source code for the examples is available over on GitHub.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.