教你如何徹底解析Kotlin中的類型系統

簡述: 已經好久沒有更新文章,這大概是2019年第二篇文章了,有不少小夥伴們都在公衆號留言說是否是斷更了、是否是跑路了。在這裏統一回復下我還好,並無跑路哈,只是在思考接下來文章主要方向在哪? 如何在提高本身的同時能夠幫助他人,以及這段時間也在不斷認清本身和了解本身,發現本身哪裏不足以及如何及時地查漏補缺。下面進入正題:java

Kotlin類型系統其中涉及到一個很重要的概念就是你們常說的可空性以及爲何Kotlin相比Java在必定程度上能下降空指針異常。此外在Kotlin中徹底採用和Java不一樣思路來定義它的類型系統。也正由於這樣類型系統自然具備讓Kotlin在空指針異常出現的頻率明顯低於Java出現的頻率的優點。此外Kotlin考慮使用和Java徹底不一樣類型系統,以及它是如何去作到極大兼容和互操做。android

1、首先思考幾個概念

在進入Kotlin類型系統以前,咱們不妨先一塊兒來思考如下幾個概念,若是不明確這幾個概念很難從根本上去理解Kotlin類型系統,以及Kotlin在類型系統方面爲何優於Java。算法

  • 一、類型的本質

類型本質是什麼呢? 爲何變量擁有類型? 這兩個問題在維基百科上給出了很好的回答. 類型實際上就是對數據的分類,決定了該類型上可能的值以及該類型的值上能夠完成的操做。 須要特別去注意一下後面的闡述: "該類型上可能的值以及該類型的值上能夠完成的操做。" 由於Java的類型系統其實並無100%符合這個規則,因此這也是Java類型系統所存在的問題,下面會作出具體的分析。設計模式

  • 二、類與類型

關於 類型估計不少開發者每每忽略它們之間的區別,由於在真正的應用場景並不會區分這麼細。咱們在使用中每每會把類等同於類型,其實是徹底不一樣兩個東西。其實在Java中也有體現,例如List<String>、Lis<Integer>List,對於前者List<String>List<Integer>只能是類型不能說是類, 而對於List它既能夠是List類也能夠是類型(Java中的原生類型)。其實在Kotlin則把這個概念提高到一個更高的層次,由於Kotlin中每一個類多了一個可空類型,例如String類就對應兩種類型String類型和String?可空類型。而在Java中除了泛型類型,每一個類只對應一種類型(就是類的自己),因此每每被忽略。數組

咱們能夠把Kotlin中的類可分爲兩大類(Java也能夠這樣劃分): 泛型類非泛型類安全

非泛型類數據結構

先說非泛型類也就是開發中接觸最多的通常類,通常的類去定義一個變量的時候,它的實際就是這個變量的類型。例如: var msg: String 這裏咱們能夠說Stringmsg變量的類型是一致的。可是在Kotlin中還有一種特殊的類型那就是可空類型,能夠定義爲var msg: String?,這裏的Stringmsg變量的String?類型就不同了。因此在Kotlin中一個通常至少對應兩種類型. 因此類和類型不是一個東西。app

泛型類函數

泛型類比非泛型類要更加複雜,實際上一個泛型類能夠對應無限種類型。爲何這麼說,其實很容易理解。咱們從前面文章知道,在定義泛型類的時候會定義泛型形參,要想拿到一個合法的泛型類型就須要在外部使用地方傳入具體的類型實參替換定義中的類型形參。咱們知道在Kotlin中List是一個類,它不是一個類型。由它能夠衍生成無限種泛型類型例如List<String>、List<Int>、List<List<String>>、List<Map<String,Int>>源碼分析

  • 三、子類、子類型與超類、超類型

咱們通常說子類就是派生類,該類通常會繼承它的超類。例如: class Student: Person(),這裏的Student通常稱爲Person的子類, PersonStudent的超類。

子類型和超類型定義則徹底不同,咱們從上面類和類型區別就知道一個類能夠有不少類型,那麼子類型不只僅是想子類那樣繼承關係那麼嚴格。 子類型定義的規則通常是這樣的: 任什麼時候候若是須要的是A類型值的任何地方,均可以使用B類型的值來替換的,那麼就能夠說B類型是A類型的子類型或者稱A類型是B類型的超類型。能夠明顯看出子類型的規則會比子類規則更爲寬鬆。那麼咱們能夠一塊兒分析下面幾個例子:

