顯示具有 kotlin 標籤的文章。 顯示所有文章
顯示具有 kotlin 標籤的文章。 顯示所有文章

2019/04/15

Kotlin Higher-order functions: lambdas as parameters and return values

higher-order functions: 自訂 function 且使用 lambda 為 parameters 或是 return values

inline functions: 可減輕使用 lambda 的 performance overhead

宣告 higher-order functions

例如 list.filter { x > 0 } 以 predicate function 為參數,就是一種 higher-order function

Function types

要知道如何定義 lambda 為參數的 function

我們已經知道,再不需要定義 type 也能使用 lambda

val sum = { x: Int, y: Int -> x + y }
val action = { println(42) }

compiler 是將程式轉換為

// 以兩個 Int 為參數,回傳 Int
val sum: (Int, Int) -> Int = { x, y -> x + y }
// 沒有參數,沒有 return value
val action: () -> Unit = { println(42) }

val sum: (Int, Int) -> Int 其中 (Int, Int) 是 parameter types,後面的 Int 是 return type

如果可以回傳 null,就這樣定義

var canReturnNull: (Int, Int) -> Int? = { null }

宣告為 funcation type with a nullable return type

var funOrNull: ((Int, Int) -> Int)? = null

callback 是有參數名稱的 function type

fun performRequest(
       url: String,
       callback: (code: Int, content: String) -> Unit
) {
    /*...*/
}

fun main(args: Array<String>) {
    val url = "http://kotl.in"
    performRequest(url) { code, content -> /*...*/ }
    performRequest(url) { code, page -> /*...*/ }
}
calling functions passed as arguments

twoAndTree 的參數中 operation: (Int, Int) -> Int 是 function type

fun twoAndThree(operation: (Int, Int) -> Int) {
    val result = operation(2, 3)
    println("The result is $result")
}

fun main(args: Array<String>) {
    twoAndThree { a, b -> a + b }
    twoAndThree { a, b -> a * b }
}

另一個例子

fun String.filter(predicate: (Char) -> Boolean): String {
    val sb = StringBuilder()
    for (index in 0 until length) {
        val element = get(index)
        if ( predicate(element) ) sb.append(element)
    }
    return sb.toString()
}

fun main(args: Array<String>) {
    println("ab1c".filter { it in 'a'..'z' })
}

fun String.filter(predicate: (Char) -> Boolean): String

  • 前面的 String 是 receiver type
  • predicate 是 parameter name
  • (Char) -> Boolean 是 function type
Using function type from Java

kotlin 宣告的有 function type 為參數的 function

/* Kotlin declaration */
fun processTheAnswer(f: (Int) -> Int) {
    println(f(42))
}

在 Java 可用 lambda 使用

public class ProcessTheAnswerLambda {
    public static void main(String[] args) {
        processTheAnswer(number -> number + 1);
    }
}

舊版 java 要用 anonymous class

import static ch08.ProcessTheAnswer.ProcessTheAnswer.*;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;

/* Java */

public class ProcessTheAnswerAnonymous {
    public static void main(String[] args) {
        processTheAnswer(
            new Function1<Integer, Integer>() {
                @Override
                public Integer invoke(Integer number) {
                    System.out.println(number);
                    return number + 1;
                }
            });
    }
}

在 java 使用 forEach

import java.util.ArrayList;
import java.util.Collections;
import kotlin.Unit;
import kotlin.collections.CollectionsKt;
import java.util.List;

/* Java */

public class UsingForEach {
    public static void main(String[] args) {
        List<String> strings = new ArrayList();
        strings.add("42");
        CollectionsKt.forEach(strings, s -> {
           System.out.println(s);
           return Unit.INSTANCE;
        });
    }
}
default and null values for parameters with function types

先前 joinToString 的實作

fun <T> Collection<T>.joinToString(
        separator: String = ", ",
        prefix: String = "",
        postfix: String = "",
        transform: (T) -> String = { it.toString() }
): String {
    val result = StringBuilder(prefix)

    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(transform(element))
    }

    result.append(postfix)
    return result.toString()
}

fun main(args: Array<String>) {
    val letters = listOf("Alpha", "Beta")
    println(letters.joinToString())
    println(letters.joinToString { it.toLowerCase() })
    println(letters.joinToString(separator = "! ", postfix = "! ",
           transform = { it.toUpperCase() }))
}

缺點是 transform 裡面永遠會使用 toString 轉換字串

修改為 nullable parameter of a function type

fun <T> Collection<T>.joinToString(
        separator: String = ", ",
        prefix: String = "",
        postfix: String = "",
        transform: ((T) -> String)? = null
): String {
    val result = StringBuilder(prefix)

    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        val str = transform?.invoke(element)
            ?: element.toString()
        result.append(str)
    }

    result.append(postfix)
    return result.toString()
}

fun main(args: Array<String>) {
    val letters = listOf("Alpha", "Beta")
    println(letters.joinToString())
    println(letters.joinToString { it.toLowerCase() })
    println(letters.joinToString(separator = "! ", postfix = "! ",
           transform = { it.toUpperCase() }))
}
returning functions from functions

計算 cost of shipping depending on the selected shipping method,實作 logic variant,回傳對應的 function

enum class Delivery { STANDARD, EXPEDITED }

class Order(val itemCount: Int)

// 宣告回傳 function 的 function
fun getShippingCostCalculator(
        delivery: Delivery): (Order) -> Double {
    if (delivery == Delivery.EXPEDITED) {
        // return lambdas from the function
        return { order -> 6 + 2.1 * order.itemCount }
    }

    return { order -> 1.2 * order.itemCount }
}

fun main(args: Array<String>) {
    val calculator =
        getShippingCostCalculator(Delivery.EXPEDITED)
    // 呼叫 returned function
    println("Shipping costs ${calculator(Order(3))}")
}

另一個例子,GUI contract management application,需要根據 UI 狀態,決定要顯示哪些 contracts

data class Person(
        val firstName: String,
        val lastName: String,
        val phoneNumber: String?
)

class ContactListFilters {
    var prefix: String = ""
    var onlyWithPhoneNumber: Boolean = false

    // 宣告產生 function 的 function
    fun getPredicate(): (Person) -> Boolean {
        val startsWithPrefix = { p: Person ->
            p.firstName.startsWith(prefix) || p.lastName.startsWith(prefix)
        }
        if (!onlyWithPhoneNumber) {
            return startsWithPrefix
        }
        return { startsWithPrefix(it)
                    && it.phoneNumber != null }
    }
}

fun main(args: Array<String>) {
    val contacts = listOf(Person("Dmitry", "Jemerov", "123-4567"),
                          Person("Svetlana", "Isakova", null))
    val contactListFilters = ContactListFilters()
    with (contactListFilters) {
        prefix = "Dm"
        onlyWithPhoneNumber = true
    }
    println(contacts.filter(
        contactListFilters.getPredicate()))
}
removing duplication through lambdas

analyzes visits to a website: SiteVisit 儲存 path of each visit, duration, OS

data class SiteVisit(
    val path: String,
    val duration: Double,
    val os: OS
)

enum class OS { WINDOWS, LINUX, MAC, IOS, ANDROID }

val log = listOf(
    SiteVisit("/", 34.0, OS.WINDOWS),
    SiteVisit("/", 22.0, OS.MAC),
    SiteVisit("/login", 12.0, OS.WINDOWS),
    SiteVisit("/signup", 8.0, OS.IOS),
    SiteVisit("/", 16.3, OS.ANDROID)
)

要計算windows 的平均使用時間,直接寫一個 function

val averageWindowsDuration = log
    .filter { it.os == OS.WINDOWS }
    .map(SiteVisit::duration)
    .average()

fun main(args: Array<String>) {
    println(averageWindowsDuration)
}

改寫,讓 averageDurationFor 可傳入 OS 參數

fun List<SiteVisit>.averageDurationFor(os: OS) =
        filter { it.os == os }.map(SiteVisit::duration).average()

fun main(args: Array<String>) {
    println(log.averageDurationFor(OS.WINDOWS))
    println(log.averageDurationFor(OS.MAC))
}

同時計算 ios, android

val averageMobileDuration = log
    .filter { it.os in setOf(OS.IOS, OS.ANDROID) }
    .map(SiteVisit::duration)
    .average()

fun main(args: Array<String>) {
    println(averageMobileDuration)
}

以 function 動態調整過濾的條件

fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) =
        filter(predicate).map(SiteVisit::duration).average()

fun main(args: Array<String>) {
    println(log.averageDurationFor {
        it.os in setOf(OS.ANDROID, OS.IOS) })
    println(log.averageDurationFor {
        it.os == OS.IOS && it.path == "/signup" })
}

inline function: 減輕 lambda 的 overhead

how inlining works

宣告 function 為 inline,就表示會直接替代 code,而不是用呼叫 function 的方式

以下是 synchronized 的做法,鎖定 lock 物件,執行 action,最後 unlock

inline fun <T> synchronized(lock: Lock, action: () -> T): T {
    lock.lock()
    try {
        return action()
    } finally {
        lock.unlock()
    }
}

val l = Lock()
synchronized(l) {
    // ...
}

因宣告為 inline,以下這些 code

fun foo(l: Lock) {
    println("Before sync")
    synchronized(l) {
        println("Action")
    }
    println("After sync")
}

synchronized(l) 會轉換為

    l.lock()
    try {
        println("Action")
    } finally {
        l.unlock()
    }
inline function 的限制

並非每一個使用 lambdas 的 function 都可以改為 inline

當 function 為 inlined,body of the lambda expression 傳入當作參數,會直接替換為 inline function,如果 function 直接被呼叫,就可以 inline,但如果需要儲存 function,後面才要使用,就不能用 inline

inlining collection operations

kotlin 的 filter 是定義為 inline, map 也是,因為 inline,就不會產生額外的 classes or objects

data class Person(val name: String, val age: Int)

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun main(args: Array<String>) {
    println(people.filter { it.age < 30 })
    
    println(people.filter { it.age > 30 }.map(Person::name))
}
什麼時候要用 inline functon

使用 inline 只會在 function 使用 lambda 為參數的狀況下,改進效能

JVM 有支援 inlining support,他會分析 code,並嘗試 inline code

除了避免產生多餘的 class for each lambda 及 object for the lambda instance,另外 JVM 還不夠聰明,能夠找出所有可以 inline 的狀況

Using inlined lambdas for resource management

