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

2019/03/11

Kotlin 宣告與呼叫函數

以下說明這些功能:collections, strings 及 regular expressions 的相關函數。以命名的參數及預設值,定義參數。透過擴增的 functions 及 properties,使用 Java Libraries。以 top-level 及 local function 的方式重組程式碼。

建立 collections

kotlin 的 .javaClass 就等同 java 的 getClass()

//println(set.javaClass)
//class java.util.HashSet
val set = hashSetOf(1, 7, 53)

//println(list.javaClass)
//class java.util.ArrayList
val list = arrayListOf(1, 7, 53)

//println(map.javaClass)
//class java.util.HashMap
val map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three")

kotlin 沒有建立自己的 collection classes 而是沿用 java 的 collection classes

// last() 取得最後一個 element
val strings = listOf("first", "second", "fourteenth")
println(strings.last())

// max() 取得最大值
val numbers = setOf(1, 14, 2)
println(numbers.max())
// println 會呼叫 list 的 toString()
val list = listOf(1, 2, 3)
println(list)

joinToString 將 collection 以 prefix, separator, postfix 的格式列印出來

fun <T> joinToString(
        collection: Collection<T>,
        separator: String,
        prefix: String,
        postfix: String
): String {

    val result = StringBuilder(prefix)

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

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

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3)
    println(joinToString(list, "; ", "(", ")"))
}

執行結果

(1; 2; 3)

為了避免每一次都需要填寫四個參數的麻煩,可利用以下方式處理

  • Named Arguments 呼叫 function 時,將參數命名
joinToString(collection, separator = " ", prefix = " ", postfix = ".")
  • Default parameter values 函數的參數提供預設值
fun <T> joinToString(
        collection: Collection<T>,
        separator: String = ", ",
        prefix: String = "",
        postfix: String = ""
): String

接下來可用以下方式呼叫函數

joinToString(list, ", ", "", "")
joinToString(list)
joinToString(list, "; ")
joinToString(list, prefix = "# ")

top-level functions and properties

取代 static utility clsses

建立 join.kt,放入以下 code

package strings

fun <T> joinToString(
        collection: Collection<T>,
        separator: String,
        prefix: String,
        postfix: String
): String {

    val result = StringBuilder(prefix)

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

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

$ kotlinc join.kt 編譯後,會產生 strings/JoinKt.class

所有 top-level function 都會成為該 class 的 static methods

如果加上 @file:JvmName("StringFunctions") 就可改變 class 的名稱,為 strings.StringFunctions

@file:JvmName("StringFunctions")

package strings
fun joinToString(...): String { ... }

top-level properties 轉換到 java 時,一樣會產生 accessor methods, val 變數會產生 getter,var 變數產生 getter/setter。

const val UNIX_LINE_SEPARATOR = "\n" 加上 const,就會轉換為 java 的 public static final String UNIX_LINE_SEPARATOR = "\n";

extension functions and properties,將 method 增加到另一個 class 裡面

因 kotlin 通常會增加到某一個既有的 Java project 裡面,故須要將 kotlin 跟既有的 java code 整合在一起。可使用 extension function,在 class 外面訂製一個新的 function

package strings

fun String.lastChar(): Char = this.get(this.length - 1)

String 稱為 receiver type,也就是要擴充的 class 名稱,this 稱為 receiver object

以下的例子,String 為 receiver type,"Kotlin" 為 receiver object

>>> println("Kotlin".lastChar())
n

也可以將 this 省略

package strings

fun String.lastChar(): Char = get(length - 1)

extension functions 必須要另外 import 才能使用

import strings.lastChar
// import strings.*

val c = "Kotlin".lastChar()

也可以在 import 後,用 as 產生別名,這可以解決 conflict 問題

import strings.lastChar as last
val c = "Kotlin".last()

從 java 呼叫 extension functions

如果剛剛的 code 是放在 StringUtil.kt 裡面

/* Java */
char c = StringUtilKt.lastChar("Java");

以 utility class 作為 extensions

將 joinToString 擴充到 Collection 裡面

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

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

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

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3)
    println(list.joinToString(separator = "; ",
          prefix = "(", postfix = ")"))
}