注意: 某個類型也是它本身自己的子類型,很明顯Person類型的值任意出現地方,Person確定都是能夠替換的。屬於子類關係的通常也是子類型關係。像String類型值確定不能替代Int類型值出現的地方,因此它們不存在子類型關係

再來看個例子,全部類的非空類型都是該類對應的可空類型的子類型,可是反過來講就不行,就好比Person非空類型是Person?可空類型的子類型,很明顯嘛,任何Person?可空類型出現值的地方,均可以使用Person非空類型的值來替換。其實這些我在開發過程當中是能夠體會獲得的,好比細心的同窗就會發現,咱們在Kotlin開發過程,若是一個函數接收的是一個可空類型的參數,調用的地方傳入一個非空類型的實參進去是合法的。可是若是一個函數接收的是非空類型參數,傳入一個可空類型的實參編譯器就會提示你,可能存在空指針問題,須要作非空判斷。 由於咱們知道非空類型比可空類型更安全。來幅圖理解下:

2、Java類型系統存在空指針異常的本質問題

有了上述關於類型本質的闡述,咱們一塊兒來看下Java中的一些基本類型來套用類型本質的定義,來看看有什麼問題。

  • 使用類型的定義驗證int類型:

例如一個int類型的變量,那麼代表它只能存儲int類型的數據,咱們都知道它用4個字節存儲,數值表示範圍是-2147483648 ~ 2147483647,那麼規定該類型可能存在的值,而後咱們能夠對該類型的值進行運算操做。彷佛沒毛病,int類型和類型本質闡述契合的是如此完美。可是String類型呢?也是這樣的嗎?請接着往下看

  • 使用類型的定義驗證String類型或其餘定義類對應的類型:

例如一個String類型的變量,在Java中它卻能夠存在兩種值: 一個是String類的實例另外一種則是null。而後咱們能夠對這些值進行一些操做,第一種String類實例固然容許你調用String類全部操做方法,可是對於第二種null值,操做則很是有限,若是你強行使用null值去操做String類中的操做方法,那麼恭喜你,你將得到一個NullPointerException空指針異常。在Java中爲了程序的健壯性,這就要求開發者對String類型的值還得須要作額外的判斷,而後再作相應的處理,若是不作額外判斷處理那麼就很容易獲得空指針異常。 這就出現同一種類型變量存在多種值,卻不能獲得平等一致的對待。對比上述int類型的存在的值都是一致對待,全部該類型上全部可能的值均可以進行相同的運算操做。下面接着看着一個頗有趣例子:

貌似連Java中的instanceof都不認可null是一個String類型的值。這兩種值的操做也徹底不同: 真實的String容許你調用它的任何方法,而null值只容許很是有限的操做。那麼Kotlin類型系統是如何解決這樣的問題的呢? 請接着往下看。

3、Kotlin類型系統如何解決問題(爲何會設計出可空類型)

Java中的類型系統中String類型或其餘自定義類的類型,貌似和類型本質定義不太符合,該類型的全部可能值卻被區別對待,存在二義性。還得額外判斷,直接問題就是給開發者帶來了額外負擔得作非空判斷,一旦處理很差就會出現空指針致使程序崩潰。這就是Java中引起空指針問題的本質。

抓住問題的本質,Kotlin作一個很偉大的舉措那就是類型的拆分,將Kotlin中全部的類型拆分紅兩種: 一種是非空類型,另外一種則是可空類型;其中非空類型變量不容許null值的賦值操做,換句話說就是String非空類型只存在String類的實例不存在null值,因此針對String非空類型的值你能夠大膽使用String類全部相關方法,不存在二義性。 固然也會存在null狀況,那就可使用可空類型,在使用可空類型的變量的時候編譯器在編譯時期會作針對可空類型作必定判斷,若是存在可空類型的變量操做該對應類的方法,就提示你須要作額外判空處理,這時候開發者就根據提示去作判空處理了,想象下都這樣處理了,你的Kotlin代碼還會出現空指針嗎?(可是有一點很重要就是定義了一個變量你須要明確它是可空仍是非空,若是定義了可空類型你就須要對它負責,而且編譯器也會提示幫助你對它作額外判空處理。)。一塊兒來看下幾個例子:

  • 一、非空類型變量或常量不能接收null值

  • 二、非空類型的變量或常量中is(至關於java中instanceof)

  • 三、可空類型的變量或常量直接操做相應方法會有明顯的編譯錯誤並提示判空操做

