Kotlin Language
a.k.a Android Language
Intro
TODO
Data Types
The official doc has a complete and pretty easy text about it. These notes aren’t supposed to be a mirror of the documentation already available online.
Any, Unit & Nothing
Any
: The root of the Kotlin class hierarchy. Every Kotlin class has Any
as a superclass.
// The returned value's type isn't pre-defined
fun returnsAnyType(): Any? {
val randomNumber = (0..5).random()
return when (randomNumber) {
0 -> "string"
1 -> 's'
2 -> 1
3 -> Double.MAX_VALUE
4 -> Float.MAX_VALUE
5 -> true
else -> null
}
}
Unit
: The type with only one value. This type corresponds to the void
type in Java.
fun hasNoReturn(): Unit {
}
Nothing
: Nothing
has no instances. You can use Nothing
to represent “a value that never exists”: for example, if a function has the return type of Nothing
, it means that it never returns (always throws an exception).
// This function can't return anything, not even an Unit
fun returnsNothing(): Nothing {
throw Exception()
}
Functions
Higher-order functions
A higher-order function is a function that takes functions as parameters, or returns a function. That’s not exclusive to Kotlin.
Functions literals
Function literals are functions that are not declared but are passed immediately as an expression. There are two kinds of function literal in Kotlin: Lambda expressions and Anonymous functions.
Lambda expressions
Below, an example of the usage of a lambda function that compares two strings by its lengths:
max(strings, { a, b -> a.length < b.length })
Since a lambda is an expression, it can be attributed into a variable:
// Types inside lambda
val isStringShorterThanInt = { a: String, b: Int -> a.length < b }
// Types outside lambda
val isGreaterThan: (Int, Int) -> Boolean = { a, b -> a > b }
There are a few cool syntax rules that applies to lambda expressions:
Inferred Return Value
If the inferred return type of the lambda is not Unit, the last (or possibly single) expression inside the lambda body is treated as the return value:
val compare: (String, String) -> Boolean = { str1, str2 ->
val len1 = str1.length
val len2 = str2.length
len1 > len2
}
Passing Trailing Lambdas
According to Kotlin convention, if the last parameter of a function is a function, then a lambda expression passed as the corresponding argument can be placed outside the parentheses:
val product = items.fold(1) { acc, e -> acc * e }
Such syntax is also known as trailing lambda.
If the lambda is the only argument in that call, the parentheses can be omitted entirely:
data class Person(
val age: Short
)
fun main() {
val person = Person(27)
// option 1
val v = person.takeIf({ it -> it.age > 18 })
// option 2
val p = person.takeIf { it -> it.age > 18 }
}
it: Implicit Name Of A Single Parameter
If the compiler can parse the signature without any parameters, the parameter does not need to be declared and ->
can be omitted. The parameter will be implicitly declared under the name it
.
// option 1
person.let { it -> print(it.age) }
// since there is only one argument, it can be omitted
person.let { print(it.age) }
Returning A Value From A Lambda Expression
In Kotlin, function can be nested using functions literals, local functions and expressions. When a return
statement is called inside a lambda expression, will take effect on the outer calling function:
fun foo() {
listOf(1, 2, 3, 4, 5).forEach {
if (it == 3) return // non-local return directly to the caller of foo()
print(it)
}
println("this point is unreachable")
}
To return from a lambda expression, qualify the return
:
fun foo() {
listOf(1, 2, 3, 4, 5).forEach {
if (it == 3) return@forEach // local return to the caller of the lambda - the forEach loop
print(it)
}
print(" done with implicit label")
}
Alternatively, you can replace the lambda expression with an anonymous function:
fun foo() {
listOf(1, 2, 3, 4, 5).forEach(fun(value: Int) {
if (value == 3) return // local return to the caller of the anonymous function - the forEach loop
print(value)
})
print(" done with anonymous function")
}
Underscore For Unused Variables
If the lambda parameter is unused, you can place an underscore instead of its name:
map.forEach { (_, value) -> println("$value!") }
Anonymous Functions
They’re simply normal functions without the name:
// with name
fun sum(a: Int, b: Int): Int {
return a + b
}
// without name, with body
var sum = fun(a: Int, b: Int): Int {
return a + b
}
// without name, expression body
var sum = fun(a: Int, b: Int): Int = a + b
The parameters and the return type are specified in the same way as for regular functions, except the parameter types can be omitted if they can be inferred from the context:
integers.filter(fun(item) = item > 0)
In the example above, the function filter
expects an argument of type (Int) -> Boolean
so, when passing just fun(item) = item > 0
the compiler infers the type Int
to item
.
Note that when filter
receives an anonymous function, this is placed inside ()
. If we gave a lambda expression, this could be placed inside {}
:
integers.filter { it > 0 }
Working with Dates
TODO
Ranges and progressions
There’s an official text about ranges in Kotlin. Below, I will cover some concepts with examples.
Comparing ranges
Ranges can be compared using equality operator (==
).
fun main() {
val zeroToTen: IntRange = 0.rangeTo(10)
val zeroTo10: IntRange = 0..10
println(zeroToTen == zeroTo10) // true
val zeroToNine: IntRange = 0.rangeUntil(10)
val zeroTo9: IntRange = 0..<10
println(zeroToNine == zeroTo9) // true
println(zeroTo10 == zeroTo9) // false
println(zeroToTen == zeroToNine) // false
}
Date ranges
Syntax to define a date range:
val birth = Date.from(Instant.ofEpochMilli(834669000000L))
val now = Date()
val life: ClosedRange<Date> = birth..now
ClosedRange
doesn’t have an iterable interface, so it cannot be used in a loop:
// error: For-loop range must have an 'iterator()' method
for (date in life) {
println(date);
}
But it still have some getters:
print(life.start)
print(life.endInclusive)
And some methods that doesn’t require iterating over each date:
val newMillennium = Date.from(Instant.ofEpochMilli(946684800000L))
life.contains(newMillennium);
life.run {
start.before(now)
};
life.takeIf { range ->
range.isEmpty()
};
life.isEmpty() // that's deep
Classes and Objects
Normal Classes
class Dog(val name: String, val breed: String, var age: Int = 1) {
init {
bark()
}
fun bark() {
println("Woof Woof");
}
}
Data Classes
Given this example:
data class Coffee(
val spoonsCount: Int,
val ownerName: String,
val size: String,
val creamAmount: Int = 0
)
The compiler automatically derives the following members from all properties declared in the primary constructor:
-
.equals()
/.hashCode()
pair. -
.toString()
of the form “Coffee(spoonsCount=3, ownerName=“John”, size=“XL”, creamAmount=2)“. -
.componentN()
functions corresponding to the properties in their order of declaration. -
.copy()
function.
Constructors
Every class have an implicit constructor which doesn’t need to be written.
class Empty
The implicit constructor (a.k.a default constructor) can be private. Having only privates constructors, the class can’t be instanced, just like the Nothing
class. Example:
class NoInstance private constructor()
The class below has a default constructor which requires a String and a Short.
class Constructor(private var name: String, private var age: Short) {
/**
When defining another constructor, it needs to call the default constructor using `this`
keyword.
*/
constructor() : this("", 0) {
// Constructors can have a body
println("Warning: constructed an empty object")
}
/**
* More than one constructor can be defined, as long as they don't have the same signature.
*/
constructor(name: String) : this(name, 0)
/**
* The definition below is invalid because the default constructor already has the signature
* (String, Short)
*/
// constructor(str: String, num: Short) : this(str, num)
override fun toString(): String = "Constructor(name: $name, age: $age)"
}
Usage example:
fun main() {
Empty() // Has an implicit constructor
// NoInstance() Can't be instanced
val c1 = Constructor()
val c2 = Constructor("Victor")
val c3 = Constructor("Victor", 18)
for (c in arrayOf(c1, c2, c3)) {
println(c)
}
}
Inline Value Classes
TODO
Object Expressions and Declarations
TODO
Getters, Setters & fields
TODO
Late Initialization
TODO
With
TODO
Async Programming
TODO
Coroutines
TODO
Nice Utilities
Random Numbers
The easiest to generate a random Int way is through ranges:
val randomNumber = (0..5).random()
To generate random Double
in the range [0.0, 1.0)
, do:
val random = Math.random()
Since computers doesn’t have infinite precision, there is a limited amount of Double
numbers they can represent. Given a number N, you can verify what’s the next possible Double
right after or before N. For example, let N = 1.0
. We can verify that the next Double
that the computer can represent is 1.0000000000000002
by running:
println(1.0.nextUp())
We can verify that 0.9999999999999999
is the Double
value right before the 1.0 with the command println(1.0.nextDown())
.
The same applies to Float
:
println(1.0f.nextDown()) // 0.99999994
println(1.0f.nextUp()) // 1.0000001
Since the largest double value less than 1.0 is Math.nextDown(1.0)
, a value in the closed range where may be defined by the statements:
double f = Math.random()/Math.nextDown(1.0);
double x = x1*(1.0 - f) + x2*f;