Question

Kotlin Getters and Setters: Properties, Accessors, and Visibility

kotlingetter-setter

Question

In Java, I can write getters and setters manually or generate them with an IDE or Lombok annotations such as @Getter.

Kotlin properties provide getters and setters automatically, but I do not understand how to define and use custom accessors. For example, I want a private value with a getter similar to Java:

private val isEmpty: String
    get() = this.toString()

However, making the getter public produces the error: Getter visibility must be the same as property visibility. How do getters and setters work in Kotlin, and how can I expose a computed value safely?

Short Answer

Kotlin treats a property as its public API rather than making you call getter and setter methods explicitly. You will learn how Kotlin generates accessors, how to write custom getters and setters, why a private property cannot have a public getter, and how backing properties provide controlled public access.

Concept

In Kotlin, a property combines a value and its accessors:

val name: String = "Ada"

A val has a getter. A var has both a getter and a setter. Kotlin normally generates those accessors for you.

val name: String = "Ada" // getter only
var age: Int = 20         // getter and setter

From Kotlin, you use property syntax instead of Java-style method calls:

println(person.name) // Calls the getter
person.age = 21      // Calls the setter

On the JVM, Kotlin usually creates Java-compatible getter and setter methods behind the scenes. For example, Java code can commonly call getName() and setAge(...).

A custom getter calculates or transforms a value when the property is read:

val message: String
    get() = "Hello"

The property's visibility is also the visibility of its getter. A private property is intentionally unavailable outside its class, so Kotlin does not allow that same property's getter to become public. If outside code could call the getter, the property would not really be private.

To expose a computed value, make the exposed property public. To hide stored implementation data while exposing a safe read-only value, use a separate private backing property and a public val.

Mental Model

Think of a property as a labelled control panel on a machine.

  • A getter is the display: it lets someone read a value.
  • A setter is the input control: it lets someone change a value.
  • val means the display is available, but there is no public input control.
  • var means both display and input controls exist.
  • private means the entire control panel is inside the machine. People outside cannot use its getter either.

If you need an internal machine component but want to show selected information externally, keep the component in a private box and create a separate public display for it.

Take Quiz

Syntax and Examples

A property with Kotlin's default getter:

class User {
    val name: String = "Ada"
}

fun main() {
    val user = User()
    println(user.name)
}

name is a val, so callers can read it but cannot assign a new value.

class Counter {
    var count: Int = 0
}

fun main() {
    val counter = Counter()
    counter.count = 1
    println(counter.count)
}

count is a var, so Kotlin provides both a getter and a setter.

Custom getter

Use get() when the value should be calculated as it is read:

class Greeting(private val firstName: String, private val lastName: String) {
    val fullName: String
        get() = "$firstName $lastName"
}

fun main() {
    val greeting = Greeting("Ada", "Lovelace")
    println(greeting.fullName)
}

fullName does not need stored data of its own. Its getter creates the result each time it is accessed.

For the question's example, use a public computed property:

class Example {
    val textRepresentation: String
        get() = toString()
}

A name beginning with is usually represents a Boolean in Kotlin. For example:

class Basket(private val items: List<String>) {
    val isEmpty: Boolean
        get() = items.isEmpty()
}

Custom setter

A custom setter can validate or normalize incoming values. The special name field refers to the property's backing field:

class Profile {
    var username: String = "guest"
        set(value) {
            require(value.isNotBlank()) { "Username cannot be blank" }
            field = value.trim()
        }
}

field is available only inside an accessor. It avoids recursively assigning to the setter itself.

Step by Step Execution

Consider this class:

class Temperature {
    var celsius: Double = 0.0
        set(value) {
            require(value >= -273.15) { "Temperature is below absolute zero" }
            field = value
        }

    val fahrenheit: Double
        get() = celsius * 9 / 5 + 32
}

fun main() {
    val temperature = Temperature()
    temperature.celsius = 20.0
    println(temperature.fahrenheit)
}

Execution flow:

  1. Temperature() creates an object. celsius starts as 0.0.
  2. temperature.celsius = 20.0 calls the custom setter.
  3. The setter checks that 20.0 is not below -273.15.
  4. field = value stores 20.0 in the backing field for celsius.
  5. temperature.fahrenheit calls its custom getter.
  6. The getter reads celsius, calculates 20.0 * 9 / 5 + 32, and returns 68.0.
  7. println prints 68.0.

No value is stored for fahrenheit; it is calculated whenever it is requested.

Real World Use Cases

Custom accessors are useful when reading or changing a value needs rules.

  • Read-only API state: Store mutable data privately but expose a public val so callers can inspect it without modifying it.
  • Validation: Reject invalid values such as blank usernames, negative quantities, or impossible dates in a setter.
  • Normalization: Trim whitespace, convert a code to uppercase, or constrain a numeric value before storing it.
  • Computed UI values: Build a display name, total price, or formatted label from other properties.
  • Derived data: Calculate whether an order can be shipped from inventory and payment status.
  • Encapsulation: Change the internal storage later without changing the public property used by other code.

Example: exposing an immutable view of an internally mutable collection:

class ShoppingCart {
    private val mutableItems = mutableListOf<String>()

    val items: List<String>
        get() = mutableItems.toList()

    fun add(item: String) {
        mutableItems.add(item)
    }
}

Code outside the class can read items, but it cannot modify the cart's internal list.

Take Quiz

Real Codebase Usage

In real Kotlin projects, accessors are usually kept small and predictable. Common patterns include the following.

Public read, restricted write