然而上面那些都是Java給不了你的,因此Java程序中通常會存在三種狀態: 一種佛系判空,常常會出現空指針問題。另外一種就是一股腦所有判空,但是代碼中充斥着if-else代碼,可讀性很是差。最後一種就是很是熟悉程序邏輯以及數據流向的開發者能夠正常判斷出哪裏須要判空處理,哪裏能夠不須要,這一種對開發者要求極高,由於人老是會犯錯的。

4、可空類型

  • 一、安全調用運算符 "?."

?.至關於判空處理,若是不爲null就執行?.後面的表達式,不然就返回null

text?.substring(0,2) //至關於 if(text != null) text.substring(0,2) else null
複製代碼

其實Kotlin爲了類型判空處理可算是操碎了心,咱們都知道在Java中作判空處理無非就是if-else? xxx : xxx三目運算符來實現。可是有時候出現嵌套判空的時候整個代碼就是一個「箭頭」,可讀性就不好了。由以上例子可知?.if-else省了不少代碼,這還沒法徹底顯露它的優勢,下面這個例子就更加明顯了。

Java中的if-else 嵌套處理

Kotlin中的安全調用運算符?.鏈式調用處理

對比兩種方式的實現你會不會以爲Kotlin也許更適合你呢,利用?.鏈式調用的方式把嵌套if-else處理解開了。

  • 二、Elvis運算符 "?:"

若是?:前面表達式爲null, 就執行?:後面的表達式,它通常會和?.一塊兒使用。(注意: 它與Java中的? xxx : xxx 三目運算符不同) carbon (29).png

  • 三、安全類型轉化運算符 as?

若是類型轉化失敗就返回null值,不然返回正確的類型轉化後的值

val student = person as? Student//至關於 if(person is Student) person as Student else null
複製代碼
  • 四、非空斷言運算符 !!契約(contract) 簡化非空表達式

非空斷言運算符!!, 是強制告訴編譯器這個變量的值不可能null,存在使用風險。一旦存在爲null直接拋出空指針異常

不少Kotlin開發者很厭惡這個操做符,以爲寫起來不優雅很影響代碼的可讀性,關於如何避免在Kotlin的代碼中使用 !! 操做符。請參考我以前的一篇文章 [譯]如何在你的Kotlin代碼中移除全部的!!(非空斷言).

實際上是非空斷言的使用場景是存在的,例如你已經在一個函數中對某個變量進行判空處理了,可是後面邏輯中再次使用到了它而且你能夠肯定它不可能爲空,可能此時編譯器沒法識別它是不是非空,但因爲它又是一個可空類型,那麼它又會提示你進行判空處理,很煩人是不,不少人這時候可能就採用了 !! 確實缺少可讀性。

針對上述問題,除了以前文章中給出解決方案,此次又提供一個新的解決方案,那就是契約(實際上主動告訴編譯器某個規則,這樣它就不會提示作判空處理了) 契約官方正式提出來是Kotlin1.3的版本,雖然還處於Experimental(好比自定義契約)中,可是實際上Kotlin內部代碼,早就使用了契約。具體使用可參考我以前的一篇文章 JetBrains開發者日見聞(二)之Kotlin1.3的新特性(Contract契約與協程篇) 一塊兒來看下內置契約是如何解決這個問題的。

一塊兒來瞅瞅內置契約的內部實現源碼

經過上述咱們能夠知道在Kotlin中擁有着與Java中徹底不同的類型系統。在Java中是不存在所謂的可空類型和非空類型。可是咱們都知道Kotlin與Java的互操性很強,幾乎是徹底兼容Java。那麼Kotlin是如何兼容Java中的變量類型的呢?咱們在Kotlin中確定須要常常調用Java代碼,有的人可能會回答說Java中使用@NotNull和@Nullable註解來標識。確實Kotlin能夠識別多種不一樣風格的註解,包括javax.annotationandroid.support.annotationorg.jetbrains.annotation等。可是一些以前的第三方庫並無寫的這麼規範,顯然沒法經過這種方式徹底解決這個問題。

