1. Overview

In this article, we’re going to talk about the difference between the init blocks, property initializers, and constructors in Kotlin.

2. Sample Class

In order to understand the difference between constructors and init blocks in Kotlin, we’re going to use the following example throughout the article:

class Person(val firstName: String, val lastName: String) {

    private val fullName: String = "$firstName $lastName".trim()
      .also { println("Initializing full name") }

    init {
        println("You're $fullName")
    }

    private val initials =  "${firstName.firstOrEmpty()}${lastName.firstOrEmpty()}".trim()
      .also { println("Initializing initials") }

    init {
        println("You're initials are $initials")
    }

    constructor(lastName: String): this("", lastName) {
        println("I'm secondary")
    }

    private fun String.firstOrEmpty(): Char = firstOrNull()?.toUpperCase() ?: ' '
}

In the above example, we have a primary constructor that accepts two Strings as inputs for construction. Moreover, we also have a secondary constructor that only accepts the person’s last name. In addition to these two constructors, we’re declaring two variables with property initializers based on the constructor inputs. Finally, we have two initializer blocks (blocks prefixed with the keyword init).

3. Constructor and init Blocks

As opposed to secondary constructors, the primary constructor can’t contain any code. To overcome this limitation, we can put initialization logic inside init blocks and property initializers, as we did in the above example.

During the initialization of an instance, Kotlin executes the initializer blocks and property initializers in the same order as they appear in the class body. So, if we create an instance of the Person class, we’ll see a few logs in the same order as they appear in the class:

val p = Person("ali", "dehghani")

The logs for the above object instantiation will be like:

Initializing full name
You're ali dehghani
Initializing initials
You're initials are AD

As shown above, the first log is for the fullName property initializer, the second log for the first init block, the third log is for the initials property initializer, and the final one is for the last init block.

3.1. Bytecode Representation

Even though we can’t put some code in primary constructors in Kotlin, the generated bytecode for the constructor will contain all the initialization logic. Basically, the Kotlin compiler will generate a big constructor containing the logic from all the property initializers and init block initializers.

Put simply, the init blocks and property initializers will end up as part of the primary constructor. Obviously, we can verify this by taking a look at the generated bytecode:

>> kotlinc Person.kt
>> javap -c -p com.baeldung.initblock.Person
// primary constructor
public com.baeldung.initblock.Person(java.lang.String, java.lang.String);
    Code:
       // primary constructor properties
      13: invokespecial #20                 // Method Object."<init>":()V
      16: aload_0
      17: aload_1
      18: putfield      #23                 // Field firstName:LString;
      21: aload_0
      22: aload_2
      23: putfield      #25                 // Field lastName:LString;
      26: aload_0

      // full name property initializer
      27: new           #27                 // class StringBuilder
      30: dup
      31: invokespecial #28                 // Method StringBuilder."<init>":()V
      34: aload_0
      35: getfield      #23                 // Field firstName:LString;
      38: invokevirtual #32                 // Method StringBuilder.append:(LString;)LStringBuilder;
      41: bipush        32
      43: invokevirtual #35                 // Method StringBuilder.append:(C)LStringBuilder;
      46: aload_0
      47: getfield      #25                 // Field lastName:LString;
      50: invokevirtual #32                 // Method StringBuilder.append:(LString;)LStringBuilder;
      53: invokevirtual #39                 // Method StringBuilder.toString:()LString;

      // printing
      99: ldc           #57                 // String Initializing full name
     101: astore        8
     103: iconst_0
     104: istore        9
     106: getstatic     #63                 // Field System.out:LPrintStream;
     109: aload         8
     111: invokevirtual #69                 // Method PrintStream.println:(LObject;)V
     
     // first init block
     123: putfield      #78                 // Field fullName:LString;
     126: nop
     127: ldc           #80                 // String You\'re
     129: aload_0
     130: getfield      #78                 // Field fullName:LString;
     133: invokestatic  #84                 // Method ntrinsics.stringPlus:(LString;LObject;)LString;
     136: astore_3
     137: iconst_0
     138: istore        4
     140: getstatic     #63                 // Field System.out:LPrintStream;
     143: aload_3
     144: invokevirtual #69                 // Method PrintStream.println:(LObject;)V
     
     // other property initializers and init blocks

This is a highly truncated and simplified version of the bytecode! As shown above, the first part of the bytecode is initializing the firstName and lastName constructor properties. After that, the bytecode is dedicated to the fullName property initializer and printing the static log. And finally, we have a set of opcodes responsible for the first init block.

The remaining part of the bytecode is truncated for the sake of brevity. Anyway, it’s obvious that Kotlin compiles the primary constructor to hold all the logic.

3.2. Secondary Constructor

As opposed to the primary constructor, the secondary constructors can contain initialization logic. Delegation to the primary constructor happens as the first statement of a secondary constructor, either explicitly or implicitly. Therefore, Kotlin executes the code in all initializer blocks and property initializers before the body of the secondary constructor.

So, if we create an instance using the secondary constructor:

val p = Person("dehghani")

We’ll see the secondary constructor log after all other logs from the primary one:

Initializing full name
You're dehghani
Initializing initials
You're initials are D
I'm secondary

Again, the bytecode can verify the order of calls for the secondary constructor:

public com.baeldung.initblock.Person(java.lang.String);
    Code:
      // calling the primary constructor
      10: invokespecial #109                // Method "<init>":(LString;LString;)V

      // secondary log
      13: ldc           #111                // String I\'m secondary
      18: getstatic     #63                 // Field System.out:LPrintStream;
      21: aload_2
      22: invokevirtual #69                 // Method PrintStream.println:(LObject;)V

As shown above, it first calls the primary constructor and then prints the expected log on the standard output.

4. Conclusion

In this article, we saw the difference between init blocks and constructors in Kotlin. Also, to better understand this difference, we took a peek at the generated bytecode for each case.

As usual, all the examples are available over on GitHub.