JavaRush/Courses/Kotlin SELF/Inheritance in Kotlin: fi...

Inheritance in Kotlin: final by default and the : Base (...) syntax

Open

1. Introduction

Inheritance is when we create a new class based on an existing one. The new class (the derived class) gets the properties and behavior of the base class and can add its own details. Textbooks often describe this with the phrase “so you don’t have to copy code,” but in practice the idea is a bit deeper: inheritance helps express an “is-a kind of” relationship.

Imagine that in our console money-tracking app (expenses/income) we want to store different records: an expense and an income. They share a common minimum: amount, comment, identifier. But an expense has a category, and an income has a source. We want to describe the common foundation once and then extend it. That’s where inheritance comes in.

But Kotlin treats inheritance cautiously. And it’s not the language being pedantic—it’s a pragmatic stance: inheritance is a powerful tool that’s easy to turn into a “spaghetti hierarchy,” where one class drags along a dozen others and nobody understands whose code lives where anymore.

2. final by default and the open keyword

Let’s start with the most important rule: classes in Kotlin are final by default. This means you can’t inherit from them. And this behavior is the language norm: if the author of the class didn’t say “you may inherit,” then you may not.

The idea is this: inheritance isn’t just a “feature,” it’s a promise. If you allow others to inherit from your class, then as the class author you accept that someone will extend its behavior. That means you need to think about contract stability, protected details, which methods are safe to override, and so on. Kotlin makes you take this decision explicitly.

Practically, it looks like this: you write a normal class, then try to create a subclass—and the compiler says “you can’t.” Example:

class Box(val value: Int)

// class IntBox(value: Int) : Box(value) // won't compile: Box is final by default

How to allow inheritance with open

To make a class inheritable, you need to mark it with the open keyword.

open class Message(val text: String)

class ErrorMessage(text: String) : Message(text)

Here Message is open for inheritance, so ErrorMessage can inherit from it. It’s important not to mix this up: open applies specifically to the ability to be a base class. If open is not written, the class is closed.

Sometimes a beginner treats open as “let’s keep it just in case.” I wouldn’t do that. In Kotlin it’s good style to open inheritance only when you actually plan a hierarchy and want to allow extension.

3. The : Base(...) syntax and calling the base-class constructor

Now let’s move on to the most “mechanical” part: what inheritance looks like in code.

In Kotlin, inheritance is written with a colon after the class header:

open class Base(p: Int)

class Derived(p: Int) : Base(p)

After : you specify the base type, and if it’s a class, then next comes a call to its constructor.

Why we call the base-class constructor

Because the base class is part of the object. When you create a derived instance, a correct “base-class part” must be created inside it first. That means the base class must receive the parameters it needs.

If the base class has parameters in its primary constructor, the derived class must pass those parameters.

Do we need parentheses after Base

If the base type is a class, then yes: parentheses are required, even if the constructor has no parameters. This is a common small stumbling block for beginners: visually you want to write just : Base, but Kotlin expects : Base().

Example with an “empty” constructor:

open class Parent

class Child : Parent()

Parent has an implicit no-arg constructor, and we call it via Parent().

4. A derived class can add its own properties and methods

In this lecture we do not override behavior with override (that will be the next topic). Today it’s important to understand something else: even without override, inheritance is already useful because a derived class can extend the model with data and new methods.

For example, we have a base class “User,” and a derived class “Employee,” which has a job role:

open class Person(val name: String)

class Employee(name: String, val role: String) : Person(name)

Notice two things.

First, Employee receives name but doesn’t store it again: name is already declared in Person. That is, we avoid duplicating data.

Second, Employee gets a new property role. This is a classic scenario: “add details.”

5. What exactly open includes

To avoid the false expectation “I wrote open, so now I can change anything in the subclass,” it’s useful to lock in the distinction.

What we do What we need to write in the base class What we get
Allow inheriting from a class
open class ...
We can write
class Child : Base(...)
Allow overriding a method/property
open fun ...
/
open val ...
We can write
override ...

Key takeaway: open class allows inheritance, but does not make all members automatically overridable. In Kotlin, both classes and their members are final by default, and “dangerous capabilities” must be enabled explicitly.

6. Example: a common base class for budget entries

Now let’s carefully tie the topic to our “practical” console money-tracking app. Up to this point we might have had a single model, for example Expense (an expense). But as soon as we want to add income entries, it quickly becomes clear that expense and income share common fields.

Let’s create a base class LedgerEntry (a ledger entry) that will store the common minimum. For now, we won’t complicate the logic and we’ll limit ourselves to data.

Base class LedgerEntry

open class LedgerEntry(
    val id: Int,
    val amount: Int,
    val note: String,
)

Everything is straightforward here: open, because we’re planning subclasses.

Subclass: expense