因此Kotlin引入一種新的概念叫作: 平臺類型,平臺類型本質上就是Kotlin不知道可空性信息的類型,既能夠把它當作可空類型又能夠把它當作非空類型。 這就意味你要像Java代碼中同樣對你在這個類型上作的操做負所有責任,說的有味道點就是你在Java中拉的便便,Kotlin是不會給你擦屁股的。因此對於Java中函數參數,Kotlin去調用的時候系統默認會處理可空類型(爲了安全性考慮),若是你明確了不爲空,能夠直接把它修改成非空類型,系統也是不爲報編譯錯誤的,可是一旦這樣處理了,你必須保證不能爲空。

那麼問題來了,不少人就疑問出於安全性考慮爲何不直接所有轉化可空類型呢? 實際上這種方案看似可行,實際上有點不妥,對於一些明確不可能爲空的變量還須要作大量額外的判空操做就顯得冗餘。不然非空類型就沒有存在的意義了。

5、基本數據類型和其餘基本類型

  • 一、基本數據類型

咱們都知道在Java中針對基本數據類型和包裝類型作了區分。例如一個基本數據類型int的變量直接存儲了它的值。而一個引用類型(包裝類型) String的變量僅僅存儲的是指向該對象的內存地址的引用。基本數據類型有着自然的高效存儲以及傳遞的優點,可是不能直接調用這些類型的方法,並且在Java中集合中不能將它做爲泛型實參類型。

實際上在Kotlin中並無像Java那樣分爲了基本數據類型和包裝類型,在Kotlin中永遠是同一種類型。不少人估計會問了既然在Kotlin中基本數據類型和包裝類型是同樣的,那麼是否是意味着Kotlin是使用引用類型來保存數據呢?是否是很是低效呢?不是這樣的,Kotlin在運行時儘可能會把Int等類型轉換成Java中的int基本數據類型,而遇到相似集合或泛型的時候就會轉化成Java中對應的Integer等包裝類型。這其實是一個底層優化,至於什麼場景轉化成int,什麼場景轉化成Integer,關於這塊能夠參考以前一篇有關內聯類自動裝箱和拆箱的文章: [譯]Kotlin中內聯類的自動裝箱和高性能探索(二)

基本數據類型也分爲可空類型和非空類型, 具體可參考以下的類型層次結構圖:

  • 二、Any和Any?類型

Any類型是全部非空類型的超類型,Any?類型則是全部的類型的超類型,便是非空類型的超類型也是全部可空類型的超類型。由於Any?是Any的超類型。具體的層次可參考下面這張圖:

  • 三、Unit類型

Unit類型也便是Kotlin中的空類型,至關於Java中的void類型,默認狀況下它能夠被省略

  • 四、Nothing類型

Nothing類型是全部類型的子類型,它既是全部非空類型的子類型也是全部可空類型的子類型,由於Nothing是Nothing?的子類型,然而Nothing?又是全部可空類型的子類型。 具體能夠看下以下的層次結構圖:

6、集合和數組類型

  • 一、可變集合與只讀集合之間的區別和聯繫(以Collection集合爲例) Collection只讀集合與MutableCollectio可變集合區別:

在Collection只具備訪問元素的方法,不具備相似add、remove、clear之類的方法,而在MutableCollection中則相比Collection多出了修改元素的方法。

Collection只讀集合與MutableCollectio可變集合聯繫:

MutableCollection其實是Collection集合接口的子接口,他們之間是繼承關係。

  • 二、集合之間類的關係

經過Collection.kt文件中能夠了解到有這些集合Iterable(只讀迭代器)和MutableIterable(可變迭代器)、Collection和MutableCollection、List和MutableList、Set和MutableSet、Map和MutableMap。那麼它們之間的類關係圖是怎樣的。

Iterable和MutableIterable接口分別是隻讀和可變集合的父接口,Collection繼承Iterable而後List、Set接口繼承自Collection,Map接口比較特殊它是單獨的接口,而後MutableMap接口是繼承自Map.

  • 三、Java中的集合與Kotlin中集合對應關係

