Properties

Reviewed & published by Brayan K

Kotlin is a modern, concise language where properties replace Java's fields and getters/setters — you can add custom accessors, computed values, and deferred initialisation with very little code.

Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

By the end of this lesson you'll write custom getters and setters, computed properties, and use lateinit and by lazy.

What You'll Learn in This Lesson

1️⃣ Custom Getters and Setters

A property can define a custom get() that computes its value, or a custom set(value) that validates or transforms what's stored. Inside a setter, the keyword field refers to the backing storage so you avoid infinite recursion.

class Temperature(var celsius: Double) {

    // A COMPUTED property: no backing field, just a custom getter.
    val fahrenheit: Double
        get() = celsius * 9 / 5 + 32

    // A property with a custom SETTER that validates input.
    var label: String = "normal"
        set(value) {
            field = value.trim().lowercase()   // 'field' is the backing field
        }
}

fun main() {
    val t = Temperature(25.0)
    println("${t.celsius}C = ${t.fahrenheit}F")  // -> 25.0C = 77.0F

    t.celsius = 30.0
    println("Now ${t.fahrenheit}F")              // recomputed -> Now 86.0F

    t.label = "  HOT  "
    println("Label: ${t.label}")                 // -> Label: hot
}

fahrenheit has no stored value — it's recomputed from celsius every time. The label setter cleans up input before storing it in field.

2️⃣ lateinit and by lazy

lateinit var lets you declare a non-null property you'll assign later — handy when the value isn't ready at construction. by lazy computes a value once, on first access, then caches it for all future reads.

class Config {
    // lateinit: promise to set this non-null property before first use.
    // Useful for values initialized after construction (e.g. setup).
    lateinit var environment: String

    // lazy: computed ONCE on first access, then cached.
    val settings: Map<String, String> by lazy {
        println("(loading settings...)")
        mapOf("timeout" to "30", "retries" to "3")
    }

    fun setup() {
        environment = "production"
    }
}

fun main() {
    val config = Config()
    config.setup()
    println("Env: ${config.environment}")     // -> Env: production

    // First access triggers the lazy block:
    println(config.settings["timeout"])       // prints loading, then 30
    // Second access uses the cached value (no loading message):
    println(config.settings["retries"])       // -> 3
}

Notice the loading message prints only once, even though settings is read twice — that's lazy caching the first result.

Your turn. Replace the TODO, then run and compare.

// 🎯 YOUR TURN — replace each TODO then run main()
class Circle(var radius: Double) {

    // 1) A computed property for the area: pi * r * r
    //    (use Math.PI). Give it a custom getter.
    val area: Double
        get() = Math.PI * radius TODO radius   // 👉 replace TODO with '*'

    // A computed property for the diameter:
    val diameter: Double
        get() = radius * 2
}

fun main() {
    val c = Circle(2.0)
    println("Diameter: ${c.diameter}")        // expect Diameter: 4.0
    println("Area ~ ${"%.2f".format(c.area)}") // expect Area ~ 12.57
}

Mini-Challenge: A Validated Name

Combine a custom setter (to capitalise) with a computed property (the greeting).

// 🎯 MINI-CHALLENGE: a User with a validated name.
// Save as User.kt and run main().
//
// 1. class User with a var name property.
// 2. Give name a custom setter that capitalizes the first letter
//    using value.replaceFirstChar { it.uppercase() }.
// 3. Add a computed val greeting: String = get() = "Hi, " + name + "!".
// 4. In main(), set name = "sam" and print the greeting.
//
// ✅ Example output:
//    Hi, Sam!

