class User(val username: String) fun main() { val user = User("bob") println(user.username) }
- In one line we've declared a class with a property on that class
- Note that public is the default visibility
- Also note no new keyword when creating an instance
- Since val was used to define the username, you can only get the value, and not change it
- If you want to change it (e.g. have a setter), then use var
class User(var username: String) fun main() { val user = User("bob") user.username = "robert" println(user.username) }
- Also, you can set default values for class properties
class User(var username: String = "unknown") fun main() { val user = User() println(user.username) }
- You can also define multiple classes in the same file
class User(val username: String) class Comment(val message: String, val author: User) fun main() { val user = User("bob") val comment = Comment("Hi there!", user) }
- Classes like these that only contain data are called value objects, and are frequently known as pojos in Java
- Typically with these kinds of classes, you should implement the toString, equals, and hashCode methods
- In Kotlin, these methods can be auto implemented for you by declaring the class a data class
class User(val username: String) class Comment(val message: String, val author: User) fun main() { val user1 = User("bob") val comment1 = Comment("Hi there!", user1) val user2 = User("bob") val comment2 = Comment("Hi there!", user2) println(comment1) println(comment2) println(comment1 == comment2) println(comment1.hashCode()) println(comment2.hashCode()) }compared to
data class User(val username: String) data class Comment(val message: String, val author: User) fun main() { val user1 = User("bob") val comment1 = Comment("Hi there!", user1) val user2 = User("bob") val comment2 = Comment("Hi there!", user2) println(comment1) println(comment2) println(comment1 == comment2) println(comment1.hashCode()) println(comment2.hashCode()) }Let's look at something a little more complicated:
class User(val username: String) class Comment(initialMessage: String, val author: User) { private val msgHist = mutableListOf(initialMessage) init { println("Logging initial message: $initialMessage") } var message: String get() { println("Message retrieved") return msgHist.last() } set(value: String) { println("Message edited: $value") msgHist.add(value) } fun compareHistory(startIndex: Int, endIndex: Int): List<String> { return listOf(msgHist[startIndex], msgHist[endIndex]) } } fun main() { val comment = Comment("hi thre!", User("bob")) comment.message = "hi there!" comment.message = "Hi there!" println(comment.message) println(comment.compareHistory(0, 2)) }
- We didn't want initialMessage to be a property, so ommitted the val/var, making it just a parameter
- Parameters can be used in the init block or to initialize properties
- Properties can have custom accessors
- Here's an example of a function in a class
- Also note that if you need more than the primary constructor, you can create secondary constructors like this:
class User(val username: String) class Comment(messageHistory: List<String>, val author: User) { private val msgHist = messageHistory.toMutableList() init { println("Logging initial messages: $messageHistory") } constructor(initialMessage: String, author: User): this(listOf(initialMessage), author) { println("Secondary constructor used") } var message: String get() { println("Message retrieved") return msgHist.last() } set(value: String) { println("Message edited: $value") msgHist.add(value) } fun compareHistory(startIndex: Int, endIndex: Int): List<String> { return listOf(msgHist[startIndex], msgHist[endIndex]) } } fun main() { val comment = Comment("hi thre!", User("bob")) comment.message = "hi there!" comment.message = "Hi there!" println(comment.message) println(comment.compareHistory(0, 2)) }
- You are also not required to have a primary constructor
class User(val username: String) class Comment { private val msgHist: MutableList<String> val author: User init { println("Logging init block usage") } constructor(initialMessage: String, author: User) { println("Logging initial message: $initialMessage") msgHist = mutableListOf(initialMessage) this.author = author } constructor(messageHistory: List<String>, author: User) { println("Logging initial messages: $messageHistory") msgHist = messageHistory.toMutableList() this.author = author } var message: String get() { println("Message retrieved") return msgHist.last() } set(value: String) { println("Message edited: $value") msgHist.add(value) } fun compareHistory(startIndex: Int, endIndex: Int): List<String> { return listOf(msgHist[startIndex], msgHist[endIndex]) } } fun main() { val comment = Comment("hi thre!", User("bob")) comment.message = "hi there!" comment.message = "Hi there!" println(comment.message) println(comment.compareHistory(0, 2)) }
- Also, you can have different visibilities for the get and set accessors
class User(val username: String) class Comment { private val msgHist: MutableList<String> var author: User private set init { println("Logging init block usage") } constructor(initialMessage: String, author: User) { println("Logging initial message: $initialMessage") msgHist = mutableListOf(initialMessage) this.author = author } constructor(messageHistory: List<String>, author: User) { println("Logging initial messages: $messageHistory") msgHist = messageHistory.toMutableList() this.author = author } var message: String get() { println("Message retrieved") return msgHist.last() } set(value: String) { println("Message edited: $value") msgHist.add(value) } fun compareHistory(startIndex: Int, endIndex: Int): List<String> { return listOf(msgHist[startIndex], msgHist[endIndex]) } fun anonymize() { author = User("anonymous") } } fun main() { val comment = Comment("hi thre!", User("bob")) comment.message = "hi there!" comment.message = "Hi there!" println(comment.message) println(comment.compareHistory(0, 2)) }
- And you can access the backing field for a property with the field keyword
data class User(val username: String) class Comment { private val msgHist: MutableList<String> var author: User private set(value: User) { println("User changed from $field to $value") field = value } init { println("Logging init block usage") } constructor(initialMessage: String, author: User) { println("Logging initial message: $initialMessage") msgHist = mutableListOf(initialMessage) this.author = author } constructor(messageHistory: List<String>, author: User) { println("Logging initial messages: $messageHistory") msgHist = messageHistory.toMutableList() this.author = author } var message: String get() { println("Message retrieved") return msgHist.last() } set(value: String) { println("Message edited: $value") msgHist.add(value) } fun compareHistory(startIndex: Int, endIndex: Int): List<String> { return listOf(msgHist[startIndex], msgHist[endIndex]) } fun anonymize() { author = User("anonymous") } } fun main() { val comment = Comment("hi thre!", User("bob")) comment.message = "hi there!" comment.message = "Hi there!" println(comment.message) println(comment.compareHistory(0, 2)) comment.anonymize() }
- You can also create infix functions
class Thread() { val comments = mutableListOf<Comment>() } data class User(val username: String) class Comment { private val msgHist: MutableList<String> var author: User private set(value: User) { println("User changed from $field to $value") field = value } init { println("Logging init block usage") } constructor(initialMessage: String, author: User) { println("Logging initial message: $initialMessage") msgHist = mutableListOf(initialMessage) this.author = author } constructor(messageHistory: List<String>, author: User) { println("Logging initial messages: $messageHistory") msgHist = messageHistory.toMutableList() this.author = author } var message: String get() { println("Message retrieved") return msgHist.last() } set(value: String) { println("Message edited: $value") msgHist.add(value) } fun compareHistory(startIndex: Int, endIndex: Int): List<String> { return listOf(msgHist[startIndex], msgHist[endIndex]) } fun anonymize() { author = User("anonymous") } infix fun on(thread: Thread) { thread.comments.add(this) } } fun main() { val thread = Thread() val comment = Comment("hi thre!", User("bob")) comment.message = "hi there!" comment.message = "Hi there!" println(comment.message) println(comment.compareHistory(0, 2)) comment.anonymize() comment on thread }Visibility
- Let's touch on visibility real quick
Modifier | Class member | Top-level declaration |
---|---|---|
public (default) | Visible everywhere | Visible everywhere |
internal | Visible in a module | Visible in a module |
protected | Visible in subclass | -- |
private | Visible in a class | Visible in a file |
Enums
- Enums are declared via enum class, other than that they're the same as in Java
enum class Colors { RED, GREEN, BLUE }Interfaces
- Interfaces work very similarly to interfaces in Java
interface Clickable { fun click() } class Button : Clickable { override fun click() = println("Click!") } fun main() { Button().click() }
- Note the colon used to specify interface implementation
- Also note the required override keyword
- Unlike Java, Kotlin classes are final by default. Use the open keyword to make a class inheritable
- Also, functions must be marked as open to be overridable
open class Animal { open fun speak() { println("...") } fun sleep() { println("zzz") } } class Dog : Animal() { override fun speak() { println("Bark") } } class Cat : Animal() { override fun speak() { println("Meow") } } fun main() { val animal = Animal() animal.speak() animal.sleep() val dog = Dog() dog.speak() dog.sleep() val cat = Cat() cat.speak() cat.sleep() }
- You can also use the final keyword for a final override
open class Animal { open fun speak() { println("...") } fun sleep() { println("zzz") } } open class Dog : Animal() { final override fun speak() { println("Bark") } } class Bulldog : Dog() { //Can't override speak() fun growl() { println("Grr") } } class Cat : Animal() { override fun speak() { println("Meow") } } fun main() { val animal = Animal() animal.speak() animal.sleep() val dog = Dog() dog.speak() dog.sleep() val bulldog = Bulldog() bulldog.speak() bulldog.sleep() bulldog.growl() val cat = Cat() cat.speak() cat.sleep() }
- You can also create abstract classes, where the base class can't be instantiated
abstract class Animal { open fun speak() { println("...") } fun sleep() { println("zzz") } } class Dog : Animal() { override fun speak() { println("Bark") } } class Cat : Animal() { override fun speak() { println("Meow") } } fun main() { val animal = Animal() //This won't compile val dog = Dog() dog.speak() dog.sleep() val cat = Cat() cat.speak() cat.sleep() }
- You can also create sealed classes, where subclasses are required to be defined in the same file
- This limits subclasses to a pre defined set of classes
sealed class Animal { open fun speak() { println("...") } fun sleep() { println("zzz") } } class Dog : Animal() { override fun speak() { println("Bark") } } class Cat : Animal() { override fun speak() { println("Meow") } } fun main() { val dog = Dog() dog.speak() dog.sleep() val cat = Cat() cat.speak() cat.sleep() }Class delegation and the by keyword
- Most experts encourage composition over inheritance, but many languages don't make composition easy
- Kotlin addresses this
interface Door { fun enter() } class WoodDoor : Door { override fun enter() { println("Entered by door") } } interface Window { fun openWindow() } class ClearWindow : Window { override fun openWindow() { println("Opened the window") } } class House : Door by WoodDoor(), Window by ClearWindow() class Shed(door: Door) : Door by door class WindowDisplay(window: Window = ClearWindow()) : Window by window fun main() { val house = House() house.enter() house.openWindow() val shed = Shed(WoodDoor()) shed.enter() val windowDisplay = WindowDisplay() windowDisplay.openWindow() }Extension functions and properties
- You can, in essence, extend classes that you don't have control over through extension functions
fun main() { val helloWorld = "Hello World" println(helloWorld.removeVowels()) println(helloWorld.firstVowel) } val vowels = listOf('a', 'e', 'i', 'o', 'u') fun String.removeVowels() = this.filter { it.toLowerCase() !in vowels } val String.firstVowel get() = filter { it.toLowerCase() in vowels }.first()
- You can also use this to give classes certain functionality only in the right circumstances
fun main() { val helloWorld = "Hello World" println(helloWorld.removeVowels()) println(helloWorld.firstVowel) //This won't work MySpecialClass(helloWorld) //Inside the class it will work } val vowels = listOf('a', 'e', 'i', 'o', 'u') fun String.removeVowels() = this.filter { it.toLowerCase() !in vowels } class MySpecialClass(str:String) { init { println(str.firstVowel) } private val String.firstVowel get() = filter { it.toLowerCase() in vowels }.first() }Lambdas
fun main() { val incrementer = { a:Int -> a + 1 } println(incrementer(1)) }
- You define a lambda by surrounding it with curly braces
- You define the parameters on the left side of the arrow
- Lamdas can be multiline, and the result of the last statement is returned
fun main() { val incrementer = { a:Int -> println("Input: $a") a + 1 } println(incrementer(1)) }
- The Kotlin standard library has lots of predefined functions that use lambdas, such as map and filter
fun main() { val incrementer = { a:Int -> println("Input: $a") a + 1 } println(listOf(1, 2, 3).map(incrementer)) }
- You can of course pass the lambda into the method directly
fun main() { println(listOf(1, 2, 3).map({ a:Int -> println("Input: $a") a + 1 })) }
- And since it is being passed in, you can infer the parameter type
fun main() { println(listOf(1, 2, 3).map({ a -> println("Input: $a") a + 1 })) }
- And when there's only one parameter, Kotlin will provide a default parameter name that you can use, called it
fun main() { println(listOf(1, 2, 3).map({ println("Input: $it") it + 1 })) }
- When a lambda is the last parameter for a method, the lambda can be moved outside of the parentheses
fun main() { println(listOf(1, 2, 3).map() { println("Input: $it") it + 1 }) }
- And when a lambda is the only parameter for a method, you can remove the parentheses entirely
fun main() { println(listOf(1, 2, 3).map { println("Input: $it") it + 1 }) }
- Why does Kotlin support this? Because it allows you to create constructs that look like they're part of the language. For example:
fun main() { ifnot (false) { println("here") } } fun ifnot(conditional:Boolean, body:() -> Unit) { if (!conditional) body() }
- And here we also see how to create functions that take lambdas as parameters
- body is a lambda that has no parameters, so it uses ()
- It also returns nothing, so it specifies the return type Unit
- To specify parameters and return types, we can do something like this:
fun main() { val result = 1.singleMap { it > 0 } println(result) } fun <T, R> T.singleMap(mapper:(T) -> R) = mapper(this)
- Note that parenthesis around the parameter types is always required
- We also get a small taste of generics here
- We can also return a lambda from a function, like this:
fun main() { val add1 = curry(::add, 1) println(add1(2)) } fun add(a:Int, b:Int):Int = a + b fun <T, U, R> curry(function:(T, U) -> R, t:T): (U) -> R { return { u -> function(t, u) } }
- Also note that we can pass regularly defined functions as lambdas by prepending them with ::
- You can also inline functions, and the compiler will inline the code with the lambda at compile time
fun main() { ifnot (false) { println("In ifnot") } } inline fun ifnot(conditional:Boolean, body:() -> Unit) { if (!conditional) body() }
- This allows for some speedups, since lambda objects don't need to be created at runtime
- Note that an inlined function cannot call and pass its lambda to another function unless that function is also inlined
- It also allows you do to some extra things in your lambda, such as using a return statement
fun main() { ifnot (false) { println("In ifnot") return println("After return") //It won't reach this code } println("After ifnot") //It won't reach this code, either } inline fun ifnot(conditional:Boolean, body:() -> Unit) { if (!conditional) body() }
- Note that if you wanted to just return from the lambda, you can do so as follows:
fun main() { ifnot (false) { println("In ifnot") return@ifnot println("After return") //It won't reach this code } println("After ifnot") //But it will reach this code } inline fun ifnot(conditional:Boolean, body:() -> Unit) { if (!conditional) body() }
- You can also specify a label, instead of using the method name:
fun main() { ifnot (false) marker@{ println("In ifnot") return@marker println("After return") //It won't reach this code } println("After ifnot") //But it will reach this code } inline fun ifnot(conditional:Boolean, body:() -> Unit) { if (!conditional) body() }
- You can also create lambdas with receivers, which bind the this keyword to something specific in the lambda
- You can think of them as extension functions as lambdas
- Here's how the with statement in the standard library works (note that the standard library with statement has a little more to it)
fun main() { val result = with("Hello World") { substring(6) } println(result) } inline fun <T, R> with(receiver:T, block:T.() -> R): R { return receiver.block() }