AlgoGalaxy

AlgoGalaxy

University
CodeRush
Jobs
Tools
Insights
Explore
Personalize
JavaGoPHPRustGraphsTreesArraysDynamic ProgrammingGreedyHashingHeapLinked ListNumber TheoryStackStringsTrieUnion FindBacktrackingBinary SearchBit Manipulation

Kotlin Inheritance and open Classes

Kotlin makes classes final by default because unrestricted inheritance can quietly break the assumptions inside a base class. Mark a class or member open only when subclasses are part of the design. For independent behavior, use composition: one object receives another capability rather than pretending it is a specialized version of it.

Work through the programs in order. Each example changes one idea at a time, includes the expected result, and asks you to make a small change. That practice turns object-oriented vocabulary into code you can confidently write, review, and test.

What You Will Build

This is a code lab, not a compressed definition. The five examples begin with the smallest working form, then add one responsibility, one rule, or one alternative path. Keep a Kotlin compiler open while reading and run every block before moving to the next one.

In production code, models should make ownership, mutation, and failure visible. Give a class one reason to change, validate at its boundary, and return a named result when an operation can be rejected. Kotlin syntax supports that clarity, but it cannot make an unstated business rule safe.

Example 1: A Kotlin class is final by default

The default protects a class from surprise overrides. A small final class is often the clearest model when no extension point is required.

Kotlin

class Receipt(val number: Int) {
    fun label(): String = "Receipt #$number"
}

fun main() {
    println(Receipt(42).label())
}

Expected Result

Receipt #42

Try next: Keep this class final unless you can state exactly what a subclass may change safely.

Trace the program from construction through its printed result. Change one input before you continue; a reliable object model should make the new outcome predictable without hidden mutable state or a surprise fallback.

Example 2: Open an intentional extension point

open is a contract: subclasses are now allowed to replace behavior. Keep the base behavior small enough that a caller can still understand the promise.

Kotlin

open class Formatter {
    open fun format(value: String): String = value.trim()
}

class UppercaseFormatter : Formatter() {
    override fun format(value: String): String = super.format(value).uppercase()
}

fun main() {
    println(UppercaseFormatter().format("  kotlin  "))
}

Expected Result

KOTLIN

Try next: Remove the call to super and observe how an override can accidentally discard an existing rule.

Trace the program from construction through its printed result. Change one input before you continue; a reliable object model should make the new outcome predictable without hidden mutable state or a surprise fallback.

Example 3: Pass base behavior through a shared type

A function can accept the base type and work with every valid subtype. It should depend only on the behavior promised by that base type.

Kotlin

open class Animal {
    open fun sound(): String = "sound"
}

class Cat : Animal() {
    override fun sound(): String = "meow"
}

fun announce(animal: Animal) {
    println(animal.sound())
}

fun main() {
    announce(Cat())
}

Expected Result

meow

Try next: Add Dog without changing announce. If announce needs a subtype check, the base contract may be too weak.

Trace the program from construction through its printed result. Change one input before you continue; a reliable object model should make the new outcome predictable without hidden mutable state or a surprise fallback.

Example 4: Use composition for a changing collaborator

A report is not a formatter; it uses one. Composition makes the dependency explicit and lets another formatter be supplied without inventing an inheritance hierarchy.

Kotlin

class Formatter {
    fun format(value: String): String = value.uppercase()
}

class Report(private val formatter: Formatter) {
    fun render(title: String): String = formatter.format(title)
}

fun main() {
    println(Report(Formatter()).render("weekly report"))
}

Expected Result

WEEKLY REPORT

Try next: Create a second Formatter implementation with different behavior and pass it to Report deliberately.

Trace the program from construction through its printed result. Change one input before you continue; a reliable object model should make the new outcome predictable without hidden mutable state or a surprise fallback.

Example 5: Protect a base invariant

