寫給Android開發者的Kotlin入門

寫給Android開發者的Kotlin入門

Google在今年的IO大會上宣佈,將Android開發的官方語言更換爲Kotlin,作爲跟着Google玩兒Android的人,我們必須儘快瞭解和使用Kotlin語言。

不過Kotlin畢竟是語言級別的新事物,比起Java來說,從編程思想到代碼細節都有不少變化,我們最好先對Kotlin有個整體的基本的瞭解,然後再去學習和使用,這樣才能高效地掌握Kotlin語言。

Java的輝煌與陰影

1995年,當年如日中天的Sun公司發佈了Java語言,引起了巨大的轟動,與當時主流的C語言和Basic語言比起來,Java語言簡單、面向對象、穩定、與平臺無關、解釋型、多線程、動態等特點,就像是打開了一個新的世界,一時間風靡全球,雲集者衆,微軟爲了模仿Java搞出C#語言,Netscape爲了趕時髦硬塞出一個JavaScript語言,IBM則捏着鼻子做了Java IDE Eclipse(日蝕,呵呵)。直到現在,Java在編程世界裏還佔據着舉足輕重的地位,Andy Rubin在開發Android系統時,也很自然地採用了Java和C++(C++負責NDK開發)作爲開發語言。

但是,Java畢竟是20多年前的語言了,雖然有不斷擴展更新,但是底層設計思想是很難改動的,這就導致它很難實現一些新的語言特性,例如函數式編程、Lambda 表達式、流式API、高階函數、空指針安全等(雖然Java8實現了部分特性,但是Android還不怎麼支持Java8),這些新的語言特性大受好評,可以說解放了編程的生產力,這其實也說明了一個事實:開發效率/時間是軟件公司真正的瓶頸,任何能壓縮代碼量,提高開發效率的舉措,都應該受到重視。

而且,Android還存在Java版權危機的問題,收購了Sun公司的Oracle曾向Google索要鉅額的Java版權費,這可能也加快了Google尋找Android開發替代語言的動作。

蘋果公司已經在用Swift語言替代Object-C語言,Google也找到了替代Java的語言,也就是JetBrains公司(Android Studio也是用該公司的Intelli J改的)主推的Kotlin。

其實,Swift和Kotlin還挺相似的,有一篇Swift is like Kotlin對這兩種語言做過簡單的對比。

Kotlin的出現

Kotlin也是基於JVM設計的編程語言,算是對Java的溫和改良,她是一個開源項目的成果,擁有很高的聲望,很多公司、組織、業界大犇都很喜歡她,Square公司的Jake大神(Dagger、ButterKnife、Retrofit、OkHttp...之父)就專門寫了篇Using Project Kotlin for Android爲Kotlin站臺。

相對Java來說,Kotlin在編寫代碼時有如下優勢:代碼簡潔高效、函數式編程、空指針安全、支持lambda表達式、流式API等。

執行效率上,Kotlin和Java具有同樣的理論速度(都是編譯成JVM字節碼)。

另外,新語言必須考慮兼容性,爲了與存量項目代碼和諧共處,Kotlin和Java是互相完美兼容的,兩種代碼文件可以並存代碼可以互相調用文件可以互相轉換,庫文件也可以無障礙地互相調用,據說使用Kotlin基本不會帶來額外的成本負擔。

編程語言本質上還是工具,要運用工具提高效率和質量,還要看具體開發者,我們先看看Kotlin相對Java有哪些特色。

Kotlin的特色

Kotlin作爲Java的改良,在Android開發中有很多優勢,我們先從相對直觀的界面繪製開始瞭解,然後看看Kotlin的語法特點,再慢慢去接觸更深層次的編程思想。

簡化findViewById

我們知道,Android的架構裏,xml佈局文件和Activity是鬆耦合的,Activity中要使用界面元素,必須藉助R文件對xml控件的記錄,用findViewById找到這個元素。

Kotlin中我們可繼續使用findViewById去綁定xml佈局中的控件:(TextView)findViewById(R.id.hello);

進一步引用Anko之後,可以使用find函數去綁定控件:find(R.id.hello),不需要類型轉換

同時,Kotlin還提供一種更激進的方法,通過在gradule中引用applyplugin:'kotlin-android-extensions',徹底取消findViewById這個函數,具體做法如下:

首先,在app的gradule中,添加引用


添加引用

然後,在Activity中直接根據id使用界面元素


直接操作界面元素

按住Ctrl鍵,會提示我們這個控件詳情


提示詳情

點擊後,可以直接跳轉到xml文件中的控件位置,光標會停留在Id處


直接跳轉到控件Id處

