1. Introduction
In this tutorial, we’ll look at four ways to create nested and inner classes in Kotlin.
2. Quick Comparison to Java
For those thinking about Java nested classes, let’s do a quick rundown of related terms:
Kotlin | Java |
---|---|
Inner Classes | Non-Static Nested Classes |
Local Classes | Local Classes |
Anonymous Objects | Anonymous Classes |
Nested Classes | Static Nested Classes |
While certainly not identical, we can use this table as a guide when thinking about the capabilities and use cases for each.
3. Inner Classes
First, we can declare a class inside another class using the keyword inner.
These classes have access to members of the enclosing class, even private members.
To use it, we need to create an instance of the outer class first; we can’t use inner classes without it.
Let’s create a HardDisk inner class inside a Computer class:
class Computer(val model: String) { inner class HardDisk(val sizeInGb: Int) { fun getInfo() = "Installed on ${this@Computer} with $sizeInGb GB" } }
Note that we use a qualified this expression to access members of the Computer class, which is similar to when we do Computer.this in the Java equivalent of HardDisk.
Now, let’s see it in action:
@Test fun givenHardDisk_whenGetInfo_thenGetComputerModelAndDiskSizeInGb() { val hardDisk = Computer("Desktop").HardDisk(1000) assertThat(hardDisk.getInfo()) .isEqualTo("Installed on Computer(model=Desktop) with 1000 GB") }
4. Local Inner Classes
Next, we can define a class inside a method’s body or in a scope block.
Let’s make a quick example to see how it works.
First, let’s define a powerOn method for our Computer class:
fun powerOn(): String { //... }
Inside of the powerOn method let’s declare a Led class and make it blink:
fun powerOn(): String { class Led(val color: String) { fun blink(): String { return "blinking $color" } } val powerLed = Led("Green") return powerLed.blink() }
Note that the scope of the Led class is only inside of the method.
With local inner classes, we can access and modify variables declared in the outer scope. Let’s add a defaultColor in the powerOn method:
fun powerOn(): String { var defaultColor = "Blue" //... }
Now, let’s add a changeDefaultPowerOnColor in our Led class:
class Led(val color: String) { //... fun changeDefaultPowerOnColor() { defaultColor = "Violet" } } val powerLed = Led("Green") log.debug("defaultColor is $defaultColor") powerLed.changeDefaultPowerOnColor() log.debug("defaultColor changed inside Led " + "class to $defaultColor")
Which outputs:
[main] DEBUG c.b.n.Computer - defaultColor is Blue [main] DEBUG c.b.n.Computer - defaultColor changed inside Led class to Violet
5. Anonymous Objects
Anonymous objects can be used to define an implementation of an interface or an abstract class without creating a reusable implementation.
A big difference between anonymous objects in Kotlin and anonymous inner classes in Java is that anonymous objects can implement multiple interfaces and methods.
First, let’s add a Switcher interface in our Computer class:
interface Switcher { fun on(): String }
Now, let’s add an implementation of this interface inside the powerOn method:
fun powerOn(): String { //... val powerSwitch = object : Switcher { override fun on(): String { return powerLed.blink() } } return powerSwitch.on() }
As we can see, to define our anonymous powerSwitch object we use an object expression. Also, we need to take into count that every time the object expression is invoked a new instance of the object is created.
With anonymous objects like inner classes, we can modify variables previously declared in the scope. This is because Kotlin doesn’t have the effectively final restriction we’ve come to expect in Java.
Now, let’s add a changeDefaultPowerOnColor in our PowerSwitch object and call it:
val powerSwitch = object : Switcher { //... fun changeDefaultPowerOnColor() { defaultColor = "Yellow" } } powerSwitch.changeDefaultPowerOnColor() log.debug("defaultColor changed inside powerSwitch " + "anonymous object to $defaultColor")
We’ll see an output like this:
... [main] DEBUG c.b.n.Computer - defaultColor changed inside powerSwitch anonymous object to Yellow
Also, note that if our object is an instance of an interface or a class with a single abstract method; we can create it using a lambda expression.
6. Nested Classes
And last, we can define a class inside another class without the keyword inner:
class Computer(val model: String) { class MotherBoard(val manufacturer: String) }
In this type of class, we don’t have access to the outer class instance. But, we can access companion object members of the enclosing class.
So, let’s define a companion object inside our Computer class to see it:
companion object { const val originCountry = "China" fun getBuiltDate(): String { return "2018-07-15T01:44:25.38Z" } }
And then a method inside MotherBoard to get information about it and the outer class:
fun getInfo() = "Made by $manufacturer - $originCountry - ${getBuiltDate()}"
Now, we can test it to see how it works:
@Test fun givenMotherboard_whenGetInfo_thenGetInstalledAndBuiltDetails() { val motherBoard = Computer.MotherBoard("MotherBoard Inc.") assertThat(motherBoard.getInfo()) .isEqualTo( "Made by MotherBoard Inc. installed in China - 2018-05-23") }
As we can see, we create motherBoard without an instance of Computer class.
7. Conclusion
In this article, we’ve seen how to define and use nested and inner classes in Kotlin to make our code more concise and encapsulated.
Also, we’ve seen some similarities to the corresponding Java concepts.
A fully working example for this tutorial can be found over on GitHub.