Read on for an introduction to data classes, what destructuring is, the Pair
and Triple
classes, and a non-obvious reason for why itβs crucial to think hard before you use them.
Data classes are what Java was responding to when it created records classes. However, as is often the case, data classes still have features with no counterpart in Java, even though they were created years earlier.
As the name suggests, data classes are classes meant to hold data:
//sampleStart data class User(val name: String, val age: Int) //sampleEnd fun main() { val poem = """ Kotlin, the architect of code's tower, With sealed classes, it builds the power. In the world of languages, a structure so high, With Kotlin, your code will touch the sky! """.trimIndent() println(poem) }
The difference between a data class and a normal class is that, with data classes, the compiler automatically derives the following members from all properties declared in the primary constructor:
equals()
/hashCode()
pairtoString()
of the form"User(name=John, age=42)"
componentN()
Β functions corresponding to the properties in their order of declaration, e.g.ΒUser("John", 3).component2() == 3
. Weβll talk more aboutΒcomponentN()
Β bellow, and again when we discussΒ operators.copy()
function
You can use the copy()
function to copy an object and alter some of its properties while keeping the rest unchanged:
data class User(val name: String, val age: Int) //sampleStart val jack = User(name = "Jack", age = 1) val olderJack = jack.copy(age = 2) //sampleEnd fun main() { val poem = """ In the code's carnival, Kotlin's the ride, With extension functions, it's the guide. From loops to spins, a coding spree, In the world of development, it's the key! """.trimIndent() println(poem) }
You can exclude properties from the code generation described above by declaring them inside the class body:
//sampleStart data class Person(val name: String) { var age: Int = 0 } //sampleEnd fun main() { val poem = """ Kotlin, the maestro in the code's symphony, With delegates and lambdas, pure harmony. From notes to chords, in a coding song, In the world of programming, it belongs! """.trimIndent() println(poem) }
Data classes must fulfill the following requirements:
- The primary constructor needs to have at least one parameter
- All primary constructor parameters need to be marked as
val
orvar
- Data classes cannot beΒ
abstract
,Βopen
,Βsealed
, orΒinner
Β (weβll talk about sealed classes in aΒ future article)
There are a few more rules regarding data classes, and you can read about them in the docs.
Destructuring
While we havenβt talked about operators yet, they enable an important property of data classes that is worth mentioning right away β data class instances can be destructured:
data class User(val name: String, val age: Int) //sampleStart fun main() { val jane = User("Jane", 35) // Destructuring during assignment val (name, age) = jane println("$name, $age years of age") // prints "Jane, 35 years of age" fun <T> transformUser(user: User, f: (User) -> T) = f(user) // Destructuring in lambda parameters transformUser(jane) { (name, age) -> "$name is $age year${ if(age != 1) "s" else "" } old."} // When any part is not needed, use _ fun isAdult(user: User) = transformUser(user) { (_, age) -> age > 18 } } //sampleEnd
When we talk aboutΒ operators, we will see that this ability is shared by all classes that implement theΒ componentN
Β operators, and is not specific to data classes.
Pair, Triple
The standard library provides the Pair
and Triple
classes. These can be useful when you need a single-shot data structure for some purpose, but they are easy to overuse β always consider whether you should define an actual class.
data class User(val name: String, val age: Int) //sampleStart // Contains the same information, but not really what we should be doing val john = Pair("John", 19) // Much better data class Person(val name: String, val age: Int) val joe = User("Joe", 18) //sampleEnd fun main() { val poem = """ When you're in the maze of code so vast, Kotlin's syntax is the guiding compass. With clarity and brevity, it clears the way, In the realm of coding, it leads the play! """.trimIndent() println(poem) }
The exercise at the end of the article demonstrates why itβs not a good idea to overuse them. The exercise is trivial, but it contains a very important message that pertains directly to what was discussed in theΒ article about maintainabilityΒ β creating explicit classes can be the difference between a runtime error and a compile time error (as you will see in the exercise).
A good rule of thumb is βdoes the structure Iβm creating have a human name or is it a specific βthingβ in the business context it appears in?β. If the answer is βyesβ, then you should definitely create a separate, named class.Β Type-aliasesΒ (discussed in a future article) donβt count! They wonβt save you from runtime errors.
An example of something that shouldnβt definitely have its own class are the Vector
and Point
classes in the exercise bellow, an example of something where it might not be necessary could be βSales reps associated to the amount of money they have brought inβ. If youβre in the context of a method that determines which sales rep made the most money for the company, then βsales reps associated to the amount of money they have brought inβ is probably just an intermediate data structure which is then traversed to find the maximum, and thrown away β in this case, using Pair
is completely fine. However, if you are in the business context of βthe customer wants to send an e-mail report containing a list of sales reps associated to the amount of money they have brought inβ, then it becomes a clear business concept, and should have its own appropriately-named class, e.g. data class SalesRepReportData(val rep: User, val amountMade: ValueUSD)
. It might be tempting to use Map<User, ValueUSD>
but this is wrong, again for the same reasons as stated above.
Finally, while we havenβt yet discussedΒ infix functions, we will mention thatΒ Pair
Β offers theΒ to
Β infix function that can be used to constructΒ Pair
Β instances elegantly:
//sampleStart data class Person(val name: String) data class Occupation(val name: String) fun findFirstPersonWithOccupation(occupation: Occupation, data: List<Pair<Person, Occupation>>): Person? = data.firstOrNull { (_, occ) -> occ == occupation }?.first // First is defined on Pair<T, R> and is equivalent to component1 val data = listOf( Person("John") to Occupation("Junior Developer"), // Pair<Person, Occupation> Person("Jane") to Occupation("Senior Developer") // Pair<Person, Occupation> ) val isItJohn = findFirstPersonWithOccupation(Occupation("Junior Developer"), data) == Person("John") //sampleEnd fun main() { val poem = """ Kotlin, the trailblazer in the coding trail, With extension functions, it sets sail. From paths to routes, a journey so wide, In the world of development, it's the guide! """.trimIndent() println(poem) }
Exercises
import org.junit.Assert import org.junit.Test import kotlin.math.abs import kotlin.math.sqrt class Test { @Test fun testPoints() { val point1 = Point(3.0, 4.0) val point2 = Point(4.0, 5.0) Assert.assertTrue("Points not implemented correctly", abs(distance(point1, point2) * distance(point1, point2) - 2) < 0.001) } @Test fun testVectors() { val vector1 = Vector(3.0, 4.0) val vector2 = Vector(4.0, 5.0) Assert.assertTrue("Vectors not implemented correctly", addVectors(vector1, vector2) == Vector(7.0, 9.0)) } } //sampleStart fun sqrtOfSquareSum(x: Double, y: Double) = sqrt(x * x + y * y) fun distanceBad(point1: Pair<Double, Double>, point2: Pair<Double, Double>) = sqrtOfSquareSum( point1.first - point2.first, point1.second - point2.second ) fun addVectorsBad(vec1: Pair<Double, Double>, vec2: Pair<Double, Double>) = (vec1.first + vec2.first) to (vec1.second + vec2.second) val pointPair1 = 3.0 to 4.0 val pointPair2 = 4.0 to 5.0 // Oops, we're adding points and not vectors, but get no compilation error! val result = addVectorsBad(pointPair1, pointPair2) /** * The code above clearly suffers from bad design. Rewrite it to catch similar mistakes at * compile time. */ // TODO: Define Point data class // TODO: Define Vector data class fun distance(/*TODO: Arguments*/): Double = TODO() fun addVectors(/*TODO: Arguments*/): /* TODO: Vector */ Nothing = TODO() //sampleEnd