改成這樣,就只接受 String 的 collection

fun Collection<String>.join(
        separator: String = ", ",
        prefix: String = "",
        postfix: String = ""
) = joinToString(separator, prefix, postfix)

extension functions 無法被 overriding

在 class 裡面定義的 function 可以被 overriding

open class View {
    open fun click() = println("View clicked")
}

class Button: View() {
    override fun click() = println("Button clicked")
}

fun main(args: Array<String>) {
    val view: View = Button()
    view.click()
}

執行結果

Button clicked

extension functions 無法被 overriding,因為 extension function 是 static method

open class View {
    open fun click() = println("View clicked")
}

class Button: View() {
    override fun click() = println("Button clicked")
}

fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")

fun main(args: Array<String>) {
    val view: View = Button()
    view.showOff()
}

執行結果

I'm a view!

extension properties

extension property 就像是 property 再加上 receiver type,壹定要定義 getter,不能夠定義初始值,在 StringBuilder.lastChar 是定義為 var,因為 StringBuilder 裡面存的資料是會變動的。

val String.lastChar: Char
    get() = get(length - 1)

var StringBuilder.lastChar: Char
    get() = get(length - 1)
    set(value: Char) {
        this.setCharAt(length - 1, value)
    }

fun main(args: Array<String>) {
    // 使用起來就像是 property
    println("Kotlin".lastChar)
    
    val sb = StringBuilder("Kotlin?")
    sb.lastChar = '!'
    println(sb)
}

其他支援 collections 的功能

Kotlin standard library 其他支援 collection 的功能

  • 關鍵字 vararg: 可定義有任意參數數量的 function
  • infix calls
  • destructuring declarations,可 unpack composite value 為 multiple variables

為什麼 Kotlin 跟 Java 使用相同的 collection class 但是卻有更多功能?原因是 last, max function 都是以 extension function 實作的。

>>> val strings: List<String> = listOf("first", "second", "fourteenth")
>>> strings.last()
fourteenth
>>> val numbers: Collection<Int> = setOf(1, 14, 2)
>>> numbers.max()
14
fun <T> List<T>.last(): T { /* returns the last element */ }
fun Collection<Int>.max(): Int { /* finding a maximum in a collection */ }

  • varargs: 讓 function 可接受不定數量的參數
fun listOf<T>(vararg values: T): List<T> { ... }

直接在參數前面加上 * ,稱為 spread operator

fun main(args: Array<String>) {
    val list = listOf("args: ", *args)
    println(list)
}

  • infix call
// regular way 呼叫函數
1.to("one")

// infix call
1 to "one"

以下是簡化版 infix 的定義方式

infix fun Any.to(other: Any) = Pair(this, other)

Pair 是 kotlin standard library 的 class,代表一組 elements,例如 val (number, name) = 1 to "one" 可將 1 to "one" 轉換為 Pair 再提供給 (number, name) 設定變數

for ((index, element) in collection.withIndex()) {
    println("$index: $element")
}
fun <K, V> mapOf(vararg values: Pair<K, V>): Map<K, V>

val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")

使用 strings 及 regular expressions

  • splitting strings

kotlin 使用跟 java 一樣的 regular expression syntax

fun main(args: Array<String>) {
    println( "12.345-6.A".split("\\.|-".toRegex()) )
    println( "12.345-6.A".split(".", "-") )
    
    // [12, 345, 6, A]
}
  • 將 file full path name 切割為 directory, filename, extension

使用標準函式庫的 substring functions

fun parsePath(path: String) {
    val directory = path.substringBeforeLast("/")
    val fullName = path.substringAfterLast("/")

    val fileName = fullName.substringBeforeLast(".")
    val extension = fullName.substringAfterLast(".")

    println("Dir: $directory, name: $fileName, ext: $extension")
}

fun main(args: Array<String>) {
    parsePath("/Users/yole/kotlin-book/chapter.adoc")
}

使用 regular expression 的方法:regular expression 以 triple-quoted string 撰寫,這樣就不需要 escape characters,所以直接寫 \. 就可以了

