Koans
Classes
- Classes in Kotlin are declared using the keyword class:
class Person { /*...*/ }
- Can have a primary constructor and one or more secondary constructors
class Person constructor(firstName: String) { /*...*/ }
- If the primary constructor does not have any annotations or visibility modifiers, the constructor keyword can be omitted:
class Person(firstName: String) { /*...*/ }
- Kotlin has a concise syntax for declaring properties and initializing them from the primary constructor:
class Person(val firstName: String, val lastName: String, var isEmployed: Boolean = true)
Much like regular properties, properties declared in the primary constructor can be mutable (
var
) or read-only (val
).
Data Classes
Classes whose main purpose is to hold data.
In such classes, some standard functionality and some utility functions are often mechanically derivable from the data.
In Kotlin, these are called data classes and are marked with data:
data class User(val name: String, val age: Int)
The compiler automatically derives the following members from all properties declared in the primary constructor:
equals()
/hashCode()
pairtoString()
of the formUser(name=John, age=42)
componentN()
functions corresponding to the properties in their order of declarationcopy()
function
Prerequisites
- The primary constructor needs to have at least 1 parameter.
- All primary constructor parameters need to be marked as val or var.
- Data classes cannot be abstract, open, sealed, or inner.
Copy()
Use the copy()
function to copy an object allowing to alter some of its properties while keeping the rest unchanged
val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)
Destructuring
Component functions generated for data classes make it possible to use them in destructuring declarations:
val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age") // prints "Jane, 35 years of age"
Exercise
- Migrate this java code to a data class in Kotlin
- Instantiate 2 persons : “Neo”, “Trinity”
- Use the
copy
function to create a new Person called “NeoV2” based onpublic class Person { private final String name; private final int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } }
Objects
The Singleton pattern can be useful in several cases, and Kotlin makes it easy to declare singletons:
object DataProviderManager {
fun registerDataProvider(provider: DataProvider) {
// ...
}
val allDataProviders: Collection<DataProvider>
get() = // ...
}
Companion Objects
An object declaration inside a class can be marked with the companion keyword:
class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
// The name of the companion object can be omitted
companion object { }
}
- A good way to define new more explicit types with enforced rules
- Makes it impossible to represent invalid states
Exercise
- Create a
NonZeroPositiveInteger
class with its companion - We use the companion to create
NonZeroPositiveIntegers
- The companion should contain 2 functions :
fun from(value: Int): NonZeroPositiveInteger = { ... } fun toInt(i: NonZeroPositiveInteger): Int = { ... }
Inline classes
Sometimes it is necessary for business logic to create a wrapper around some type. Kotlin introduces a special kind of class called inline class
. They don’t have an identity and can only hold values.
To declare an inline class for the JVM backend, use the value modifier along with the @JvmInline annotation before the class declaration:
// For JVM backends
@JvmInline
value class Password(private val s: String)
An inline class must have a single property initialized in the primary constructor. At runtime, instances of the inline class will be represented using this single property
You have 2 examples of it in the koans-solution.ws.kts
file
Extension functions
Kotlin provides the ability to extend a class with new functionality without having to inherit from the class or use design patterns such as Decorator.
For example, you can write new functions for a class from a third-party library that you can’t modify
- To declare an extension function :
prefix its name with a receiver type
, which refers to the type being extendedfun MutableList<Int>.swap(index1: Int, index2: Int) { val tmp = this[index1] // 'this' corresponds to the list this[index1] = this[index2] this[index2] = tmp }
The
this
keyword inside an extension function corresponds to the receiver object (the one that is passed before the dot). Now, you can call such a function on any MutableList:
Companion object extensions
If a class has a companion object defined, you can also define extension functions and properties for the companion object. Just like regular members of the companion object, they can be called using only the class name as the qualifier:
class MyClass {
companion object { } // will be called "Companion"
}
fun MyClass.Companion.printCompanion() { println("companion") }
fun main() {
MyClass.printCompanion()
}
Let : scope functions
Several functions whose sole purpose is to execute a block of code within the context of an object. When you call such a function on an object with a lambda expression provided, it forms a temporary scope.
- In this scope, you can access the object without its name.
- Such functions are called scope functions.
- There are five of them:
let
,run
,with
,apply
, andalso
.
// Without let :
val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)
// With let
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
- let scope invoked only when not null
val listWithNulls: List<String?> = listOf("Kotlin", null, "Clojure", null, "Scala")
for (item in listWithNulls) {
item?.let { println(it) }
}
Higher-oder functions
A higher-order function is a function that takes functions as parameters, or returns a function
fun <T, R> Collection<T>.fold(
initial: R,
// accepts any function matching (R, T) -> R
// Taking an R and a T as parameters and returning an R
combine: (acc: R, nextElement: T) -> R
): R {
var accumulator: R = initial
for (element: T in this) {
accumulator = combine(accumulator, element)
}
return accumulator
}
Inline functions
Using higher-order functions imposes certain runtime penalties:
- each function is an object
- it captures a closure
A closure is a scope of variables that can be accessed in the body of the function.
Every time we declare a higher-order function, at least one instance of those special Function* types will be created.
The extra memory allocations get even worse when a lambda captures a variable: The JVM creates a function type instance on every invocation.
When using inline functions, the compiler inlines the function body.
It substitutes the body directly into places where the function gets called.
By default, the compiler inlines the code for both the function itself and the lambdas passed to it.
inline fun <T> Collection<T>.each(block: (T) -> Unit) { for (e in this) block(e) }
For example, The compiler will translate :
val numbers = listOf(1, 2, 3, 4, 5)
numbers.each { println(it) }
To something like:
val numbers = listOf(1, 2, 3, 4, 5)
for (number in numbers)
println(number)
When using inline functions, there is no extra object allocation and no extra virtual method calls.