Skip to main content

Kotlin Getters and Setters

Introduction

In object-oriented programming, encapsulation is an important concept that involves bundling data and methods that operate on that data within a single unit (a class) and restricting direct access to some of the object's components. Getters and setters are key components of encapsulation, allowing controlled access to a class's properties.

Kotlin provides a modern and concise approach to implementing getters and setters through its property system. Unlike Java, where you need to explicitly write getter and setter methods, Kotlin generates them automatically for properties, making your code cleaner and more maintainable.

In this tutorial, we'll explore how to work with getters and setters in Kotlin and how they differ from traditional approaches in other languages.

Basic Properties in Kotlin

In Kotlin, a class property is declared using the var keyword (for mutable properties) or the val keyword (for read-only properties):

kotlin
class Person {
var name: String = "John"
val birthYear: Int = 1990
}

When you declare a property like this, Kotlin automatically creates:

  • A field to store the property's value
  • A getter for retrieving the value
  • A setter for updating the value (only for var properties)

You can use these properties like this:

kotlin
fun main() {
val person = Person()

// Using the getter (automatically called)
println("Name: ${person.name}")
println("Birth year: ${person.birthYear}")

// Using the setter (automatically called)
person.name = "Alice"
println("Updated name: ${person.name}")

// This would cause a compilation error since birthYear is read-only (val)
// person.birthYear = 1995
}

Output:

Name: John
Birth year: 1990
Updated name: Alice

Custom Getters and Setters

While Kotlin provides default implementations for getters and setters, you can define your own custom implementations to add additional logic.

Custom Getters

Custom getters allow you to compute the property value on-the-fly rather than retrieving it from a backing field:

kotlin
class Rectangle(val width: Int, val height: Int) {
val area: Int
get() = width * height
}

In this example, area doesn't have a backing field. Instead, its value is calculated every time the property is accessed.

kotlin
fun main() {
val rectangle = Rectangle(5, 3)
println("Area: ${rectangle.area}") // Output: Area: 15
}

Custom Setters

Custom setters allow you to add validation or other logic when a property is assigned a new value:

kotlin
class User {
var name: String = ""
set(value) {
if (value.isNotEmpty()) {
field = value
} else {
println("Name cannot be empty")
}
}

var age: Int = 0
set(value) {
if (value >= 0) {
field = value
} else {
println("Age cannot be negative")
}
}
}

The field identifier is a special reference to the backing field of the property. It's only available inside the accessors (getters and setters).

kotlin
fun main() {
val user = User()

user.name = "Alice"
println("Name: ${user.name}") // Output: Name: Alice

user.name = "" // Output: Name cannot be empty
println("Name: ${user.name}") // Output: Name: Alice (unchanged)

user.age = 25
println("Age: ${user.age}") // Output: Age: 25

user.age = -5 // Output: Age cannot be negative
println("Age: ${user.age}") // Output: Age: 25 (unchanged)
}

Visibility Modifiers for Getters and Setters

Kotlin allows you to change the visibility of getters and setters:

kotlin
class Account {
var balance: Double = 0.0
private set // The setter is private, but the getter remains public

fun deposit(amount: Double) {
if (amount > 0) {
balance += amount
}
}

fun withdraw(amount: Double): Boolean {
if (amount > 0 && balance >= amount) {
balance -= amount
return true
}
return false
}
}

In this example, the balance property can be read from outside the class, but it can only be modified through the deposit and withdraw methods.

kotlin
fun main() {
val account = Account()

account.deposit(100.0)
println("Balance: ${account.balance}") // Output: Balance: 100.0

val withdrawSuccess = account.withdraw(50.0)
println("Withdrawal successful: $withdrawSuccess") // Output: Withdrawal successful: true
println("New balance: ${account.balance}") // Output: New balance: 50.0

// This would cause a compilation error:
// account.balance = 1000.0 // Cannot assign to 'balance': the setter is private
}

Late-Initialized Properties

Sometimes you need to create properties that will be initialized later, but you don't want to make them nullable. Kotlin provides the lateinit modifier for this purpose:

kotlin
class School {
lateinit var principal: String

fun assignPrincipal(name: String) {
principal = name
}

fun getPrincipalInfo(): String {
return if (::principal.isInitialized) {
"The principal is $principal"
} else {
"No principal assigned yet"
}
}
}

Note the use of ::principal.isInitialized to check if the property has been initialized.

kotlin
fun main() {
val school = School()

println(school.getPrincipalInfo()) // Output: No principal assigned yet

school.assignPrincipal("Dr. Smith")
println(school.getPrincipalInfo()) // Output: The principal is Dr. Smith
}

Backing Properties

Sometimes you may want to have a different representation of a property externally than what is used internally. You can achieve this with backing properties:

