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):
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:
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:
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.
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:
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).
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:
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.
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:
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.
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:
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.
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:
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:
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:
- Default accessors: Kotlin automatically generates getters for all properties and setters for mutable properties.
- Custom getters and setters: You can define your own implementation to add validation or compute values.
- Access control: You can modify the visibility of getters and setters independently.
- Backing properties: A pattern to control the external representation of a property.
- 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:
-
Create a
BankAccount
class with a privatebalance
property and methods to deposit and withdraw money. Add validation to ensure that withdrawals don't exceed the balance. -
Implement a
Circle
class with aradius
property. Add custom setters to prevent negative radius values. Include computed properties forarea
andcircumference
. -
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). -
Extend the library management system to include a
Member
class with properties for name, membership status, and borrowed books.
Additional Resources
- Kotlin Official Documentation on Properties
- Kotlin In Action - Chapter 4 covers properties in depth
- Kotlin Bootcamp for Programmers - Google's free Kotlin course
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)