Kotlin Primer
Data Classes
Kotlin has many shortcuts, and one of my favorites is data class. Data classes automatically
create the following functions in the class
- toString() with all primary-constructor parameters (in order)
- equals()/hashCode()
- copy() to create a copy of the object with possible property overrides
- componentN() for use in destructuring declarations
Note
Data classes can have additional properties inside the class definition, as well as functions,
nested types, etc. Specifying data class just adds above functions for you.
toString()
Note
The toString() function in Java and Kotlin is intended as a debugging aid, not as
a way to format data for a user! Output formatting for user display should be a function of
your user interface, not your data definitions.
If we use a non-data class, the toString() is inherited from Any (the superclass of all
classes), which just prints the type name and hash code
class Person(
val name: String,
val age: Int,
)
fun main() {
val scott = Person("Scott", 59)
println(scott) // calls scott.toString()
}
Making the class a data class adds a nicely-readable dump of the type name and all
primary-constructor parameters. Note that member properties will not appear in the output!
data class Person(
val name: String,
val age: Int,
) {
var someData: Int = 0 // won't be printed!
}
fun main() {
val scott = Person("Scott", 59)
println(scott) // calls scott.toString()
}
equals() and hashCode()
These functions serve the same purpose as they do in Java, for value comparison and hashCode generation (most-often used when using the object as a key in a hash map). They will use the values and hash codes of properties defined in the primary constructor.
data class Person(
val name: String,
val age: Int,
) {
var someData: Int = 0 // won't be used in equals/hashCode
}
fun main() {
val scott = Person("Scott", 59)
val mikey = Person("Mikey", 10)
println(mikey == scott) // calls mikey.equals(scott)
}
copy()
The copy function is a simple way to copy all the primary-constructor parameter properties while allowing individual properties to be changed.
data class Person(
val name: String,
val age: Int,
)
fun main() {
val scott = Person("Scott", 59)
val youngScott = scott.copy(age = 10)
// all properties are copied, except for those specified
println(youngScott)
}
In this example, the copy() function is basically
data class Person(
val name: String,
val age: Int,
) {
fun copy(
val name: String = this.name,
val age: Int = this.age,
) = Person(name, age)
}
componentN()
A destructuring declaration allows you to create multiple properties at once, initializing them
to values pulled from componentN() functions. If you define a non-data class, you can create any
number of componentN() functions, starting with N=1, such as component1()
class Person(
val name: String,
val age: Int,
) {
operator fun component1() = name
operator fun component2() = age
}
fun main() {
val scott = Person("Scott", 59)
// destructuring declaration
// calls component1() and component2() to get
// values in order and assigns
// component1, component2, etc are defined by order
// of parameters in primary constructor
val (name, age) = scott
println(name)
println(age)
}
These do not need to return property values; they could return constants or derived values!
When you define a data class, componentN() functions are automatically generated for each
primary-constructor property, in order of definition.
data class Person(
val name: String,
val age: Int,
)
fun main() {
val scott = Person("Scott", 59)
// destructuring declaration
// calls component1 and component2 to get
// values in order and assigns
// component1, component2, etc are defined by order
// of parameters in primary constructor
val (name, age) = scott
println(name)
println(age)
}
Data Objects
You can also specify data object to add a nice base toString() that just returns the name of
the class (without its hashCode() appended). The other functisonality is not added.