Kotlin介紹

Google在今年的IO大會上宣布,將Android開發(fā)的官方語言更換為Kotlin,作為跟著Google玩兒Android的人,我們必須盡快了解和使用Kotlin語言。

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

Java的輝煌與陰影

1995年,當(dāng)年如日中天的Sun公司發(fā)布了Java語言,引起了巨大的轟動,與當(dāng)時主流的C語言和Basic語言比起來,Java語言簡單、面向?qū)ο?、穩(wěn)定、與平臺無關(guān)、解釋型、多線程、動態(tài)等特點,就像是打開了一個新的世界,一時間風(fēng)靡全球,云集者眾,微軟為了模仿Java搞出C#語言,Netscape為了趕時髦硬塞出一個JavaScript語言,IBM則捏著鼻子做了Java IDE Eclipse(日蝕,呵呵)。直到現(xiàn)在,Java在編程世界里還占據(jù)著舉足輕重的地位,Andy Rubin在開發(fā)Android系統(tǒng)時,也很自然地采用了Java和C++(C++負(fù)責(zé)NDK開發(fā))作為開發(fā)語言。

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

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

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

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

Kotlin的出現(xiàn)

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

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

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

另外,新語言必須考慮兼容性,為了與存量項目代碼和諧共處,Kotlin和Java是互相完美兼容的,兩種代碼文件可以并存,代碼可以互相調(diào)用文件可以互相轉(zhuǎn)換,庫文件也可以無障礙地互相調(diào)用,據(jù)說使用Kotlin基本不會帶來額外的成本負(fù)擔(dān)。

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

Kotlin的特色

Kotlin作為Java的改良,在Android開發(fā)中有很多優(yōu)勢,我們先從相對直觀的界面繪制開始了解,然后看看Kotlin的語法特點,再慢慢去接觸更深層次的編程思想。

簡化findViewById

我們知道,Android的架構(gòu)里,xml布局文件和Activity是松耦合的,Activity中要使用界面元素,必須借助R文件對xml控件的記錄,用findViewById找到這個元素。

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

進(jìn)一步引用Anko之后,可以使用find函數(shù)去綁定控件:find(R.id.hello),不需要類型轉(zhuǎn)換

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

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

image

然后,在Activity中直接根據(jù)id使用界面元素

image

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

image

點擊后,可以直接跳轉(zhuǎn)到xml文件中的控件位置,光標(biāo)會停留在Id處

image

這種特性令人聯(lián)想起C#語言中對界面控件的管理,在C#里,界面的控件可以直接調(diào)用,不需要find,這是因為在創(chuàng)建一個Form1.cs界面文件時,IDE會自動創(chuàng)建一個對應(yīng)的額Form1.designer.cs類,在這個類里,自動管理所有界面控件的對象。

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

image

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

Anko

Anko其實是一種DSL(領(lǐng)域相關(guān)語言),是專門用代碼方式來寫界面和布局的。

上一節(jié)針對findViewById,最激進(jìn)的方式是取消這個函數(shù),這一節(jié)更加激進(jìn),我們可以連XML布局文件也取消掉。

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

Anko卻實現(xiàn)了在代碼中簡潔優(yōu)雅地定義界面和布局,而且由于不需要讀取和解析XML布局文件,Anko的性能表現(xiàn)更佳

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

image

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

image

然后參照Anko在Github中的示例,實現(xiàn)這6行代碼。

image

Activity本來會在加載時在onCreate函數(shù)里用setContentView函數(shù)來尋找布局文件,并加載為自己的界面,在這里,Anko代碼替代了setContentView,直接告訴Activity應(yīng)該如何繪制界面。

(在Fragment里不可以這樣直接寫verticalLayout,因為加載機(jī)制不一樣,F(xiàn)ragment需要在onCreateView函數(shù)里inflate并返回一個View對象,所以對應(yīng)的Anko代碼也需要寫在onCreateView函數(shù)里并返回一個View,可以用return with(context){verticalLayout[...]}或者return UI{verticalLayout[...]}.view)

可以看到,代碼非常簡潔干練,不像以往的Android代碼那樣拖沓,這既與Kotlin的語法有關(guān),也與Anko能用代碼實現(xiàn)界面和布局有關(guān)。

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

image

然后在Activity引用這個布局類來繪制界面

image

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

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

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

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

