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()
}