kotlin
class Temperature {
private var _celsius: Double = 0.0

var celsius: Double
get() = _celsius
set(value) {
_celsius = value
}

var fahrenheit: Double
get() = _celsius * 9/5 + 32
set(value) {
_celsius = (value - 32) * 5/9
}
}

This pattern is commonly used when implementing observables or when you need more control over property access.

kotlin
fun main() {
val temp = Temperature()

temp.celsius = 25.0
println("Temperature in Celsius: ${temp.celsius}°C") // Output: Temperature in Celsius: 25.0°C
println("Temperature in Fahrenheit: ${temp.fahrenheit}°F") // Output: Temperature in Fahrenheit: 77.0°F

temp.fahrenheit = 68.0
println("Updated temperature in Celsius: ${temp.celsius}°C") // Output: Updated temperature in Celsius: 20.0°C
println("Updated temperature in Fahrenheit: ${temp.fahrenheit}°F") // Output: Updated temperature in Fahrenheit: 68.0°F
}

Real-World Example: A Library Management System

Let's create a more complex example that demonstrates the use of getters and setters in a practical scenario:

kotlin
class Book(title: String, author: String) {
var title: String = title
set(value) {
if (value.isBlank()) {
println("Title cannot be blank")
} else {
field = value
}
}

var author: String = author
set(value) {
if (value.isBlank()) {
println("Author cannot be blank")
} else {
field = value
}
}

var isCheckedOut: Boolean = false
private set

var checkoutCount: Int = 0
private set

val isPopular: Boolean
get() = checkoutCount > 10

fun checkOut(): Boolean {
if (!isCheckedOut) {
isCheckedOut = true
checkoutCount++
return true
}
return false
}

fun returnBook() {
isCheckedOut = false
}

override fun toString(): String {
return "$title by $author (Checked out: $isCheckedOut, Checkout count: $checkoutCount)"
}
}

class Library {
private val books = mutableListOf<Book>()

val bookCount: Int
get() = books.size

val availableBookCount: Int
get() = books.count { !it.isCheckedOut }

val popularBooks: List<Book>
get() = books.filter { it.isPopular }

fun addBook(book: Book) {
books.add(book)
}

fun findBookByTitle(title: String): Book? {
return books.find { it.title.equals(title, ignoreCase = true) }
}
}

Let's see how we can use this library system:

kotlin
fun main() {
val library = Library()

// Add some books
library.addBook(Book("1984", "George Orwell"))
library.addBook(Book("To Kill a Mockingbird", "Harper Lee"))
library.addBook(Book("The Great Gatsby", "F. Scott Fitzgerald"))

println("Total books in library: ${library.bookCount}")
println("Available books: ${library.availableBookCount}")

// Check out a book
val book = library.findBookByTitle("1984")
if (book != null) {
val checkoutSuccess = book.checkOut()
println("Checkout successful: $checkoutSuccess")
println("Book details: $book")
}

println("Available books after checkout: ${library.availableBookCount}")

// Simulate multiple checkouts to make a book popular
val gatsby = library.findBookByTitle("The Great Gatsby")
if (gatsby != null) {
repeat(11) {
gatsby.checkOut()
gatsby.returnBook()
}
println("Is 'The Great Gatsby' popular? ${gatsby.isPopular}")
}

println("Popular books in the library: ${library.popularBooks.map { it.title }}")

// Try to update a book with invalid data
book?.title = "" // This should show: Title cannot be blank
println("Book title after invalid update: ${book?.title}")
}

Output:

Total books in library: 3
Available books: 3
Checkout successful: true
Book details: 1984 by George Orwell (Checked out: true, Checkout count: 1)
Available books after checkout: 2
Is 'The Great Gatsby' popular? true
Popular books in the library: [The Great Gatsby]
Title cannot be blank
Book title after invalid update: 1984

Summary

In this tutorial, we've explored how Kotlin handles getters and setters:

  1. Default accessors: Kotlin automatically generates getters for all properties and setters for mutable properties.
  2. Custom getters and setters: You can define your own implementation to add validation or compute values.
  3. Access control: You can modify the visibility of getters and setters independently.
  4. Backing properties: A pattern to control the external representation of a property.
  5. Late initialization: The lateinit modifier allows non-null properties to be initialized later.

Kotlin's property system eliminates a lot of the boilerplate code that would be required in other languages while still providing full control over property access when needed.

Exercises

To solidify your understanding of getters and setters in Kotlin, try these exercises:

  1. Create a BankAccount class with a private balance property and methods to deposit and withdraw money. Add validation to ensure that withdrawals don't exceed the balance.

  2. Implement a Circle class with a radius property. Add custom setters to prevent negative radius values. Include computed properties for area and circumference.

  3. Design a Student class with properties for name, age, and a list of grades. Add computed properties for the average grade and whether the student passed (average >= 60).

  4. Extend the library management system to include a Member class with properties for name, membership status, and borrowed books.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)