2019年3月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

沒有留言:

張貼留言