另外,Anko還在異步、日志、Toast、對話框、數(shù)據(jù)庫等方面提供優(yōu)化服務(wù),是否采用就看自身需要了。

Kotlin語法特點

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

1.沒有“;”

在Kotlin語法里,代碼行不需要用“;”結(jié)尾,什么都不寫就好

2.重要的“:”

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

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

定義變量類型

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

定義參數(shù)的類型

fun makeTool(id:Int){ //參數(shù)id為Int類型

}

定義函數(shù)的返回值

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

}

聲明類/接口的繼承

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

使用Java類

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

****3.沒有“new”****

Kotlin實例化一個對象時不需要new關(guān)鍵字

var list=ArrayList()

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

用var定義變量(像js)

var name:String="my name"

用val定義常量(相當(dāng)于final)

val TAG:String="ClassName"

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

var name="my name"

val TAG="ClassName"

5.初始化和延遲加載

在Java里,我們可以定義一個變量,但是并不賦值(int和boolean會有默認(rèn)值)

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

var name

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

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

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

image

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

image

6.空指針安全

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

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

var num:Int?=null

聲明允許為空時,不能使用類型推斷,必須聲明其數(shù)據(jù)類型

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

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

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

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

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

7.定義函數(shù)

在Kotlin語法里,定義函數(shù)的格式是這樣的

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

}

所以,一般來說,函數(shù)是這樣寫的

 fun getAddress(id:Int,name:String):String{
 
     return"got it"

 }

由于Kotlin可以對函數(shù)的返回值進(jìn)行類型推斷,所以經(jīng)常用“=”代替返回類型和“return”關(guān)鍵字,上面這段代碼也可以寫成

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

如果函數(shù)內(nèi)代碼只有一行,我們甚至可以去掉{}

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

}

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

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

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

fun addAddress(id:Int,name:String):Unit{ //相當(dāng)于java的void

}

不過,在函數(shù)無返回時,一般不寫Unit

fun addAddress(id:Int,name:String){ //相當(dāng)于java的void

}

****8.用is取代了instance of****

代碼很簡單

if(obj is String)...

9.in、區(qū)間和集合

Kotlin里有區(qū)間的概念,例如1..5表示的就是1-5的整數(shù)區(qū)間

可以用in判斷數(shù)字是否在某個區(qū)間

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

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

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

可以用in遍歷整個集合

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

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

另外,in在遍歷集合時功能相當(dāng)強大:

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