A base operation can be final even when the class is open. This prevents a subclass from bypassing the rule that creates a valid result.

Kotlin

open class Message {
    final fun send(): String = "sent: ${body()}"
    open fun body(): String = "base"
}

class Welcome : Message() {
    override fun body(): String = "welcome"
}

fun main() {
    println(Welcome().send())
}

Expected Result

sent: welcome

Try next: Identify which behavior must never change in your own base class and keep that method final.

Trace the program from construction through its printed result. Change one input before you continue; a reliable object model should make the new outcome predictable without hidden mutable state or a surprise fallback.

Example 6: Make the input visible

Start by naming the value the next line depends on. This keeps the lesson input explicit before we apply a transformation or call an external boundary. We are applying that habit while revisiting Kotlin Inheritance and open Classes.

Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.

Kotlin

fun review(value: String?): String =
    value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
        ?: "needs a value"

fun main() {
    println(review("Kotlin Inheritance and open Classes"))
}

Expected Result

KOTLIN INHERITANCE AND OPEN CLASSES

Try next: Replace the sample value with one from your own notes, predict the result, and then run it.

Example 7: Guard an empty boundary

A small guard makes the allowed and rejected path visible. We want a caller to receive an intentional result instead of discovering an unexplained failure later. We are applying that habit while revisiting Kotlin Inheritance and open Classes.

Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.

Kotlin

fun review(value: String?): String =
    value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
        ?: "needs a value"

fun main() {
    println(review("Kotlin Inheritance and open Classes"))
}

Expected Result

KOTLIN INHERITANCE AND OPEN CLASSES

Try next: Try an empty value as well as the normal value and describe why the two outcomes differ.

Example 8: Keep the transformation pure

This step returns a value instead of mixing the rule with display or storage work. That separation gives us a small unit we can run, test, and reuse. We are applying that habit while revisiting Kotlin Inheritance and open Classes.

Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.

Kotlin

fun review(value: String?): String =
    value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
        ?: "needs a value"

fun main() {
    println(review("Kotlin Inheritance and open Classes"))
}

Expected Result

KOTLIN INHERITANCE AND OPEN CLASSES

Try next: Call the same rule twice with different inputs and confirm that each result depends only on its input.

Example 9: Make a branch deliberate

Use a named branch where the business rule changes. A future reader should be able to point at the condition and state the result for both sides. We are applying that habit while revisiting Kotlin Inheritance and open Classes.

Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.

Kotlin

fun review(value: String?): String =
    value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
        ?: "needs a value"

fun main() {
    println(review("Kotlin Inheritance and open Classes"))
}

Expected Result

KOTLIN INHERITANCE AND OPEN CLASSES

Try next: Change the boundary value by one and explain exactly which branch becomes active.

Example 10: Use a collection without hidden mutation

Collections become safer when we can see which value is read, which value is derived, and whether the original collection remains owned by the caller. We are applying that habit while revisiting Kotlin Inheritance and open Classes.

Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.

Kotlin

fun review(value: String?): String =
    value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
        ?: "needs a value"

fun main() {
    println(review("Kotlin Inheritance and open Classes"))
}

Expected Result

KOTLIN INHERITANCE AND OPEN CLASSES

Try next: Add one more item and predict the derived result before executing the code.

Example 11: Return a named outcome

A named outcome is easier to test than an implied side effect. We can now distinguish a successful path from a rejected path without guessing from console noise. We are applying that habit while revisiting Kotlin Inheritance and open Classes.

Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.

Kotlin

fun review(value: String?): String =
    value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
        ?: "needs a value"

fun main() {
    println(review("Kotlin Inheritance and open Classes"))
}

Expected Result

KOTLIN INHERITANCE AND OPEN CLASSES

Try next: Rename the outcome in your own words while preserving the same success and failure rule.

Example 12: Trace a small loop

