- Meta has adopted Kotlin as its new primary language for Android development
- Meta is migrating its Android codebase from Java to Kotlin
- over 10 million lines of Kotlin code
- Kotlin is now the recommended programming language
- not only for Android programming
- but also for server-side JVM usage at Google
- over 8 million lines of Kotlin code
- “Java after spring cleaning”
- “Java without the boilerplate”
- Killer features:
- Null safety
- Extension functions
- Properties
- Compilation targets:
- JVM 8+ bytecode (excellent Kotlin⟷Java interop)
- JavaScript
- WebAssembly
- Native (Windows, macOS, iOS, Linux, Android)
1.1 1.3 1.5 1.8
Java Scala Kotlin 1.0 | 1.2 | 1.4| 1.6 | 1.9
| | | | | | | | | | 1.7| |
+---+---+---+---|---+---+---+---+---|---+---+---+---+---|---+---+---+---+---|---+---+---+---+---|---+---+---+---
96 00 | 05 10 | 15 | 20
| IntelliJ IDEA | | |
| Karel skorbut Karel
JetBrains (Scala) (Kotlin) (Kotlin)
fun main(args: Array<String>): Unit {
println("Hello Kotlin!");
}
- Dedicated
fun
keyword for functions - Top-level functions (
main
,println
) - Classic
name: type
declaration syntax - No special syntax for arrays
args
array can be omitted
Unit
is Kotlin-speak forvoid
- Return type
Unit
can be omitted
- Return type
- Semicolons can be omitted
fun main() {
println("Hello Kotlin!")
}
Exercise
- Download IntelliJ IDEA Community Edition 👇 Scroll Down 👇
- Import the Hello World project
- File > New > Project from Version Control...
- URL: https://github.com/frectures/kotlin.git
- Clone
- If the Project View is hidden, press Alt+1
- Open
src/main/kotlin/Kotlin.kt
- Start the
main
function via the green triangle beside it- Modify the program to print something else
public static String interviewQuestion(List<String> strings) {
// return "";
// return null;
// throw new NotImplementedYet();
TODO(); // Missing return statement
}
public static void TODO() {
throw new NotImplementedYet();
}
void
means “returns normally, without a result”- same as
Unit
in Kotlin
- same as
Nothing
means “always throws an exception”:
fun TODO(): Nothing {
throw NotImplementedError()
}
fun interviewQuestion(strings: List<String>): String {
TODO() // okay
}
Int
is usuallyint
, unless boxing necessitatesInteger
:
fun sumNumbers(numbers: List<Int>): Int {
// Integer int
var result = 0
// int
for (x in numbers) {
// int
result += x
}
return result
}
fun main() {
println(sumNumbers(listOf(2, 3, 5, 7)))
// Summing numbers is already implemented in the standard library though:
println(listOf(2, 3, 5, 7).sum())
}
var
infers the type of the variable from its initializer:
var a: Int = 1
var b = 2
// Int <<<< Int
- Type inference is not dynamic typing:
var c
// This variable must either have a type annotation or be initialized
var theAnswerToLife = 42
theAnswerToLife = "fish"
// Required: Int Found: String
fun digitOrNumber(x: Int): String {
if (x in 0..9) {
return "digit"
} else {
return "number"
}
}
fun digitOrNumber(x: Int): String {
return if (x in 0..9) "digit" else "number"
}
fun digitOrNumber(x: Int): String = if (x in 0..9) "digit" else "number"
fun digitOrNumber(x: Int) = if (x in 0..9) "digit" else "number"
fun averageMonthLength(month: Int): Double = when (month) {
2 -> 28.2425
4, 6, 9, 11 -> 30.0
1, 3, 5, 7, 8, 10, 12 -> 31.0
else -> throw IllegalArgumentException("illegal month $month")
}
fun count(x: Any): Int = when (x) {
is Array<*> -> x.size
is String -> x.length
is Collection<*> -> x.size
else -> 0
}
fun signum(x: Int): Int = when {
x<0 -> -1
x>0 -> +1
else -> 0
}
Exercise
- Implement a function
gcd
which computes the greatest common divisor of two integers
public class Joiner {
public static String join(Iterable<String> strings, String delimiter, String prefix, String suffix) {
// ...
}
public static void main(String[] args) {
var fruits = List.of("apple", "banana", "cherry", "date", "elderberry");
String a = join(fruits, ", ", "[", "]");
String b = join(fruits, ", ");
String c = join(fruits);
}
public static String join(Iterable<String> strings, String delimiter) {
return join(strings, delimiter, "", "");
}
public static String join(Iterable<String> strings) {
return join(strings, "");
}
}
fun join(strings: Iterable<String>, delimiter: String = "", prefix: String = "", suffix: String = ""): String {
// ...
}
fun main() {
val fruits = listOf("apple", "banana", "cherry", "date", "elderberry")
val a: String = join(fruits, ", ", "[", "]")
val b: String = join(fruits, ", ")
val c: String = join(fruits)
val d: String = join(fruits, delimiter = ", ", prefix = "[", suffix = "]")
val e: String = join(fruits, prefix = "[", delimiter = ", ", suffix = "]")
val f: String = join(fruits, prefix = "\t")
}
- Default arguments are evaluated inside the called function
- arbitrary expressions
- not hardwired into call sites
- Unnamed arguments are forbidden after the first named argument
- Every class without an explicit parent inherits from
Any
:class Child
class Child : Any()
package kotlin
public open class Any {
public open fun equals(other: Any): Boolean
public open fun hashCode(): Int
public open fun toString(): String
}
open class
es are inheritableopen fun
ctions are overridable
class CountingList<E> extends ArrayList<E> {
private int elementsAdded;
@Override
public boolean add(E e) {
elementsAdded += 1;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
elementsAdded += c.size();
// Does super.addAll call this.add internally?
return super.addAll(c);
}
}
- Designing a class for inheritance is hard work
- You must document all its self-use patterns, and once you've documented them, you must commit to them for the life of the class
- If you fail to do this, subclasses may become dependent on implementation details of the superclass and may break if the implementation of the superclass changes
- Unless you know there is a real need for subclasses, you are probably better off prohibiting inheritance by declaring your class
final
- Kotlin has classes and their members
final
by default
- which makes it inconvenient to use frameworks and libraries such as Spring AOP that require classes to be
open
- The
all-open
compiler plugin adapts Kotlin to the requirements of those frameworks
- and makes classes annotated with a specific annotation and their members open without the explicit
open
keyword- e.g.
@Transactional
classes
public static void login() {
Scanner scanner = new Scanner(System.in);
String password;
do {
System.out.print("Password? ");
password = scanner.nextLine();
} while (password != "java");
System.out.println("Welcome to the system!");
}
fun login() {
val scanner = Scanner(System.`in`)
do {
print("Password? ")
val password = scanner.nextLine()
} while (password != "kotlin")
println("Welcome to the system!")
}
- Kotlin has no
new
keyword for constructor calls- Constructor calls look like function calls
- Human disambiguation by first letter case
- Clashing keywords like
in
must be escaped with backticks- Backticks also enable readable test names like
square root should fail for negative inputs
- Backticks also enable readable test names like
do
scope extends towhile
condition- i.e.
password
still accessible for comparison
- i.e.
==
and!=
on references are null-safeequals
comparisons===
and!==
compare the references themselves
println("hello".uppercase() == "HELLO") // true
println("hello".uppercase() === "HELLO") // false
record Name(String forename, String surename) {
Name {
if (forename.isBlank()) throw new IllegalArgumentException();
if (surename.isBlank()) throw new IllegalArgumentException();
}
// auto-generated: equals, hashCode, toString
}
data class Name(val forename: String, val surename: String) {
init {
require(forename.isNotBlank())
require(surename.isNotBlank())
}
// auto-generated: equals, hashCode, toString, copy, component1, component2
}
Exercise
- Implement a data class
Address
with fields of your choice- Populate a list of addresses
- Print the addresses to the console
fun mustPassString(s: String) {
// ...
}
fun canPassString(s: String?) {
// ...
}
fun main() {
mustPassString("hello")
canPassString("world")
mustPassString(null) // Null can not be a value of a non-null type String
canPassString(null) // okay
}
fun mustPassString(s: String) {
val length: Int = s.length
} // ^ okay
fun canPassString(s: String?) {
val length: Int = s.length
// ^ Only safe (?.) or non-null asserted (!!.) calls
// are allowed on nullable receiver of type String?
if (s != null) {
val length: Int = s.length
} // ^ Smart cast to kotlin.String
val lengthOrNull: Int? = s?.length
val lengthOrZero: Int = s?.length ?: 0
}
val shortestString = strings.minByOrNull(String::length) ?: ""
// @return user's input, or null meaning the user canceled the input
val input = JOptionPane.showInputDialog( /* ... */ ) ?: return
// ...
// colors: Map<FlexerState, Int>
return colors[endState] ?: 0x000000
fun largestDownload(): File {
return File(System.getProperty("user.home") + File.separator + "Downloads")
.listFiles() // null if Downloads folder does not exist
?.maxByOrNull(File::length) // null if Downloads folder is empty
?: throw IOException("no Downloads")
}
String!
denotes unknown nullability for Kotlin→Java interop:
public
functions checknull
parameters for Java→Kotlin interop:
Kotlin | Java |
---|---|
|
|
Exercise
- Add a nullable field
remark
to theAddress
class- Print all addresses with remarks before all addresses without remarks
val jamesGosling = Name("James", "Gosling")
val jamesBond = jamesGosling.copy(surename = "Bond")
- The
copy
method has default arguments for all fields:
fun copy(forename: String = this.forename, surename: String = this.surename) = Name(forename, surename)
Exercise
- Implement a
withoutRemark(): Address
method, using thecopy
method
fun String.isAscii(): Boolean {
for (ch in this) {
if (ch > '\u007f') return false
}
return true
}
fun main() {
println("Kaesebroetchen/Muesli".isAscii()) // true
println("Käsebrötchen/Müsli".isAscii()) // false
}
- Extension functions are compiled to static helper methods with an additional
$receiver
parameter - Inside an extension function, only the public interface of the
$receiver
is available
Exercise
- Implement an extension function
String.isPalindrome()
- Implement an extension function such that
listOf(2, 3, 5, 7).product()
returns 210
data class Name(val forename: String, val surename: String)
enum class Month(val averageDays: Double) {
JAN(31.0),
FEB(28.2425),
MAR(31.0),
APR(30.0),
MAY(31.0),
JUN(30.0),
JUL(31.0),
AUG(31.0),
SEP(30.0),
OCT(31.0),
NOV(30.0),
DEC(31.0);
}
class PersonDto(var forename: String, var surename: String)
class MyService(
private val someOtherService: SomeOtherService,
private val yetAnotherService: YetAnotherService,
) {
// ...
}
// The first instruction starts at address 256.
// This makes it easier to distinguish addresses
// from truth values and loop counters on the stack.
const val ENTRY_POINT = 256
class VirtualMachine(
private val program: List<Instruction>,
// ...
) {
var pc: Int = ENTRY_POINT
private set(value) {
assert(value in ENTRY_POINT..program.lastIndex) { "invalid pc $value" }
field = value
}
val currentInstruction: Instruction
get() = program[pc]
// ...
}
// _Strings.kt
public inline fun CharSequence.all(predicate: (Char) -> Boolean): Boolean {
// Function1<Char, Boolean>
for (element in this) {
if (!predicate(element)) return false
// predicate.invoke(element)
}
return true
}
fun String.isAscii1(): Boolean {
return this.all({ ch -> ch <= '\u007f' })
}
fun String.isAscii2(): Boolean {
return this.all { ch -> ch <= '\u007f' }
}
fun String.isAscii3(): Boolean {
return this.all { it <= '\u007f' }
}
- If the only argument is a lambda, the parentheses can be omitted
- The last lambda argument can be moved out of the argument list
fun main() {
repeat(10) {
println(it)
}
}
// Standard.kt
public inline fun repeat(times: Int, action: (Int) -> Unit) {
for (index in 0 until times) {
action(index)
}
}
fun addBook(book: Book) {
synchronized(books) {
books.add(book)
}
}
// Synchronized.kt
public inline fun <R> synchronized(lock: Any, block: () -> R): R {
monitorEnter(lock)
try {
return block()
} finally {
monitorExit(lock)
}
}
val inputStream = largestDownload().inputStream().buffered()
// ... arbitrary code, might throw exception before close ...
inputStream.close()
val inputStream = largestDownload().inputStream().buffered()
inputStream.use {
// ...
}
// ... arbitrary code, might use inputStream after close ...
largestDownload().inputStream().buffered().use { inputStream ->
// ...
}
private fun Expression.evaluate(): Value {
if (value != null) {
// error: 'value' is a mutable property
// that could have been changed by this time
return value
}
value?.let { return it }
// ...
}
fun lookup(name: Token): Symbol? {
val text = name.text
for (i in current downTo 0) {
scopes[i].get(text)?.let { symbol -> return symbol }
}
return null
}
private val randomize = JButton("🎲").apply {
isEnabled = false
}
val temp = result.evaluate()
memory.popStackFrameUnlessEntryPoint()
return temp
return result.evaluate().also { memory.popStackFrameUnlessEntryPoint() }
var previousValue = controlPanel.slider.value
controlPanel.pause.addActionListener {
val slider = controlPanel.slider
if (slider.value != slider.minimum) {
if (slider.value != slider.maximum) {
previousValue = slider.value
}
slider.value = slider.minimum
} else {
slider.value = previousValue
}
}
var previousValue = controlPanel.slider.value
controlPanel.pause.addActionListener {
with(controlPanel.slider) {
if (value != minimum) {
if (value != maximum) {
previousValue = value
}
value = minimum
} else {
value = previousValue
}
}
}
- with(controlPanel.slider) {
+ controlPanel.slider.run {
virtualMachine = VirtualMachine(
instructions, initialWorld,
onCall = editor::push.takeIf { compiledFromSource },
onReturn = editor::pop.takeIf { compiledFromSource },
)
package people
data class Person(val name: String, val birthday: LocalDate)
val people = listOf(
Person("Miles", LocalDate.of(1979, 3, 28)),
Person("Toby", LocalDate.of(1986, 4, 26)),
Person("Tina", LocalDate.of(2011, 3, 11)),
)
val fortyYearsAgo = LocalDate.now().minusYears(40)
val firstSinceThen = people
.map { person -> println(person); person.birthday }
.filter { day -> println(day); day.isAfter(fortyYearsAgo) }
.first()
Person(name=Miles, birthday=1979-03-28)
Person(name=Toby, birthday=1986-04-26)
Person(name=Tina, birthday=2011-03-11)
1979-03-28
1986-04-26
2011-03-11
val firstSinceThen = people
.asSequence()
.map { person -> println(person); person.birthday }
.filter { day -> println(day); day.isAfter(fortyYearsAgo) }
.first()
Person(name=Miles, birthday=1979-03-28)
1979-03-28
Person(name=Toby, birthday=1986-04-26)
1986-04-26
public static List<Fruit> fruitBowl() {
if (Math.random() < 0.5) {
return new LinkedList<Apple>();
} else {
return new ArrayList<Banana>();
}
}
interface Fruit {
}
class Apple implements Fruit {
}
class Banana implements Fruit {
}
- A subtype has all the operations of its supertype(s)
List<Fruit>
has an operationvoid add(Fruit)
List<Apple>
has no such operation- Hence,
List<Apple>
is not aList<Fruit>
- Unfortunately,
Apple[]
is aFruit[]
in Java:
Apple[] apples = new Apple[3];
apples[0] = new Apple();
Fruit[] fruits = apples;
fruits[1] = new Apple();
fruits[2] = new Banana(); // java.lang.ArrayStoreException: Banana
public static List<? extends Fruit> fruitBowl() {
if (Math.random() < 0.5) {
return new LinkedList<Apple>();
} else {
return new ArrayList<Banana>();
}
}
fun fruitBowl(): List<Fruit> {
if (Math.random() < 0.5) {
return LinkedList<Apple>()
} else {
return ArrayList<Banana>()
}
}
- Kotlin
List
s are read-only, hence this is type-safe:
/**
* Methods in this interface support only read-only access to the list;
* read/write access is supported through the [MutableList] interface.
*
* The list is covariant in its element type.
*/
public interface List<out E> : Collection<E> {
// size
// isEmpty
// contains
// get
// indexOf
// ...
}
/**
* A generic ordered collection of elements
* that supports adding and removing elements.
*
* The mutable list is invariant in its element type.
*/
public interface MutableList<E> : List<E>, MutableCollection<E> {
// add
// set
// clear
// remove
// removeAt
// ...
}
- Similar relations exist for
Set
/MutableSet
andMap
/MutableMap
for each element:
val fruits = listOf("apple", "banana", "cherry")
for (fruit in fruits) {
println(fruit)
}
fruits.forEach { fruit ->
println(fruit)
}
fruits.forEach {
println(it)
}
fruits.forEach(::println)
for each index & element:
for (index in 0 until fruits.size) {
val fruit = fruits[index]
println("$index: $fruit")
}
for (index in 0 .. fruits.lastIndex) {
val fruit = fruits[index]
println("$index: $fruit")
}
for (index in fruits.indices) {
val fruit = fruits[index]
println("$index: $fruit")
}
for ((index, fruit) in fruits.withIndex()) {
println("$index: $fruit")
}
fruits.forEachIndexed { index, fruit ->
println("$index: $fruit")
}
for each key & value:
val fruits = mapOf("apple" to "🍎", "banana" to "🍌", "cherry" to "🍒")
for (entry in fruits) {
println("${entry.key}: ${entry.value}")
}
fruits.forEach { entry ->
println("${entry.key}: ${entry.value}")
}
for ((key, value) in fruits) {
println("$key: $value")
}
fruits.forEach { (key, value) ->
println("$key: $value")
}
data class Address(val street: String, val streetNumber: String) {
companion object {
val ADDRESS: Regex = """(\D+?)\s*(\d+.*)""".toRegex()
@JvmStatic
fun parse(line: String): Address {
val (street, streetNumber) = ADDRESS.matchEntire(line)?.destructured ?: error(line)
return Address(street, streetNumber)
}
}
}
fun main() {
val text = """
Musterstr.123
Muster-Gasse 4e
Unter der Ulme 6g
""".trimIndent()
val addresses = text.lines().map(Address::parse)
println(addresses)
}
- Companion objects replace
static
members- adopted from Scala
@JvmStatic
providesstatic
bridge for Java interop
- Triple quotes introduce raw strings
\
instead of\\
- actual line break instead of
\n