for(i in 3..list.size-2){...相當(dāng)于for (int i = 3; i <= list.size()-2; i++)

可以倒序遍歷

for(i in list.size downTo 0) {...相當(dāng)于for (int i = list.size(); i >= 0; i--)

可以反轉(zhuǎn)列表

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

可以指定步長

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

Kotlin里的集合還都自帶foreach函數(shù)

list.forEach {...

10.用when取代了switch

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

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

image

代碼中的參數(shù)類型Any,相當(dāng)于Java中的Obejct,是Kotlin中所有類的基類,至于object關(guān)鍵字,在Kotlin中另有用處...

11.字符串模板

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

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

在字符串較長時,你就很難讀出字符串想表達(dá)什么

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

"{para0}**xivehribuher**{para1}xhvihuehewogweg"

12.數(shù)據(jù)類

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

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

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

Kotlin會自動幫你實現(xiàn)前面說的那些特性。

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

image

****13.單例模式****

單例是很常見的一種設(shè)計模式,Kotlin干脆從語言級別提供單例,關(guān)鍵字為object,如果你在擴(kuò)展了Kotlin的IDE里輸入singleton,IDE也會自動幫你生成一個伴隨對象,也就是一個單例

image

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

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

image

如果我們修改一下newInstance這個函數(shù)

image

那么,我們用

BlankFragment.newInstance()

就可以調(diào)用這個fragment的單例了

14.為已存在的類擴(kuò)展方法和屬性

為了滿足開放封閉原則,類是允許擴(kuò)展,同時嚴(yán)禁修改的,但是實現(xiàn)擴(kuò)展并不輕松,在Java里,我們需要先再造一個新的類,在新類里繼承或者引用舊類,然后才能在新類里擴(kuò)展方法和屬性,實際上Java里層層嵌套的類也非常多。

在Kotlin里,這就簡潔優(yōu)雅地多,她允許直接在一個舊的類上做擴(kuò)展,即使這是一個final類。

例如,Android中常見的Toast,參數(shù)較多,寫起來也相對繁瑣,我們一般是新建一個Util類去做一個相對簡單的函數(shù),比如叫做showLongToast什么的,我們不會想在Activity或Fragment中擴(kuò)展這個函數(shù),因為太麻煩,我們需要繼承Activity做一個比如叫ToastActivity的類,在里面擴(kuò)展showLongToast函數(shù),然后把業(yè)務(wù)Activity改為繼承這個ToastActivity...

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

image

就完成了Activity類的函數(shù)擴(kuò)展,我們可以在Activity及其子類里隨意調(diào)用了

image

需要注意的是,你無法用擴(kuò)展去覆蓋已存在的方法,例如,Activity里已經(jīng)有一個onBackPressed方法,那么你再擴(kuò)展一個Activity.onBackPressed方法是無用的,當(dāng)你調(diào)用Activity().onBackPressed()時,它只會指向Activity本身的那個onBackPressed方法。

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

image

不過,Kotlin的擴(kuò)展其實是偽裝的,我們并沒有真正給Activity類擴(kuò)展出新的函數(shù)或?qū)傩裕阍贏類里為Activity擴(kuò)展了函數(shù),換到B類里,你就找不到這個函數(shù)了。

這是因為,Kotlin為類擴(kuò)展函數(shù)時,并沒有真的去修改對應(yīng)的類文件,只是借助IDE和編譯器,使他看起來像擴(kuò)展而已。

所以,如果類的某些函數(shù)只在特殊場景下使用,可以使用靈活簡潔的擴(kuò)展函數(shù)來實現(xiàn)。

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

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

15.類的家族結(jié)構(gòu)

Kotlin關(guān)于類的家族結(jié)構(gòu)的設(shè)計,和Java基本相似,但是略有不同:

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

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

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

new:取消,Kotlin不需要new關(guān)鍵字

private: 仍然表示私有

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

internal: 模塊內(nèi)可見

inner:內(nèi)部類

public: 仍然表示共有,但是Kotlin的內(nèi)部類和參數(shù)默認(rèn)為public

abstract:仍然表示抽象類

interface:仍然表示接口

final:取消,Kotlin的繼承和Java不同,Java的類默認(rèn)可繼承,只有final修飾的類不能繼承;Kotlin的類默認(rèn)不能繼承,只有為open修飾的類能繼承

open:新增,作用見上一條

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

繼承:在Kotlin里,繼承關(guān)系統(tǒng)一用“:”,不需要向java那樣區(qū)分implement和extend,在繼承多個類/接口時,中間用“,”區(qū)分即可,另外,在繼承類時,類后面要跟()。所以在Kotlin里,繼承類和接口的代碼一般是這樣的:

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

16.構(gòu)造函數(shù)

在Java里,類的構(gòu)造函數(shù)是這樣的

public 類名作為函數(shù)名 (參數(shù)) {...}

Java里有時會重載多個構(gòu)造函數(shù),這些構(gòu)造函數(shù)都是并列的

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

主構(gòu)造函數(shù)一般直接寫在類名后面,像這么寫

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

這其實是個縮寫,完全版本應(yīng)該是

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

主構(gòu)造函數(shù)的這個結(jié)構(gòu),基本決定了,在這個主構(gòu)造函數(shù)里,沒法寫初始化代碼...

而二級構(gòu)造函數(shù)必須代理主構(gòu)造函數(shù),寫出來的效果是這樣的

image

17.初始化模塊init

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

image

18.****其他

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

函數(shù)式編程

讀到這里,我們發(fā)現(xiàn)熟悉Java的人好像很容易學(xué)會Kotlin,甚至?xí)杏XKotlin不像一門新語言。但語法只是讓我們能用Kotlin,要想用好Kotlin,就必須理解Kotlin背后的函數(shù)式編程理念。

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

我們先重新理解一下什么是計算機(jī),什么是編程:

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

2.計算:計算機(jī)很笨,它其實只會計算0和1;但是人很聰明,人發(fā)現(xiàn)只要能把問題轉(zhuǎn)換成0和1的運算,就可以丟給計算機(jī)去處理了,然后,幾乎所有的問題,都可以設(shè)法轉(zhuǎn)換成0和1的計算問題。

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

4.編程:編程就是編寫計算機(jī)程序,目的是把具體問題轉(zhuǎn)換成0和1的運算問題,然后交給計算機(jī)去處理。

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

image

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

7.流派:關(guān)于如何對真實世界進(jìn)行抽象,是有不同流派的,面向?qū)ο笫呛兔嫦蜻^程對應(yīng)的,函數(shù)式編程是和命令式編程對應(yīng)的