另一個 lambda 能減少重複程式碼的狀況是 resource management,取得 Resource -> 使用 -> release

Resource 可能是 file, lock, database transaction ....

通常會用 try/finally statement 包裝起來

kotlint 提供 "withLock" function,可處理 synchronized 的工作

val l: Lock = ...

l.withLock {
    // access the resource protected by this lock
}

withLock 是這樣實作的

fun <T> Lock.withLock(action: () -> T): T {
    lock()
    try {
        return action()
    } finally {
        unlock()
    }
}

java 提供 try-with-resources statement

/* Java */
static String readFirstLineFromFile(String path) throws IOException {
    try (BufferedReader br =
        new BufferedReader(new FileReader(path))) {
            return br.readLine();
    }
}

kotlin 沒有對應的語法,但可以用 "use" 搭配 lambda 提供相同的功能

import java.io.BufferedReader
import java.io.FileReader
import java.io.File

fun readFirstLineFromFile(path: String): String {
    BufferedReader(FileReader(path)).use { br ->
        return br.readLine()
    }
}

control flow in higher-order functions

return statements in lambdas: return from an enclosing function

在 list of Person 裡面尋找 Alice

data class Person(val name: String, val age: Int)

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    for (person in people) {
        if (person.name == "Alice") {
            println("Found!")
            return
        }
    }
    println("Alice is not found")
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

將 for 改為 forEach,在 lambda function 中留下 return,這個 return 是 non-local return,這種 return 只對 function 使用 inlined lambda function 為參數有作用

data class Person(val name: String, val age: Int)

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach {
        if (it.name == "Alice") {
            println("Found!")
            return
        }
    }
    println("Alice is not found")
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

因為 forEach 的 lambda function 是用 inlined,因此可使用 return

returning form lambdas: return with label

local return 類似 break 的功能,會中斷 lambda 的執行,

區分 local return 與 non-local,可使用 labels,可將 lambda expression 加上 label,並在 return 時參考此 label

data class Person(val name: String, val age: Int)

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach label@{
        if (it.name == "Alice") {
            println("Found!")
            return@label
        }
    }
    // 永遠會執行這一行
    println("Alice might be somewhere")
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

也可以用 lambda 做為 label

data class Person(val name: String, val age: Int)

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach {
        if (it.name == "Alice") {
            println("Found!")
            return@forEach
        }
    }
    println("Alice might be somewhere")
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

也可以將 this 加上 label

fun main(args: Array<String>) {
    // implicit receiver 為 this@sb
    println(StringBuilder().apply sb@{
       listOf(1, 2, 3).apply {
           // this 參考到 closest implicit receiver in the scope
           this@sb.append(this.toString())
       }
    })
}
anonymous functions: local returns by default

使用 anonymous function 替代 lambda expression

return 會參考到 closest functon: 也就是 anonymous function

data class Person(val name: String, val age: Int)

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach(fun (person) {
        if (person.name == "Alice") return
        println("${person.name} is not Alice")
    })
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

另一個例子

people.filter(fun (person): Boolean {
    return person.age < 30
})

people.filter(fun (person) = person.age < 30)

References

Kotlin in Action

2019/04/08

Kotlin Operator Overloading and other conventions

kotlin 提供 operator overloading 機制,可以覆寫 operator 的行為,例如提供了 plus method,就可以將物件以 + 進行運算。

Overloading arithmetic operators

overloading binary arithmetic operations

a + b -> a.plus(b)

data class Point(val x: Int, val y: Int) {

    // 覆寫 plus 方法
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
}

fun main(args: Array<String>) {
    val p1 = Point(10, 20)
    val p2 = Point(30, 40)
    
    // + 就是呼叫 plus
    println(p1 + p2)
}
Expression function name
a*b times
a/b div
a%b mod
a+b plus
a-b minus

如果是 Java 的 class,也可以用 operator fun 的方式定義 plus

data class Point(val x: Int, val y: Int)

// extension function,附加到 Point
operator fun Point.plus(other: Point): Point {
    return Point(x + other.x, y + other.y)
}

fun main(args: Array<String>) {
    val p1 = Point(10, 20)
    val p2 = Point(30, 40)
    println(p1 + p2)
}

兩個 operands 不需要有相同的 type,Point 可以乘上 Double

data class Point(val x: Int, val y: Int)

// 兩個 operands 不需要有相同的 type
operator fun Point.times(scale: Double): Point {
    return Point((x * scale).toInt(), (y * scale).toInt())
}

fun main(args: Array<String>) {
    val p = Point(10, 20)
    println(p * 1.5)
}

可改變 return data type

operator fun Char.times(count: Int): String {
    return toString().repeat(count)
}

fun main(args: Array<String>) {
    println('a' * 3)
}

kotlin 沒有定義 bitwise operators,改以 function 並用 index call syntax 處理

function meaning
shl signed shift left
shr signed shift right
ushr unsigned shift right
and
or
xor
inv bitwise inversion
fun main(args: Array<String>) {
    println(0x0F and 0xF0)
    println(0x0F or 0xF0)
    println(0x1 shl 4)
}
Overloading compound assignment operators

除了 + 也能使用 +=

data class Point(val x: Int, val y: Int)

operator fun Point.plus(other: Point): Point {
    return Point(x + other.x, y + other.y)
}

fun main(args: Array<String>) {
    var point = Point(1, 2)
    point += Point(3, 4)
    println(point)
}

有時只需要用 += 不需要 +,可以提供 plusAssign 另外加上 Unit return type,類似的 method 為 minusAssign, timesAssign

operator fun <T> MutableCollection<T>.plusAssign(element: T) {
    this.add(element)
}

如果在程式裡面呼叫 a+=b,其實會先呼叫 a = a.plus(b) 然後是 a.plusAssign(b)


standard libary 的 +, - 都會產生新的 collection,因此 +=, -= 可同時用在 read-only, mutable collection

fun main(args: Array<String>) {
    val list = arrayListOf(1, 2)
    /// 改變了原本的 list
    list += 3
    
    // 產生新的 list,但原本的 list 不變
    val newList = list + listOf(4, 5)
    println(list)
    println(newList)
}
Overloading unary operators

+a 就是 a.unaryPlus(), -a 是 a.unaryMinus

data class Point(val x: Int, val y: Int)

operator fun Point.unaryMinus(): Point {
    return Point(-x, -y)
}

fun main(args: Array<String>) {
    val p = Point(10, 20)
    println(-p)
}
expression function name
+a unaryPlus
-a unaryMinus
!a not
++a, a++ inc
--a, a-- dec
import java.math.BigDecimal

// 覆寫 ++
operator fun BigDecimal.inc() = this + BigDecimal.ONE

fun main(args: Array<String>) {
    var bd = BigDecimal.ZERO
    
    // 在 println 後,呼叫 inc
    println(bd++)
    // 在 println 前,呼叫 inc
    println(++bd)
}

Overloading comparison operators

comparison operators: ==, !=, >, <

euqals ==

a==b 會轉換為 equals 及 null check a?.equals(b) ?: b ==null

class Point(val x: Int, val y: Int) {
    override fun equals(obj: Any?): Boolean {
        if (obj === this) return true
        if (obj !is Point) return false
        return obj.x == x && obj.y == y
    }
}

fun main(args: Array<String>) {
    println(Point(10, 20) == Point(10, 20))
    println(Point(10, 20) != Point(5, 5))
    println(null == Point(1, 2))
}
Ordering operators: compareTo

java 的 class 實作 Comparable 介面,就可以比較大小

kotlin 也有支援 Comparable 介面,但是 compareTo method 是用在 call by convention

a>=b 會轉換為 a.compareTo(b) >=0

import kotlin.comparisons.compareValuesBy

class Person(
        val firstName: String, val lastName: String
) : Comparable<Person> {

    override fun compareTo(other: Person): Int {
        return compareValuesBy(this, other,
            Person::lastName, Person::firstName)
    }
}

fun main(args: Array<String>) {
    val p1 = Person("Alice", "Smith")
    val p2 = Person("Bob", "Johnson")
    println(p1 < p2)
}

Conventions used for collections and ranges

Accessing elements by index: get and set

kotlin 要使用 map,是用 map[index] 取得某個 element,類似 java 的 array

如果定義 get function,並標記為 operator fun,就可以用 [] 呼叫 get

x[a,b] -> x.get(a,b)

data class Point(val x: Int, val y: Int)

operator fun Point.get(index: Int): Int {
    return when(index) {
        0 -> x
        1 -> y
        else ->
            throw IndexOutOfBoundsException("Invalid coordinate $index")
    }
}

fun main(args: Array<String>) {
    val p = Point(10, 20)
    println(p[1])
}

如果定義 operator fun get(rowIndex: Int, colIndex: Int) 就可以用 matrix[row, col]


set 跟 get 類似

x[a,b] = c 會轉換為 x.set(a, b, c)

data class MutablePoint(var x: Int, var y: Int)

operator fun MutablePoint.set(index: Int, value: Int) {
    when(index) {
        0 -> x = value
        1 -> y = value
        else ->
            throw IndexOutOfBoundsException("Invalid coordinate $index")
    }
}

fun main(args: Array<String>) {
    val p = MutablePoint(10, 20)
    p[1] = 42
    println(p)
}
"in" convention

in 是呼叫 contains

a in c -> c.contains(a)

data class Point(val x: Int, val y: Int)

data class Rectangle(val upperLeft: Point, val lowerRight: Point)

operator fun Rectangle.contains(p: Point): Boolean {
    return p.x in upperLeft.x until lowerRight.x &&
           p.y in upperLeft.y until lowerRight.y
}

fun main(args: Array<String>) {
    val rect = Rectangle(Point(10, 20), Point(50, 50))
    println(Point(20, 30) in rect)
    println(Point(5, 5) in rect)
}
rangeTo

start..end 轉換為 start.rangeTo(end)

standard library 有定義一個 rangeTo,可被任何 comparable element 呼叫

operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>
import java.time.LocalDate

fun main(args: Array<String>) {
    // 產生十天
    val now = LocalDate.now()
    val vacation = now..now.plusDays(10)
    
    val n = 9
    // 0..10
    println(0..(n + 1))

    // 加上 () 再呼叫 forEach
    (0..n).forEach { print(it) }
}
"iterator" convention for "for" loop

iterator method

import java.util.Date
import java.time.LocalDate

operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
        // 實作 Iterator
        object : Iterator<LocalDate> {
            var current = start

            // 使用 compareTo convention
            override fun hasNext() =
                    current <= endInclusive

            // 修改前會回傳 current date
            override fun next() = current.apply {
                current = plusDays(1)
            }
        }

fun main(args: Array<String>) {
    val newYear = LocalDate.ofYearDay(2017, 1)
    val daysOff = newYear.minusDays(1)..newYear
    for (dayOff in daysOff) {
        println(dayOff)
    }
    
    //2016-12-31
    //2017-01-01
}

Destructuring declarations and component functions

destructuring declarations 可以 unpacke composite values,儲存在不同的變數

val p = Point(10, 20)
val (x, y) = p

val (x, y) = p 等同

val a = p.component1()
val b = p.component2()

data class 會自動為每一個 property 產生 componentN 的 function

class Point(val x: Int, val y: Int) {
    operator fun component1() = x
    operator fun component2() = y
}

但也能自己處理

import java.io.File

data class NameComponents(val name: String,
                          val extension: String)

fun splitFilename(fullName: String): NameComponents {
    val result = fullName.split('.', limit = 2)
    return NameComponents(result[0], result[1])
}

fun main(args: Array<String>) {
    val (name, ext) = splitFilename("example.kt")
    println(name)
    println(ext)
}

splitFilename 也可以寫成

fun splitFilename(fullName: String): NameComponents {
    val (name, extension) = fullName.split('.', limit = 2)
    return NameComponents(name, extension)
}

for ((key, value) in map) 可 iterate map

fun printEntries(map: Map<String, String>) {
    for ((key, value) in map) {
        println("$key -> $value")
    }
}

>>> val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
>>> printEntries(map)
Oracle -> Java
JetBrains -> Kotlin

Reusing property accessor logic: delegated properties

delegated properties 可實作更複雜的 perperties

Delegated properties: the basics

delegated property 語法如下: p delegates the logic to an instance of Delegate class

class Foo {
    var p: Type by Delegate()
}

實際上 compiler 會產生以下的 code

class Foo {
    private val delegate = Delegate()
    var p: Type
        set(value: Type) = delegate.setValue(..., value)
        get() = delegate.getValue(...)
}

搭配 Delegate 的實作,完整的範例如下

class Delegate {
    operator fun getValue(...) { ... }
    operator fun setValue(..., value: Type) { ... }
}

class Foo {
    var p: Type by Delegate()
}

>>> val foo = Foo()
// 使用 property,會呼叫 delegate.get
>>> val oldValue = foo.p
// 修改 property value 會呼叫 delegate.set
>>> foo.p = newValue
使用 delegated properties: lazy initialization and by lazy()

lazy initialization

例如 Person 可提供多個 emails,但emails 存在 DB 裡面,需要一段時間才能取得,我們需要在第一次使用 emails 時,載入 email 並將 email 暫存。

使用 backing property _emails 的方式,提供這樣的功能

class Email { /*...*/ }
fun loadEmails(person: Person): List<Email> {
    println("Load emails for ${person.name}")
    return listOf(/*...*/)
}

class Person(val name: String) {
    private var _emails: List<Email>? = null

    val emails: List<Email>
       get() {
           if (_emails == null) {
               _emails = loadEmails(this)
           }
           return _emails!!
       }
}

fun main(args: Array<String>) {
    val p = Person("Alice")
    p.emails
    p.emails
}

如果用 lazy initialization 的方式

class Email { /*...*/ }
fun loadEmails(person: Person): List<Email> {
    println("Load emails for ${person.name}")
    return listOf(/*...*/)
}

class Person(val name: String) {
    val emails by lazy { loadEmails(this) }
}

fun main(args: Array<String>) {
    val p = Person("Alice")
    p.emails
    p.emails
}
implementing delegated properties

notifying listeners when a property of an object changes

Java 提供 PropertyChangeSupport, PropertyChangeEvent classes

在 kotlin 使用 PropertyChangeSupport, PropertyChangeEvent 的範例

import java.beans.PropertyChangeSupport
import java.beans.PropertyChangeListener

open class PropertyChangeAware {
    // 儲存 list of listeners, dispatch PropertyChangeEvent events to them
    protected val changeSupport = PropertyChangeSupport(this)

    fun addPropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.addPropertyChangeListener(listener)
    }

    fun removePropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.removePropertyChangeListener(listener)
    }
}

class Person(
        val name: String, age: Int, salary: Int
) : PropertyChangeAware() {

    var age: Int = age
        set(newValue) {
            // field 可取得 property backing field
            val oldValue = field
            field = newValue
            // 通知 listeners
            changeSupport.firePropertyChange(
                    "age", oldValue, newValue)
        }

    var salary: Int = salary
        set(newValue) {
            val oldValue = field
            field = newValue
            changeSupport.firePropertyChange(
                    "salary", oldValue, newValue)
        }
}

fun main(args: Array<String>) {
    val p = Person("Dmitry", 34, 2000)
    // 將 property change listener 加入 p
    p.addPropertyChangeListener(
        PropertyChangeListener { event ->
            println("Property ${event.propertyName} changed " +
                    "from ${event.oldValue} to ${event.newValue}")
        }
    )
    p.age = 35
    p.salary = 2100
}

以 ObservableProperty 修改

為每一個 property 都產生 ObservableProperty instance,並 delegate getter, setter

import java.beans.PropertyChangeSupport
import java.beans.PropertyChangeListener

open class PropertyChangeAware {
    protected val changeSupport = PropertyChangeSupport(this)

    fun addPropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.addPropertyChangeListener(listener)
    }

    fun removePropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.removePropertyChangeListener(listener)
    }
}

class ObservableProperty(
    val propName: String, var propValue: Int,
    val changeSupport: PropertyChangeSupport
) {
    fun getValue(): Int = propValue
    fun setValue(newValue: Int) {
        val oldValue = propValue
        propValue = newValue
        changeSupport.firePropertyChange(propName, oldValue, newValue)
    }
}

class Person(
    val name: String, age: Int, salary: Int
) : PropertyChangeAware() {
    
    val _age = ObservableProperty("age", age, changeSupport)
    var age: Int
        get() = _age.getValue()
        set(value) { _age.setValue(value) }

    val _salary = ObservableProperty("salary", salary, changeSupport)
    var salary: Int
        get() = _salary.getValue()
        set(value) { _salary.setValue(value) }
}

fun main(args: Array<String>) {
    val p = Person("Dmitry", 34, 2000)
    p.addPropertyChangeListener(
        PropertyChangeListener { event ->
            println("Property ${event.propertyName} changed " +
                    "from ${event.oldValue} to ${event.newValue}")
        }
    )
    p.age = 35
    p.salary = 2100
}

調整 ObservableProperty,將 getValue, setValue 改為 operator fun

去掉 name property,改為使用 KProperty.name

class ObservableProperty(
    var propValue: Int, val changeSupport: PropertyChangeSupport
) {
    operator fun getValue(p: Person, prop: KProperty<*>): Int = propValue

    operator fun setValue(p: Person, prop: KProperty<*>, newValue: Int) {
        val oldValue = propValue
        propValue = newValue
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }
}

最精簡的版本

import java.beans.PropertyChangeSupport
import java.beans.PropertyChangeListener
import kotlin.properties.Delegates
import kotlin.reflect.KProperty

open class PropertyChangeAware {
    protected val changeSupport = PropertyChangeSupport(this)

    fun addPropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.addPropertyChangeListener(listener)
    }

    fun removePropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.removePropertyChangeListener(listener)
    }
}

class Person(
    val name: String, age: Int, salary: Int
) : PropertyChangeAware() {

    private val observer = {
        prop: KProperty<*>, oldValue: Int, newValue: Int ->
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }

    var age: Int by Delegates.observable(age, observer)
    var salary: Int by Delegates.observable(salary, observer)
}

fun main(args: Array<String>) {
    val p = Person("Dmitry", 34, 2000)
    p.addPropertyChangeListener(
        PropertyChangeListener { event ->
            println("Property ${event.propertyName} changed " +
                    "from ${event.oldValue} to ${event.newValue}")
        }
    )
    p.age = 35
    p.salary = 2100
}
Delegated-property translation rules

整理 delegated properties 的運作方式

以下是 class with delegated property

class Foo {
    var c: Type by MyDelegate()
}
val foo = Foo()

實際上 compiler 會產生以下的 code

class Foo {
    private val <delegate> = MyDelegate()
    var c: Type
        set(value: Type) = <delegate>.setValue(c, <property>, value)
        get() = <delegate>.getValue(c, <property>)
}

使用 property 時,會呼叫

val x=c.prop 就是 val x = <delegate>.getValue(c, <property>)

c.prop=x 就是 <delegate>.setValue(c, <property>, x)

Storing property values in a map

expando objects: 有動態定義的 set of attributes

Person 沒有固定的 properties,將 property 放在 hashMap 裡面

class Person {
    private val _attributes = hashMapOf<String, String>()

    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }

    val name: String
        get() = _attributes["name"]!!
}

fun main(args: Array<String>) {
    val p = Person()
    val data = mapOf("name" to "Dmitry", "company" to "JetBrains")
    for ((attrName, value) in data)
       p.setAttribute(attrName, value)
    println(p.name)
}

可以使用 delegated property

class Person {
    private val _attributes = hashMapOf<String, String>()

    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }

    val name: String by _attributes
}

Delegated properties in frameworks

Database framework

// 將 object 關聯至 table in the database
object Users : IdTable() {
    // property 為 columns
    val name = varchar("name", length = 50).index()
    val age = integer("age")
}

// 每一個 User instance 都有特定的 id
class User(id: EntityID) : Entity(id) {
    // name 對應到 DB 的 name
    var name: String by Users.name
    var age: Int by Users.age
}

另一種方式,是定義 Column 類別

object Users : IdTable() {
    val name: Column<String> = varchar("name", 50).index()
    val age: Column<Int> = integer("age")
}

framework 為 Column class 定義了 getValue, setValue

operator fun <T> Column<T>.getValue(o: Entity, desc: KProperty<*>): T {
    // retrieve the value from the database
}
operator fun <T> Column<T>.setValue(o: Entity, desc: KProperty<*>, value: T) {
    // update the value in the database
}

References

Kotlin in Action

2019/03/31

Kotlin Type System

說明如何處理 nullable types 及 read-only collections,同時去掉了 java 的 raw types 與 first-class support for arrays

