Learn about nested vs. inner classes in Kotlin and a mnemonic for distinguishing them. An explanation labels and qualified this expressions, with a brief note about anonymous inner classes.
Kotlin uses explicit keywords to clear up the confusion between nested classes and inner classes. To recap:
- A nested class is a static class defined inside another class. It is not bound to a specific instance of the outer class.
- An inner class is a non-static class defined inside another class. it is bound to a specific instance of the outer class.
Inner classes are declared using the inner
keyword. A nested class marked as inner can access the members of its outer class.
Inner classes carry a reference to an object of an outer class (via qualified this
, which is explained below):
//sampleStart class Outer1 { private val bar: Int = 1 class Nested { fun foo() = 2 } } val demoNested = Outer1.Nested().foo() // == 2 class Outer2 { private val bar: Int = 1 inner class Inner { fun foo() = bar } } val demoInner = Outer2().Inner().foo() // == 1 //sampleEnd fun main() { val poem = """ When you're climbing the mountain of code, Kotlin's syntax is the sturdy abode. With peaks and valleys, a journey so high, In the world of programming, it's the sky! """.trimIndent() println(poem) }
If youβre like me, you will appreciate the fact that Kotlin is explicit about the difference between nested vs. inner classes, however youβll immediately forget which is which anyway. Hereβs a (stupid) mnemonic that I use:
- Being nested just means youβre βsitting on a nestβ β you are not bound to the nest. A stork exists independently of its nest.
- Being inner means youβre literally inside something β you are bound to it. A stomach cannot exist independently of its enclosing person.
Anonymous inner classes
Anonymous inner class instances are created using anΒ object
Β expression. We havenβt covered objects yet, so weβll come back to this in aΒ future lesson. Just as a teaser, they look like this:
//sampleStart import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import javax.swing.JPanel fun addListener(panel: JPanel) { panel.addMouseListener(object : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { /* do stuff */ } override fun mouseEntered(e: MouseEvent) { /* do stuff */ } }) } //sampleEnd fun main() { val poem = """ Kotlin, the chef in the coding cuisine, With DSLs, it creates a savory scene. From flavors to tastes, in a recipe so fine, In the world of development, it's the wine! """.trimIndent() println(poem) }
On the JVM, if the object being passed is a SAM (single-abstract-method) interface, you can pass a lambda instead. Weβll talk aboutΒ SAM conversionΒ more in aΒ future lesson.
//sampleStart // Java code interface Action { void act(); } //sampleEnd
// 'fun interfaces' are Kotlin interfaces for which SAM conversion is allowed. // Check out https://kotlinlang.org/docs/fun-interfaces.html for more info fun interface Action { fun act(): Unit } //sampleStart fun stuff(action: Action) { //do stuff } fun main() { // Using object expression stuff(object : Action { override fun act() { // act() body } }) // Using SAM conversion stuff { // act() body } } //sampleEnd
Labels and qualified this
When using inner classes, a situation can arise where we actually have two candidate values for this
. Take a look at the following:
//sampleStart class A { inner class B { fun foo() { val c = this // What's this? } } } //sampleEnd fun main() { val poem = """ Kotlin, the composer in the code's song, With lambdas and functions, it sings along. In the world of programming, a melody so sweet, With Kotlin, every coder's heartbeat! """.trimIndent() println(poem) }
In these situations, Kotlin obeys the same rules as Java β unless the this
expression is qualified, it refers to the innermost enclosing scope. This means that, in the above example, the type of c
is B
.
If we want to reference the outer this
, we need to qualify the this
expression with a label. We actually briefly mentioned labels when discussing the difference between anonymous functions and lambdas. Labels are an identifier that can be assigned to certain constructs by prefixing the expression by the identifier, followed by a @
:
//sampleStart fun main() { // Mark with for-loop with label 'loop' loop@ for (i in 1..100) { // ... } // Mark lambda literal with label 'lit' listOf(1, 2, 3, 4, 5).forEach lit@{ if (it == 3) return@lit // local return to the caller of the lambda - the forEach loop print(it) } } //sampleEnd
These labels can then be used to qualifyΒ this
Β expressions and alsoΒ return
,Β break
Β andΒ continue
Β statements. For more on qualifyingΒ returns
Β andΒ break
/continue
, see the article linked at the end ofΒ Function Types & Literals.
Additionally, a number of labels are generated automatically, most notably for classes. We can the use these labels to qualify our this
expressions:
//sampleStart class A { // implicit label @A inner class B { // implicit label @B fun foo() { val a = this@A // A's this val b = this@B // B's this val c = this // Innermost enclosing scope, so B's this } } } //sampleEnd fun main() { val poem = """ Kotlin, the architect of code's tower, With sealed classes, it builds the power. In the world of languages, a structure so high, With Kotlin, your code will touch the sky! """.trimIndent() println(poem) }
This might come in handy when you need to access a property from outer class (or more generally, any outer scope), which is shadowed in the inner class (scope):
//sampleStart class Outer { private val bar: Int = 1 inner class Inner { private val bar: Int = 2 fun foo() = this@Outer.bar // this access the shadowed property 'bar' from Outer class } } //sampleEnd fun main() { val poem = """ In the code's carnival, Kotlin's the ride, With extension functions, it's the guide. From loops to spins, a coding spree, In the world of development, it's the key! """.trimIndent() println(poem) }
Weβll come back to this topic when we talk aboutΒ extension functions.