這種特性令人聯想起C#語言中對界面控件的管理,在C#裏,界面的控件可以直接調用,不需要find,這是因爲在創建一個Form1.cs界面文件時,IDE會自動創建一個對應的額Form1.designer.cs類,在這個類裏,自動管理所有界面控件的對象。

Kotlin也是類似的思路,它會遍歷你的xml文件,創建對應的虛擬包給你引用(用Alt+Enter引用),我們使用的控件對象,其實是這個虛擬包裏的控件對象。


引用虛擬界面類

爲什麼說這個包是虛擬的,因爲它是kotlin臨時創建的,你無法打開它的文件,在編譯apk時,Kotlin會自動幫你補充findViewbyId的代碼,最終得到的產品其實沒變,它只是方便了程序員的書寫。

Anko

Anko其實是一種DSL(領域相關語言),是專門用代碼方式來寫界面和佈局的。

上一節針對findViewById,最激進的方式是取消這個函數,這一節更加激進,我們可以連XML佈局文件也取消掉。

在XML中定義界面佈局當然是有好處的,分層清晰,代碼易讀,現在AS中預覽效果也不錯。但是它渲染過程複雜,難以重用(雖然有including),而如果我們用Java代碼去替換xml,代碼會更加複雜和晦澀。

Anko卻實現了在代碼中簡潔優雅地定義界面和佈局,而且由於不需要讀取和解析XML佈局文件,Anko的性能表現更佳

我們可以看看Anko在Github上的代碼示例,用6行代碼就做出了一個有輸入框、按鈕、點擊事件和Toast的界面和功能


示例的界面和功能

我們自己寫一下這6行代碼,首先需要在gradle中添加引用,主要是sdk和v4/v7包


Gradle中引用Anko

然後參照Anko在Github中的示例,實現這6行代碼。


參考Github的Anko實例代碼

Activity本來會在加載時在onCreate函數裏用setContentView函數來尋找佈局文件,並加載爲自己的界面,在這裏,Anko代碼替代了setContentView,直接告訴Activity應該如何繪製界面。

(在Fragment裏不可以這樣直接寫verticalLayout,因爲加載機制不一樣,Fragment需要在onCreateView函數裏inflate並返回一個View對象,所以對應的Anko代碼也需要寫在onCreateView函數裏並返回一個View,可以用return with(context){verticalLayout[...]}或者return UI{verticalLayout[...]}.view)

可以看到,代碼非常簡潔幹練,不像以往的Android代碼那樣拖沓,這既與Kotlin的語法有關,也與Anko能用代碼實現界面和佈局有關。

這段代碼雖然簡潔,可是卻失去了MVC分層的好處,因爲V直接寫在業務代碼裏了,這個問題好解決,我們可以把Anko佈局代碼放到一個專門的類文件裏


抽出Anko佈局代碼

然後在Activity引用這個佈局類來繪製界面


引用Anko佈局類

雖然Anko效率很高,代碼簡潔,清爽直觀,但是目前還有很多坑,主要包括:

1.AS並不支持直接預覽Anko界面,雖然有個Anko DSL Preview插件,但是需要make才能刷新,而且和現在的AS不兼容。

2.如果要在多版本中動態替換外部資源,需要用動態類加載才能實現,無法借用資源apk實現。

3.不方便根據view的id去即時引用view控件(R文件和inflate這時反而更加靈活)。

另外,Anko還在異步、日誌、Toast、對話框、數據庫等方面提供優化服務,是否採用就看自身需要了。

Kotlin語法特點

看了上面這些例子,我們發現Kotlin本身的語法和Java有些不一樣,新語言嘛,相對Java而言,主要的變化有這麼幾條:

1.沒有“;”

在Kotlin語法裏,代碼行不需要用“;”結尾,什麼都不寫就好

2.重要的“:”

在Java裏,“:”主要在運算符裏出現(for/switch/三元運算符等)。

在Kotlin裏,“:”的地位大大提升了,它的用途非常廣泛,包括:

定義變量類型

var name:String="my name" //變量name爲String類型

定義參數的類型

fun makeTool(id:Int){ //參數id爲Int類型

}

定義函數的返回值

fun getAddr(id:Int):String{ //返回值爲String類型

}

聲明類/接口的繼承