Nullability

kotlin 的 nullability 可避免 java 的 NullPointerException error。kotlin 將問題由 runtime 移到 compile time。

如果傳入的參數為 null,會發生 NullPointerException

/* Java */
int strLen(String s) {
    return s.length();
}

在 Kotlin 呼叫 strLen 時,就不允許傳入 null

>>> fun strLen(s: String) = s.length
>>> strLen(null)
error: null can not be a value of a non-null type String
strLen(null)

如果在 String 後面加上 ?,就代表傳入的字串可以是 String 或是 null,但 compiler 會檢查該 method 是否可以使用 null

>>> fun strLenSafe(s: String?) = s.length()
error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
fun strLenSafe(s: String?) = s.length()
                              ^
error: expression 'length' of type 'Int' cannot be invoked as a function. The function 'invoke()' is not found
fun strLenSafe(s: String?) = s.length()

最正確的方式,是以下這樣

fun strLenSafe(s: String?): Int =
    if (s != null) s.length else 0

fun main(args: Array<String>) {
    val x: String? = null
    println(strLenSafe(x))
    println(strLenSafe("abc"))
}

java 的變數雖然宣告為 String,但該 value 會有兩種狀況:String 或是 null,除非多寫檢查 null 的程式碼,否則很容易發生問題。Kotlin 將 nullable 與 non-null type 區隔開,用以避免這樣的問題。

safe call operator ?.

s?.toUpperCase() 就等同於 if (s != null) s.toUpperCase() else null ,將 null check 及 method call 合併為一個 operation

fun printAllCaps(s: String?) {
    // allCaps 可以是 null
    val allCaps: String? = s?.toUpperCase()
    println(allCaps)
}

fun main(args: Array<String>) {
    printAllCaps("abc")
    printAllCaps(null)
}

employee.manager 可能是 null,managerName回傳的值,也可能是 null

class Employee(val name: String, val manager: Employee?)

fun managerName(employee: Employee): String? = employee.manager?.name

fun main(args: Array<String>) {
    val ceo = Employee("Da Boss", null)
    val developer = Employee("Bob Smith", ceo)
    println(managerName(developer))
    println(managerName(ceo))
}

可連續使用 ?.

class Address(val streetAddress: String, val zipCode: Int,
              val city: String, val country: String)

class Company(val name: String, val address: Address?)

class Person(val name: String, val company: Company?)

fun Person.countryName(): String {
   // company?. address?. 都有可能是 null
   val country = this.company?.address?.country
   return if (country != null) country else "Unknown"
}

fun main(args: Array<String>) {
    val person = Person("Dmitry", null)
    println(person.countryName())
}
Elvis operator ?:

以 default value 取代 null,Elvis operator 也稱為 null-coalescing operator

當 s?.length 是 null,就設定為 0

fun strLenSafe(s: String?): Int = s?.length ?: 0

fun main(args: Array<String>) {
    println(strLenSafe("abc"))
    println(strLenSafe(null))
}
Safe casts as?

java 的 as 可能會產生 ClassCastException。

Kotlin 提供 as? 語法,會嘗試轉換資料型別,發生問題就回傳 null

物件中的 equals method,因為參數是 Any? 任何一種資料型別都可傳入,因此在一開始,就用 as? 嘗試轉換資料型別,再進行欄位檢查。

class Person(val firstName: String, val lastName: String) {
   override fun equals(o: Any?): Boolean {
      val otherPerson = o as? Person ?: return false

      return otherPerson.firstName == firstName &&
             otherPerson.lastName == lastName
   }

   override fun hashCode(): Int =
      firstName.hashCode() * 37 + lastName.hashCode()
}

fun main(args: Array<String>) {
    val p1 = Person("Dmitry", "Jemerov")
    val p2 = Person("Dmitry", "Jemerov")
    println(p1 == p2)
    println(p1.equals(42))
}
Not-null assertions !!

!! 確認是否為 null,如果是 null 就會 throw kotlin.KotlinNullPointerException

fun ignoreNulls(s: String?) {
    val sNotNull: String = s!!
    println(sNotNull.length)
}

fun main(args: Array<String>) {
    ignoreNulls(null)
}

執行後會得到 Exception in thread "main" kotlin.KotlinNullPointerException

實務上的例子 CopyRowAction 會複製某一行的資料到 clipboard,isEnabled 用 list.selectedValue 是否為 null 進行檢查,確保

class CopyRowAction(val list: JList<String>) : AbstractAction() {
    override fun isEnabled(): Boolean = list.selectedValue != null
    override fun actionPerformed(e: ActionEvent) {
        val value = list.selectedValue!!
        // val value = list.selectedValue ?: return
        // copy value to clipboard
    }
}

因為 !! 會產生 exception,所以不要寫這樣的 code

person.company!!.address!!.country
let

用來處理要傳入某個需要 non-null 參數的 function,但是卻遇到 nullable argument。

用 let 搭配 lambda email?.let { sendEmailTo(it) },如果 email 是 null,就不會呼叫 lambda function,藉此避免因 null 呼叫 sendEmailTo 產生 exception

fun sendEmailTo(email: String) {
    println("Sending email to $email")
}

fun main(args: Array<String>) {
    var email: String? = "yole@example.com"
    email?.let { sendEmailTo(it) }
    email = null
    email?.let { sendEmailTo(it) }
}
late-initialized properties

有很多 framework 需要的功能,在 object instance 產生後,要呼叫某個 method,類似 Android 的 onCreate,或是 JUnit 的 @Before

但因為 constructor 不能產生 non-null property,但如果還是要有 null property,就將該 property 宣告為 ?= null,並在 @Before 初始化 myService

import org.junit.Before
import org.junit.Test
import org.junit.Assert

class MyService {
    fun performAction(): String = "foo"
}

class MyTest {
    private var myService: MyService? = null

    @Before fun setUp() {
        myService = MyService()
    }

    @Test fun testAction() {
        Assert.assertEquals("foo",
            myService!!.performAction())
    }
}

