In this chapter, we'll cover Kotlin's type system, object-oriented programming (OOP) with Kotlin, modifiers, destructuring declarations, and more.
Kotlin is, primarily, an OOP language with some functional features. When we use OOP languages to resolve problems, we try to model the objects that are a part of our problem in an abstract way with the information that is relevant to the problem.
If we're designing an HR module for our company, we'll model employees with state or data (name, date of birth, social security number, and others) and behavior (pay salary, transfer to another division, and others). Because a person can be very complex, there is information that isn't relevant for our problem or domain. For example, the employee's favorite style of bicycle isn't relevant for our HR system, but it is very relevant for an online cycling shop.
Once we identify the objects (with data and behavior) and the relationship with other objects of our domain, we can start developing and writing the code that we'll make a part of our software solution. We'll use language constructs (construct is a fancy way to say allowed syntax) to write the objects, categories, relationships, and so on.
Kotlin has many constructs that we can use to write our programs and, in this chapter, we'll cover many of those constructs, such as:
- Classes
- Inheritance
- Abstract classes
- Interfaces
- Objects
- Generics
- Type alias
- Null types
- Kotlin's type system
- Other types
Classes are the foundational type in Kotlin. In Kotlin, a class is a template that provides a state, a behavior, and a type to instances (more on that later).
To define a class, only a name is required:
class VeryBasic
VeryBasic
is not very useful, but is still a valid Kotlin syntax.
The VeryBasic
class doesn't have any state or behavior; nonetheless, you can declare values of type VeryBasic
, as shown in the following code:
fun main(args: Array<String>) { val basic: VeryBasic = VeryBasic() }
As you can see, the basic
value has a VeryBasic
type. To express it differently, basic
is an instance of VeryBasic
.
In Kotlin, types can be inferred; so, the previous example is equivalent to the following code:
fun main(args: Array<String>) { val basic = VeryBasic() }
By being a VeryBasic
instance, basic
has a copy of the VeryBasic
type's state and behavior, namely, none. So sad.
As discussed previously, classes can have a state. In Kotlin, a class's state is represented by properties. Let's have a look at the blueberry cupcake example:
class BlueberryCupcake { var flavour = "Blueberry" }
The BlueberryCupcake
class has an has-a property flavour
of type String
.
Of course, we can have instances of the BlueberryCupcake
class:
fun main(args: Array<String>) { val myCupcake = BlueberryCupcake() println("My cupcake has ${myCupcake.flavour}") }
Now, because we declare the flavour
property as a variable, its internal value can be changed at runtime:
fun main(args: Array<String>) { val myCupcake = BlueberryCupcake() myCupcake.flavour = "Almond" println("My cupcake has ${myCupcake.flavour}") }
That is impossible in real life. Cupcakes do not change their flavor (unless they become stale). If we change the flavour
property to a value, it cannot be modified:
class BlueberryCupcake { val flavour = "Blueberry" } fun main(args: Array<String>) { val myCupcake = BlueberryCupcake() myCupcake.flavour = "Almond" //Compilation error: Val cannot be reassigned println("My cupcake has ${myCupcake.flavour}") }
Let's declare a new class for almond cupcakes:
class AlmondCupcake { val flavour = "Almond" } fun main(args: Array<String>) { val mySecondCupcake = AlmondCupcake() println("My second cupcake has ${mySecondCupcake.flavour} flavour") }
There is something fishy here. BlueberryCupcake
and AlmondCupcake
are identical in structure; only an internal value is changed.
In real life, you don't have different baking tins for different cupcake flavors. The same good quality baking tin can be used for various flavors. In the same way, a well-designed Cupcake
class can be used for different instances:
class Cupcake(flavour: String) { val flavour = flavour }
The Cupcake
class has a constructor with a parameter, flavour
, that is assigned to a flavour
value.
Because this is a very common idiom, Kotlin has a little syntactic sugar to define it more succinctly:
class Cupcake(val flavour: String)
Now, we can define several instances with different flavors:
fun main(args: Array<String>) { val myBlueberryCupcake = Cupcake("Blueberry") val myAlmondCupcake = Cupcake("Almond") val myCheeseCupcake = Cupcake("Cheese") val myCaramelCupcake = Cupcake("Caramel") }
In Kotlin, a class's behavior is defined by methods. Technically, a method is a member function, so, anything that we learn about functions in the following chapters also applies to the methods:
class Cupcake(val flavour: String) { fun eat(): String { return "nom, nom, nom... delicious $flavour cupcake" } }
The eat()
method returns a String
value. Now, let's call the eat()
method, as shown in the following code:
fun main(args: Array<String>) { val myBlueberryCupcake = Cupcake("Blueberry") println(myBlueberryCupcake.eat()) }
The following expression is the output of the preceding code:

Nothing mind-blowing, but this is our first method. Later on, we'll do more interesting stuff.
As we continue modelling our domain in Kotlin, we realize that specific objects are quite similar. If we go back to our HR example, an employee and a contractor are quite similar; both have a name, a date of birth, and so on; they also have some differences. For example, a contractor has a daily rate, while an employee has a monthly salary. It is obvious that they are similar—both of them are people; people is a superset where both contractor and employee belong. As such, both have their own specific features that make them different enough to be classified into different subsets.
This is what inheritance is all about, there are groups and subgroups and there are relationships between them. In an inheritance hierarchy, if you go up in the hierarchy, you will see more general features and behaviors, and if you go down, you will see more specific ones. A burrito and a microprocessor are both objects, but they have very different purposes and uses.
Let's introduce a new Biscuit
class:
class Biscuit(val flavour: String) { fun eat(): String { return "nom, nom, nom... delicious $flavour biscuit" } }
Again, this class looks almost exactly same as Cupcake
. We could refactor these classes to reduce code duplication:
open class BakeryGood(val flavour: String) { fun eat(): String { return "nom, nom, nom... delicious $flavour bakery good" } } class Cupcake(flavour: String): BakeryGood(flavour) class Biscuit(flavour: String): BakeryGood(flavour)
We introduced a new BakeryGood
class, with the shared behavior and state of both Cupcake
and Biscuit
classes and we made both classes extend BakeryGood
. By doing so, Cupcake
(and Biscuit
) has an is-a relationship with BakeryGood
now; on the other hand, BakeryGood
is the Cupcake
class's super or parent class.
Note that BakeryGood
is marked as open
. This means that we specifically design this class to be extended. In Kotlin, you can't extend a class that isn't open
.
The process of moving common behaviors and states to a parent class is called generalisation. Let's have a look at the following code:
fun main(args: Array<String>) { val myBlueberryCupcake: BakeryGood = Cupcake("Blueberry") println(myBlueberryCupcake.eat()) }
Let's try out our new code:

Bummer, not what we were expecting. We need to refract it more:
open class BakeryGood(val flavour: String) { fun eat(): String { return "nom, nom, nom... delicious $flavour ${name()}" } open fun name(): String { return "bakery good" } } class Cupcake(flavour: String): BakeryGood(flavour) { override fun name(): String { return "cupcake" } } class Biscuit(flavour: String): BakeryGood(flavour) { override fun name(): String { return "biscuit" } }
It works! Let's have a look at theoutput:

We declared a new method, name()
; it should be marked as open
, because we designed it to be optionally altered in its subclasses.
Modifying a method's definition on a subclass is called override and that is why the name()
method in both subclasses is marked as override
.
The process of extending classes and overriding behavior in a hierarchy is called specialisation.
Note
Rule of thumb
Put general states and behaviors at the top of the hierarchy (generalisation), and specific states and behaviors in subclasses (specialisation).
Now, we can have more bakery goods! Let's have a look at the following code:
open class Roll(flavour: String): BakeryGood(flavour) { override fun name(): String { return "roll" } } class CinnamonRoll: Roll("Cinnamon")
Subclasses can be extended too. They just need to be marked asopen
:
open class Donut(flavour: String, val topping: String) : BakeryGood(flavour) { override fun name(): String { return "donut with $topping topping" } } fun main(args: Array<String>) { val myDonut = Donut("Custard", "Powdered sugar") println(myDonut.eat()) }
We can also create classes with more properties and methods.
So far, so good. Our bakery looks good. However, we have a problem with our current model. Let's look at the following code:
fun main(args: Array<String>) { val anyGood = BakeryGood("Generic flavour") }
We can instantiate the BakeryGood
class directly, which is too generic. To correct this situation, we can mark BakeryGood
as abstract
:
abstract class BakeryGood(val flavour: String) { fun eat(): String { return "nom, nom, nom... delicious $flavour ${name()}" } open fun name(): String { return "bakery good" } }
An abstract class is a class designed solely to be extended. An abstract class can't be instantiated, which fixes our problem.
What makes abstract
different from open
?
Both modifiers let us extend a class, but open
lets us instantiate while abstract
does not.
Now that we can't instantiate, our name()
method in the BakeryGood
class isn't that useful anymore, and all our subclasses, except for CinnamonRoll
, override it anyway (CinnamonRoll
relays on the Roll
implementation):
abstract class BakeryGood(val flavour: String) { fun eat(): String { return "nom, nom, nom... delicious $flavour ${name()}" } abstract fun name(): String }
A method marked as abstract
doesn't have a body, just the signature declaration (a method signature is a way to identify a method). In Kotlin, a signature is composed of the method's name, its number, the type of parameters, and the return type.
Any class that extends BakeryGood
directly must override the name()
method. The technical term for overriding an abstract method is implement and, from now on, we will use it. So, the Cupcake
class implements the name()
method (Kotlin doesn't have a keyword for method implementation; both cases, method implementation, and method overriding, use the keyword override
).
Let's introduce a new class, Customer
; a bakery needs customers anyway:
class Customer(val name: String) {
fun eats(food: BakeryGood) {
println("$name is eating... ${food.eat()}")
}
}
fun main(args: Array<String>) {
val myDonut = Donut("Custard", "Powdered sugar")
val mario = Customer("Mario")
mario.eats(myDonut)
}
The eats(food: BakeryGood)
method takes a BakeryGood
parameter, so any instance of any class that extends the BakeryGood
parameter, it doesn't matter how many hierarchy levels. Just remember that we can instantiate BakeryGood
directly.
What happens if we want a simple BakeryGood
? For example, testing.
There is an alternative, an anonymous subclass:
fun main(args: Array<String>) { val mario = Customer("Mario") mario.eats(object : BakeryGood("TEST_1") { override fun name(): String { return "TEST_2" } }) }
A new keyword is introduced here, object
. Later on, we'll cover object
in more detail, but for now, it is enough to know that this is an object expression. An object expression defines an instance of an anonymous class that extends a type.
In our example, the object expression (technically, the anonymous class) must override the name()
method and pass a value as the parameter for the BakeryGood
constructor, exactly as a standard class would do.
Remember that an object
expression is an instance, so it can be used to declare values:
val food: BakeryGood = object : BakeryGood("TEST_1") { override fun name(): String { return "TEST_2" } } mario.eats(food)
The open and abstract classes are great for creating hierarchies, but sometimes they aren't enough. Some subsets can span between apparently unrelated hierarchies, for example, birds and great apes are bipedal, and both are animals and vertebrates, but they not directly related. That is why we need a different construct and Kotlin gives us interfaces (other languages deal with this problem differently).
Our bakery goods are great, but we need to cook them first:
abstract class BakeryGood(val flavour: String) { fun eat(): String { return "nom, nom, nom... delicious $flavour ${name()}" } fun bake(): String { return "is hot here, isn't??" } abstract fun name(): String }
With our new bake()
method , it will cook all our amazing products, but wait, donuts aren't baked, but fried.
What if we could move the bake()
method to a second abstract class, Bakeable
? Let's try it in the following code:
abstract class Bakeable {
fun bake(): String {
return "is hot here, isn't??"
}
}
class Cupcake(flavour: String) : BakeryGood(flavour), Bakeable() { //Compilation error: Only one class may appear in a supertype list
override fun name(): String {
return "cupcake"
}
}
Wrong! In Kotlin, a class can't extend two classes at the same time. Let's have a look at the following code:
interface Bakeable { fun bake(): String { return "is hot here, isn't??" } } class Cupcake(flavour: String) : BakeryGood(flavour), Bakeable { override fun name(): String { return "cupcake" } }
However, it can extend many interfaces. An interface is a type that defines a behavior; in the Bakeable
interface's case, that is the bake()
method.
So, what are the differences between an open/abstract class and an interface?
Let's start with the following similarities:
- Both are types. In our example,
Cupcake
has an is-a relationship withBakeryGood
and has an is-a relationship withBakeable
. - Both define behaviors as methods.
- Although open classes can be instantiated directly, neither abstract classes nor interfaces can.
Now, let's look at the following differences:
- A class can extend just one class (open or abstract), but can extend many interfaces.
- An open/abstract class can have constructors.
- An open/abstract class can initialize its own values. An interface's values must be initialized in the classes that extend the interface.
- An open class must declare the methods that can be overridden as open. An abstract class could have both open and abstract methods.
In an interface, all methods are open and a method with no implementation doesn't need an abstract modifier:
interface Fried { fun fry(): String } open class Donut(flavour: String, val topping: String) : BakeryGood(flavour), Fried { override fun fry(): String { return "*swimming on oil*" } override fun name(): String { return "donut with $topping topping" } }
When should you use one or the other?:
- Use open class when:
- The class should be extended and instantiated
- Use abstract class when:
- The class can't be instantiated
- A constructor is needed it
- There is initialization logic (using
init
blocks)
Let's have a look at the following code:
abstract class BakeryGood(val flavour: String) {
init {
println("Preparing a new bakery good")
}
fun eat(): String {
return "nom, nom, nom... delicious $flavour ${name()}"
}
abstract fun name(): String
}
- Use interface when:
- Multiple inheritances must be applied
- No initialized logic is needed
Note
My recommendation is that you should always start with an interface. Interfaces are more straightforward and cleaner; they also allow a more modular design. In the case that data initialization/constructors are needed, move to abstract/open.
As with abstract classes, object expressions can be used with interfaces:
val somethingFried = object : Fried { override fun fry(): String { return "TEST_3" } }
We already covered object expressions, but there is more on objects. Objects are natural singletons (by natural, I mean to come as language features and not as behavior pattern implementations, as in other languages). A singleton is a type that has just one and only one instance and every object in Kotlin is a singleton. That opens a lot of interesting patterns (and also some bad practices). Objects as singletons are useful for coordinating actions across the system, but can also be dangerous if they are used to keep global state.
Object expressions don't need to extend any type:
fun main(args: Array<String>) { val expression = object { val property = "" fun method(): Int { println("from an object expressions") return 42 } } val i = "${expression.method()} ${expression.property}" println(i) }
In this case, the expression
value is an object that doesn't have any specific type. We can access its properties and functions.
There is one restriction—object expressions without type can be used only locally, inside a method, or privately, inside a class:
class Outer { val internal = object { val property = "" } } fun main(args: Array<String>) { val outer = Outer() println(outer.internal.property) // Compilation error: Unresolved reference: property }
In this case, the property
value can't be accessed.
An object can also have a name. This kind of object is called an object declaration:
object Oven { fun process(product: Bakeable) { println(product.bake()) } } fun main(args: Array<String>) { val myAlmondCupcake = Cupcake("Almond") Oven.process(myAlmondCupcake) }
Objects are singletons; you don't need to instantiate Oven
to use it. Objects also can extend other types:
interface Oven { fun process(product: Bakeable) } object ElectricOven: Oven { override fun process(product: Bakeable) { println(product.bake()) } } fun main(args: Array<String>) { val myAlmondCupcake = Cupcake("Almond") ElectricOven.process(myAlmondCupcake) }
Objects declared inside a class/interface can be marked as companion objects. Observe the use of companion objects in the following code:
class Cupcake(flavour: String) : BakeryGood(flavour), Bakeable { override fun name(): String { return "cupcake" } companion object { fun almond(): Cupcake { return Cupcake("almond") } fun cheese(): Cupcake { return Cupcake("cheese") } } }
Now, methods inside the companion object can be used directly, using the class name without instantiating it:
fun main(args: Array<String>) { val myBlueberryCupcake: BakeryGood = Cupcake("Blueberry") val myAlmondCupcake = Cupcake.almond() val myCheeseCupcake = Cupcake.cheese() val myCaramelCupcake = Cupcake("Caramel") }
Companion object's methods can't be used from instances:
fun main(args: Array<String>) { val myAlmondCupcake = Cupcake.almond() val myCheeseCupcake = myAlmondCupcake.cheese() //Compilation error: Unresolved reference: cheese }
Companion objects can be used outside the class as values with the name Companion
:
fun main(args: Array<String>) { val factory: Cupcake.Companion = Cupcake.Companion }
Alternatively, a Companion
object can have a name:
class Cupcake(flavour: String) : BakeryGood(flavour), Bakeable { override fun name(): String { return "cupcake" } companion object Factory { fun almond(): Cupcake { return Cupcake("almond") } fun cheese(): Cupcake { return Cupcake("cheese") } } } fun main(args: Array<String>) { val factory: Cupcake.Factory = Cupcake.Factory }
They can also be used without a name, as shown in the following code:
fun main(args: Array<String>) { val factory: Cupcake.Factory = Cupcake }
Don't be confused by this syntax. The Cupcake
value without parenthesis is the companion object; Cupcake()
is an instance.
This section is just a short introduction to generics; later, we'll cover it in detail.
Generic programming is a style programming that focuses on creating algorithms (and collaterally, data structures) that work on general problems.
The Kotlin way to support generic programming is using type parameters. In a few words, we wrote our code with type parameters and, later on, we pass those types as parameters when we use them.
Let's take, for example, our Oven
interface:
interface Oven { fun process(product: Bakeable) }
An oven is a machine, so we could generalize it more:
interface Machine<T> { fun process(product: T) }
The Machine<T>
interface defines a type parameter T
and a method process(T)
.
Now, we can extend it with Oven
:
interface Oven: Machine<Bakeable>
Now, Oven
is extending Machine
with the Bakeable
type parameter, so the process
method now takes Bakeable
as a parameter.
Type alias provides a way to define names of types that already exist. Type alias can help to make complex types easier to read, and can also provide other hints.
The Oven
interface is, in some sense, just a name, for a Machine<Bakeable>
:
typealias Oven = Machine<Bakeable>
Our new type alias, Oven
, is exactly like our good old Oven
interface. It can be extended and have the values of the type Oven
.
Types alias also can be used to enhance information on types, providing meaningful names related to your domain:
typealias Flavour = String abstract class BakeryGood(val flavour: Flavour) {
It can also be used on collections:
typealias OvenTray = List<Bakeable>
It can also be used with objects:
typealias CupcakeFactory = Cupcake.Companion
One of the main features of Kotlin is nullable types. Nullable types allow us to define if a value can contain or being null explicitly:
fun main(args: Array<String>) { val myBlueberryCupcake: Cupcake = null //Compilation error: Null can not be a value of a non-null type Cupcake }
This isn't valid in Kotlin; the Cupcake
type doesn't allow null values. To allow null values, myBlueberryCupcake
must have a different type:
fun main(args: Array<String>) { val myBlueberryCupcake: Cupcake? = null }
In essence, Cupcake
is a non-null type and Cupcake?
is a nullable type.
In the hierarchical structure, Cupcake
is a subtype of Cupcake?
. So, in any situation where Cupcake?
is defined, Cupcake
can be used, but not the other way around:
fun eat(cupcake: Cupcake?){ // something happens here } fun main(args: Array<String>) { val myAlmondCupcake = Cupcake.almond() eat(myAlmondCupcake) eat(null) }
Kotlin's compiler makes a distinction between instances of nullable and non-null types.
Let's take these values, for example:
fun main(args: Array<String>) { val cupcake: Cupcake = Cupcake.almond() val nullabeCupcake: Cupcake? = Cupcake.almond() }
Next, we will invoke the eat()
method on both nullable and non-null types:
fun main(args: Array<String>) { val cupcake: Cupcake = Cupcake.almond() val nullableCupcake: Cupcake? = Cupcake.almond() cupcake.eat() // Happy days nullableCupcake.eat() //Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Cupcake? }
Calling the eat()
method on cupcake
is easy as pie; calling eat()
on nullableCupcake
is a compilation error.
Why? For Kotlin, calling a method from a nullable value is dangerous, a potential NullPointerException
(NPE from now on) could be thrown. So, to be safe, Kotlin marks this as a compilation error.
What happens if we really want to invoke a method or access a property from a nullable value?
Well, Kotlin provides you options to deal with nullable values, with a catch—all are explicit. In some sense, Kotlin is telling you, Show me that you know what you are doing.
Let's review some options (there are more options that we'll cover in the following chapters).
Check for null as a condition in the if
block:
fun main(args: Array<String>) { val nullableCupcake: Cupcake? = Cupcake.almond() if (nullableCupcake != null) { nullableCupcake.eat() } }
Kotlin will do a smart cast. Inside the if
block, nullableCupcake
is a Cupcake
, not a Cupcake?
; so, any method or property can be accessed.
This is similar to the previous one, but it checks directly for the type:
if (nullableCupcake is Cupcake) { nullableCupcake.eat() }
It also works with when
:
when (nullableCupcake) { is Cupcake -> nullableCupcake.eat() }
Both options, checking for null and non-null types, are a little bit verbose. Let's check other options.
Safe calls let you access methods and properties of nullable values if the value isn't null (under the hood, at the bytecode level, a safe call is transformed into if(x != null)
):
nullableCupcake?.eat()
But, what if you use it in an expression?
val result: String? = nullableCupcake?.eat()
It will return null if our value is null, so result
must have a String?
type.
That opens up the chance to use safe calls on a chain, as follows:
val length: Int? = nullableCupcake?.eat()?.length
The Elvis operator (?:
) returns an alternative value if a null value is used in an expression:
val result2: String = nullableCupcake?.eat() ?: ""
If nullabluCupcake?.eat()
is null
, the ?:
operator will return the alternative value ""
.
Obviously, the Elvis operator can be used with a chain of safe calls:
val length2: Int = nullableCupcake?.eat()?.length ?: 0
Instead of a null
value, the !!
operator will throw an NPE:
val result: String = nullableCupcake!!.eat()
If you can deal with an NPE, the !!
operator gives you a pretty convenient feature, a free smart cast:
val result: String = nullableCupcake!!.eat() val length: Int = nullableCupcake.eat().length
If nullableCupcake!!.eat()
doesn't throw an NPE, Kotlin will change its type from Cupcake?
to Cupcake
from the next line and onwards.
Type systems are a set of rules that determine the type of a language construct.
A (good) type system will help you with:
- Making sure that the constituent parts of your program are connected in a consistent way
- Understanding your program (by reducing your cognitive load)
- Expressing business rules
- Automatic low-level optimizations
We have already covered enough ground to understand Kotlin's type system.
All types in Kotlin extend from the Any
type (hold on a second, actually this isn't true but for the sake of the explanation, bear with me).
Every class and interface that we create implicitly extends Any
. So, if we write a method that takes Any
as a parameter, it will receive any value:
fun main(args: Array<String>) { val myAlmondCupcake = Cupcake.almond() val anyMachine = object : Machine<Any> { override fun process(product: Any) { println(product.toString()) } } anyMachine.process(3) anyMachine.process("") anyMachine.process(myAlmondCupcake) }
What about a nullable value? Let's have a look at it:
fun main(args: Array<String>) { val anyMachine = object : Machine<Any> { override fun process(product: Any) { println(product.toString()) } } val nullableCupcake: Cupcake? = Cupcake.almond() anyMachine.process(nullableCupcake) //Error:(32, 24) Kotlin: Type mismatch: inferred type is Cupcake? but Any was expected }
Any
is the same as any other type and also has a nullable counterpart, Any?
. Any
extends from Any?
. So, in the end, Any?
is the top class of Kotlin's type system hierarchy.
Due to its type inference and expression evaluation, sometimes there are expressions in Kotlin where it is not clear which type is being returned. Most languages resolve this problem by returning the minimum common type between the possible type options. Kotlin takes a different route.
Let's take a look at an example of an ambiguous expression:
fun main(args: Array<String>) { val nullableCupcake: Cupcake? = Cupcake.almond() val length = nullableCupcake?.eat()?.length ?: "" }
What type does length
have? Int
or String
? No, length
value's type is Any
. Pretty logical. The minimum common type between Int
and String
is Any
. So far, so good. Let's look at the following code now:
val length = nullableCupcake?.eat()?.length ?: 0.0
Following that logic, in this case, length
should have the Number
type (the common type between Int
and Double
), shouldn't it?
Wrong, length
is still Any
. Kotlin doesn't search for the minimum common type in these situations. If you want a specific type, it must be explicitly declared:
val length: Number = nullableCupcake?.eat()?.length ?: 0.0
Kotlin doesn't have methods with void
return (as Java or C do). Instead, a method (or, to be precise, an expression) could have a Unit
type.
A Unit
type means that the expression is called for its side effects, rather than its return. The classic example of a Unit
expression is println()
, a method invoked just for its side effects.
Unit
, like any other Kotlin type, extends from Any
and could be nullable. Unit?
looks strange and unnecessary, but is needed to keep consistency with the type system. Having a consistent type system have several advantages, including better compilation times and tooling:
anyMachine.process(Unit)
Nothing
is the type that sits at the bottom of the entire Kotlin hierarchy. Nothing
extends all Kotlin types, including Nothing?
.
But, why do we need a Nothing
and Nothing?
types?
Nothing
represents an expression that can't be executed (basically throwing exceptions):
val result: String = nullableCupcake?.eat() ?: throw RuntimeException() // equivalent to nullableCupcake!!.eat()
On one hand of the Elvis operator, we have a String
. On the other hand, we have Nothing
. Because the common type between String
and Nothing
is String
(instead of Any
), the value result
is a String
.
Nothing
also has a special meaning for the compiler. Once a Nothing
type is returned on an expression, the lines after that are marked as unreachable.
Nothing?
is the type of a null value:
val x: Nothing? = null val nullsList: List<Nothing?> = listOf(null)
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" } }
In this chapter, we covered the basics of OOP and how Kotlin supports it. We learned how to use classes, interfaces, objects, data classes, annotations, and enums. We also explored the Kotlin type system and saw how it helps us to write better and safer code.
In the next chapter, we will start with an introduction to functional programming.