fun parsePath(path: String) {
    val regex = """(.+)/(.+)\.(.+)""".toRegex()
    val matchResult = regex.matchEntire(path)
    if (matchResult != null) {
        val (directory, filename, extension) = matchResult.destructured
        println("Dir: $directory, name: $filename, ext: $extension")
    }
}

fun main(args: Array<String>) {
    parsePath("/Users/yole/kotlin-book/chapter.adoc")
}

  • multiline triple-quoted strings

在 triple-quoted string 裡面包含所有的字元,trimMargin 可去掉 . 前面所有字元

val kotlinLogo = """| //
                   .|//
                   .|/ \"""

fun main(args: Array<String>) {
    println(kotlinLogo.trimMargin("."))
}

結果為

| //
|//
|/ \

用 embedded expression 填寫 $ 字元

>>> val price = """${'$'}99.9"""
>>> println(price)
$99.9

利用 local functions 及 extensions 簡化程式碼

以下程式碼中,name 及 address 欄位檢查部分的程式碼有重複

class User(val id: Int, val name: String, val address: String)

fun saveUser(user: User) {
    if (user.name.isEmpty()) {
        throw IllegalArgumentException(
            "Can't save user ${user.id}: empty Name")
    }

    if (user.address.isEmpty()) {
        throw IllegalArgumentException(
            "Can't save user ${user.id}: empty Address")
    }

    // Save user to the database
}

fun main(args: Array<String>) {
    saveUser(User(1, "", ""))
}

建立一個新的 local function: validate,利用該 local function 檢查欄位

class User(val id: Int, val name: String, val address: String)

fun saveUser(user: User) {

    fun validate(user: User,
                 value: String,
                 fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException(
                "Can't save user ${user.id}: empty $fieldName")
        }
    }

    validate(user, user.name, "Name")
    validate(user, user.address, "Address")

    // Save user to the database
}

fun main(args: Array<String>) {
    saveUser(User(1, "", ""))
}

因為是 local function,可直接使用 user 這個物件

class User(val id: Int, val name: String, val address: String)

fun saveUser(user: User) {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException(
                "Can't save user ${user.id}: " +
                    "empty $fieldName")
        }
    }

    validate(user.name, "Name")
    validate(user.address, "Address")

    // Save user to the database
}

fun main(args: Array<String>) {
    saveUser(User(1, "", ""))
}

將 validate 改成 extension function,讓程式碼更簡潔

class User(val id: Int, val name: String, val address: String)

fun User.validateBeforeSave() {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException(
               "Can't save user $id: empty $fieldName")
        }
    }

    validate(name, "Name")
    validate(address, "Address")
}

fun saveUser(user: User) {
    user.validateBeforeSave()

    // Save user to the database
}

fun main(args: Array<String>) {
    saveUser(User(1, "", ""))
}

References

Kotlin in Action

2019/03/04

Kotlin Basics

kotlin 基本的 functions, variables, classes, enums, properties,以及型別轉換,及 exception 處理。

functions and variables

這是最基本的 HelloWorld.kt

fun main(args: Array<String>) {
    println("Hello, world!")
}

fun 是宣告 function,參數的型別寫在參數名稱後面,fun不需要放在 class 裡面,可直接放在檔案中,以 println 取代 System.out.println,可省略每一行程式最後面的分號。

kotlin 沒有 array 這種資料型別,必須要使用 Array 這個 class。


  • if 是 expression 不是 statement
fun max(a: Int, b: Int): Int {
    return if (a > b) a else b
}

fun main(args: Array<String>) {
    println(max(1, 2))
}

以 return 指定 fun 的回傳值

kotlin 的 if 是 expression 不是 statement,差別是 expression 一定有一個 value,可用在另一個 expression 裡面,而 statement 是程式區塊的頂層,沒有 value。所以程式中的 if 可以放在 return 後面,這種寫法在 Java 是不能用的。

kotlin 中,除了 for, do, do/while 以外,都是 expressions,expression 可用在另一個 expression 的功能,可組成 control structures。

另外 assignments 在 Java 是 expressions,但在 kotlin 是 statement。


變數可以不指定資料型別,要刻意指定也可以

val answer = 42
val answer: Int = 42

如果宣告時,沒有指定 value,則宣告變數時就一定要指定資料型別。

val answer: Int
answer = 42

val 是 immutable reference 的變數 var 是 mutable reference 的變數

在 kotlin 應該盡量使用 val,必要時才用 var

要注意的是,immutable 是指 object reference 不變,但是參考到的變數,還是可以改變 value

val languages = arrayListOf("Java")
languages.add("Kotlin")

var 雖然可以改變 object reference,但是仍然要改變為相同資料型別的變數 reference,以下的程式,會出現 type mismatch 的 error

var answer = 42
answer = "no answer"

簡化 string formatting: string templates

fun main(args: Array<String>) {
    val name = if (args.size > 0) args[0] else "Kotlin"
    println("Hello, $name!")
}

可以在 string 裡面,用 $name 參考到上面宣告過的變數,但如果是 character,前面要加上 escape,例如 println("\$x")

可用 \({} 加入一個 expression,例如: ```\){args[0]}```

fun main(args: Array<String>) {
    if (args.size > 0) {
        println("Hello, ${args[0]}!")
    }
}

因為 if 是 expression,所以可直接放到 ${} 裡面

fun main(args: Array<String>) {
    println("Hello, ${if (args.size > 0) args[0] else "someone"}!")
}

Classes and Properties

class Person(
    val name: String,
    var isMarried: Boolean
)

fun main(args: Array<String>) {
    val person = Person("Bob", true)
    println(person.name)
    println(person.isMarried)
}

以 class 宣告類別,預設就是 public,這個可省略。在 class 後面直接加上 val 參數,表示 name 是 read only 的 field,而宣告為 var 的 isMarried 則是可以改變的,自動就產生了 getter 及 setter。

如果由 Java 使用 Person 這個 class,可直接使用 getter/setter method。

Person person = new Person("Bob", true);
System.out.println(person.getName());  // Bob
System.out.println(person.isMarried()); // true

但是在 kotlin 的 getter/setter,就沒有 get 或 set 的前置,直接用 person.name person.isMarried 就可以了。


可在 class 中,自訂新的 property,並以 get() 撰寫自訂的 getter method,在 Java 是呼叫 isSquare

class Rectangle(val height: Int, val width: Int) {
    val isSquare: Boolean
        get() {
            return height == width
        }
}

fun main(args: Array<String>) {
    val rectangle = Rectangle(41, 43)
    println(rectangle.isSquare)
}

Directories and packages

每一個 kotlin .kt 原始檔在開頭可有一個 package 宣告,不同 package 的 kotlin class 必須要 import 後才能使用。

package geometry.shapes

import java.util.Random

class Rectangle(val height: Int, val width: Int) {
    val isSquare: Boolean
        get() = height == width
}

fun createRandomRectangle(): Rectangle {
    val random = Random()
    return Rectangle(random.nextInt(), random.nextInt())
}

可以直接 import top-level function

package geometry.example

import geometry.shapes.createRandomRectangle

fun main(args: Array<String>) {
    println(createRandomRectangle().isSquare)
}

可以將多個 classes 放到同一個 kotlin file 中,kotlin source file name 可以任意自訂,不需要跟 class 名稱一樣。但最好還是遵循 Java 的目錄原則,將同一個 package 的檔案,放到跟 package 相同的目錄中,這樣才能順利找到正確的原始檔。

處理多重選擇: enum 及 when

以 enum class 定義 enum(在 Java 是用 enum)

enum class Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

enum 並不是單純的 list of values,可以在 enum 中定義 properties 及 methods。宣告了 enum properties 後,每一個 enum value 都必須指定 properties 的數值,在 enum value 定義後面,一定要加上一個分號,在分號後面才能加上 fun methods

enum class Color(
        // 宣告 enum 的 properties
        val r: Int, val g: Int, val b: Int
) {
    RED(255, 0, 0), ORANGE(255, 165, 0),
    YELLOW(255, 255, 0), GREEN(0, 255, 0), BLUE(0, 0, 255),
    INDIGO(75, 0, 130), VIOLET(238, 130, 238);

    fun rgb() = (r * 256 + g) * 256 + b
}

fun main(args: Array<String>) {
    println(Color.BLUE.rgb())
}

when 是用來取代 Java 的 switch 語法

enum class Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

fun getMnemonic(color: Color) =
    when (color) {
        Color.RED -> "Richard"
        Color.ORANGE -> "Of"
        Color.YELLOW -> "York"
        Color.GREEN -> "Gave"
        Color.BLUE -> "Battle"
        Color.INDIGO -> "In"
        Color.VIOLET -> "Vain"
    }

fun main(args: Array<String>) {
    println(getMnemonic(Color.BLUE))
}

可將多個 enum 對應到同一個 value

enum class Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

fun getWarmth(color: Color) = when(color) {
    Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
    Color.GREEN -> "neutral"
    Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
}

fun main(args: Array<String>) {
    println(getWarmth(Color.ORANGE))
}

在 import 時,必須要 import Color 這個 class,另外再 import enum constants

import ch02.colors.Color
import ch02.colors.Color.*

fun getWarmth(color: Color) = when(color) {
    RED, ORANGE, YELLOW -> "warm"
    GREEN -> "neutral"
    BLUE, INDIGO, VIOLET -> "cold"
}

fun main(args: Array<String>) {
    println(getWarmth(Color.ORANGE))
}

可將 when 用在任意的 object(Java 的 switch 只能用在 enum constants, strings, number literals)。

when 裡面是 setOf 集合,後面每一個條件,都必須同樣檢查為 setOf,最後用 else 設定其他的條件。

import ch02.colors.Color
import ch02.colors.Color.*

fun mix(c1: Color, c2: Color) =
        when (setOf(c1, c2)) {
            setOf(RED, YELLOW) -> ORANGE
            setOf(YELLOW, BLUE) -> GREEN
            setOf(BLUE, VIOLET) -> INDIGO
            else -> throw Exception("Dirty color")
        }

fun main(args: Array<String>) {
    println(mix(BLUE, YELLOW))
}

上面的 code 會產生一些 Set 物件,為了程式效能,可去掉 when 的參數,直接

import ch02.colors.Color
import ch02.colors.Color.*

fun mixOptimized(c1: Color, c2: Color) =
    when {
        (c1 == RED && c2 == YELLOW) ||
        (c1 == YELLOW && c2 == RED) ->
            ORANGE

        (c1 == YELLOW && c2 == BLUE) ||
        (c1 == BLUE && c2 == YELLOW) ->
            GREEN

        (c1 == BLUE && c2 == VIOLET) ||
        (c1 == VIOLET && c2 == BLUE) ->
            INDIGO

        else -> throw Exception("Dirty color")
    }

fun main(args: Array<String>) {
    println(mixOptimized(BLUE, YELLOW))
}

smart casts: 合併了 type checks 及 casts

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

fun eval(e: Expr): Int {
    if (e is Num) {
        // 將 e 轉型為 Num,可以省略
        val n = e as Num
        return n.value
    }
    if (e is Sum) {
        return eval(e.right) + eval(e.left)
    }
    throw IllegalArgumentException("Unknown expression")
}

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

一開始定義了兩個 class 分別都實作 Expr 介面, eval 函數區分了 Num 與 Sum 的回傳值。

is 是在檢查資料型別的保留字,且在通過檢查後,compiler 會自動將 e 轉型為該資料型別。 as 是強制轉型的保留字。

eval(Sum(Sum(Num(1), Num(2)), Num(4))) 就是在計算 (Num(1) + Num(2)) + Num(4) 的結果。

將 if 取代為 when

在 kotlin 的 if 是 expression,會有 return value。所以可以將剛剛的 eval 改寫為 if expression。

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

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

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

剛剛的 if 裡面,都只有一個 expression,可以用 when 改寫。

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(Num(1), Num(2))))
}

when 的 is 裡面,超過一個 expression,就用 { } 包括在一起,最後一個 statement 就是 return value。

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

fun evalWithLogging(e: Expr): Int =
    when (e) {
        is Num -> {
            println("num: ${e.value}")
            e.value
        }
        is Sum -> {
            val left = evalWithLogging(e.left)
            val right = evalWithLogging(e.right)
            println("sum: $left + $right")
            left + right
        }
        else -> throw IllegalArgumentException("Unknown expression")
    }

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

while 及 for loops

1..100 是 ranges,由 1 到 100

100 downTo 1 step 2,是由 100 到 1,每一次都 -2

fun fizzBuzz(i: Int) = when {
    i % 15 == 0 -> "FizzBuzz "
    i % 3 == 0 -> "Fizz "
    i % 5 == 0 -> "Buzz "
    else -> "$i "
}

fun main(args: Array<String>) {
    for (i in 1..100) {
        print(fizzBuzz(i))
    }
    
    for (i in 100 downTo 1 step 2) {
        print(fizzBuzz(i))
    }
}

for (x in 0 until size) 也可以寫成 for (x in 0..size-1)


如何 iterate maps

import java.util.TreeMap

fun main(args: Array<String>) {
    val binaryReps = TreeMap<Char, String>()

    for (c in 'A'..'F') {
        val binary = Integer.toBinaryString(c.toInt())
        binaryReps[c] = binary
    }

    for ((letter, binary) in binaryReps) {
        println("$letter = $binary")
    }
    
    val list = arrayListOf("10", "11", "1001")
    for ((index, element) in list.withIndex()) {
        println("$index: $element")
    }
}

執行結果

A = 1000001
B = 1000010
C = 1000011
D = 1000100
E = 1000101
F = 1000110
0: 10
1: 11
2: 1001

用 in !in 檢查 ranges,也可以用在 when 裡面

"Kotlin" in "Java".."Scala" Kotlin 是在 Java 到 Scala 範圍之間,所有字串的一員

fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'

fun recognize(c: Char) = when (c) {
    in '0'..'9' -> "It's a digit!"
    in 'a'..'z', in 'A'..'Z' -> "It's a letter!"
    else -> "I don't know…​"
}

fun main(args: Array<String>) {
    println("q isLetter: "+isLetter('q'))
    println("x isNotDigit: "+isNotDigit('x'))

    println()
    println("8:"+ recognize('8'))

    println()
    println("Kotlin" in "Java".."Scala")

    println()
    println("Kotlin" in setOf("Java", "Scala"))
}

執行結果

q isLetter: true
x isNotDigit: true

8:It's a digit!

true

false

Exceptions

kotlin 的 throw,後面不需要加上 new 再搭某一個 Exception class

if (percentage !in 0..100) {
    throw IllegalArgumentException(
        "A percentage value must be between 0 and 100: $percentage")
}

跟 Java 類似,也適用 try ... catch ... finally 的語法,但是在 method 宣告的地方,不需要宣告該 method function 可能會 throw Exception。

readNumber 這個 method 是回傳 Int? ,增加 ? 的用意,是表示該 method 會回傳整數或是 null。

import java.io.BufferedReader
import java.io.StringReader

fun readNumber(reader: BufferedReader): Int? {
    try {
        val line = reader.readLine()
        return Integer.parseInt(line)
    }
    catch (e: NumberFormatException) {
        return null
    }
    finally {
        reader.close()
    }
}

fun main(args: Array<String>) {
    val reader = BufferedReader(StringReader("239"))
    println(readNumber(reader))
}

要改變習慣,將 try 當作 expression,以往會在 catch Exception 的部分,直接 return

import java.io.BufferedReader
import java.io.StringReader

fun readNumber(reader: BufferedReader) {
    val number = try {
        Integer.parseInt(reader.readLine())
    } catch (e: NumberFormatException) {
        return
    }

    println(number)
}

fun main(args: Array<String>) {
    val reader = BufferedReader(StringReader("not a number"))
    readNumber(reader)
}

將 try 的 catch exception 部分,直接寫成 null,就表示 number 是 Int 或是 null。

import java.io.BufferedReader
import java.io.StringReader

fun readNumber(reader: BufferedReader) {
    val number = try {
        Integer.parseInt(reader.readLine())
    } catch (e: NumberFormatException) {
        null
    }

    println(number)
}

fun main(args: Array<String>) {
    val reader = BufferedReader(StringReader("not a number"))
    readNumber(reader)
}

References

Kotlin in Action