Work through one value at a time. The point is not to memorize syntax; it is to see how the accumulator or output changes after each repeatable step. We are applying that habit while revisiting Kotlin Inheritance and open Classes.

Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.

Kotlin

fun review(value: String?): String =
    value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
        ?: "needs a value"

fun main() {
    println(review("Kotlin Inheritance and open Classes"))
}

Expected Result

KOTLIN INHERITANCE AND OPEN CLASSES

Try next: Use a shorter collection first, then add one value and trace the extra iteration aloud.

Example 13: Keep display at the edge

The core rule prepares a value, while the outer line decides how to show it. That keeps the result usable by a UI, test, API, or command-line caller. We are applying that habit while revisiting Kotlin Inheritance and open Classes.

Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.

Kotlin

fun review(value: String?): String =
    value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
        ?: "needs a value"

fun main() {
    println(review("Kotlin Inheritance and open Classes"))
}

Expected Result

KOTLIN INHERITANCE AND OPEN CLASSES

Try next: Change only the display text and verify that the underlying rule and result stay unchanged.

Example 14: Test the closest boundary

A useful example includes the nearest boundary, not only the happy path. We are making the behavior inspectable before a real caller relies on it. We are applying that habit while revisiting Kotlin Inheritance and open Classes.

Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.

Kotlin

fun review(value: String?): String =
    value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
        ?: "needs a value"

fun main() {
    println(review("Kotlin Inheritance and open Classes"))
}

Expected Result

KOTLIN INHERITANCE AND OPEN CLASSES

Try next: Try the lowest supported value and then one value just outside the accepted range.

Example 15: Write a tiny regression check

A compact assertion turns the lesson into a repeatable promise. It helps us catch a changed rule before a larger program hides the source of the mistake. We are applying that habit while revisiting Kotlin Inheritance and open Classes.

Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.

Kotlin

fun review(value: String?): String =
    value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
        ?: "needs a value"

fun main() {
    println(review("Kotlin Inheritance and open Classes"))
}

Expected Result

KOTLIN INHERITANCE AND OPEN CLASSES

Try next: Deliberately change the expected value once, observe the failed check, and then restore the correct contract.

Practice Checklist

Reproduce all five programs from memory before moving forward. Then make one small original model with a meaningful constructor, a named operation, and an explicit result for invalid input or an alternate state.

  • Practice 1: recreate “A Kotlin class is final by default” without copying, change one value, and explain the result before running it.
  • Practice 2: recreate “Open an intentional extension point” without copying, change one value, and explain the result before running it.
  • Practice 3: recreate “Pass base behavior through a shared type” without copying, change one value, and explain the result before running it.
  • Practice 4: recreate “Use composition for a changing collaborator” without copying, change one value, and explain the result before running it.
  • Practice 5: recreate “Protect a base invariant” without copying, change one value, and explain the result before running it.

If you cannot explain who owns a mutable value, which inputs are valid, or what the caller sees on failure, the design needs one more clear boundary before it grows.

Common Mistakes

  • Making every property public and mutable before defining the class operation that should own the change.
  • Using inheritance just to reuse a few lines when a focused collaborator would make the relationship clearer.
  • Returning null or an unlabelled Boolean when a caller needs an explicit success, rejection, loading, or failure state.
  • Copying a working program without changing an input and therefore never testing whether the rule was understood.
logo of AlgoGalaxy

Newsletter

Subscribe to our newsletter for weekly updates and promotions.

AlgoGalaxy University

University HomeTechnology LearningCoding ProblemsLearning ProgressCareer OpportunitiesTechnology InsightsUtilNexa ToolsAlgoGalaxy HomeContact Support

AlgoGalaxy CodeRush

CodeRush HomeProblem SetsProgress TrackerLearn TechnologyTechnology JobsEngineering InsightsDeveloper UtilitiesAlgoGalaxy HomePrivacy Policy
Privacy Policy

 • 

Terms of Service

Copyright © AlgoGalaxy 2026