Kotlin Backing Fields
Introduction
In Kotlin, properties are a powerful feature that allows you to define getters and setters to control how a class's attributes are accessed and modified. When you create custom getters and setters for a property, Kotlin provides a special mechanism called a backing field to store the actual value of the property.
Understanding backing fields is essential for working with Kotlin properties effectively, especially when you need custom behavior while still maintaining state. This guide will explain what backing fields are, when they're automatically generated, and how to use them properly in your Kotlin code.
What is a Backing Field?
A backing field is a special field that Kotlin generates automatically for a property when it's needed. It's accessed using the field
identifier within property accessors (getters and setters). The backing field serves as the actual storage for the property's value.
Think of a backing field as the "behind the scenes" storage that holds your property's data while your custom getters and setters control how that data is accessed and modified.
When is a Backing Field Generated?
Kotlin will automatically generate a backing field for a property if:
- You use the default getter and/or setter implementation, or
- Your custom getter and/or setter references the
field
identifier
If your property doesn't need to store state (e.g., it calculates its value on the fly), Kotlin won't generate a backing field.
Basic Example of Backing Fields
Let's start with a simple example to understand how backing fields work:
class Person {
var name: String = "Unknown"
// Default getter is used
set(value) {
println("Name changing from $field to $value")
field = value // Using the backing field
}
}
fun main() {
val person = Person()
println("Initial name: ${person.name}")
person.name = "John"
println("Updated name: ${person.name}")
}
Output:
Initial name: Unknown
Name changing from Unknown to John
Updated name: John
In this example:
- We have a
name
property with the default getter and a custom setter - The
field
identifier within the setter refers to the backing field - When we modify the
name
property, our custom setter is called, and we usefield
to access the current value and update it
Custom Getters and Setters with Backing Fields
Now let's look at a more complex example with both custom getter and setter:
class Temperature {
var celsius: Float = 0.0f
set(value) {
if (value < -273.15f) {
throw IllegalArgumentException("Temperature below absolute zero is not possible")
}
field = value // Update the backing field
}
var fahrenheit: Float
get() = (celsius * 9/5) + 32 // No backing field needed here
set(value) {
celsius = (value - 32) * 5/9 // Converts to celsius and uses celsius's backing field
}
}
fun main() {
val temp = Temperature()
temp.celsius = 25.0f
println("Celsius: ${temp.celsius}°C")
println("Fahrenheit: ${temp.fahrenheit}°F")
temp.fahrenheit = 68.0f
println("Celsius: ${temp.celsius}°C")
println("Fahrenheit: ${temp.fahrenheit}°F")
// This would throw an exception:
// temp.celsius = -300.0f
}
Output:
Celsius: 25.0°C
Fahrenheit: 77.0°F
Celsius: 20.0°C
Fahrenheit: 68.0°F
In this example:
celsius
has a custom setter that validates the value and uses a backing field to store the valuefahrenheit
is a computed property for its getter (no backing field needed)fahrenheit
's setter updates thecelsius
property instead of using its own backing field
When Not to Use Backing Fields
Interestingly, not all properties need backing fields. If a property's value is derived from other properties or computations, you may not need to store it separately:
class Rectangle(val width: Double, val height: Double) {
val area: Double
get() = width * height // No backing field needed, computed on demand
val isSquare: Boolean
get() = width == height // No backing field needed
}
fun main() {
val rect = Rectangle(5.0, 3.0)
println("Width: ${rect.width}")
println("Height: ${rect.height}")
println("Area: ${rect.area}")
println("Is square? ${rect.isSquare}")
}
Output:
Width: 5.0
Height: 3.0
Area: 15.0
Is square? false
Neither area
nor isSquare
has a backing field because their values are computed on demand.
Practical Use Cases for Backing Fields
1. Input Validation
One of the most common use cases for backing fields is validating input:
class User {
var email: String = ""
set(value) {
if (!value.contains("@")) {
throw IllegalArgumentException("Invalid email format")
}
field = value
}
var age: Int = 0
set(value) {
if (value < 0) {
throw IllegalArgumentException("Age cannot be negative")
}
field = value
}
}
fun main() {
val user = User()
user.email = "[email protected]"
user.age = 25
println("User email: ${user.email}")
println("User age: ${user.age}")
try {
user.email = "invalid-email"
} catch (e: IllegalArgumentException) {
println("Error: ${e.message}")
}
}
Output:
User email: [email protected]
User age: 25
Error: Invalid email format
2. Data Transformation
Backing fields allow you to transform data when it's stored or retrieved:
class Profile {
var name: String = ""
set(value) {
field = value.trim().capitalize()
}
var tags: List<String> = listOf()
set(value) {
field = value.map { it.lowercase() }.distinct()
}
}
fun main() {
val profile = Profile()
profile.name = " john smith "
profile.tags = listOf("Kotlin", "Programming", "KOTLIN", "coding")
println("Name: '${profile.name}'")
println("Tags: ${profile.tags}")
}
Output:
Name: 'John Smith'
3. Logging and Debugging
Backing fields are useful for logging property changes:
class ConfigSettings {
var debugMode: Boolean = false
set(value) {
println("Debug mode changing from $field to $value")
field = value
}
var maxConnections: Int = 10
set(value) {
println("Maximum connections changing from $field to $value")
field = value
}
}
fun main() {
val config = ConfigSettings()
config.debugMode = true
config.maxConnections = 20
}
Output:
Debug mode changing from false to true
Maximum connections changing from 10 to 20
Common Mistakes with Backing Fields
Recursive Stack Overflow
One common mistake is accidentally creating infinite recursion by referring to the property itself in its accessor:
class Person {
// THIS IS INCORRECT - will cause a stack overflow!
var name: String = ""
set(value) {
name = value // This calls the setter again!
}
}
The correct version would use the backing field:
class Person {
var name: String = ""
set(value) {
field = value // Use backing field instead
}
}
Forgetting to Use the Backing Field
Another mistake is forgetting to use the backing field when needed:
class Counter {
var count: Int = 0
set(value) {
if (value >= 0) {
// Forgetting to assign to field
// The value won't be stored!
}
}
}
The correct version:
class Counter {
var count: Int = 0
set(value) {
if (value >= 0) {
field = value // Don't forget to update the field!
}
}
}
Summary
Kotlin backing fields provide a way to store property values when using custom getters and setters. Here's what we covered:
- Backing fields are automatically generated when needed and accessed via the
field
identifier - They're used in custom property accessors to store and retrieve the actual value
- Not all properties need backing fields (e.g., computed properties)
- Common use cases include validation, transformation, and logging
- Backing fields help avoid recursive stack overflows in custom accessors
Understanding backing fields is essential for creating robust Kotlin classes with proper encapsulation and data handling.
Exercises
- Create a
BankAccount
class with abalance
property that doesn't allow negative values - Implement a
Password
class with a property that enforces minimum length and complexity rules - Create a
Circle
class with aradius
property and computed properties forarea
andcircumference
- Implement a
Temperature
class that stores temperature in Kelvin but provides Celsius and Fahrenheit interfaces
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)