8.面向過程和面向?qū)ο螅河嬎銠C(jī)的使命是用來計算,所有的計算都是有具體過程的,這樣就會很自然地把真實世界映射為計算的過程,對真實世界的建模就是直接建出一個個業(yè)務(wù)的流程,然后去運轉(zhuǎn)而已。但是日益復(fù)雜的流程會變成一團(tuán)亂麻,難以理解,難以修復(fù),難以擴(kuò)展;

在面向?qū)ο笾?,不再糾結(jié)于流程本身,而是抽象出了對象的概念,把業(yè)務(wù)中的相關(guān)要素抽象為互相獨立又互相調(diào)用的對象,對象和對象之間的關(guān)系(繼承、封裝、多態(tài))成為核心,由于對象的概念更貼近人對于真實世界的理解,而且對象之間的關(guān)系也比整條復(fù)雜的流程簡單,修改或者擴(kuò)展起來的波及范圍也小,容易理解/分解/修改/組合/擴(kuò)展,所以面向?qū)ο蠓浅_m合大型的軟件工程

image

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

另一種是告訴計算機(jī),我需要什么,不去詳細(xì)地告訴它要怎么做,由于計算機(jī)不可能理解我們的需求,所以我們把函數(shù)拼接到一起,讓數(shù)據(jù)按照我們設(shè)想的方式流動,我們只要在數(shù)據(jù)流的最前面輸入?yún)?shù),等數(shù)據(jù)自己流完整個處理過程,就能得到我們需要的數(shù)據(jù)。如果數(shù)據(jù)有誤或者需要修改業(yè)務(wù),我們就去調(diào)整這個數(shù)據(jù)流,將它里面的數(shù)據(jù)流動調(diào)整為我們需要的方式。

image

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

10.區(qū)別:面向?qū)ο蠛秃瘮?shù)式編程是有區(qū)別的,面向?qū)ο蟀颜鎸嵤澜绯橄鬄轭惡蛯ο螅瘮?shù)式編程則把真實世界抽象為函數(shù);面向?qū)ο箨P(guān)心的是對象的行為,以及對象之間的關(guān)系,而函數(shù)式編程關(guān)心的是函數(shù)的行為,以及對函數(shù)的組合運用;面向?qū)ο笾灰獙ο蟛怀鲥e,對象關(guān)系不出錯就可以,函數(shù)式編程只要奔涌在函數(shù)組合里的數(shù)據(jù)流按照預(yù)期進(jìn)行轉(zhuǎn)換就可以。

11.選擇:在抽象建模的概念里,面向?qū)ο笠驗橘N近真實世界,相對簡單容易理解,工程上還容易擴(kuò)展維護(hù),所以很長一段時間以來,面向?qū)ο笤谲浖こ填I(lǐng)域備受歡迎。

12.現(xiàn)實:從時間上來看,函數(shù)式編程其實并不新潮,但是過去主要活躍在大學(xué)和實驗室里,這幾年突然變得火熱,背后一定有現(xiàn)實的原因。

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

14.數(shù)據(jù)的問題:計算機(jī)的本質(zhì)在于計算數(shù)據(jù),而軟件最大的問題則是計算錯誤(出bug),不巧的是,面向?qū)ο缶幊淘诓⑿杏嬎憷锞吞貏e容易出現(xiàn)bug,因為她的核心是各種獨立而又互相調(diào)用的對象,當(dāng)多個對象同時處理數(shù)據(jù)時,就很容易導(dǎo)致數(shù)據(jù)修改的不確定性,從而引發(fā)bug。

15.混合:編程的本質(zhì)是把真實世界抽象映射到計算機(jī)的電路上,采用的抽象模式只是工具而已,我們沒有必要排斥函數(shù)式編程,也不需要放棄面向?qū)ο螅琄otlin也同時支持這兩種方式,我們需要的是根據(jù)需要選用工具,用錘子,用扳手,或者兩者都用。

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

純函數(shù)