咱們剛剛說到在Kotlin中集合的設計與Java不同,可是每個Kotlin的接口都是其對應的Java集合接口的一個實例,也就是在Kotlin中集合與Kotlin中的集合存在必定的對應關係。Java中的ArrayList類和HashSet類實際上Kotlin中的MutableList和MutableSet集合接口的實現類。把這種關係加上,上面的類關係圖能夠進一步完善。

  • 四、集合的初始化

因爲在Kotlin中集合主要分爲了只讀集合和可變集合,那麼初始化只讀集合和可變集合的函數也不同。以List集合爲例,對於只讀集合初始化通常採用listOf()方法對於可變集合初始化通常採用mutableListOf()或者直接建立ArrayList<E>,由於mutableListOf()內部實現也是也仍是採用建立ArrayList,這個ArrayList其實是Java中的java.util.ArrayList<E>,只不過在Kotlin中使用typealias(關於typealias的使用以前博客有過詳細介紹)取了別名而已。關於具體內容請參考這個類kotlin.collections.TypeAliasesKt實現

  • 五、集合使用的注意事項

注意點一: 在代碼的任何地方都優先使用只讀集合,只在須要修改集合的狀況下才去使用可變集合

注意點二: 只讀集合不必定是不可變的,關於這個只讀和不可變相似於val的只讀和不可變原理。

注意點三: 不能把一個只讀類型的集合做爲參數傳遞給一個帶可變類型集合的函數。

  • 六、平臺類型的集合轉化規則

正如前面所說起的可空性平臺類型同樣,Kotlin中沒法知道可空性信息的類型,既能夠把它當作可空類型又能夠把它當作非空類型。集合的平臺類型和這個相似,在Java中聲明的集合類型的變量也被看作平臺類型一個平臺類型的集合本質上就是可變性未知的集合,Kotlin中能夠把它看作是隻讀的集合或者是可變的集合. 實際上這都不是很重要,由於你只須要根據你的需求選擇便可,想要執行的全部操做都能正常工做,它不像可空性平臺存在額外判斷操做以及空指針風險。

注意: 但是當你決定使用哪種Kotlin類型表示Java中集合類型的變量時,須要考慮如下三種狀況:

  • 一、集合是否爲空?

若是爲空轉換成Kotlin中集合後面添加 ?,例如Java中的List<String>轉化成Kotlin中的List<String>?

  • 二、集合中的元素是否爲空?

若是爲空轉換成Kotlin中集合泛型實參後面添加 ?,例如Java中的List<String>轉化成Kotlin中的List<String?>

  • 三、操做方法會不會修改集合?(集合的只讀或可變)

若是是隻讀的,例如Java中的List<String>轉化成Kotlin中的List<String>;若是是可變的,例如Java中的List<String>轉化成Kotlin中的MutableList<String>.

注意: 固然上面三種狀況能夠一種或多種同時出現,那麼轉化成Kotlin中的集合類型也是多種狀況最終重組的類型。

7、總結

到這裏有關Kotlin的類型系統基本就說得差很少,該涉及到的內容基本都涉及了。其實仔細去體會下爲何Kotlin的類型系統要如此設計,確實是它必定道理的。咱們常常聽別人誇Kotlin比Java優勢是啥,不少人都說少了不少空指針異常,可是爲何能Kotlin相比Java有更少的空指針異常相信這篇文章也足夠回答你了吧。

接下來再扯點別的你們都知道Android開發已經進入了一個平穩期了, 泡沫逐漸散去, 那麼對Android開發者的要求也會愈來愈高,只會使用的API時代早已通過去了,因此開發者須要不斷調整本身不斷提高本身的能力來面對這些變化。分析過源碼的小夥伴就知道看懂源碼其中最關鍵點就是源碼中使用的數據結構算法以及使用一些高級的設計模式。正由於這樣後期文章方向會針對數據結構算法、設計模式、源碼分析這塊作必定輸出,近期計劃是每週一篇Kotlin相關文章(原創或翻譯),每週一篇設計模式相關和每週一篇數據結構算法相關(結合LeetCode上的題目)

歡迎關注Kotlin開發者聯盟,這裏有最新Kotlin技術文章,每週會不按期翻譯一篇Kotlin國外技術文章。若是你也喜歡Kotlin,歡迎加入咱們~~~

Kotlin系列文章,歡迎查看:

原創系列:

Effective Kotlin翻譯系列

翻譯系列:

實戰系列:

相關文章
相關標籤/搜索