Although Kotlin is a multi-paradigm language, it has a strong affinity to the Java programming language, which is based on classes. Keeping Java and JVM interoperability in mind, it's no wonder that Kotlin also has the notion of classes and classical inheritance.
In this section, we'll cover the syntax for declaring classes, interfaces, abstract classes, and data classes.
Classes
A class is a collection of data, called properties, and methods. To declare a class, we use the class
keyword, exactly like Java.
Let's imagine we're building a video game. We can define a class to represent the player as follows:
class Player {
}
The instantiation of a class simply looks like this:
val player = Player()
Note that there's no new
keyword in Kotlin. The Kotlin compiler knows that we want to create a new instance of that class by the round brackets after the class name.
If the class has no body, as in this simple example, we can omit the curly braces:
class Player // Totally fine
Classes without any functions or properties aren't particularly useful, but we'll explore in Chapter 4, Getting Familiar with Behavioral Patterns, why this syntax exists and how it is consistent with other language features.
Primary constructor
It would be useful for the player to be able to specify their name during creation. In order to do that, let's add a primary constructor to our class:
class Player(name: String)
Now, this declaration won't work anymore:
val player = Player()
Also, we'll have to provide a name for every new player we instantiate:
val player = Player("Roland")
We'll return to constructors soon enough. But for now, let's discuss properties.
Properties
In Java, we are used to the concept of getters and setters. If we were to write a class representing a player in a game in Kotlin using Java idioms, it may have looked like this:
class Player(name: String) {
private var name: String = name
fun getName(): String {
return name
}
fun setName(name: String) {
this.name = name;
}
}
If we want to get a player's name, we invoke the getName()
method. If we want to change a player's name, we invoke the setName()
method. That's quite simple to follow but very verbose.
It is the first time we see the this
keyword in Kotlin, so let's quickly explain what it means. Similar to many other languages, this
holds the reference to the current object of that class. In our case, it points to the instance of a Player
class.
Why don't we write our classes like that, though?
class Player {
var name: String = ""
}
Seems like this approach has lots of benefits. It is much less verbose for sure. Reading a person's name is now much shorter – player.name
.
Also, changing the name is much more intuitive – player.name = "Alex";
.
But by doing so, we lost a lot of control over our object. We cannot make Player
immutable, for example. If we want everybody to be able to read the player's name, they'll also be able to change it at any point in time. This is a significant problem if we want to change that code later. With a setter, we can control that, but not with a public field.
Kotlin properties provide a solution for all those problems. Let's look at the following class definition:
class Player(val name: String)
Note that this is almost the same as the example from the Primary constructor section, but now name
has a val
modifier.
This may look the same as the PublicPerson
Java example, with all its problems. But actually, this implementation is similar to ImmutablePerson
, with all its benefits.
How is that possible? Behind the scenes, Kotlin will generate a member and a getter with the same name for our convenience. We can set the property value in the constructor and then access it using its name:
val player = Player("Alex")
println(player.name)
Trying to change the name of our Player
will result in an error, though:
player.name = "Alexey" // value cannot be reassigned
Since we defined this property as a value, it is read-only. To be able to change a property, we need to define it as mutable. Prefixing a constructor parameter with var
will automatically generate both a getter and a setter:
class Player(val name: String, var score: Int)
If we don't want the ability to provide the value at construction time, we can move the property inside the class body:
class Player(val name: String) {
var score: Int = 0
}
Note that now we must also provide a default value for that property, since it cannot be simply null
.
Custom setters and getters
Although we can set a score now easily, its value may be invalid. Take the following example:
player.score = -10
If we want to have a mutable property with some validations, we need to define an explicit setter for it, using set
syntax:
class Player(val name: String) {
var score: Int = 0
set(value) {
field = if (value >= 0) {
value
} else {
0
}
}
}
Here, value
is the new value of the property and field
is its current value. If our new value is negative, we decide to use a default value.
Coming from Java, you may be tempted to write the following code in your setter instead:
set(value) {
this.score = if (value >= 0) value else 0
}
But, in Kotlin, this will create an infinite recursion. You must remember that Kotlin generates a setter for mutable properties. So, the previous code will be translated to something like this:
// This is a pseudocode, not real Kotlin code!
...
fun setValue(value: Int) {
setValue(value) // Infinite recursion!
}
...
For that reason, we use the field
identifier, which is provided automatically.
In a similar manner, we can declare a custom getter:
class Player(name: String) {
val name = name
get() = field.toUpperCase()
}
First, we save a value received as a constructor argument into a field with the same name. Then, we define a custom getter that will convert all characters in this property to uppercase:
println(player.name)
We'll get this as our output:
> ALEX
Interfaces
You are probably already familiar with the concept of interfaces from other languages. But let's quickly recap.
In typed languages, interfaces provide a way to define behavior that some class will have to implement. The keyword to define an interface is simply interface
.
Let's now define an interface for rolling a die:
interface DiceRoller {
fun rollDice(): Int
}
To implement the interface, a class specifies its name after a colon. There's no implement
keyword in Kotlin.
import kotlin.random.*
class Player(...) : DiceRoller
{
...
fun rollDice() = Random.nextInt(0, 6)
}
This is also the first time we see the import
keyword. As the name implies, it allows us to import another package, such as kotlin.random
, from the Kotlin standard library.
Interfaces in Kotlin also support default functions. If a function doesn't rely on any state, such as this function that simply rolls a random number between 0
and 5
, we can move it into the interface:
interface DiceRoller {
fun rollDice() = Random.nextInt(0, 6)
}
Abstract classes
Abstract classes, another concept familiar to many, are similar to interfaces in that they cannot be instantiated directly. Another class must extend them first. The difference is that unlike interface
, an abstract class can contain state.
Let's create an abstract class that is able to move our player on the board or, for the sake of simplicity, just store the new coordinates:
abstract class Moveable() {
private var x: Int = 0
private var y: Int = 0
fun move(x: Int, y: Int) {
this.x = x
this.y = y
}
}
Any class that implements Moveable
will inherit a move()
function as well.
Now, let's discuss in some more detail the private
keyword you see here for the first time.
Visibility modifiers
We mentioned the private
keyword earlier in this chapter but didn't have a chance to explain it. The private
properties or functions are only accessible to the class that declared them – Moveable
, in this case.
The default visibility of classes and properties is public, so there is no need to use the public
keyword all the time.
In order to extend an abstract class, we simply put its name after a colon. There's also no extends
keyword in Kotlin.
class ActivePlayer(name: String) : Moveable(), DiceRoller {
...
}
How would you be able to differentiate between an abstract class and an interface, then?
An abstract class has round brackets after its name to indicate that it has a constructor. In the upcoming chapters, we'll see some uses of that syntax.
Inheritance
Apart from extending abstract classes, we can also extend regular classes as well.
Let's try to extend our Player
class using the same syntax we used for an abstract class. We will attempt to create a ConfusedPlayer
class, that is, a player that when given (x and y) moves to (y and x) instead.
First, let's just create a class that inherits from Player
:
class ConfusedPlayer(name: String ): ActivePlayer(name)
Here, you can see the reason for round brackets even in abstract classes. This allows passing arguments to the parent class constructor. This is similar to using the super
keyword in Java.
Surprisingly, this doesn't compile. The reason for this is that all classes in Kotlin are final by default and cannot be inherited from.
To allow other classes to inherit from them, we need to declare them open
:
open class ActivePlayer (...) : Moveable(), DiceRoller {
...
}
Let's now try and override the move
method now:
class ConfusedPlayer(name : String): Player(name) {
// move() must be declared open
override fun move(x: Int, y: Int) {
this.x = y // must be declared protected
this.y = x // must be declared protected
}
}
Overriding allows us to redefine the behavior of a function from a parent class. Whereas in Java, @Override
is an optional annotation, in Kotlin override
is a mandatory keyword. You cannot hide supertype methods, and code that doesn't use override
explicitly won't compile.
There are two other problems that we introduced in that piece of code. First, we cannot override a method that is not declared open
as well. Second, we cannot modify the coordinates of our player from a child class since both coordinates are private
.
Let's use the protected
visibility modifier the makes the properties accessible to child classes and mark the function as open
to be able to override it:
abstract class Moveable() {
protected var x: Int = 0
protected var y: Int = 0
open fun move(x: Int, y: Int) {
this.x = x
this.y = y
}
}
Now, both of the problems are fixed. You also see the protected
keyword here for the first time. Similar to Java, this visibility modifier makes a property or a method visible only to the class itself and to its subclasses.
Data classes
Remember that Kotlin is all about productiveness. One of the most common tasks for Java developers is to create yet another Plain Old Java Object (POJO). If you're not familiar with POJO, it is basically an object that only has getters, setters, and implementation of equals
or hashCode
methods. This task is so common that Kotlin has it built into the language. It's called a data class.
Let's take a look at the following example:
data class User(val username: String, private val
password: String)
This will generate us a class with two getters and no setters (note the val
part), which will also implement equals
, hashCode
, and clone
functions in the correct way.
The introduction of data
classes is one of the most significant improvements in reducing the amount of boilerplate in the Kotlin language. Just like the regular classes, data
classes can have their own functions:
data class User(val username: String, private val
password: String) {
fun hidePassword() = "*".repeat(password.length)
}
val user = User("Alexey", "abcd1234")
println(user.hidePassword()) // ********
Compared to regular classes, the main limitation of data
classes is that they are always final
, meaning that no other class can inherit from them. But it's a small price to pay to have equals
and hashCode
functions generate automatically.
Kotlin data classes versus Java records
Learning from Kotlin, Java 15 introduced the notion of records. Here is how we can represent the same data as a Java record
:
public record User(String username, String password) {}
Both syntaxes are pretty concise. Are there any differences, though?
- Kotlin
data
classes a have copy()
function that records lack. We'll cover it in Chapter 2, Working with Creational Patterns, while discussing the prototype design pattern.
- In a record, all properties must be
final
, or, in Kotlin terms, records support only values and not variables.
- The
data
classes can inherit from other classes, while records don't allow that.
To summarize, data
classes are superior to records in many ways. But both are great features of the respective languages. And since Kotlin is built with interoperability in mind, you can also easily mark a data
class as a record to be accessible from Java:
@JvmRecord
data class User(val username: String, val password: String)