開發(fā)者在學(xué)習(xí)編程之前,其實都學(xué)過數(shù)學(xué),在數(shù)學(xué)的范疇里,函數(shù)的運算是不受干擾的,比如你算一個數(shù)字的平方根,只要參數(shù)確定,計算的過程永遠(yuǎn)是一致的,算出來的結(jié)果永遠(yuǎn)是一樣的。

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

image

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

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

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

不變性

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

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

x = x + 1

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

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

不過,不變性最大的好處在于多線程安全,它可以完美地規(guī)避多個線程同時修改一個數(shù)據(jù)時的同步問題(變量不再允許修改,每個線程需要各自生成變量),這一點對于目前大量應(yīng)用多線程的工程現(xiàn)狀來說,特別有實際價值。

可是,如果變量不可變,我們還要怎樣去做業(yè)務(wù)邏輯呢,函數(shù)式編程給出的方式就是——用函數(shù)去返回一個復(fù)制的新對象,在這個新的對象里,改掉你想改的那個值。

更徹底地說,函數(shù)式編程里,沒有變量,一切都是函數(shù)(就像面向?qū)ο缶幊汤?,一切都是對象),變量實際上被函數(shù)取代了

image

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

另外,在函數(shù)式編程里還有一個特點——沒有循環(huán),因為for(i: i<9;i++)是非法的(當(dāng)然,在Kotlin里你還可以這樣寫,因為Kotlin既支持函數(shù)式編程,又支持面向?qū)ο螅?/p>

高階函數(shù)

既然變量已經(jīng)被函數(shù)取代了,那么函數(shù)里的參數(shù)和返回值呢?這些對象是不是也可以被替換成為函數(shù)呢?

在面向函數(shù)編程里,有個重要的概念,叫做“函數(shù)是一等公民”,核心就是,函數(shù)擁有和數(shù)據(jù)一樣的地位,都可以作為參數(shù)和返回值,相應(yīng)的就出現(xiàn)了高階函數(shù)的概念,簡單理解,高階函數(shù)就是參數(shù)為函數(shù),或者返回值為函數(shù)的函數(shù)。

image

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

那么我們看下面這兩個函數(shù),要怎么優(yōu)化?

fun getA(){

doA()

}

fun getB(){

doB()

}

在面向?qū)ο缶幊汤?,我們第一反?yīng)是用接口和類來解決問題,當(dāng)然,那樣就得好幾個類和接口,然后層層嵌套

有了高階函數(shù)的話,開頭那段代碼就可以這樣優(yōu)化了

fun getAB(doA()){

}

(在Kotlin里不能直接這么寫,需要用Lambda表達(dá)式才行)

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

image

調(diào)用這個lambda類型的“對象”,與調(diào)用函數(shù)無異

image

閉包

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

image

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

在系統(tǒng)資源上,閉包是持久使用的,它會一直在系統(tǒng)里,不像函數(shù)那樣會被系統(tǒng)注銷掉。

閉包在函數(shù)式編程里可以簡化參數(shù)量、減少變量,會更加方便我們的開發(fā)。

其他

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

接下來,我們看看Kotlin里支撐起函數(shù)式編程的Lambda表達(dá)式、流式API等特性。

Lambda表達(dá)式

為了寫高階函數(shù)和閉包,Kotlin支持我們使用Lambda表達(dá)式。

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

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

事件響應(yīng)的簡化:

 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表達(dá)式,我們就可以編寫高階函數(shù),傳遞一個函數(shù)(或者一段代碼)作為參數(shù)。

流式(Stream)API

前面提過,函數(shù)式編程以數(shù)據(jù)流為中心,通過組合函數(shù)來整理一個數(shù)據(jù)流,通過調(diào)整這個函數(shù)組合得出需要的數(shù)據(jù)。

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

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

image

一些有趣的函數(shù)

Kotlin里提供了一些有趣的函數(shù),包括it,let,apply,run,with,inline等

1.it

我們知道,用lambda表達(dá)式,我們可以把一些函數(shù)的寫法簡化成“輸入?yún)?shù)->(運算)輸出”,其中,如果只有一個參數(shù)時,寫出來的代碼就像是

val dints=ints.map{value->value*2}

對于這種單個參數(shù)的運算式,可以進(jìn)一步簡化,把參數(shù)聲明和->都簡化掉,只保留運算輸出,不過這要用it來統(tǒng)一代替參數(shù),代碼就變成

val dints2=ints.map{ it*2}