Let’s add a category to an expense. Our categories might have already been implemented using an enum class in previous lectures.

enum class Category { FOOD, TRANSPORT, OTHER }

data class ExpenseEntry(
    val category: Category,
    val merchant: String,
    val entryId: Int,
    val entryAmount: Int,
    val entryNote: String,
) : LedgerEntry(entryId, entryAmount, entryNote)

Yes, there’s a bit of “mechanics” in the parameters here: to keep the example short, we explicitly named the parameters entryId/entryAmount/entryNote, and then passed them into the base class.

The most important part of this code is the final part:

) : LedgerEntry(entryId, entryAmount, entryNote)

This is exactly the : Base(...) syntax we’re studying today.

Subclass: income

Let’s add a source to an income entry:

data class IncomeEntry(
    val source: String,
    val entryId: Int,
    val entryAmount: Int,
    val entryNote: String,
) : LedgerEntry(entryId, entryAmount, entryNote)

So far, we’re not doing any overrides: we’re just creating a common “frame” and a few concrete variants.

A small test in main

To see that this works at all, let’s print a little output:

fun main() {
    val e = ExpenseEntry(Category.FOOD, "Coffee Shop", 1, 450, "Latte")
    val i = IncomeEntry("Salary", 2, 120_000, "I'm doing great")

    println(e.note) // Latte
    println(i.note) // I'm doing great
}

We access note even though note is declared in LedgerEntry and the object is a subclass. That’s the basic practical value of inheritance: shared properties live in one place.

7. Inheritance limitations in Kotlin

data class and inheritance

It’s useful to state a point that often surprises people.

A data class can inherit from a regular class. That’s normal and often used when the base class provides some common structure or contract. At the same time, it’s important to remember that a data class cannot be open.

So this is allowed:

open class Base(val id: Int)

data class Item(val title: String, val itemId: Int) : Base(itemId)

But this is not:

open data class Item(...) // this doesn't exist: a data class cannot be open

Why does this matter? Because beginners often want to make a “universal data model” and then inherit from it. Kotlin resists that: a data class is designed as a “concrete data model,” not as an “extension point.”

One base class

Another Kotlin rule: a class can have only one base class. That is, you can write class A : B(), but you can’t write class A : B(), C() if C is also a class.

If you’ve ever seen multiple inheritance of classes in other languages, you may have thought: “Why can’t Kotlin do that?” In plain terms—because it almost always leads to conflicts: which fields to take, which methods to call, what to do if two base classes define the same thing.

Hierarchy diagram

classDiagram
    LedgerEntry <|-- ExpenseEntry
    LedgerEntry <|-- IncomeEntry

    class LedgerEntry {
        +id: Int
        +amount: Int
        +note: String
    }

    class ExpenseEntry {
        +category: Category
        +merchant: String
    }

    class IncomeEntry {
        +source: String
    }

The diagram is simple: ExpenseEntry and IncomeEntry are different classes, but they share the same foundation: LedgerEntry.

8. Typical mistakes

Mistake #1: trying to inherit from a class without open.
This is the most common early mistake: you write class Child : Parent() and sincerely wonder why the compiler is unhappy. In Kotlin, everything is final by default, and inheritance must be enabled explicitly. If it’s your class—add open. If it’s someone else’s class (for example, from a library)—then the library author deliberately forbade inheritance, and it’s best to respect that.

Mistake #2: forgetting that the base class must be initialized.
A beginner sometimes thinks inheritance is just “a colon and a name,” and the parameters will somehow “flow through.” But the base class is part of the object, and it must be created correctly. Therefore, if the base constructor has parameters, the derived class must call Base(...) with suitable arguments.

Mistake #3: writing : Base instead of : Base() and then staring at the error for a long time.
If the base type is a class, its constructor is called, which means parentheses are required. Even if there are no parameters, write Base().

Mistake #4: expecting that open class automatically allows overriding everything.
open class means “allow inheritance,” not “allow changing any methods.” In Kotlin, members are also final by default, and to override a method it must be open.

Mistake #5: trying to make a data class the base of a hierarchy.
Intuitively you want: “let the base be a data class so it has a nice toString().” But Kotlin doesn’t allow making a data class open for inheritance, and it’s worth accepting this limitation as a design hint: the base class is more often a framework, while a data class is a specific record. If you need both—make the base an open class and the data classes the subclasses.

1
Task
Kotlin SELF,  35 level,  0 lecture
Locked
Broken inheritance
Broken inheritance
1
Task
Kotlin SELF,  35 level,  0 lecture
Locked
Article in a CMS
Article in a CMS
1
Task
Kotlin SELF,  35 level,  0 lecture
Locked
Service plugin
Service plugin
1
Task
Kotlin SELF,  35 level,  0 lecture
Locked
Error in the log
Error in the log
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet