ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

Kotlin Under the Hood: Exploring Constructors and Init Blocks

Hello! Have you ever wondered how Kotlin’s constructors and init blocks work under the hood? In this blog, we’ll dive into it.

Before we dive into the details, let’s first understand what constructors and init blocks are.
Kotlin has two main types of constructors: the Primary Constructor and Secondary Constructors.

Primary Constructor

The primary constructor is defined in the class header, directly after the class name, and is part of the class declaration itself. It has the following features:

  • You can use val for read-only properties and var for mutable properties.
  • No need to use the constructor keyword, unlike in other languages.
  • It’s implicitly called when an instance of the class is created.
class User(val name: String, val age: Int){
// Properties 'name' and 'age' are automatically initialized via the primary constructor.
}

Secondary Constructor

A secondary constructor which also known as an auxiliary constructor is an additional constructor you can define within the class body using the constructor keyword. It allows you to provide alternative ways to instantiate an object when the primary constructor doesn’t meet your specific requirements.

Here are some key points about secondary constructors:

  • Defined within the class using the constructor keyword.
  • Can call the primary constructor using this(...).
  • Useful for providing different ways to create instances of a class.
  • Each secondary constructor can delegate to the primary constructor or to another secondary constructor.
  • There can be multiple secondary constructors.
class User(val name: String, val age: Int) {

constructor(name: String) : this(name, 22) {
println("User created with name: $name, age: $age")
}
}

In this example:

  • The primary constructor takes both name and age.
  • The secondary constructor allows you to create a User object by passing only a name. It automatically assigns a default age of 22.
  • this(name, 22) is a constructor delegation.

Constructor delegation refers to the mechanism by which a secondary constructor can call a primary constructor or another secondary constructor to reuse initialization logic.

Init Block

The init block is used for initializing properties or executing startup logic right after an object is created. It runs after the primary constructor and before any secondary constructors.

Key Characteristics:

  • The init block is called automatically when an instance of the class is created.
  • You can have multiple init blocks, and they will execute in the order they are defined within the class.
  • Any properties initialized in the primary constructor can be accessed and used inside the init block.
class User(val name: String, val age: Int) {

init {
println("name: $name and age: $age")
}
}

Decoding Kotlin Constructors and Init Blocks

Now, let’s dive into how it works. First, to gain deeper insights, we can use IntelliJ IDEA’s decompilation feature. By navigating to Tools -> Kotlin -> Kotlin Bytecode and selecting Decompile, we can view the underlying Java code generated from our Kotlin constructs.

Now, let’s check what happens when we create a primary constructor in Kotlin. Here’s our code:

class User(val name: String, val age: Int) {}

Now, let’s take a look at the underlying Java code generated from this Kotlin class:

public final class User {
@NotNull
private final String name;
private final int age;

@NotNull
public final String getName() {
return this.name;
}

public final int getAge() {
return this.age;
}

public User(@NotNull String name, int age) {
super();
this.name = name;
this.age = age;
}
}

I’ve simplified it by removing assertions and other metadata for clarity.

So, what do we see here?

  • The User class is final because Kotlin classes are final by default.
  • The primary constructor is translated into a Java constructor.
  • getName() and getAge() methods provide access to these properties.
  • The @NotNull annotation indicates that name cannot be null, ensuring safety.
  • There are no setter methods since the properties are defined as immutable in the constructor.

Let’s discuss the init block.

class User(val name: String, val age: Int) {
init {
println("This is the first init block. Name: $name & Age: $age")
}

init {
println("This is the second init block.")
}
}

Here’s the underlying Java code:

public final class User {
@NotNull
private final String name;
private final int age;

@NotNull
public final String getName() {
return this.name;
}

public final int getAge() {
return this.age;
}

public User(@NotNull String name, int age) {
super();
this.name = name;
this.age = age;
String var3 = "This is the first init block. Name: " + this.name + " & Age: " + this.age;
System.out.println(var3);
var3 = "This is the second init block.";
System.out.println(var3);
}
}

So, what do we observe here?

  • The code from the init blocks is added to the primary constructor and executes after the properties in the constructor are initialized.
  • The init blocks execute in the order they appear, right after the primary constructor and before any secondary constructors.

When we create an instance of the User class using User("Abhay", 22), the output will be:

This is the first init block. Name: Abhay & Age: 22
This is the second init block.

Let’s explore the secondary constructor.

class User(val name: String, val age: Int){
init {
println("This is the first init block. Name: $name & Age: $age")
}
// secondary constructor
constructor(name: String): this(name, 22){
println("This is the secondary constructor: Name: $name & Age: $age")
}
init {
println("This is the second init block.")
}
}

Here’s the underlying Java code:

public final class User {
@NotNull
private final String name;
private final int age;

@NotNull
public final String getName() {
return this.name;
}

public final int getAge() {
return this.age;
}

public User(@NotNull String name, int age) {
super();
this.name = name;
this.age = age;
String var3 = "This is the first init block. Name: " + this.name + " & Age: " + this.age;
System.out.println(var3);
var3 = "This is the second init block.";
System.out.println(var3);
}

public User(@NotNull String name) {
this(name, 22);
String var2 = "This is the secondary constructor: Name: " + name + " & Age: " + this.age;
System.out.println(var2);
}
}
  • A secondary constructor in Kotlin overloads the constructor in Java.
  • The secondary constructor can call the primary constructor or another secondary constructor using this().
  • The secondary constructor executes after all init blocks have been processed.
  • In this example, when you create an instance of User using the secondary constructor with just a name, it initializes the age property to a default value of 22.

When you create an instance of User using the secondary constructor, as shown in the following code:

fun main() {
// Calling the secondary constructor
User("Abhay")
}

You’ll get the output:

This is the first init block. Name: Abhay & Age: 22
This is the second init block.
This is the secondary constructor: Name: Abhay & Age: 22

Here, you can see that all the init blocks are executed first, followed by the secondary constructor.

Thanks for reading this blog! 😊 If you want to explore more “under the hood” insights and deep dives into Kotlin, be sure to follow me for future updates and posts.

Feel free to connect with me on:

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Written by Abhaysing Bhosale

Android app developer | Kotlin | Jetpack Compose

No responses yet

Write a response