這就是it的用法,進(jìn)一步簡化單參數(shù)的lambda表達(dá)式。

2.let

let能把更復(fù)雜的對象賦給it,比如

File("a.text").let{

it.absoluteFile  //let把file對象賦給了it

}

這個特性可以稍微擴(kuò)展一下,比如增加?檢查

getVaraiable()?.let{

it.length    // when not null

}

這樣可以先檢查返回值是否為空,不為空才繼續(xù)進(jìn)行

3.apply

apply可以操作一個對象的任意函數(shù),再結(jié)合let返回該對象,例如

ints.apply{//拿到一個arraylist對象

add(0,3)  //操作該對象的函數(shù)

}.let{ it.size} // 返回該對象(已被修改),繼續(xù)處理

4.run

apply是操作一個對象,run則是操作一塊兒代碼

apply返回操作的對象,run的返回則是最后一行代碼的對象

ints.run(){ //操作一個集合

add(0,3) //操作該集合

var a=Activity()

a //會返回最后一行的對象

}.let{ it.actionBar}

5.with

with有點兒像apply,也是操作一個對象,不過它是用函數(shù)方式,把對象作為參數(shù)傳入with函數(shù),然后在代碼塊中操作,例如

with(ints){ //傳入一個集合

add(0,3) //操作該集合

var a=Activity()

a //會返回最后一行的對象

}.let{ it.actionBar}

但是返回像run,也是最后一行

6.inline

inline內(nèi)聯(lián)函數(shù),其實相當(dāng)于對代碼塊的一個標(biāo)記,這個代碼塊將在編譯時被放進(jìn)代碼的內(nèi)部,相當(dāng)于說,內(nèi)聯(lián)函數(shù)在編譯后就被打散到調(diào)用它的函數(shù)里的,目的是得到一些性能上的優(yōu)勢。

Kotlin的潛在問題

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

思維方式的問題

我們已經(jīng)知道Kotlin的核心在于函數(shù)式編程,問題在于函數(shù)式編程的核心不是語法的問題,而是思維方式的問題,語法容易轉(zhuǎn)變,思維卻很難,所以沒有函數(shù)式編程經(jīng)驗的話,切換到Kotlin其實會相當(dāng)困難。

Kotlin->Java的轉(zhuǎn)換

我們應(yīng)該注意,AS中只提供了從Java文件轉(zhuǎn)換為Kotlin文件的工具,并沒有逆向轉(zhuǎn)換的工具,就是說目前你還不能很輕松地把Kotlin代碼轉(zhuǎn)換為Java代碼,一件事情如果不能回退,就必須小心謹(jǐn)慎。

團(tuán)隊開發(fā)的問題

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

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

在AS中使用Kotlin語言

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

1.安裝

image

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

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

image

搜索并安裝kotlin

安裝

image

Kotlin安裝中

重啟AS

image

重啟AS

2.使用

創(chuàng)建項目:沒有變化。

創(chuàng)建Activity:增加了Kotlin Activity的選項。

image

增加了Kotlin Activity

創(chuàng)建類/文件:增加了Kotlin文件/類的選項,同上圖。

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

image

Kotlin文件

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

image

提示配置Kotlin

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

image

選擇哪些module是Kotlin的

在project的gradule里增加了kotlin version和dependencies的引用

image

project的gradule設(shè)置

在app的gradule里增加了關(guān)于Kotlin的app plugin和dependencies

image

app的gradule設(shè)置

針對已經(jīng)存在的Java文件,可以轉(zhuǎn)換為Kotlin文件

image

轉(zhuǎn)換文件

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

image

文件擴(kuò)展名為kt。

現(xiàn)在,我們可以編寫Kotlin代碼了。

參考

Kotlin官方文檔

Using Project Kotlin for Android

Swift is like Kotlin

Kotlin相對于Java的優(yōu)勢比較

為什么我要改用Kotlin

用 Kotlin 寫 Android ,難道只有環(huán)境搭建這么簡單?

用 Kotlin 寫 Android 02 說說 Anko

使用Kotlin&Anko, 扔掉XML開發(fā)Android應(yīng)用

400% faster layouts with Anko

NullPointException 利器 Kotlin 可選型

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

函數(shù)式編程掃盲篇

So You Want to be a Functional Programmer

進(jìn)行Kotlin實戰(zhàn)開發(fā)前,你應(yīng)了解的那些技術(shù)點

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容