fun main() {
    // your code here
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Properties

ConceptKotlin Syntax
Computed propertyval f get() = c * 2
Custom setterset(v) { field = v }
Backing fieldfield
Late initlateinit var x: String
Lazyval s by lazy { ... }

🎉 Lesson Complete!

Practice quiz

What is a computed property?

  • A property cached after first access
  • A property with no stored value that calculates via a custom getter
  • A property that can never change
  • A property stored in a database

Answer: A property with no stored value that calculates via a custom getter. A computed property has only a custom getter and recalculates each time it is read.

Inside a custom setter, what does the field keyword refer to?

  • The property's backing field, the actual stored value
  • The class itself
  • A new local variable
  • The getter function

Answer: The property's backing field, the actual stored value. field is the backing field; using it avoids the infinite recursion of writing the property by name.

Why assign to field rather than the property name inside its own setter?

  • It is just a style preference
  • The property name is read-only there
  • Writing the property name calls the setter again, causing infinite recursion
  • field is faster to type

Answer: Writing the property name calls the setter again, causing infinite recursion. Assigning name = value inside name's setter calls the setter forever; field writes storage directly.

When is a property declared with lateinit allowed?

  • Only with val
  • Only with primitive types
  • With var and non-primitive types
  • With any type at all

Answer: With var and non-primitive types. lateinit works only with var and non-primitive types.

What happens if you read a lateinit property before assigning it?

  • It returns null
  • It returns a default value
  • It silently does nothing
  • It throws an UninitializedPropertyAccessException

Answer: It throws an UninitializedPropertyAccessException. Reading an unassigned lateinit throws UninitializedPropertyAccessException.

How does a by lazy property behave?

  • It recomputes on every read
  • It computes once on first access, then caches the result
  • It must be set in the constructor
  • It can only hold nullable values

Answer: It computes once on first access, then caches the result. lazy defers computation to first access and caches the value for later reads.

How does a computed property differ from a by lazy property?

  • They are identical
  • Computed caches once; lazy recomputes each time
  • Computed recomputes each read; lazy computes once and caches
  • Only lazy can have a getter

Answer: Computed recomputes each read; lazy computes once and caches. Computed properties recalculate every read; lazy computes at most once and caches.

Which property kind is best for a value derived from changing state that must always be current?

  • by lazy
  • A computed property
  • lateinit
  • A const

Answer: A computed property. A computed property always reflects current state because it recalculates on each read.

What is a good use case for lateinit?

  • A constant known at compile time
  • A non-null var initialized after construction, e.g. in a setup method
  • A read-only val
  • Caching an expensive computation

Answer: A non-null var initialized after construction, e.g. in a setup method. lateinit suits non-null vars set after construction, like dependency injection or setup.

What is the purpose of a custom setter that validates input?

  • To make the property read-only
  • To transform or check values before storing them in field
  • To cache the value
  • To defer initialization

Answer: To transform or check values before storing them in field. A custom setter can clean, validate, or transform the value before writing it to field.

Continue this course

Frequently asked questions

What is a computed property?

A computed property has no stored value of its own; instead it defines a custom getter that calculates its value each time it is read. For example val fahrenheit: Double get() = celsius * 9 / 5 + 32 always reflects the current celsius value. Computed properties are perfect for values that are derived from other state, keeping them automatically in sync without manual recalculation.

What is the 'field' keyword in a setter?

Inside a custom getter or setter, field refers to the property's backing field — the actual stored value. You use it to read or write the underlying storage without causing infinite recursion. Writing field = value.trim() stores the trimmed value; writing name = value inside name's own setter would call the setter again forever. Only properties that need stored state have a backing field.

When should I use lateinit?

lateinit lets you declare a non-null var that you promise to initialize before its first use — useful for properties set in a setup method, dependency injection, or test fixtures, where a value isn't available at construction time. It avoids making the type nullable just to satisfy the compiler. The trade-off: if you read it before assigning, you get an UninitializedPropertyException. lateinit works only with var and non-primitive types.

How is 'by lazy' different from a computed property?

A computed property recalculates every time it is read. A by lazy property computes its value only once, on first access, then caches and reuses that result for all later reads. Use lazy for expensive initialization you want to defer and perform at most once — like loading configuration or building a heavy object. Use a computed property when the value depends on changing state and must always be current.