Kotlin Classes and Inheritance Fundamentals

Class Definition

Kotlin uses the class keyword to declare classes. A class declaration consists of a name, an optional class header (specifying type parameters, primary constructor, etc.), and a class body enclosed in curly braces. Both the header and body are optional; if a class has no body, you can omit the curly braces.

// Simple class definition
class Receipt { /* ... */ }
// Class without a body
class NoContent

Constructors

A Kotlin class can have one primary constructor and one or more secondary constructors. The primary constructor is part of the class header, following the class name (and optional type parameters).

// Primary constructor with constructor keyword
class User constructor(firstName: String) { /* ... */ }

// Omit constructor keyword if no annotations or visibility modifiers
class User(firstName: String) { /* ... */ }

The primary constructor cannot contain any code. Initialization logic should be placed in initialization blocks prefixed with the init keyword. During instance creation, initialization blocks execute in the order they appear in the class body, interleaved with property initializers.

class InitializationSequenceDemo(itemName: String) {
    val firstField = "First field: $itemName".also(::println)

    init {
        println("First initializer block printing: $itemName")
    }

    val secondField = "Second field: ${itemName.length}".also(::println)

    init {
        println("Second initializer block printing length: ${itemName.length}")
    }
}

When initialized with "Test", the output would be: First field: Test First initializer block printing: Test Second field: 4 Second initializer block printing length: 4

Primary constructor parameters can be used in initialization blocks and property initializers within the class body:

class Client(clientName: String) {
    val clientIdentifier = clientName.uppercase()
}

You can also declare and initialize properties directly in the primary constructor, using val for read-only or var for mutable properties:

class User(val firstName: String, val lastName: String, var age: Int) { /* ... */ }

If the primary constructor has annotations or visibility modifiers, the constructor keyword is required, with modifiers placed before it:

class Client public @Inject constructor(clientName: String) { /* ... */ }

Secondary Constructors

Declare secondary constructors using the constructor keyword:

class FamilyMember {
    var kids: MutableList<FamilyMember> = mutableListOf()
    constructor(parent: FamilyMember) {
        parent.kids.add(this)
    }
}

If a class has a primary constructor, all secondary constructors must delegate to it, either directly or via another secondary constructor using the this keyword:

class FamilyMember(val memberName: String) {
    var kids: MutableList<FamilyMember> = mutableListOf()
    constructor(memberName: String, parent: FamilyMember) : this(memberName) {
        parent.kids.add(this)
    }
}

Initialization blocks and the primary constructor execute before any secondary constructor bodies. For example:

class ConstructorOrderDemo {
    init {
        println("Initialization block executed")
    }

    constructor(input: Int) {
        println("Secondary constructor executed")
    }
}

When creating an instance, the output is: Initialization block executed Secondary constructor executed

Creating Class Instances

Instantiate classes like calling a regular function, without the new keyword:

val receipt = Receipt()
val client = Client("Alice Johnson")

Inheritance and Open Classes

All Kotlin classes inherit from Any, which provides equals(), hashCode(), and toString(). By default, all Kotlin classes are final (cannot be inherited). To allow inheritance, mark the class with the open keyword:

open class BaseClass // Open for inheritance

// Example of subclassing with primary constructor
open class BaseClass(param: Int)
class SubClass(param: Int) : BaseClass(param)

If the subclass has a primary constructor, the base class must be initialized immediately using the subclass's primary constructor parameters.

Overriding Methods

Methods in open classes can be marked open to allow overriding in subclasses. Overridden methods require the override modifier; omitting it will trigger a compiler error. Methods not marked open cannot be overridden in subclasses. Overridden method are themselves open by default, unless marked final:

open class GeometricShape {
    open fun render() { /* ... */ }
    fun fillColor() { /* ... */ }
}

class RoundShape : GeometricShape() {
    override fun render() { /* ... */ }
}

// Prevent further overriding with final
open class RectangularShape : GeometricShape() {
    final override fun render() { /* ... */ }
}

Overriding Properties

Property overriding follows similar rules to method overriding. Open properties can be overridden in subclasses, and you can change a read-only property (val) to a mutable one (var) in the subclass:

// Example 1
open class GeometricShape {
    open val vertexCount: Int = 0
}

class RectangularShape : GeometricShape() {
    override val vertexCount = 4
}

// Example 2
interface ShapeInterface {
    val vertexCount: Int
}

class Square(override val vertexCount: Int = 4) : ShapeInterface

class Polygon : ShapeInterface {
    override var vertexCount: Int = 0 // Mutable property overriding read-only interface property
}

Note: Avoid using open members in constructors, property initializers, or init blocks when designing base classes.

Calling Superclass Implementations

Use the super keyword in subclasses to call base class methods or access property getters:

open class RectangularShape {
    open fun render() { println("Rendering a rectangle") }
    val borderShade: String get() = "dark gray"
}

class FilledRectangle : RectangularShape() {
    override fun render() {
        super.render()
        println("Filling the rectangle")
    }
    val fillShade: String get() = super.borderShade
}

To access the outer class's superclass from an inner class, use the qualified super@OuterClassName syntax:

class FilledRectangle : RectangularShape() {
    override fun render() { /* ... */ }
    override val borderShade: String get() = "black"

    inner class ShapeFiller {
        fun fill() { /* ... */ }

        fun renderAndFill() {
            super@FilledRectangle.render() // Call RectangularShape's render()
            println("Drawn filled rectangle with border color: ${super@FilledRectangle.borderShade}")
        }
    }
}

Override Resolution Rules

If a class inherits multiple implementations of the same member from its supertypes, it must override that member and provide its own implementation. To reference a specific supertype's implementation, use super<SupertypeName>:

open class RectangularShape {
    open fun render() { /* ... */ }
}

interface PolygonInterface {
    fun render() { /* ... */ } // Interface members are open by default
}

class Square : RectangularShape(), PolygonInterface {
    // Compiler requires overriding render()
    override fun render() {
        super<RectangularShape>.render() // Call RectangularShape's render()
        super<PolygonInterface>.render() // Call PolygonInterface's render()
    }
}

Abstract Classes

Abstract classes and members can override non-abstract open members from a base class. Abstract members do not provide an implementation:

open class Polygon {
    open fun render() {}
}

abstract class AbstractRectangle : Polygon() {
    abstract override fun render()
}

Companion Objects

If you need to define functions that can be invoked without an instance of the class but require access to the class's internals (such as factory methods), you can declare them within a companion object inside the class. Companion object members are accessible using the class name as a qualifier, eliminating the need to create an instance of the class to call them.

Tags: kotlin Object-Oriented Programming Kotlin Classes Kotlin Inheritance Kotlin Constructors

Posted on Sat, 09 May 2026 20:23:40 +0000 by creet0n