Use a var with a private setter when other code may read a value but only the class may change it:

class Download {
    var progress: Int = 0
        private set

    fun updateProgress(newProgress: Int) {
        progress = newProgress.coerceIn(0, 100)
    }
}

This is often preferable to a public setter because it protects the object's invariants.

Private backing property and public read-only property

Use a private mutable value plus a public immutable view:

class Session {
    private var _token: String? = null

    val isAuthenticated: Boolean
        get() = _token != null

    fun signIn(token: String) {
        _token = token
    }

    fun signOut() {
        _token = null
    }
}

Derived properties instead of duplicated state

Avoid storing values that can be reliably calculated from other values:

class Invoice(val subtotal: Double, val taxRate: Double) {
    val total: Double
        get() = subtotal * (1 + taxRate)
}

This prevents total from becoming out of sync with subtotal or taxRate.

Guard clauses in setters

For simple validation, require clearly communicates that invalid input is a caller error:

var quantity: Int = 1
    set(value) {
        require(value > 0) { "Quantity must be positive" }
        field = value
    }

Common Mistakes

Trying to make a private property's getter public

This does not work:

class Example {
    private val secret: String
        public get() = "visible"
}

A private property cannot expose a public getter. Instead, define a separate public property:

class Example {
    private val secret = "visible"

    val displayValue: String
        get() = secret
}

Using var when callers should not assign values

If a value should be read-only from outside the class, prefer val:

val fullName: String
    get() = "$firstName $lastName"

Or use a private setter when the class needs to update it internally:

var status: String = "New"
    private set

Recursive setter assignment

This setter calls itself forever:

var score: Int = 0
    set(value) {
        score = value // Incorrect: calls the setter again
    }

Use field to assign the backing field:

var score: Int = 0
    set(value) {
        field = value
    }

Expecting field in every getter

A computed property may not have a backing field:

val currentTime: Long
    get() = System.currentTimeMillis()

Use field only when the property actually stores a value and the accessor needs that stored value.

Naming a non-Boolean value isEmpty

By convention, isEmpty should be Boolean:

val isEmpty: Boolean
    get() = items.isEmpty()

Use names such as text, description, or displayValue for String properties.

Comparisons

FeaturevalvarCustom getterCustom setter
Can be readYesYesYesYes
Can be assigned after initializationNoYesDepends on whether it is a varYes, if setter is accessible
Has a getterYesYesYou define its behaviorUses the getter normally
Has a setterNoYesNo effect by itselfYou define assignment behavior
Typical useRead-only valuesMutable stateDerived or formatted valuesValidation and controlled updates
ApproachBest whenExample
Public valThe value is public and read-onlyval fullName get() = ...
var with private setEveryone may read, but only the class may change itvar progress = 0; private set
Private backing property + public valInternal data and public API should be separateprivate var _items; val items get() = ...
Public varExternal callers are allowed to update state directlyForm input or simple data holder

A Kotlin property is similar to Java fields plus getter/setter methods, but Kotlin presents them as one language feature. Kotlin callers normally use object.property, even when custom accessor code runs.

Take Quiz

Cheat Sheet

// Read-only property: getter is generated
val name: String = "Ada"

// Mutable property: getter and setter are generated
var age: Int = 20

// Computed read-only property
val fullName: String
    get() = "$firstName $lastName"

// Public read, class-only write
var status: String = "New"
    private set

// Validate before storing
var quantity: Int = 1
    set(value) {
        require(value > 0)
        field = value
    }

// Private storage, public read-only view
private val _items = mutableListOf<String>()
val items: List<String>
    get() = _items.toList()

Rules to remember:

  • val has a getter but no setter.
  • var has both a getter and setter.
  • Use get() for calculated or transformed reads.
  • Use set(value) for validation or transformation during assignment.
  • Use field inside an accessor to access stored property data.
  • A private property cannot have a public getter.
  • Use private set to allow public reads and private writes.
  • Prefer isSomething: Boolean for names beginning with is.

FAQ

Does Kotlin automatically create getters and setters?

Yes. Every val has a getter, and every var has both a getter and a setter unless you define custom behavior.

How do I call a getter in Kotlin?

Use property syntax: user.name. Do not normally write user.getName() in Kotlin.

Can a private Kotlin property have a public getter?

No. The getter's visibility must match the property's visibility. Create a separate public property that returns the private value instead.

How can I make a property readable publicly but writable only inside the class?

Use var with private set:

var state = "idle"
    private set

What is field in a Kotlin getter or setter?

field is the property's backing field. It is available only inside an accessor and lets you read or write stored data without recursively calling the accessor.

Should I use a custom getter for every property?

No. Use the default generated accessors unless you need computed output, validation, transformation, or access control.

Does a computed val store its result?

Usually not. A getter such as get() = firstName + lastName calculates the result every time it is read unless you explicitly store or cache it.

Take Quiz

Mini Project

Description

Build a small Account class that protects its internal balance while exposing useful derived information. The project demonstrates a private backing property, a public read-only property, a custom computed getter, and controlled methods for state changes.

Goal

Create an account object that allows deposits and withdrawals while preventing outside code from assigning the balance directly.

Requirements

  1. Store the account balance as private mutable state.
  2. Expose a public read-only balance property.
  3. Add an isOverdrawn Boolean computed property.
  4. Provide a deposit function that accepts only positive amounts.
  5. Provide a withdraw function that accepts only positive amounts.
  6. Print the final balance and overdraft status.
Take Quiz

Keep learning

Related questions

View more