寫在開頭:本人打算開始寫一個Kotlin系列的教程,一是使本身記憶和理解的更加深入,二是能夠分享給一樣想學習Kotlin的同窗。系列文章的知識點會以《Kotlin實戰》這本書中順序編寫,在將書中知識點展現出來同時,我也會添加對應的Java代碼用於對比學習和更好的理解。java
Kotlin教程(一)基礎
Kotlin教程(二)函數
Kotlin教程(三)類、對象和接口
Kotlin教程(四)可空性
Kotlin教程(五)類型
Kotlin教程(六)Lambda編程
Kotlin教程(七)運算符重載及其餘約定
Kotlin教程(八)高階函數
Kotlin教程(九)泛型編程
Java把基本數據類型和引用類型作了區分。一個基本數據類型(如int)的變量直接存儲了它的值,而一個引用類型(如String)的變量存儲的是指向包含該對象的內存地址的引用。
基本數據類型的值可以更高效地存儲和傳遞,可是你不能對這些值調用方法,或是把他們存放在集合中。Java提供了特殊的包裝類型(如Integer)在你須要對象的時候對基本數據類型進行封裝。所以,你不能用Collection<int>
來定義一個整數的集合,而必須用Collection<Integer>
來定義。
Kotlin並不區分基本數據類型和包裝類型,你使用的永遠是同一個類型(如Int):數組
val i: Int = 1
val list: List<Int> = listOf(1, 2, 3)
複製代碼
這樣很方便。此外,你還能對一個數字類型的值調用方法。例以下面使用了標準庫的函數coerceIn
來把值限制在特定的範圍內:安全
fun showProgress(progress: Int) {
val percent = progress.coerceIn(0, 100)
println("We're $percent% done!")
}
>>> showProgress(146)
We're 100% done! 複製代碼
若是基本數據類型和引用類型是同樣的,是否是意味着Kotlin使用對象來表示全部的數字是很是低效的?確實低效,因此Kotlin並無這樣作。
在運行時,數字類型會盡量地使用最高效的方式來表示。大多數狀況下Kotlin的Int類型會被編譯成Java基本數據類型int。固然,泛型、集合之類的仍是會被編譯成對應的Java包裝類型。
像Int這樣的Kotlin類型在底層能夠輕易地編譯成對應的Java基本數據類型,由於兩種類型都不能存儲null引用。反過來也同樣,當你在Kotlin中使用Java聲明時,Java基本數據類型會變成非空類型(而不是平臺類型,平臺類型詳見《Kotlin教程(四)可空性》),由於他們不能持有null值。bash
Kotlin中的可空類型(如Int?
)不能用Java的基本數據類型表示,由於null只能被存儲在Java的引用類型的變量中。這意味着任什麼時候候只要使用了基本數據類型的可空版本,它就會編譯成對應的包裝類型Int?
-> Integer
。編程語言
/* Kotlin */
class Dog(val name: String, val age: Int? = null)
/* Java */
Dog dog = new Dog("julie", 3);
Integer age = dog.getAge();
複製代碼
能夠看到val age: Int?
在Java中使用編譯成了Integer
,所以,在Java中使用的時候須要注意可能爲null的狀況。固然在Kotlin中也須要使用?.
、!!
等安全調用方式。ide
Kotlin和Java之間一條重要的區別就是處理數字轉換的方式。Kotlin不會自動地把數字從一段類型轉換成另一種,即使是轉換成範圍更大的類型。函數式編程
val i = 1
val l: Long = i //錯誤:類型不匹配
複製代碼
必須顯示進行轉換:函數
val i = 1
val l: Long = i.toLong()
複製代碼
每一種基本數據類型(Boolean除外)都定義有轉換函數:toByte()
、 toShort()
、toChar()
等。這些函數支持雙向轉換:便可以把小範圍的類型擴展到大範圍Int.toLong()
,也能夠把大範圍的類型截取到小範圍Long.toInt()
,固然於Java中相似首先要確保大範圍的類型的值超太小範圍上限。
爲了不意外發生,Kotlin要求轉換必須是顯式的,尤爲是在比較裝箱值的時候。比較兩個裝箱值的equals
方法不只會檢查他們存儲的值,還要比較裝箱類型。在Java中new Integer(42).equals(new Long(42))
會返回false。假設Kotlin支持隱式轉換,你可能會這樣寫:post
val x = 1
val list = listOf(1L, 2L, 3L)
x in list //假設Kotlin支持隱式轉換的話返回false
複製代碼
但這與咱們指望是不一樣的。所以,x in list
這行代碼根本不會編譯。Kotlin要求你顯式轉換類型,這樣只有類型相同的值才能比較:
>>> val x = 1
>>> println(x.toLong() in listOf(1L, 2L, 3L))
true
複製代碼
若是代碼中同時用到了不一樣的數字類型,你就必須顯式的轉換這些變量,來避免意想不到的行爲。
基本數據類型字面值
Kotlin除了支持簡單的十進制數字以外,還支持下面這些在代碼中書寫數字字面值的方式:
注意,Kotlin 1.1 纔開始支持數字字面值中下劃線。對字符字面值來講,可使用和Java幾乎同樣的語法。把字符寫在單引號中,必要時還可使用轉義序列:'1' ,'\t'(製表符), '\u0009'(使用Unicode轉義序列表示的製表符)。
當你書寫數字字面值的時候通常不須要使用轉換函數。算數運算符也被重載了,他們能夠接收全部適當的數字類型:
fun foo(l: Long) = println(l)
>>> val b: Byte = 1
>>> val l = b + 1L //Byte + Long -> Long
>>> foo(42) //42被當作是Long類型
42
複製代碼
Kotlin標準庫提供了一套類似的擴展方法,用來把字符串轉換成基本數據類型:"42".toInt() 。每一個這樣的函數都會嘗試吧字符串的內容解析成對應的類型,若是解析失敗則拋出NumberFormatException。
和Object做爲Java類層級結構的根差很少,Any類型是Kotlin全部非空類型的超類型,若是可能持有null值,則是Any?類型。在底層,Any類型對應java.lang.Object。Kotlin吧Java方法參數和返回類型中用到Object類型看作Any(更切確地說是平臺類型,由於其可空性未知)。當Kotlin函數使用Any時,它會被編譯成Java字節碼的Object。
/* Kotlin */
fun a(any: Any) {}
/* 編譯成的Java */
public static final void a(@NotNull Object any) {}
複製代碼
全部Kotlin類都包含下面三個方法:toString、equals、hashCode。這些方法都繼承自Any。Any並不能使用其餘Object的方法(如wait和notify),若是你確認想用這些方法,能夠經過手動把值轉換成Object來調用這些方法。
Kotlin中的Unit類型完成了Java中的void同樣的功能。當函數沒有什麼有意思的結果要返回時,它能夠用做函數的返回類型:
fun f(): Unit {}
複製代碼
在教程(一)中,咱們就說到Unit能夠直接省略:fun f() {}
。
大多數狀況下,你不會留意到void和Unit之間的區別。若是你的Kotlin函數使用Unit做爲返回類型而且沒有重寫泛型函數,在底層它會被編譯成舊的void函數。 那麼Kotlin的Unit和Java的void到底有什麼不同呢?Unit是一個完備的類型,能夠做爲類型參數,而void卻不行。只存在一個值是Unit類型,這個值也叫作Unit,而且在函數中會被隱式返回(不須要再顯示return null)。當你在重寫返回泛型參數的函數時這很是有用,只須要讓方法返回Unit類型的值:
interface Processor<T> {
fun process(): T
}
class NoResultProcessor : Processor<Unit> {
override fun process() {
//do something 不須要顯式return
}
}
複製代碼
和Java對比一下,Java中爲了解決使用「沒有值」做爲參數類型的任何一種可能解法,都沒有Kotlin的解決方案這樣漂亮。一種選這是使用分開的接口定義來區別表示須要和不須要返回值的接口。另外一種是用特殊的Void類型做爲類型參數。即使選擇了後者,仍是須要加入一個return null;
語句來返回惟一能匹配這個類型的值,由於只要返回類型不是void,你就必須始終有顯式餓return語句。
你也許會奇怪爲何Kotlin選擇使用一個不同的名字Unit而不是把它叫作Void。在函數式編程語言中,Unit這個名字習慣上被用來表示「只有一個實例」,這正式Kotlin的Unit和Java的void的區別。Kotlin本能夠沿用Void這個名字,可是還有一個Nothing的類型,它有着徹底不一樣的功能。Void和Nothing兩種類型的名字含義如此相近,會使人困惑。
對某些Kotlin函數來講,返回類型的概念沒有任何意義,由於他們歷來不會成功地結束,例如,許多測試庫中都有一個叫作fail的函數,它經過拋出帶有特定消息的異常來讓當前測試失敗。一個包含無線循環的函數也永遠不會成功地結束。
當分析調用這樣函數的代碼時,知道函數永遠不會正常終止時頗有幫助的。Kotlin使用一種特殊的返回類型Nothing來表示:
fun fail(message: String): Nothing {
throw IllegalStateException(message)
}
複製代碼
Nothing類型沒有任何值,只有被當作函數返回值使用,或者被當作泛型函數返回值的類型參數使用纔會有意義。
返回Nothing的函數能夠方法Elvis運算符的右邊來作先決條件檢查:
val address = company.address ?: fail("No address")
println(address)
複製代碼
上面這個例子展現了在類型系統中擁有Nothing爲何極其有用。編譯器知道這種返回類型的函數從不正常終止,而後在分析調用這個函數的代碼時利用這個信息。在這例子中,編譯器會把address的類型推斷成非空,由於它爲null時的分支處理會始終拋出異常。
對先後一致的類型系統來講知道集合是否能夠持有null元素是很是重要的一件事情。而Kotlin就能夠很是顯眼的表示。
fun readNumbers(reader: BufferedReader): List<Int?> {
val result = ArrayList<Int?>()
for (line in reader.lineSequence()) {
try {
val number = line.toInt()
result.add(number)
} catch (e: NumberFormatException) {
result.add(null)
}
}
return result
}
複製代碼
這個函數從一個文件中讀取文本行的列表,並嘗試把每一行文本解析成一個數字。List<Int?>
是能持有Int?
類型值得雷柏啊,換句話說能夠持有Int或者null。
注意List<Int?>
和List<Int>?
的區別。前一種表示列表自己始終不爲null,但列表中的每一個值均可覺得null。後一種類型的變量可能包含空引用而不是列表實例,但列表的元素保證是非空的。
處理可空值得集合時,經過要首先要判斷是否爲null,若是是則不處理,也即過濾掉null值。Kotlin提供了一個標準庫函數filterNotNull
來完成它:
>>> val list = listOf(1L, null, 3L)
>>> println(list)
[1, null, 3]
>>> println(list.filterNotNull())
[1, 3]
複製代碼
這種過濾也影響了集合的類型。過濾前是List<Long?>
,過濾後是List<Long>
,由於過濾保證了集合不會在包含任何爲null的元素。
Kotlin將Java的集合中訪問集合數據的接口和修改集合數據的接口進行了拆分。分離出只讀集合kotlin.collections.Collection,使用這個接口能夠遍歷集合中的元素,獲取集合大小、判斷集合中是否包含某個元素,以及執行其餘從該集合中讀取數據的操做,但這個接口沒有任何添加或移除元素的方法。
public interface Collection<out E> : Iterable<E> {
public val size: Int
public fun isEmpty(): Boolean
public operator fun contains(element: @UnsafeVariance E): Boolean
override fun iterator(): Iterator<E>
public fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
}
複製代碼
另外一個則是kotlin.collections.MutableCollection 接口能夠修改集合中的數據。它繼承kotlin.collections.Collection,提供了方法來添加和移除元素,清空集合等:
public interface MutableCollection<E> : Collection<E>, MutableIterable<E> {
override fun iterator(): MutableIterator<E>
public fun add(element: E): Boolean
public fun remove(element: E): Boolean
public fun addAll(elements: Collection<E>): Boolean
public fun removeAll(elements: Collection<E>): Boolean
public fun retainAll(elements: Collection<E>): Boolean
public fun clear(): Unit
}
複製代碼
就像val和var之間的分離同樣,只讀集合接口與可變集合接口的分離能讓程序中的數據發生的事情更容易理解。若是函數接受Collection而不是MutableCollection做爲參數,你就知道它不會修改集合,而只是讀取集合中的數據。若是函數要求你傳遞給他MutableCollection做爲參數,能夠認爲它將會修改數據。
fun <T> copyElements(source: Collection<T>, target: MutableCollection<T>) {
for (item in source) {
target.add(item)
}
}
複製代碼
這個例子中咱們讀取source中的元素添加到target中,所以聲明函數的時候能夠很好的區別:一個只讀,一個可變。
使用集合接口時須要牢記一點只讀集合不必定是不可變的。若是你使用的變量擁有一個只讀接口類型,它可能只是同一個集合的衆多引用中的一個。可能有另外一個可變集合也指向這個集合,在另外一個地方(線程)中對這個集合做出了改變。
這種分離只在Kotlin的代碼中有效,上面這個例子轉換成Java代碼後:
public static final void copyElements(@NotNull Collection source, @NotNull Collection target) {
Iterator var3 = source.iterator();
while(var3.hasNext()) {
Object item = var3.next();
target.add(item);
}
}
複製代碼
能夠看到都變成了Java中Collection
接口,也便是可變的完整的集合接口。也就是說便是Kotlin中把集合聲明成只讀的。Java代碼也可以修改這個集合。Kotlin編譯器不能徹底分析Java代碼到底對集合作了什麼,所以Kotlin沒法拒絕向能夠修改集合的Java代碼傳遞只讀Collection。若是你將定義的函數中會將只讀集合傳遞給Java,你有責任將參數聲明成正確的參數類型,取決於Java代碼是否會修改集合。
這個注意事項也一樣適用於Kotlin定義的非空元素集合傳遞給Java時,可能會存入null值。
*集合建立函數
集合類型 | 只讀 | 可變 |
---|---|---|
List | listOf | mutableListOf, arrayListOf |
Set | setOf | mutableSetOf, hashSetOf, linkedSetOf, sortedSetOf |
Map | mapOf | mutableMapOf,hashMapOf,linkedMapOf,sortedMapOf |
上一篇關於可空性的文章,Kotlin中把哪些定義在Java代碼中的類型當作平臺類型。Kotlin沒有任何關於平臺類型的可空性信息,因此編譯器容許Kotlin代碼將其視爲可空或者非空。一樣,Java中聲明的集合類型的變量也被視爲平臺類型,一個平臺類型的集合本質上就是可變性未知的集合。特別是當你在Kotlin中重寫或者實現簽名中有集合類型的Java方法時,就要考慮到底用哪種類型來重寫:
/* Java */
interface Processor {
void process(List<String> values);
}
/* Kotlin */
class ProcessorImpl : Processor {
override fun process(values: MutableList<String?>?) {}
}
class ProcessorImpl2 : Processor {
override fun process(values: MutableList<String>?) {}
}
class ProcessorImpl3 : Processor {
override fun process(values: MutableList<String>) {}
}
class ProcessorImpl4 : Processor {
override fun process(values: List<String>) {}
}
複製代碼
這些繼承方法的定義都是能夠的,你要根據實際狀況作出選擇:
固然若是你不肯定,能夠用最保險的方式:
override fun process(values: MutableList<String?>?) {}
複製代碼
可是使用的時候就得考慮各類可能爲空的狀況了。
以前的好多例子其實都出現了Kotlin的數組:
Array<String>
複製代碼
Kotlin中的一個數組是一個帶有類型參數的類,其元素類型被指定爲相應的類型參數。能夠經過arrayOf
、arrayOfNulls
和Array構造方法來建立數組。
Kotlin代碼中最多見的建立數組的狀況之一是須要調用參數爲數組的Java方法時,或是調用帶有vararg參數的Kotlin函數。在這些狀況下,一般已經將數據存儲在集合中,只須要將其轉換爲數組便可。可使用toTypeArray方法的來執行:
>>> val strings = listOf("a", "b", "c")
>>> println("%s/%s/%s".format(*strings.toTypeArray())) //指望varvag參數時使用展開運算符傳遞數組
a/b/c
複製代碼
數組類型的類型參數始終會變成對象類型。若是你聲明瞭一個Array<Int>
它將會是一個包含裝箱整型的數組Integer[]
。若是你須要建立沒有裝箱的基本數據類型的數組,必須使用一個基本數據類型數組的特殊類。 爲了表示基本數據類型的數組。Kotlin提供了若干獨立的類,每一種基本數據類型都對應一個,例如Int類型的數組叫作IntArray,還有ByteArray,BooleanArray等等。這些對應Java中的基本數據類型數組:int[]
、btye[]
、boolean[]
等等。 要穿件一個基本數據類型的數組,你能夠經過intArrayOf
之類的工廠方法,或者構造方法傳入size或者lambda。