Classes, interfaces, and objects are a good starting point for an OOP type system, but Kotlin offers more constructs, such as data classes, annotations, and enums (there is an additional type, named sealed class, that we'll cover later).
Creating classes whose primary purpose is to hold data is a common pattern in Kotlin (is a common pattern in other languages too, think of JSON or Protobuff).
Kotlin has a particular kind of class for this purpose:
data class Item(val product: BakeryGood, val unitPrice: Double, val quantity: Int)
To declare data class
, there are some restrictions:
- The primary constructor should have at least one parameter
- The primary constructor's parameters must be
val
orvar
- Data classes can't be abstract, open, sealed, or inner
With these restrictions, data classes give a lot of benefits.
Canonical methods are the methods declared in Any
. Therefore, all instances in Kotlin have them.
For data classes, Kotlin creates correct implementations of all canonical methods.
The methods are as follows:
equals(other: Any?): Boolean
: This method compares value equivalence, rather than reference.hashCode(): Int
: A hash code is a numerical representation of an instance. WhenhashCode()
is invoked several times in the same instance, it should always return the same value. Two instances that return true when they are compared withequals
must have the samehashCode()
.toString(): String
: AString
representation of an instance. This method will be invoked when an instance is concatenated to aString
.
Sometimes, we want to reuse values from an existing instance. The copy()
method lets us create new instances of a data class, overriding the parameters that we want:
val myItem = Item(myAlmondCupcake, 0.40, 5) val mySecondItem = myItem.copy(product = myCaramelCupcake) //named parameter
In this case, mySecondItem
copies unitPrice
and quantity
from myItem
, and replaces the product
property.
By convention, any instance of a class that has a series of methods named component1()
, component2()
and so on can be used in a destructuring declaration.
Kotlin will generate these methods for any data class:
val (prod: BakeryGood, price: Double, qty: Int) = mySecondItem
The prod
value is initialized with the return of component1()
, price
with the return of component2()
, and so on. Although the preceding example use explicit types, those aren't needed:
val (prod, price, qty) = mySecondItem
In some circumstances, not all values are needed. All unused values can be replaced by (_
):
val (prod, _, qty) = mySecondItem
Annotations are a way to attach meta info to your code (such as documentation, configuration, and others).
Let's look at the following example code:
annotation class Tasty
An annotation
itself can be annotated to modify its behavior:
@Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) annotation class Tasty
In this case, the Tasty
annotation can be set on classes, interfaces, and objects, and it can be queried at runtime.
For a complete list of options, check the Kotlin documentation.
Annotations can have parameters with one limitation, they can't be nullable:
@Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) annotation class Tasty(val tasty:Boolean = true) @Tasty(false) object ElectricOven : Oven { override fun process(product: Bakeable) { println(product.bake()) } } @Tasty class CinnamonRoll : Roll("Cinnamon") @Tasty interface Fried { fun fry(): String }
To query annotation values at runtime, we must use the reflection API (kotlin-reflect.jar
must be in your classpath):
fun main(args: Array<String>) { val annotations: List<Annotation> = ElectricOven::class.annotations for (annotation in annotations) { when (annotation) { is Tasty -> println("Is it tasty? ${annotation.tasty}") else -> println(annotation) } } }
Enum in Kotlin is a way to define a set of constant values. Enums are very useful, but not limited, as configuration values:
enum class Flour { WHEAT, CORN, CASSAVA }
Each element is an object that extends the Flour
class.
Like any object, they can extend interfaces:
interface Exotic { fun isExotic(): Boolean } enum class Flour : Exotic { WHEAT { override fun isExotic(): Boolean { return false } }, CORN { override fun isExotic(): Boolean { return false } }, CASSAVA { override fun isExotic(): Boolean { return true } } }
Enum can also have abstract methods:
enum class Flour: Exotic { WHEAT { override fun isGlutenFree(): Boolean { return false } override fun isExotic(): Boolean { return false } }, CORN { override fun isGlutenFree(): Boolean { return true } override fun isExotic(): Boolean { return false } }, CASSAVA { override fun isGlutenFree(): Boolean { return true } override fun isExotic(): Boolean { return true } }; abstract fun isGlutenFree(): Boolean }
Any method definition must be declared after the (;
) separating the last element.
When enums are used with when
expressions, Kotlin's compiler checks that all cases are covered (individually or with an else
):
fun flourDescription(flour: Flour): String { return when(flour) { // error Flour.CASSAVA -> "A very exotic flavour" } }
In this case, we're only checking for CASSAVA
and not the other elements; therefore, it fails:
fun flourDescription(flour: Flour): String { return when(flour) { Flour.CASSAVA -> "A very exotic flavour" else -> "Boring" } }