說明如何處理 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")
}
}