Data Classes
Some of the classes we create in a software application are there to
represent the fundamental ‘business entities’ of a use case. For example,
in an e-commerce scenario, we might have classes named Customer and
Order, involved in placing an order for a product.
These entity classes are data-focused. Instances of them may represent the results of a database query, or data that needs to be stored in a database. We may need to manage multiple instances using one of Kotlin’s collection types. We may need to test whether two instances contain the same data. We may even need the ability to generate strings giving details of the data stored inside an instance (for testing and debugging purposes, if nothing else).
Kotlin classes have methods named equals(), hashCode() and toString()
that support these tasks, but the default implementations generally won’t
provide us with what we require. They need to be overridden in order to
be useful.
This is where Kotlin’s data classes come in. When we designate a class
as a data class, the compiler will synthesize useful versions of equals(),
hashCode() and toString() for us. We can use the data class feature to
implement entity classes more quickly, with less code.
Task 12.7
-
Edit the file
Data.kt, in thetasks/task12_7subdirectory of your repository. This file contains two things:-
A class
Person, whose implementation should be familiar to you by now -
A program that creates and manipulates two instances of
Person
-
-
Compile and run the program. Study the output carefully. This shows you the behaviour of the ‘default’ versions of
toString(),hashCode()andequals().You should see something similar to this:
p1 == p1? : true p1 == p2? : false p1.hashCode() : 504bae78 p2.hashCode() : 53e25b76 p1.toString() : Person@504bae78 p2.toString() : Person@53e25b76Although objects
p1andp2contain exactly the same data, comparing them with==returnsfalse, indicating that they are not considered to be equal.This is because using
==invokes theequals()method, and the default implementation of this method tests for equality of identity (do the variables reference the same object?) rather than equality of state (do the variables represent objects containing the same data?)p1andp2also have different hash codes and different string representations. The hash codes differ because the default implementation ofhashCode()is based on the memory address at which an object is located. The string representations differ because the default behaviour oftoString()is to include an object’s hash code. -
Now make one small change to the
Personclass. Add the keyworddatato the start of the definition. The class should now look like this:data class Person(var name: String, val birth: LocalDate) { var isMarried = false }Recompile the program and run it. Notice how the output changes.
p1 == p1? : true p1 == p2? : true p1.hashCode() : 042a878b p2.hashCode() : 042a878b p1.toString() : Person(name=Dave, birth=1997-08-23) p2.toString() : Person(name=Dave, birth=1997-08-23)Now,
p1andp2are considered equal, and they have the same hash codes. They also have the same string representation, and that representation is much more useful, showing us property values (although one property is missing). -
The last five lines of
Data.ktare currently commented out. Uncomment these lines, then recompile the program and run it. The program will generate the following additional lines of output:p1 marries p1 == p2? : true p1.hashCode() : 042a878b p1.toString() : Person(name=Dave, birth=1997-08-23)Think about what is happening here. the
isMarriedproperties ofp1andp2now have different values, but the objects are still being reported as equal, and they still have the same hash codes and string representations.You may have already guessed the reason for this. The
isMarriedproperty is currently ignored by the implementations ofequals(),hashCode()andtoString()that were generated for us when we madePersona data class. -
To fix the problem, modify the
Personclass so thatisMarriedis now defined as part of the primary constructor. The class should now look like this:data class Person( var name: String, val birth: LocalDate, var isMarried: Boolean = false )If you recompile and run the program, it should now behave as expected.
Data classes are a handy way of creating fully functional entities without having to write lots of ‘boilerplate’ code yourself.
Just remember that any properties that are important in determining object equality, hash code or string representation need to be defined within the primary constructor.
Should All Classes Be Data Classes?
The short answer is No.
Entity classes are not the only kind of class that we find when we analyze
a use case. Some classes are boundary classes that represent points
of interaction with a user of a system. For example, an OrderForm class
might represent the UI with which a customer places an order.
Other classes turn out to be control classes, which orchestrate the flow
of events in a use case. For example, an OrderPlacement class might be
used to capture the logic of checking stock levels, taking payment from a
customer and then placing the order for an item.
Boundary classes and control classes are not data-focused like entity classes are. We won’t need to compare instances of them for equality, store instances of them in a collection, or display the state of an instance. Hence there is no benefit in adding to these classes the extra code that supports such activities.