kotlin 提供 lateinit 語法,只能用在 `var 變數,如果在還沒初始化前,使用了該變數,會得到 exception

import org.junit.Before
import org.junit.Test
import org.junit.Assert

class MyService {
    fun performAction(): String = "foo"
}

class MyTest {
    private lateinit var myService: MyService

    @Before fun setUp() {
        myService = MyService()
    }

    @Test fun testAction() {
        Assert.assertEquals("foo",
            myService.performAction())
    }
}
extensions on nullable types

為 nullable types 定義 extension function

fun verifyUserInput(input: String?) {
    if (input.isNullOrBlank()) {
        println("Please fill in the required fields")
    }
}

fun main(args: Array<String>) {
    verifyUserInput(" ")
    verifyUserInput(null)
}

將 input 以 isEmpty, isBland, isEmptyOrNull, isNullOrBlank 進行檢查

Nullability of type parameters

kotlin 的 functions, classes 的 type parameters 都是 nullable,type parameter 可替換為任意一種 type 包含 nullable type。

t:T 可以為 null

fun <T> printHashCode(t: T) {
    println(t?.hashCode())
}

fun main(args: Array<String>) {
    printHashCode(null)
}
nullability and java

某些 java code 裡面有 @Nullable annotation,kotlin 會使用這些設定。

Java 的 @Nullable + Type 就等於 Kotlin 的 Type? Java 的 @NotNull + Type 就等於 Kotlin 的 Type


platform type: kotlin 沒有 nullability information 的 type,如果確定一定不是 null,可直接使用,否則就要用 null-safe operation ?

Java 的 Type 等於 Kotlin 的 Type? 或是 Type

Person.java

/* Java */
public class Person {
    private final String name;
    public Person(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

getName 可能會回傳 null

import ch06.Person

fun yellAtSafe(person: Person) {
    println((person.name ?: "Anyone").toUpperCase() + "!!!")
}

fun main(args: Array<String>) {
    yellAtSafe(Person(null))
}

在 kotlin override java method 時,可自行決定 return type 為 nullable 或 non-null

/* Java */
interface StringProcessor {
    void process(String value);
}

以下兩種 kotlin 實作的方式都可以

class StringPrinter : StringProcessor {
    override fun process(value: String) {
        println(value)
    }
}

class NullableStringPrinter : StringProcessor {
    override fun process(value: String?) {
        if (value != null) {
            println(value)
        }
    }
}

operators for safe operations

  • safe call ?.
  • Elvis operator ?:
  • safe cast as?)

operator for unsafe dereference

  • not-null assertion !!

let non-null check

Primitive and other basic types

Int, Boolean

kotlin 沒有區分 primitive types 及 wrapper types

coerceIn 可限制 value 為某個特定的範圍

fun showProgress(progress: Int) {
    val percent = progress.coerceIn(0, 100)
    println("We're ${percent}% done!")
}

fun main(args: Array<String>) {
    showProgress(146)
}

kotlin 會將 Int 編譯為 Java 的 int,除非是用在 collections。

  • Integer types: Byte, Short, Int, Long
  • Floating-point number types: float, Double
  • character type: Char
  • boolean type: Boolean
nullable primitive types: Int?, Boolean?

kotlin 的 nullable types 不能轉換為 java 的 primitive type,要轉成 wrapper type

data class Person(val name: String,
                  val age: Int? = null) {

    fun isOlderThan(other: Person): Boolean? {
        if (age == null || other.age == null)
            return null
        return age > other.age
    }
}

fun main(args: Array<String>) {
    println(Person("Sam", 35).isOlderThan(Person("Amy", 42)))
    println(Person("Sam", 35).isOlderThan(Person("Jane")))
}
Number conversions

kotlin 跟 java 有個重要的差異:處理 numeric conversion 的方法不同。kotlin不會自動轉換 number 的資料型別。

以下的 code 會造成 type mismatch error

val i = 1
val l: Long = i

i 必須強制作型別轉換

val i = 1
val l: Long = i.toLong()

除了 Boolean 以外,其他的資料型別有提供 toByte(), toShort(), toChar() 這些轉換的 function


Java 中 new Integer(42).equals(new Long(42)) 會 returns false

kotlin 並不支援 implicit conversion,因此以下這個 code 會 return false

val x = 1
x in listOf(1L, 2L, 3L)

必須要將 x 強制轉換為 Long,才會 return true

val x = 1
x.toLong() in listOf(1L, 2L, 3L)

在進行數字運算時,會自動進行型別轉換,這是因為 operator 有 overloaded 接受所有 numeric types

fun foo(l: Long) = println(l)

fun main(args: Array<String>) {
    val b: Byte = 1
    val l = b + 1L
    
    foo(42)
}

kotlin 也有提供 string 轉成 number 的 function: toInt, toByte, toBoolean

println("42".toInt())
Any and Any?

類似 Java 的 Object 是所有類別的 root class,Any 是 Kotlin 所有 non-nullable 類別的 root class

Java 提供 autoboxing 功能,編譯器自動將基本數據類型值轉換成對應的 wrapper class 的物件,例如將 int 轉換為 Integer 對象,將 boolean 轉換 Boolean 對象。而 unboxing 則是反過來轉換。

kotlin 的 Any 也有 autoboxing 的功能

val answer: Any = 42

所有 kotlin class 都有繼承自 Any 的 toString(), equals(), hashCode(),但是在 java.lang.Object 中定義的其他 method (ex: wait, notify) 則必須要將型別轉換成 java.lang.Object 才能使用。

Unit type: kotlin 的 "void"

Unit 等同 java 的 void

fun f(): Unit { ... }

上面的例子中,Unit 可省略不寫

fun f() { ... }

Unit 跟 void 的差異是,Unit 是一種類別,void 是 type argument。

interface Processor<T> {
    fun process(): T
}

class NoResultProcessor : Processor<Unit> {
    // process 會回傳 Unit,但省略不寫
    override fun process() {
        // do stuff

        // 不需要寫 return Unit
    }
}
Nothing type: this function never returns

對於某些 function 來說,有可能完全不需要 return value

fun fail(message: String): Nothing {
    throw IllegalStateException(message)
}

Nothing 沒有任何 value,可用在 function return type 或是 type argument

這個例子中,fail 不會有任何 return value,只會 throw exception

val address = company.address ?: fail("No address")
println(address.city)

Collections and arrays

nullability and collections

可以用 String.toIntOrNull 取代 readNumbers 裡面做的事情

mport java.util.ArrayList
import java.io.BufferedReader
import java.io.StringReader

fun readNumbers(reader: BufferedReader): List<Int?> {
    // 產生 a list of nullable Int values
    val result = ArrayList<Int?>()
    for (line in reader.lineSequence()) {
        // 可轉換為 Int 就放到 list,否則就放入 null
        try {
            val number = line.toInt()
            result.add(number)
        }
        catch(e: NumberFormatException) {
            result.add(null)
        }
    }
    return result
}

fun addValidNumbers(numbers: List<Int?>) {
    var sumOfValidNumbers = 0
    var invalidNumbers = 0

    // 讀取 list 裡面的 value,判斷是否為 null
    for (number in numbers) {
        if (number != null) {
            sumOfValidNumbers += number
        } else {
            invalidNumbers++
        }
    }
    println("Sum of valid numbers: $sumOfValidNumbers")
    println("Invalid numbers: $invalidNumbers")
}

fun main(args: Array<String>) {
    val reader = BufferedReader(StringReader("1\nabc\n42"))
    val numbers = readNumbers(reader)
    addValidNumbers(numbers)
}

addValidNumbers 可以簡化為 numbers.filterNotNull() 這樣的寫法

fun addValidNumbers(numbers: List<Int?>) {
    val validNumbers = numbers.filterNotNull()
    println("Sum of valid numbers: ${validNumbers.sum()}")
    println("Invalid numbers: ${numbers.size - validNumbers.size}")
}

要注意 List<Int?>List<Int>? 的差異,前面的 list 永遠不會是 null,但每個 value 可以是 null,後面的 list 本身可以是 null,但 value 都不能是 null

read-only and mutable collections

kotlin.Collection 介面 (有 size, iterator(), contains()),切割了存取 data in a collection 及 modifying data 的工作

如果需要修改 collection 的資料,要使用 kotlin.MutableColelction (有 add(), remove(), clear())

這就像區分 val, var 一樣

fun <T> copyElements(source: Collection<T>,
                     target: MutableCollection<T>) {
    // loop all items, add to MutableCollection
    for (item in source) {
        target.add(item)
    }
}

fun main(args: Array<String>) {
    val source: Collection<Int> = arrayListOf(3, 5, 7)
    val target: MutableCollection<Int> = arrayListOf(1)
    copyElements(source, target)
    println(target)
}

read-only collection 並不一定永遠不會改變,如果有一個 read-only 及 一個 mutable collection 指到同一個 collection object,還是可以透過 mutable collection 修改內容。使用 read-only collection 同時資料被修改時,會發生 ConcurrentModificationException

Kotlin collections and Java

每一個 Java collection interface 都有兩種 Kotlin class,一個是 read-only,一個是 mutable

collection creation functions

Collection type Read-Only Mutable
List listOf() arrayListOf()
Set setOf() hashSetOf(), linkedSetOf(), sortedSetOf()
Map mapOf() hadhMapOf(), linkedMapOf(), sortedMapOf()

Java method 如使用 java.util.Collection 為參數,可傳入 kotlin 的 Collection 或是 MutableCollection,因為 Java 無法分辨 read-only, mutable collection,可修改 read-only collection。

使用 Collection 跟 Java 互動,必須要自己注意 read-only collection 的狀態

CollecitonUtils.java

import java.util.List;

/* Java */
// CollectionUtils.java
public class CollectionUtils {
    public static List<String> uppercaseAll(List<String> items) {
        for (int i = 0; i < items.size(); i++) {
            items.set(i, items.get(i).toUpperCase());
        }
        return items;
    }
}
import CollectionUtils

// Kotlin
// collections.kt
// 宣告為 read-only list 參數
fun printInUppercase(list: List<String>) {
    // 呼叫 Java method 修改 list
    println(CollectionUtils.uppercaseAll(list))
    // 列印修改後的 list
    println(list.first())
}

fun main(args: Array<String>) {
    val list = listOf("a", "b", "c")
    printInUppercase(list)
}
Collections as platform types

java code 定義的 type 在 kotlin 視為 platform types

當要在 kotlin 覆寫或實作 java method,且裡面包含 collection type時,必須要注意如何定義 kotlin 的參數

  • collection 是否為 nullable
  • elements 是否為 nullable
  • 這個 method 是否會修改 collection
/* Java */
interface FileContentProcessor {
    void processContents(File path, byte[] binaryContents, List<String> textContents);
}

在 kotlin 實作此 interface,要注意以下的事情

  • list 為 nullable,因有些檔案是 text 不是 binary
  • elements in the list 為 non-null,因為 lines in a file are never null
  • list 為 red-only,因沒有要修改內容
class FileIndexer : FileContentProcessor {
    override fun processContents(path: File, binaryContents: ByteArray?, textContents: List<String>?) {
        // ...
    }
}

另一個例子

Java interface

/* Java */
interface DataParser<T> {
    void parseData(String input,
    List<T> output,
    List<String> errors);
}
  • List 為 non-null
  • elements in the list 為 nullable
  • List 為 mutable

kotlin 實作

class PersonParser : DataParser<Person> {
    override fun parseData(input:
        String,
        output: MutableList<Person>,
        errors: MutableList<String?>) {
        // ...
    }
}
Arrays of objects and primitive types

kotlin 的 array 是 class with a type parameter

fun main(args: Array<String>) {
    for (i in args.indices) {
         println("Argument $i is: ${args[i]}")
    }
}

產生 array 的方式

  • arrayOf
  • arrayOfNulls 可包含 null elements
  • Array 用 size 及 lambda function 產生array

以下是 Array 的範例

fun main(args: Array<String>) {
    val letters = Array<String>(26) { i -> ('a' + i).toString() }
    println(letters.joinToString(""))
    // abcdefghijklmnopqrstuvwxyz
}

toTypeArray 是另一種產生 array 的方式,前面的 * 代表室 vararg 參數,將 collection 轉換為 array

fun main(args: Array<String>) {
    val strings = listOf("a", "b", "c")
    println("%s/%s/%s".format(*strings.toTypedArray()))
}

kotlin 提供 IntArray, ByteArray, CharArray, BooleanArray 對應 java 的 int[], byte[], char[] 等等


用 size 及 lambda function 產生 array

fun main(args: Array<String>) {
    // 產生 5 個 0 的 IntArray
    val fiveZeros = IntArray(5)
    val fiveZerosToo = intArrayOf(0, 0, 0, 0, 0)

    // 用 size 及 lambda function 產生 array
    val squares = IntArray(5) { i -> (i+1) * (i+1) }
    println(squares.joinToString())
}

改寫 for (i in args.indices),使用 forEachIndexed

fun main(args: Array<String>) {
    args.forEachIndexed { index, element ->
        println("Argument $index is: $element")
    }
}

References

Kotlin in Action

2019/03/25

Kotlin Lambda

kotlin standard library 大量使用了 lambda 語法,最常見的就是用在 collections,另外也提供如何從 java 呼叫 kotlin lambda 的方式。最後說明一種特別的 lambda with receivers。

lambda expression

直到 Java 8 才提供了 lambda 語法。以下說明 lambda 的重要性

block of code as method parameters

如果要實作當某個事件發生時,執行一個 handler 的 code,以 java 來說可以用 anonymous inner class 實作。如果用 functional programming 的方式,可以將 function 當作 value,也就是將 function 當作參數傳遞給另一個 function,也就是把一小段 code 當作method 參數。

listener 實作 OnclickListener 介面,覆寫 onClick

/* Java */
button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View view) {
        /* actions on click */
    }
});

換成functional progeamming 的方式

button.setOnClickListener { /* actions on click */ }
lambda and collections

假設有個 list of data class: Person,需要找到裡面年紀最大的人,直覺會以 function 實作

data class Person(val name: String, val age: Int)

fun findTheOldest(people: List<Person>) {
    var maxAge = 0
    var theOldest: Person? = null
    for (person in people) {
        if (person.age > maxAge) {
            maxAge = person.age
            theOldest = person
        }
    }
    println(theOldest)
}

fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 29), Person("Bob", 31))
    findTheOldest(people)
}

kotlin 有提供這個 libray function: maxBy,maxBy 可用在任何一種 collection,後面的 {} 是 lambda implementation

>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> println(people.maxBy { it.age })
Person(name=Bob, age=31)

maxBy 也可以直接指定 member reference

people.maxBy(Person::age)
lambda expression 語法

lambda 語法是在 {} 裡面,前面是參數,後面是 body

val sum = { x: Int, y: Int -> x + y }
println(sum(1, 2))
// 3

也可以直接呼叫 lambda

>>> { println(42) }()
42

但最好用 run 封裝起來,code 會比較容易閱讀

>>> run { println(42) }
42

剛剛的 people.maxBy { it.age } 其實原本應該寫成,使用 p:Person 這個參數,運算 p.age

people.maxBy({ p: Person -> p.age })

可以將 () 省略

people.maxBy { p: Person -> p.age }

將 p 的資料型別省略

people.maxBy { p -> p.age }

將 p 改成 lambda 的預設參數名稱 it

people.maxBy { it.age }

joinToString 也有在 kotlin 標準函式庫裡面,其中 transform 就是需要提供一個 lambda function

>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> val names = people.joinToString(separator = " ", transform = { p: Person -> p.name } )
>>> println(names)
Alice Bob

也可以簡化寫成這樣

people.joinToString(" ") { p: Person -> p.name }

lambda 語法裡面,也可以寫超過一個 expression 或 statement

val sum = { x: Int, y: Int ->
    println("Computing the sum of $x and $y")
    x + y
}
accessing variables

如果在 method 裡面宣告 anonymous inner class 時,可使用 method 裡面的 local 變數以及 parameters,lambda 也是一樣。

messages.forEach 裡面的 lambda function 使用了外部 method 的參數 $prefix

fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) {
    messages.forEach {
        println("$prefix $it")
    }
}

fun main(args: Array<String>) {
    val errors = listOf("403 Forbidden", "404 Not Found")
    printMessagesWithPrefix(errors, "Error:")
}

kotlin 跟 java 最大的差異是 kotlin 沒有限制只能使用 final variables,也可以在 lambda function 裡面修改變數的值。

fun printProblemCounts(responses: Collection<String>) {
    var clientErrors = 0
    var serverErrors = 0
    responses.forEach {
        if (it.startsWith("4")) {
            clientErrors++
        } else if (it.startsWith("5")) {
            serverErrors++
        }
    }
    println("$clientErrors client errors, $serverErrors server errors")
}

fun main(args: Array<String>) {
    val responses = listOf("200 OK", "418 I'm a teapot",
                           "500 Internal Server Error")
    printProblemCounts(responses)
}
member references

如果 lambda 要傳送一個已經定義好的 function 時,要使用 :: operator,將 function 轉換為 value,這種語法稱為 member reference,前面是 class,後面是 method 或是 property

val getAge = Person::age

等同

val getAge = { person: Person -> person.age }

如果是 top-level function,就不用寫 class name

fun salute() = println("Salute!")

fun main(args: Array<String>) {
    run(::salute)
}

將 lambda function 轉送給 sendEmail function

val action = { person: Person, message: String ->
    sendEmail(person, message)
}

直接使用 member reference

val nextAction = ::sendEmail

也可以將 member reference 套用在 class constructor

>>> data class Person(val name: String, val age: Int)
>>> val createPerson = ::Person
>>> val p = createPerson("Alice", 29)
>>> println(p)
Person("Alice", 29)

也可以使用 extension function

fun Person.isAdult() = age >= 21
val predicate = Person::isAdult

collection 使用的 functional APIs

filter and map

取得偶數的元素,it 是 lambda 的預設變數名稱

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3, 4)
    println(list.filter { it % 2 == 0 })
}

取得年齡超過30歲的 Person

data class Person(val name: String, val age: Int)

fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 29), Person("Bob", 31))
    println(people.filter { it.age > 30 })
}

取得每一個元素的平方 list

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3, 4)
    println(list.map { it * it })
}

取得所有人的名字 list

data class Person(val name: String, val age: Int)

fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 29), Person("Bob", 31))
    println(people.map { it.name })
}

也可以寫成people.map(Person::name)

可以將 filter 與 map 連在一起 people.filter { it.age > 30 }.map(Person::name)

找到年齡最大的 personpeople.filter { it.age == people.maxBy(Person::age).age } 但因為 people.maxBy(Person::age).age 重複做了很多次,所以將這兩個分開做,效能會比較好

val maxAge = people.maxBy(Person::age).age
people.filter { it.age == maxAge }

使用 filter 及 transformation functions to maps

fun main(args: Array<String>) {
    val numbers = mapOf(0 to "zero", 1 to "one")
    println(numbers.mapValues { it.value.toUpperCase() })
}

//{0=ZERO, 1=ONE}
all, any, count, find

檢查是不是所有元素都符合某個條件

data class Person(val name: String, val age: Int)

val canBeInClub27 = { p: Person -> p.age <= 27 }

fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 27), Person("Bob", 31))
    println( people.all(canBeInClub27) )
}

檢查是否至少有一個元素符合某個條件

println(people.any(canBeInClub27))

!allany 的意義是相反的,可以互換

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3)
    println(!list.all { it == 3 })
    println(list.any { it != 3 })
}

count 計算數量

al people = listOf(Person("Alice", 27), Person("Bob", 31))
println(people.count(canBeInClub27))

用 find 找到一個符合條件的元素

data class Person(val name: String, val age: Int)

val canBeInClub27 = { p: Person -> p.age <= 27 }

fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 27), Person("Bob", 31))
    println(people.find(canBeInClub27))
}
groupBy: 將 list 轉換為 map of groups

將 list 以 age 分組

data class Person(val name: String, val age: Int)

fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 31),
            Person("Bob", 29), Person("Carol", 31))
    println(people.groupBy { it.age })
}

執行結果為 Map<Int, List<Person>>

{31=[Person(name=Alice, age=31), Person(name=Carol, age=31)], 29=[Person(name=Bob, age=29)]}

以 string 的第一個 character 分組

fun main(args: Array<String>) {
    val list = listOf("a", "ab", "b")
    println(list.groupBy(String::first))
}

執行結果

{a=[a, ab], b=[b]}
flatMap, flatten

flatMap 做兩件事:根據參數,轉換 (maps) 每一個 element 為 collection,合併 (flattens) 數個 lists 為一個

fun main(args: Array<String>) {
    val strings = listOf("abc", "def")
    println(strings.flatMap { it.toList() })
}

執行結果

[a, b, c, d, e, f]

set of all authors

class Book(val title: String, val authors: List<String>)

fun main(args: Array<String>) {
    val books = listOf(Book("Thursday Next", listOf("Jasper Fforde")),
                       Book("Mort", listOf("Terry Pratchett")),
                       Book("Good Omens", listOf("Terry Pratchett",
                                                 "Neil Gaiman")))
    println(books.flatMap { it.authors }.toSet())
}

執行結果

[Jasper Fforde, Terry Pratchett, Neil Gaiman]

lazy collection operations: sequences

sequence 不同於 collection,在使用到該物件時才會進行運算,而不是在定義時,就馬上進行運算,也不會產生 intermediate temporary objects

asSequence 是讓 collection 轉成 sequence,toList 則是讓 sequence 轉回 collection

fun main(args: Array<String>) {
    val seq = listOf(1, 2, 3, 4).asSequence()
            .map { print("map($it) "); it * it }
            .filter { print("filter($it) "); it % 2 == 0 }

    println( "after seq")
    val list = seq.toList()

    println( )
    println( list )
}

執行結果

after seq
map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16) 
[4, 16]

在 sequence 的 operations 中,map 及 filter 是 intermediate operations,toList 是 terminal operation,一直到 terminal operation 才會進行運算

sequence.map{...}.filter{...}.toList()

除了 asSequence() 還可以用 generateSequence() 產生 sequence,一直到呼叫 sum 的時候,才會進行運算

fun main(args: Array<String>) {
    val naturalNumbers = generateSequence(0) { it + 1 }
    val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
    println(numbersTo100.sum())
}

另一個常用的是 sequence of parents,以下是檢查 file 是否在某一個隱藏的目錄中,所以要產生 sequence of parent directories,然後檢查是否有任一個為 hidden

import java.io.File

fun File.isInsideHiddenDirectory() =
        generateSequence(this) { it.parentFile }.any { it.isHidden }

fun main(args: Array<String>) {
    val file = File("/Users/svtk/.HiddenDir/a.txt")
    println(file.isInsideHiddenDirectory())
}

using Java functional interfaces

Kotlin lambdas 可跟 Java APIs 一起使用

在 java 的 setOnClickListener

button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        ...
    }
}

在 kotlin

button.setOnClickListener { view -> ... }

其中 OnClickListener 稱為 funcational interface 或是 SAM(single abstract method) interfaces

public interface OnClickListener {
    void onClick(View v);
}
passing a lambda as a parameter to a java method

將 lambda 傳到需要 functional interface 的 java method

/* Java */
void postponeComputation(int delay, Runnable computation);

在kotlin 可這樣呼叫,compiler 會自動產生一個 instance of Runnable,也就是 instance of an anonymous class implements Runnable

postponeComputation(1000) { println(42) }

compiler 會用 run method 產生 instance of anonymous class that implements Runnable

postponeComputation(1000, object: Runnable {
    override fun run() {
        println(42)
    }
})

每一個呼叫 handleComputation 都會產生一個新的 Runnable instance 儲存 id 欄位

fun handleComputation(id: String) {
    postponeComputation(1000) {
        println(id)
    }
}
SAM constructors: explicit conversion of lambdas to functional interfaces

SAM constructor 是 compiler 產生的 function,可轉換 lambda 為 instance of functional interface

我們不能直接 return lambda,但可包裝在 SAM constructor 裡面

fun createAllDoneRunnable(): Runnable {
    return Runnable { println("All done!") }
}

fun main(args: Array<String>) {
    createAllDoneRunnable().run()
}

例如 Android 的 listener,可以產生出來給多個 button 使用

val listener = OnClickListener { view ->
    val text = when (view.id) {
        R.id.button1 -> "First button"
        R.id.button2 -> "Second button"
        else -> "Unknown button"
    }
    toast(text)
}
button1.setOnClickListener(listener)
button2.setOnClickListener(listener)

lambda with receivers: with and apply

以下這是 kotlin 的 lambda 的功能,不能在 java 使用。

可以在 lambda 呼叫 methods of a different object,不需要額外的 qualifiers

with

以下是列印英文字母的範例

fun alphabet(): String {
    val result = StringBuilder()
    for (letter in 'A'..'Z') {
         result.append(letter)
    }
    result.append("\nNow I know the alphabet!")
    return result.toString()
}

fun main(args: Array<String>) {
    println(alphabet())
}

用 with 改寫

fun alphabet(): String {
    val stringBuilder = StringBuilder()
    // 指定 receiver value
    return with(stringBuilder) {
        for (letter in 'A'..'Z') {
            // 以 this 呼叫 receiver value 的 method
            this.append(letter)
        }
        // 呼叫 method,但省略 this
        append("\nNow I know the alphabet!")
        // 由 lambda 回傳 value
        this.toString()
    }
}

fun main(args: Array<String>) {
    println(alphabet())
}

省略 this

fun alphabet() = with(StringBuilder()) {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
    toString()
}

fun main(args: Array<String>) {
    println(alphabet())
}
apply

with 的 value 會回傳 lambda code 的最後一個 expression,但有時需要 return receiver object,而不是 lambda 運算結果,這時要改用 apply

apply 用起來就像是 with,差別是 apply 會回傳傳入作為參數的 object

fun alphabet() = StringBuilder().apply {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
}.toString()

fun main(args: Array<String>) {
    println(alphabet())
}

也可以改用 buildString,buildString 會處理 StringBuffer,也會呼叫 toString,buildString 是 lambda with a receiver,且 receiver 是 StringBuilder

fun alphabet() = buildString {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
}

fun main(args: Array<String>) {
    println(alphabet())
}

References

Kotlin in Action

2019/03/18

Kotlin 類別、物件、介面

以下說明這些功能:類別、介面,關鍵字 object,data classes 及 class delegation。

不同於 java,kotlin 宣告預設為 public final,nested class 不是 inner class,沒有外層 class 的 implicit reference。將 class 宣告為 data class時,compiler 會自動產生幾個 methods。

定義 class 時使用關鍵字 keyword,同時會產生一個 instance,這可用在 singleton object, companion objects 以及 oject expressions (類似 Java anonymous classes)

class hierarchy

interface

用 interface 定義介面,用 : 表示實作 interface

override 類似 java 的 @Override annotation,Button 裡面的 override 是一定要寫的,否則程式將無法編譯

interface Clickable {
    // 只定義了 function,沒有實作
    fun click()
}
class Button : Clickable {
    override fun click() = println("I was clicked")
}

fun main(args: Array<String>) {
    Button().click()
}

有兩個以上的 interface 的範例

class Button : Clickable, Focusable {
    override fun click() = println("I was clicked")

    // 因 Clickable, Focusable 都有實作 showOff,必須要用 super<Clickable> 指定是用哪一個上層實作
    // 如果只要使用一種上層實作,可寫成這樣
    // override fun showOff() = super<Clickable>.showOff()
    override fun showOff() {
        super<Clickable>.showOff()
        super<Focusable>.showOff()
    }
}

interface Clickable {
    fun click()
    // 定義 function,並提供預設的實作
    fun showOff() = println("I'm clickable!")
}

interface Focusable {
    fun setFocus(b: Boolean) =
        println("I ${if (b) "got" else "lost"} focus.")

    fun showOff() = println("I'm focusable!")
}

fun main(args: Array<String>) {
    val button = Button()
    button.showOff()
    // Focusable 有提供 setFocus,故 Button 可使用這個 function
    button.setFocus(true)
    button.click()
}

執行結果

I'm clickable!
I'm focusable!
I got focus.
I was clicked
final by default

為了避免發生 fragile base class 問題,kotlin 預設將 class, method 都加上 final 的特性,如果要讓其他類別繼承的
class 或覆寫的 method,都必須要加上 open 這個 modifier。

interface Clickable {
    fun click()
    fun showOff() = println("I'm clickable!")
}

// 可被繼承
open class RichButton : Clickable {

    // 不可被覆寫
    fun disable() {}

    // 可被覆寫
    open fun animate() {}

    // 覆寫 click,且可被覆寫
    override fun click() {}
    
    // 如果要限制 click 不能被覆寫,要加上 final
    // final override fun click() {}
}
class modifier corresponding member comments
final can't be overridden default for class members
open can be overridden 要特別指定
abstract must be overridden 只能用在 abstract classes, abstract members 不能有實作內容
override overrides a member in a superclass 預設為 open,如要限制不能被覆寫,要加上 final

visibility modifiers 有 public, protected, private, internal 四種,預設為 public,其中 internal 表示為 "visible inside a module"

visibility modifiers class member top-level declaration
public(default) visible everywhere visible everywhere
internal visible in a module visible in a module
protected visible in subclasses --
private visible in a clsss visible in a file

protected member 只能被 class 及 subclass 使用,跟 java 的 protected,可在同一個 package 使用的定義不同

class 的 extension function 無法使用 private or protected members

interface Focusable {
    fun setFocus(b: Boolean) =
        println("I ${if (b) "got" else "lost"} focus.")

    fun showOff() = println("I'm focusable!")
}

internal open class TalkativeButton : Focusable {
    private fun yell() = println("Hey!")
    protected fun whisper() = println("Let's talk!")
}

// error: 'public' member exposes its 'internal' receiver type TalkativeButton
fun TalkativeButton.giveSpeech() {
    // error: cannot access 'yell': it is private in 'TalkativeButton'
    yell()
    // error: cannot access 'whisper': it is protected in 'TalkativeButton'
    whisper()
}
nested class, inner class

kotlin 也可在 class 裡面定義另一個 class,跟 java 的差異是 kotlin nested class 不能使用 outer class instance

首先製作兩個 interface,注意 State 為 Serializable

interface State: Serializable

interface View {
    fun getCurrentState(): State
    fun restoreState(state: State) {}
}

如果以 java 實作 ButtonState 這個 inner class,會遇到 java.io.NotSerializableException: Button,這是因為 ButtonState 裡面儲存了 outer class: Button 的 implicit reference,無法被序列化,如要解決,要將 ButtonState 宣告為 static,去掉 implicit reference to Button。

/* Java */
public class Button implements View {
    @Override
    public State getCurrentState() {
        return new ButtonState();
    }

    @Override
    public void restoreState(State state) { /*...*/ }
    
    public class ButtonState implements State { /*...*/ }
}

以 kotlin 實作 ButtonState,就像是 java 的 static nested class 一樣。

class Button : View {
    override fun getCurrentState(): State = ButtonState()

    override fun restoreState(state: State) { /*...*/ }

    class ButtonState : State { /*...*/ }
}

kotlin 的 nested class 不會參考到 outer class 的 reference,但 inner class 可以

Class A declared within another Class B in Java in Kotlin
nested class (不會儲存 outer class 的 reference) static class A class A
inner class (會儲存 outer class 的 reference) class A inner class A

可用 this@Outer 參考到 Outer class 的 reference

class Outer {
    inner class Inner {
        fun getOuterReference(): Outer = this@Outer
    }
}
sealed class

Expr 介面有兩個實作的 class: Num, Sum,在 eval 中,要注意 else 的部分,compiler 會檢查有沒有預設的條件,在這邊預設不能回傳任何數值,故直接 throw exception

interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr

fun eval(e: Expr): Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.right) + eval(e.left)
        else ->
            throw IllegalArgumentException("Unknown expression")
    }

fun main(args: Array<String>) {
    println(eval(Sum(Sum(Num(1), Num(2)), Num(4))))
}

kotlin 提供 sealed class 處理這種問題

因 Expr 限制為 sealed class,故無法被任意產生 subclass,所有 subclass 只能寫成 nested class,而在 eval 裡面,就不需要寫 else 的條件,因為系統內不可能會有其他 Expr 的 subclasses (compiler 的檢查及限制)

sealed class Expr {
    class Num(val value: Int) : Expr()
    class Sum(val left: Expr, val right: Expr) : Expr()
}

fun eval(e: Expr): Int =
    when (e) {
        is Expr.Num -> e.value
        is Expr.Sum -> eval(e.right) + eval(e.left)
    }

fun main(args: Array<String>) {
    println(eval(Expr.Sum(Expr.Sum(Expr.Num(1), Expr.Num(2)), Expr.Num(4))))
}

使用 nontrivial constructor 及 properties

kotlin 也可以實作多個 constructor,可在 initializer blocks 增加 initilization logic

primary constructor 及 initializer blocks

constructor, init 這兩個關鍵字,用 _nickname 取代 java 習慣的 this.nickname = nickname 這樣的寫法

// 單一參數的 primary constructor
class User constructor(_nickname: String) {
    val nickname: String
    
    // initializer block
    init {
        nickname = _nickname
    }
}

可省略 contructor, init 的語法

class User(_nickname: String) {
    val nickname = _nickname
}

再進一步省略 {} 的部分

class User(val nickname: String)

在 contructor 的參數,可給予預設值,使用時,就可彈性使用一或兩個參數的 constructor

class User(val nickname: String,
           val isSubscribed: Boolean = true)

fun main(args: Array<String>) {
    val alice = User("Alice")
    println(alice.isSubscribed)
    val bob = User("Bob", false)
    println(bob.isSubscribed)
    val carol = User("Carol", isSubscribed = false)
    println(carol.isSubscribed)
}

如果 super class 的 constructor 需要參數,要在 subclass 定義裡面提供該參數初始化 super class

open class User(val nickname: String) { ... }
class TwitterUser(nickname: String) : User(nickname) { ... }

預設會產生一個沒有參數的 contructor

open class Button

class RadioButton: Button()

可讓 contructor 變成 private method

class Secretive private constructor() {}

class Secretive {
    private constructor()
}
secondary constructor
open class View {
    constructor(ctx: Context) {
        // some code
    }

    // secondary constructor
    constructor(ctx: Context, attr: AttributeSet) {
        // some code
    }
}

// subclass 必須同時提供兩個 constructor
class MyButton : View {
    constructor(ctx:Context) : super(ctx) {
        // ...
    }
    constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr) {
        // ...
    }
}

可以用 this() 呼叫其他 constructor

class MyButton : View {
    constructor(ctx: Context): this(ctx, MY_STYLE) {
        // ...
    }
    constructor(ctx: Context, attr: AttributeSet):super(ctx, attr) {
        // ...
    }
}
實作 interfaces 中宣告的 properties

interface User 中定義需要有 nickname 這個 property,PrivateUser、SubscribingUser、FacebookUser 分別用三種方式,提供 nickname

interface User {
    val nickname: String
}

// primary constructor property
class PrivateUser(override val nickname: String) : User

// custom getter
class SubscribingUser(val email: String) : User {
    override val nickname: String
        get() = email.substringBefore('@')
}

// property initializer
fun getFacebookName(accountId: Int) = "fb:$accountId"
class FacebookUser(val accountId: Int) : User {
    override val nickname = getFacebookName(accountId)
}

fun main(args: Array<String>) {
    println(PrivateUser("test@kotlinlang.org").nickname)
    println(SubscribingUser("test@kotlinlang.org").nickname)
}

interface 可讓 property 有 getter setter 實作

interface User {
    val email: String
    val nickname: String
        get() = email.substringBefore('@')
}
由 getter/setter 使用 backing field

field 可在 getter/setter 中取得該 backing field property 的 value,在 setter 裡面,可修改 field 的 value

class User(val name: String) {
    var address: String = "unspecified"
        set(value: String) {
            println("""
                Address was changed for $name:
                "$field" -> "$value".""".trimIndent())
            field = value
        }
}

fun main(args: Array<String>) {
    val user = User("Alice")
    user.address = "Elsenheimerstrasse 47, 80687 Muenchen"
}
修改 accessor visibility

counter 的 set 改成 private method,因此,不能在 class 以外的地方,修改 counter 的數值

class LengthCounter {
    var counter: Int = 0
        private set

    fun addWord(word: String) {
        counter += word.length
    }
}

fun main(args: Array<String>) {
    val lengthCounter = LengthCounter()
    lengthCounter.addWord("Hi!")
    println(lengthCounter.counter)
}

compiler 產生的 methods: data classes 及 class delegation

java 在每一個 class 都有 equals(), hashCode(), toString() 這些 method,kotlin 在 compiler 中自己產生這些 method

universal object methods
  • toString()

override toString() method

class Client(val name: String, val postalCode: Int) {
    override fun toString() = "Client(name=$name, postalCode=$postalCode)"
}

fun main(args: Array<String>) {
    val client1 = Client("Alice", 342562)
    println(client1)
}
  • equals()

沒有調整 equals() 時,兩個物件的比較結果是相異的

class Client(val name: String, val postalCode: Int)

fun main(args: Array<String>) {
    val client1 = Client("Alice", 342562)
    val client2 = Client("Alice", 342562)
    println(client1 == client2)
}

覆寫 equals(),Any? 其中 Any 代表 java.lang.Object ,這也是 Kotlin 所有 classes 的 super class,? 代表 other 可以是 null

hashCode 必須要跟著 equals 一同被覆寫,因為當兩個 object 相等時,hashCode 必須要一樣

class Client(val name: String, val postalCode: Int) {
    override fun equals(other: Any?): Boolean {
        // 先檢查 other 是不是 null, 是不是 Client
        if (other == null || other !is Client)
            return false
        // 檢查兩個欄位的數值是否相等
        return name == other.name &&
               postalCode == other.postalCode
    }
    override fun toString() = "Client(name=$name, postalCode=$postalCode)"
    
    override fun hashCode(): Int = name.hashCode() * 31 + postalCode
}

fun main(args: Array<String>) {
    val processed = hashSetOf(Client("Alice", 342562))
    println(processed.contains(Client("Alice", 342562)))
}
data classes: 自動產生 universal methods 的實作

在 class 前面加上 data 關鍵字,compiler 將會自動產生 universal methods: equals(), hashCode(), toString()

data class 並沒有限制只能使用 val property,也可以用 var,但建議還是盡量使用 val,讓 data class 成為 immutable object,所以 kotlin 在提供了一個 copy() 這個 universal method

如果自己實作 copy,會類似以下這樣的 code

class Client(val name: String, val postalCode: Int) {
    ...
    fun copy(name: String = this.name, postalCode: Int = this.postalCode) = Client(name, postalCode)
}

object instance 使用 copy,並修改其中一個欄位的值

>>> val bob = Client("Bob", 973293)
>>> println(bob.copy(postalCode = 382555))
Client(name=Bob, postalCode=382555)
class delegation: 使用 "by"

在物件導向程式中,會因為繼承 class,覆寫 method,讓某些程式跟父類別產生連動關係,當 base class 有異動時,就造成整個系統不穩定的狀況。

kotlin 預設讓 class 都是 final,無法被繼承的,這可確保經過設計,可以被繼承的 class 才有被繼承的功能,

相較於繼承,更常見的方式,是利用 Decorator Pattern,建立一個新的 class,實作跟舊的 class 一樣的所有 methods,再根據自己的需求,修改對應的 methods,就像是以下的範例。

class DelegatingCollection<T> : Collection<T> {
    private val innerList = arrayListOf<T>()
    
    override val size: Int get() = innerList.size
    override fun isEmpty(): Boolean = innerList.isEmpty()
    override fun contains(element: T): Boolean = innerList.contains(element)
    override fun iterator(): Iterator<T> = innerList.iterator()
    override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)
}

kotlin 提供另一種更有效的方式: class delegation 使用 "by",以下這樣的 code,compiler 會自動產生所有 delegation codes,只需要寫有需要修改覆寫的 method。

class DelegatingCollection<T>(
    innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList {
}
import java.util.HashSet

class CountingSet<T>(
        val innerSet: MutableCollection<T> = HashSet<T>()
) : MutableCollection<T> by innerSet {

    var objectsAdded = 0

    // 不使用預設的 delegation method,改寫 add
    override fun add(element: T): Boolean {
        objectsAdded++
        return innerSet.add(element)
    }

    override fun addAll(c: Collection<T>): Boolean {
        objectsAdded += c.size
        return innerSet.addAll(c)
    }
}

fun main(args: Array<String>) {
    val cset = CountingSet<Int>()
    cset.addAll(listOf(1, 1, 2))
    println("${cset.objectsAdded} objects were added, ${cset.size} remain")
}

關鍵字 object 的用途

  • Object declaration: 定義 singleton
  • Companion objects: 包含 factory method 以及所有不需要 instance 的 methods,透過 class name 使用這些 methods
  • Object expression: 取代 Java 的 anonymous inner class
singleton

object 可讓該 class 只會產生一個 instance,使用時必須要用 class name

object Payroll {
    val allEmployees = arrayListOf<Person>()
    fun calculateSalary() {
        for (person in allEmployees) {
            ...
        }
    }
}

Payroll.allEmployees.add(Person(...))
Payroll.calculateSalary()

以下是 CaseInsensitiveFileComparator 範例

import java.util.Comparator
import java.io.File

object CaseInsensitiveFileComparator : Comparator<File> {
    override fun compare(file1: File, file2: File): Int {
        return file1.path.compareTo(file2.path,
                ignoreCase = true)
    }
}

fun main(args: Array<String>) {
    println(CaseInsensitiveFileComparator.compare(
        File("/User"), File("/user")))
    
    val files = listOf(File("/Z"), File("/a"))
    println(files.sortedWith(CaseInsensitiveFileComparator))
}

也可以將 object 放在 class 裡面,以 Person.NameComparator 的方式使用

import java.util.Comparator

data class Person(val name: String) {
    object NameComparator : Comparator<Person> {
        override fun compare(p1: Person, p2: Person): Int =
            p1.name.compareTo(p2.name)
    }
}

fun main(args: Array<String>) {
    val persons = listOf(Person("Bob"), Person("Alice"))
    println(persons.sortedWith(Person.NameComparator))
}

如果要從 Java 使用 kotlin object,要透過 INSTANCE 這個 singleton object reference

/* Java */
CaseInsensitiveFileComparator.INSTANCE.compare(file1, file2);
companion objects: factory method and static members

java 的 keyword static,在 kotlin 沒有。

kotlin 提供 companion object,使用起來就像是java 的 static method 一樣

class A {
    companion object {
        fun bar() {
            println("Companion object called")
        }
    }
}

fun main(args: Array<String>) {
    A.bar()
}

通常會針對 FacebookUser, SubscribingUser 兩種 User,實作不同的 constructor

class User {
    val nickname: String
    constructor(email: String) {
        nickname = email.substringBefore('@')
    }

    constructor(facebookAccountId: Int) {
        nickname = getFacebookName(facebookAccountId)
    }
}

如果用 companion object 的做法,可在裡面實作 factory method,FacebookUser, SubscribingUser 兩種不同的 User 就用不同的 factory method 產生物件

fun getFacebookName(accountId: Int) = "fb:$accountId"

class User private constructor(val nickname: String) {
    companion object {
        fun newSubscribingUser(email: String) =
            User(email.substringBefore('@'))

        fun newFacebookUser(accountId: Int) =
            User(getFacebookName(accountId))
    }
}

fun main(args: Array<String>) {
    val subscribingUser = User.newSubscribingUser("bob@gmail.com")
    val facebookUser = User.newFacebookUser(4)
    println(subscribingUser.nickname)
}
把 companion object 當作 regular objects

companion object 就像是在 class 中宣告的一般類別,可命名,可實作 interface,可以有 extension function 或 properties

  • json serialization
class Person(val name: String) {
    companion object Loader {
        fun fromJSON(jsonText: String): Person = ...
    }
}

fun main(args: Array<String>) {
    val person = Person.Loader.fromJSON("{name: 'Dmitry'}")
    println(person.name)
    val person2 = Person.fromJSON("{name: 'Brent'}")
    println(person2.name)
}
  • implement interface
interface JSONFactory<T> {
    fun fromJSON(jsonText: String): T
}

class Person(val name: String) {
    companion object : JSONFactory<Person> {
        override fun fromJSON(jsonText: String): Person = ...
    }
}

在 java 如果要使用 companion object

/* Java */
Person.Companion.fromJSON("...");
  • companion object extensions
// business logic module
class Person(val firstName: String, val lastName: String) {
    // 產生空白的 companion object
    companion object {
    }
}
// client/server communication module
// 產生 extension function
fun Person.Companion.fromJSON(json: String): Person {
    ...
}

val p = Person.fromJSON(json)
  • anonymous inner class

在 java 使用 event listener,會用到 anonymous inner class

window.addMouseListener(
    object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            // ...
        }
        override fun mouseEntered(e: MouseEvent) {
            // ...
        }
    }
)

在 java 的 anonymous class 中,只能使用外部的 final variables。

在 kotlin 可以建立 local 變數,並在 object 裡面使用該變數

fun countClicks(window: Window) {
    // 宣告 local 變數
    var clickCount = 0
    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            // 更新變數的數值
            clickCount++
        }
    })
    // ...
}

References

Kotlin in Action