class KotlinActivityUI :AnkoComponent<KotlinActivity>{//繼承AnkoComponent接口

使用Java類

val intent = Intent(this, MainActivity::class.java) //需要用::來使用Java類,注意是兩個“”

3.沒有“new”

Kotlin實例化一個對象時不需要new關鍵字

var list=ArrayList()

4.變量、常量、類型推斷

用var定義變量(像js)

var name:String="my name"

用val定義常量(相當於final)

val TAG:String="ClassName"

上面兩個例子用:String來定義了數據類型,這個是可以省略的,Kotlin支持類型推斷,這兩句話你可以寫成

var name="my name"

val TAG="ClassName"

5.初始化和延遲加載

在Java裏,我們可以定義一個變量,但是並不賦值(int和boolean會有默認值)

但是Kotlin裏必須爲變量賦值,如果只寫一個變量,卻不賦值,像下面這樣:

var name

編譯器會報錯,提示你未初始化,你必須賦值爲0或者null,或者別的什麼值。

不過,我們有時候就是不能在定義變量時就初始化它,比如在Android中我們經常預定義一個View控件而不初始化,但是直到onCreate或onCreateView時才初始化它。

針對這種情況,Kotlin提供了懶加載lazy機制來解決這個問題,在懶加載機制裏,變量只有在第一次被調用時,纔會初始化,代碼需要這樣寫


在初次調用時初始化變量的lazy機制

lazy只適用於val對象,對於var對象,需要使用lateinit,原理是類似的,只是代碼需要這樣寫


var變量使用lateinit機制

6.空指針安全

在Kotlin裏,可以用“?”表示可以爲空,也可以用“!!”表示不可以爲空。

空指針安全並不是不需要處理空指針,你需要用“?”聲明某個變量是允許空指針的,例如:

var num:Int?=null

聲明允許爲空時,不能使用類型推斷,必須聲明其數據類型

空指針雖然安全了,但對空指針的處理還是要視情況而定,有時候不處理,有時候做數據檢查,有時候還需要拋出異常,這三種情況可以這樣寫:

val v1 =num?.toInt() //不做處理返回 null

val v2 =num?.toInt() ?:0 //判斷爲空時返回0

val v3 =num!!.toInt() //拋出空指針異常(用“!!”表示不能爲空)

更多空指針異常處理,有一篇NullPointException 利器 Kotlin 可選型介紹的比較全面,值得借鑑

7.定義函數

在Kotlin語法裏,定義函數的格式是這樣的

fun 方法名(參數名:類型,參數名:類型...) :返回類型{

}

所以,一般來說,函數是這樣寫的

fun getAddress(id:Int,name:String):String{

    return"got it"

}

由於Kotlin可以對函數的返回值進行類型推斷,所以經常用“=”代替返回類型和“return”關鍵字,上面這段代碼也可以寫成

fun getAddress(id:Int,name:String)={ //用“=”代替return,返回String類型則交給類型推斷

     "got it" //return被“=”代替了

}

如果函數內代碼只有一行,我們甚至可以去掉{}

fun getAddress(id:Int,name:String)="got it" //去掉了{}

}

函數也允許空指針安全,在返回類型後面增加“?”即可

fun getAddress(id:Int,name:String) :String?="got it"

有時候,函數的返回類型是個Unit,這其實就是Java中的void,表示沒有返回

fun addAddress(id:Int,name:String):Unit{ //相當於java的void

}

不過,在函數無返回時,一般不寫Unit

fun addAddress(id:Int,name:String){ //相當於java的void

}

8.用is取代了instance of

代碼很簡單

if(obj is String)...

9.in、區間和集合

Kotlin裏有區間的概念,例如1..5表示的就是1-5的整數區間

可以用in判斷數字是否在某個區間

if(x in 1..5){ ...//檢查x數值是否在1到5區間

可以用in判斷集合中是否存在某個元素

if(name in list){...//檢查list中是否有某個元素(比Java簡潔多了)

可以用in遍歷整個集合

for(i in 1..5){ ...//遍歷1到5

for(item in list){...//遍歷list中的每個元素(相當於Java的for(String item : list))

另外,in在遍歷集合時功能相當強大:

在遍歷集合時,可以從第N項開始遍歷

for(i in 3..list.size-2){...相當於for (int i = 3; i <= list.size()-2; i++)

可以倒序遍歷

for(i in list.size downTo 0) {...相當於for (int i = list.size(); i >= 0; i--)

可以反轉列表

for(i in (1..5).reversed())

可以指定步長

for(i in 1.0..2.0 step 0.3) //步長0.3

Kotlin裏的集合還都自帶foreach函數

list.forEach {...

10.用when取代了switch

switch在Java裏一直不怎麼給力,在稍老一些的版本里,甚至不支持String

Kotlin乾脆用強大的when取代了switch,具體用法如下


when的用法

代碼中的參數類型Any,相當於Java中的Obejct,是Kotlin中所有類的基類,至於object關鍵字,在Kotlin中另有用處...

11.字符串模板

在Java裏使用字符串模板沒有難度,但是可讀性較差,代碼一般是

MessageFormat.format("{0}xivehribuher{1}xhvihuehewogweg",para0,para2);

在字符串較長時,你就很難讀出字符串想表達什麼

在kotlin裏,字符串模板可讀性更好

"${para0}xivehribuher${para1}xhvihuehewogweg"

12.數據類

數據類是Kotlin相對Java的一項重大改進,我們在Java裏定義一個數據Model時,要做的事情有很多,例如需要定義getter/setter(雖然有插件代寫),需要自己寫equals(),hashCode(),copy()等函數(部分需要手寫)

但是在Kotlin裏,你只需要用data修飾class的一行代碼

data class Client(var id:Long,var name:String,var birth:Int,var addr:String)

Kotlin會自動幫你實現前面說的那些特性。

數據模型裏經常需要一些靜態屬性或方法,Kotlin可以在數據類裏添加一個companion object(伴隨對象),讓這個類的所有對象共享這個伴隨對象(object在Kotlin中用來表示單例,Kotlin用Any來表示所有類的基類)


添加companion obejct

13.單例模式

單例是很常見的一種設計模式,Kotlin乾脆從語言級別提供單例,關鍵字爲object,如果你在擴展了Kotlin的IDE裏輸入singleton,IDE也會自動幫你生成一個伴隨對象,也就是一個單例


單例類

如果一個類需要被定義爲class,又想做成單例,就需要用上一節中提到的companion object

例如,如果我們用IDE新建一個blankFragment,IDE會自動幫我們寫出下面的代碼,這本來是爲了解決Fragment初始化時傳值的問題,我們注意到她已經使用了companion object單例


自動生成的代碼

如果我們修改一下newInstance這個函數


改成單例

那麼,我們用

BlankFragment.newInstance()

就可以調用這個fragment的單例了

14.爲已存在的類擴展方法和屬性

爲了滿足開放封閉原則,類是允許擴展,同時嚴禁修改的,但是實現擴展並不輕鬆,在Java裏,我們需要先再造一個新的類,在新類裏繼承或者引用舊類,然後才能在新類裏擴展方法和屬性,實際上Java裏層層嵌套的類也非常多。

在Kotlin裏,這就簡潔優雅地多,她允許直接在一箇舊的類上做擴展,即使這是一個final類。

例如,Android中常見的Toast,參數較多,寫起來也相對繁瑣,我們一般是新建一個Util類去做一個相對簡單的函數,比如叫做showLongToast什麼的,我們不會想在Activity或Fragment中擴展這個函數,因爲太麻煩,我們需要繼承Activity做一個比如叫ToastActivity的類,在裏面擴展showLongToast函數,然後把業務Activity改爲繼承這個ToastActivity...

在Kotlin裏,我們只需要這樣寫


擴展函數

就完成了Activity類的函數擴展,我們可以在Activity及其子類裏隨意調用了


調用擴展函數

需要注意的是,你無法用擴展去覆蓋已存在的方法,例如,Activity裏已經有一個onBackPressed方法,那麼你再擴展一個Activity.onBackPressed方法是無用的,當你調用Activity().onBackPressed()時,它只會指向Activity本身的那個onBackPressed方法。

我們還可以用類似的方式去擴展屬性


擴展屬性

不過,Kotlin的擴展其實是僞裝的,我們並沒有真正給Activity類擴展出新的函數或屬性,你在A類裏爲Activity擴展了函數,換到B類裏,你就找不到這個函數了。

這是因爲,Kotlin爲類擴展函數時,並沒有真的去修改對應的類文件,只是藉助IDE和編譯器,使他看起來像擴展而已。

所以,如果類的某些函數只在特殊場景下使用,可以使用靈活簡潔的擴展函數來實現。

但是,如果想爲類永久性地添加某些新的特性,還是要利用繼承或者裝飾模式(decorator)。

不過,Kotlin裏對於類的家族定義和Java有所不同,我們來看一下

15.類的家族結構

Kotlin關於類的家族結構的設計,和Java基本相似,但是略有不同:

Object:取消,在Java裏Object是所有類的基類,但在Kotlin裏,基類改成了Any

Any:新增,Kotlin裏所有類的基類

object:新增,Kotlin是區分大小寫的,object是Kotlin中的單例類

new:取消,Kotlin不需要new關鍵字

private: 仍然表示私有

protected: 類似private,在子類中也可見

internal: 模塊內可見

inner:內部類

public: 仍然表示共有,但是Kotlin的內部類和參數默認爲public

abstract:仍然表示抽象類

interface:仍然表示接口

final:取消,Kotlin的繼承和Java不同,Java的類默認可繼承,只有final修飾的類不能繼承;Kotlin的類默認不能繼承,只有爲open修飾的類能繼承

open:新增,作用見上一條

static:取消!Java用static去共享同一塊內存空間,這是一個非常實用的設計,不過Kotlin移除了static,用伴隨對象(前面提到過的compaion object)的概念替換了static,伴隨對象其實是個單例的實體,所以伴隨對象比static更加靈活一些,能去繼承和擴展。

繼承:在Kotlin裏,繼承關係統一用“:”,不需要向java那樣區分implement和extend,在繼承多個類/接口時,中間用“,”區分即可,另外,在繼承類時,類後面要跟()。所以在Kotlin裏,繼承類和接口的代碼一般是這樣的:

class BaseClass : Activity(), IBinder{ //示例

16.構造函數

在Java裏,類的構造函數是這樣的

pulic 類名作爲函數名 (參數) {...}

Java裏有時會重載多個構造函數,這些構造函數都是並列的

在Kotlin裏,類也可以有多個構造函數(constructor),但是分成了1個主構造函數和N個二級構造函數,二級構造函數必須直接或間接代理主構造函數,也就是說,在Kotlin裏,主構造函數有核心地位

主構造函數一般直接寫在類名後面,像這麼寫

class ClientInfo(id:Long,name:String,addr:String){

這其實是個縮寫,完全版本應該是

class ClientInfo constructor(id:Long,name:String,addr:String){

主構造函數的這個結構,基本決定了,在這個主構造函數裏,沒法寫初始化代碼...

而二級構造函數必須代理主構造函數,寫出來的效果是這樣的


二級構造函數

17.初始化模塊init

上一節提到過,主構造函數裏不能寫代碼,這就很麻煩了,不過還好,Kotlin提供了初始化模塊,基本上就是用init修飾符修飾一個{},在類初始化時執行這段兒代碼,代碼像這樣寫就行


初始化模塊

18.其他

Kotlin還有很多其他的語言特性,本文主要是爲了建立對Kotlin的大概印象,更多細節就不再列舉了,建議仔細閱讀Kotlin官方文檔,並且多動手寫一些代碼。

函數式編程

讀到這裏,我們發現熟悉Java的人好像很容易學會Kotlin,甚至會感覺Kotlin不像一門新語言。但語法只是讓我們能用Kotlin,要想用好Kotlin,就必須理解Kotlin背後的函數式編程理念。

一個用慣了錘子的人,看什麼都像是釘子,我們必須先扔掉錘子,再去理解函數式編程。

我們先重新理解一下什麼是計算機,什麼是編程:

1.計算機:人發明計算機是爲了計算數據(二戰期間爲了把炮彈打得更準,需要解大量的微積分,就造了臺計算機幫忙,我們知道第一臺通用計算機叫做ENIAC,這名字不是它的暱稱綽號,就是它的功能,ENIAC的全稱爲Electronic Numerical Integrator And Computer,即電子數字積分計算機),直到現在,計算機程序在底層硬件電路上仍然是0和1的計算問題。

2.計算:計算機很笨,它其實只會計算0和1;但是人很聰明,人發現只要能把問題轉換成0和1的運算,就可以丟給計算機去處理了,然後,幾乎所有的問題,都可以設法轉換成0和1的計算問題。

3.程序:一次或幾次0和1的計算,幾乎不能解決任何問題,需要很多次,步驟很複雜,過程很詳細的0和1的計算才行,這種專爲計算機提供的複雜而詳細的計算步驟,就是計算機程序(爲了向計算機傳遞程序,早期用打孔的紙帶,後來用磁帶,再後來用軟盤,再後來是硬盤、光盤、閃存什麼的...)。

4.編程:編程就是編寫計算機程序,目的是把具體問題轉換成0和1的運算問題,然後交給計算機去處理。

5.語言:編寫計算機程序是給計算機用的,所以早期用的都是機器語言(全是0和1)。這樣寫出來的程序全是0和1,人自己反而看不懂,所以就抽象出彙編語言,就像把英文翻譯成中文一樣,這樣人比較容易看懂。但是彙編語言描述的是底層電路的運算過程(把數據從內存的這裏搬到那裏,寄存器裏的一個數據減去1,另一個數據乘以2),具體的輸入、輸出以及運算的目的都很難識別出來,所以又抽象出高級語言(C、BASIC等),不用再寫底層電路如何操作(高級語言需要先經過編譯器生成對應的彙編語言,再交給計算機去操作底層電路),只關心如何實現真實世界的業務邏輯。


編程語言和現實世界

6.抽象:編程的目的是把具體問題轉成0和1的計算問題,在高級語言裏不用再考慮0和1了,我們可以更自由地把真實世界抽象爲某種模型以便編寫代碼,這種抽象建模的過程,就是我們編程的核心能力

7.流派:關於如何對真實世界進行抽象,是有不同流派的,面向對象是和麪向過程對應的,函數式編程是和命令式編程對應的

8.面向過程和麪向對象:計算機的使命是用來計算,所有的計算都是有具體過程的,這樣就會很自然地把真實世界映射爲計算的過程,對真實世界的建模就是直接建出一個個業務的流程,然後去運轉而已。但是日益複雜的流程會變成一團亂麻,難以理解,難以修復,難以擴展;

在面向對象中,不再糾結於流程本身,而是抽象出了對象的概念,把業務中的相關要素抽象爲互相獨立又互相調用的對象,對象和對象之間的關係(繼承、封裝、多態)成爲核心,由於對象的概念更貼近人對於真實世界的理解,而且對象之間的關係也比整條複雜的流程簡單,修改或者擴展起來的波及範圍也小,容易理解/分解/修改/組合/擴展,所以面向對象非常適合大型的軟件工程


面向過程和麪向對象

9.命令式編程和函數式編程:換個角度來看,在計算機中實現業務邏輯有兩種書寫方式,一種是像輸入命令一樣,一步一步告訴計算機如何處理業務邏輯(還記得嗎,計算機很笨,只會做它懂的事情),這就是命令式編程。如果命令有誤,就是處理失敗,如果要修改業務,就要把整個業務相關的命令都去檢查和修改一遍。

另一種是告訴計算機,我需要什麼,不去詳細地告訴它要怎麼做,由於計算機不可能理解我們的需求,所以我們把函數拼接到一起,讓數據按照我們設想的方式流動,我們只要在數據流的最前面輸入參數,等數據自己流完整個處理過程,就能得到我們需要的數據。如果數據有誤或者需要修改業務,我們就去調整這個數據流,將它裏面的數據流動調整爲我們需要的方式。


命令式和函數式

我們看到,函數式編程的運算過程是高度抽象的,能節省大量運算細節的代碼編寫和debug工作。

10.區別:面向對象和函數式編程是有區別的,面向對象把真實世界抽象爲類和對象,函數式編程則把真實世界抽象爲函數;面向對象關心的是對象的行爲,以及對象之間的關係,而函數式編程關心的是函數的行爲,以及對函數的組合運用;面向對象只要對象不出錯,對象關係不出錯就可以,函數式編程只要奔涌在函數組合裏的數據流按照預期進行轉換就可以。

11.選擇:在抽象建模的概念裏,面向對象因爲貼近真實世界,相對簡單容易理解,工程上還容易擴展維護,所以很長一段時間以來,面向對象在軟件工程領域備受歡迎。

12.現實:從時間上來看,函數式編程其實並不新潮,但是過去主要活躍在大學和實驗室裏,這幾年突然變得火熱,背後一定有現實的原因。

13.硬件和並行:這些年來,對計算機的應用越來越廣泛,丟給計算機處理的問題越來越多,計算量越來越大,所以計算機CPU就越來越快,一開始還能每18個月翻一番(摩爾定律),到了這幾年單核CPU逼近物理極限,提升有限,就開始着重搞多核,並行計算也越來越重要。

14.數據的問題:計算機的本質在於計算數據,而軟件最大的問題則是計算錯誤(出bug),不巧的是,面向對象編程在並行計算裏就特別容易出現bug,因爲她的核心是各種獨立而又互相調用的對象,當多個對象同時處理數據時,就很容易導致數據修改的不確定性,從而引發bug。

15.混合:編程的本質是把真實世界抽象映射到計算機的電路上,採用的抽象模式只是工具而已,我們沒有必要排斥函數式編程,也不需要放棄面向對象,Kotlin也同時支持這兩種方式,我們需要的是根據需要選用工具,用錘子,用扳手,或者兩者都用。

要更深入地理解函數式編程,有一篇So You Want to be a Functional Programmer,寫的非常好,在函數式編程裏,我們需要用到純函數、不變性、高階函數、閉包等概念。

純函數

開發者在學習編程之前,其實都學過數學,在數學的範疇裏,函數的運算是不受干擾的,比如你算一個數字的平方根,只要參數確定,計算的過程永遠是一致的,算出來的結果永遠是一樣的。

但是在學習編程(命令式編程)之後,函數就變了,變得“不純潔”了,函數的運算會受到干擾,而且干擾無處不在,例如,我們可以在函數裏使用一個會變化的全局變量,只要在任何位置/時間/線程裏修改這個全局變量,函數就會輸出不同的結果。


函數會受到干擾

如果這種變化是開發者故意設計的,開發者就把它稱爲業務邏輯;如果這種變化不符合開發者的預期,開發者就把它稱爲——bug,悲劇的是,在命令式編程裏,有無數的對象、時間點、線程可能對函數造成干擾。

在函數式編程裏,重心是函數組合和數據流,更加不允許有干擾,所以要求我們編寫純函數。

不過,純函數就像是編碼規範,Kotlin鼓勵而不是強制寫出函數,畢竟,編程是爲了與真實世界交互的,有時候必須使用一些“不純潔”的函數,所以我們不要求徹底的純函數化,只要求儘量寫出純函數

不變性

函數式編程不僅要求純函數,還要求保存不變性(Kotlin用val和集合表示不變性,是的,集合默認是不可變的)

還是先回到數學上,在數學裏,不允許這樣的表達(我在剛學編程時,看到這個式子也是顛覆三觀的)

x = x + 1

在函數式編程裏,這種表達也是非法的,也就是說,在函數式編程裏,沒有可變變量,一個變量一旦被賦值,就不可更改。

不變性有很多好處,這意味着程序運行的整個流程是固定可重現的,如果出了問題,只要跟着數據流走一遍就能找到出錯點,再也不會有稀奇古怪的變化來爲難我們。

可是,如果變量不可變,我們還要怎樣去做業務邏輯呢,函數式編程給出的方式就是——用函數去返回一個複製的新對象,在這個新的對象裏,改掉你想改的那個值。

更徹底地說,函數式編程裏,沒有變量,一切都是函數(就像面向對象編程裏,一切都是對象),變量實際上被函數取代了


函數式編程裏的變量也是函數

所以,函數式編程裏只能新增變量,不能修改變量,所以函數式編程可能會非常耗內存(生成的變量太多了,而且業務不走完,變量不釋放)

所以,在函數式編程裏還有一個特點——沒有循環,因爲for(i: i<9;i++)是非法的(當然,在Kotlin裏你還可以這樣寫,因爲Kotlin既支持函數式編程,又支持面向對象)

高階函數

既然變量已經被函數取代了,那麼函數裏的參數和返回值呢?這些對象是不是也可以被替換成爲函數呢?

在面向函數編程裏,有個重要的概念,叫做“函數是一等公民”,核心就是,函數擁有和數據一樣的地位,都可以作爲參數和返回值,相應的就出現了高階函數的概念,簡單理解,高階函數就是參數爲函數,或者返回值爲函數的函數。


可以把函數作爲輸入輸出

我們知道,在開發過程中,複用是非常重要的優化手段,說白了,能用1個函數就別用多個函數,不容易出錯,出錯也容易檢查和修改

那麼我們看下面這兩個函數,要怎麼優化?

fun getA(){

doA()

}

fun getB(){

doB()

}

在面向對象編程裏,我們第一反應是用接口和類來解決問題,當然,那樣就得好幾個類和接口,然後層層嵌套

有了高階函數的話,開頭那段代碼就可以這樣優化了

fun getAB(doA()){

}

(在Kotlin裏不能直接這麼寫,需要用Lambda表達式才行)

在Kotlin裏,lambda還可以作爲一種類型,可以被定義爲val


一個lambda的類型

調用這個lambda類型的“對象”,與調用函數無異


調用

閉包

前面說過,函數式編程裏的函數是第一等公民,所以,一個val可以是一段代碼,這就是一個閉包


一個閉包

不過,閉包不是函數,閉包在邏輯上是封閉的,它使用自己內部的數據,用自己內部的邏輯進行處理,外部只能得到閉包的輸出,無法輸入,也無法干擾。

在系統資源上,閉包是持久使用的,它會一直在系統裏,不像函數那樣會被系統註銷掉。

閉包在函數式編程裏可以簡化參數量、減少變量,會更加方便我們的開發。

其他

另外,函數式編程還有柯里化、inline、with、apply、let、run、it等概念,我們以後可以慢慢了解

接下來,我們看看Kotlin裏支撐起函數式編程的Lambda表達式、流式API等特性。

Lambda表達式

爲了寫高階函數和閉包,Kotlin支持我們使用Lambda表達式。

Lambda表達式也叫λ表達式,它看起來就是對匿名方法(如:回調、事件響應、Runnable等)的簡化寫法,目的是爲了更貼近函數式編程把函數作爲參數的思想。

Lambda表達式包括最外面的“{}”,用“()”來定義的參數列表,箭頭->,以及一個表達式或語句塊。

事件響應的簡化:

textView.setOnClickListener(newOnClickListener(){

   @Override

   public void onClick(View view){//todo}

   }

);

簡化爲

textView.setOnClickListener{/*todo*/}

Runnable的簡化:

executor.submit(

   newRunnable(){

     @Override

     public void run(){

         //todo

     }

  }

);

簡化爲:

executor.submit({//todo })

使用lambda表達式,我們就可以編寫高階函數,傳遞一個函數(或者一段代碼)作爲參數。

流式(Stream)API

前面提過,函數式編程以數據流爲中心,通過組合函數來整理一個數據流,通過調整這個函數組合得出需要的數據。

要讓數據流在組合函數裏流動起來,就需要使用流式API,流式API使我們更容易把函數組合起來,而且使整個數據流動過程更加直觀。

如果接觸過Java8或者RxAndroid,應該很容易理解流式API,我以前寫過RxAndroid使用初探—簡潔、優雅、高效,感興趣可以去讀一下,流式API寫出來的代碼風格如下


流式API的代碼風格

Kotlin的潛在問題

Kotlin也有一些潛在的問題是我們需要注意的,主要是開發時容易遇到的一些問題。

思維方式的問題

我們已經知道Kotlin的核心在於函數式編程,問題在於函數式編程的核心不是語法的問題,而是思維方式的問題,語法容易轉變,思維卻很難,所以沒有函數式編程經驗的話,切換到Kotlin其實會相當困難。

Kotlin和Java需要在module級別隔離

雖然Kotlin和Java的兼容性非常好,但畢竟是不同的編程語言,編譯過程不同,所以同一個module裏不能同時存在java和kotlin兩種文件,編譯器會告訴你Unable to read class file,也就是說如果你將某個module配置爲kotlin模塊,裏面就不能有java文件,反之亦然。

所以,如果覺得Kotlin和Java兼容良好就開始向工程項目裏添加Kotlin文件,是行不通的,你至少要新建一個module才行。

Kotlin->Java的轉換

我們應該注意,AS中只提供了從Java文件轉換爲Kotlin文件的工具,並沒有逆向轉換的工具,就是說目前你還不能很輕鬆地把Kotlin代碼轉換爲Java代碼,一件事情如果不能回退,就必須小心謹慎。

團隊開發的問題

一般來說,鑑於Kotlin和Java兼容良好,可以一邊維持舊的Java代碼,一邊開發新的Kotlin代碼和新的Java代碼,但是團隊開發不僅是兼容性的問題,Kotlin語法糖背後的很多思維方式也許會對團隊造成衝擊,例如,一旦某個模塊採用了流式API的話,其他團隊成員在調用這個模塊時,也需要理解並且能夠編寫流式API才能完成工作銜接,這就可能帶來額外的成本和意外的延期。

最後,簡單介紹一下怎樣開始在AS中使用Kotlin語言。

在AS中使用Kotlin語言

Android Studio對Kotlin的支持非常友好(畢竟算是同門),我們先簡單地看一下怎樣安裝和使用Kotlin(AS版本2.2.3),再來體會Kotlin在編程上的優勢。

1.安裝


插件

打開settings-plugins-install JetBrains plugin...

點擊“Install JetBrains Plugin...”,然後搜索kotlin。


搜索

搜索並安裝kotlin

安裝


下載

Kotlin安裝中

重啓AS


重啓

重啓AS

2.使用

創建項目:沒有變化。

創建Activity:增加了Kotlin Activity的選項。


activity

增加了Kotlin Activity

創建類/文件:增加了Kotlin文件/類的選項,同上圖。

Kotlin的文件類型在右下角都有個“K”字形的角標。


文件

Kotlin文件

初次創建時會提示需要進行配置,實際就是告訴編譯器,這個module用kotlin編譯還是用java編譯。


配置

提示配置Kotlin

Kotlin和Java可以無縫兼容,但是需要你通過配置,說明哪些module是Kotlin的,哪些module是Java的。


選擇編譯語言

選擇哪些module是Kotlin的

在project的gradule裏增加了kotlinversion和dependencies的引用


gradule

project的gradule設置

在app的gradule裏增加了關於Kotlin的app plugin和dependencies


app graduate

app的gradule設置

針對已經存在的Java文件,可以轉換爲Kotlin文件


convert

轉換文件

Kotlin文件的後綴名不再是.java,而是.kt


後綴名

文件擴展名爲kt(上圖是爲了對比,實際編譯時不能在一個module裏有兩種文件)。

現在,我們可以編寫Kotlin代碼了。

參考

Kotlin官方文檔

Using Project Kotlin for Android

Swift is like Kotlin

Kotlin相對於Java的優勢比較

爲什麼我要改用Kotlin

用 Kotlin 寫 Android ,難道只有環境搭建這麼簡單?

用 Kotlin 寫 Android 02 說說 Anko

使用Kotlin&Anko, 扔掉XML開發Android應用

400% faster layouts with Anko

NullPointException 利器 Kotlin 可選型

Data Classes in Kotlin: save a good bunch of lines of code (KAD 10)

函數式編程掃盲篇

So You Want to be a Functional Programmer


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章