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處
這種特性令人聯想起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包
然后參照Anko在Github中的示例,實現這6行代碼。
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布局代碼放到一個專門的類文件里
然后在Activity引用這個布局類來繪制界面
雖然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只適用于val對象,對于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,具體用法如下
代碼中的參數類型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來表示所有類的基類)
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里,類的構造函數是這樣的
public 類名作為函數名 (參數) {...}
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類型的“對象”,與調用函數無異
閉包
前面說過,函數式編程里的函數是第一等公民,所以,一個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寫出來的代碼風格如下
一些有趣的函數
Kotlin里提供了一些有趣的函數,包括it,let,apply,run,with,inline等
1.it
我們知道,用lambda表達式,我們可以把一些函數的寫法簡化成“輸入參數->(運算)輸出”,其中,如果只有一個參數時,寫出來的代碼就像是
val dints=ints.map{value->value*2}
對于這種單個參數的運算式,可以進一步簡化,把參數聲明和->都簡化掉,只保留運算輸出,不過這要用it來統一代替參數,代碼就變成
val dints2=ints.map{ it*2}
這就是it的用法,進一步簡化單參數的lambda表達式。
2.let
let能把更復雜的對象賦給it,比如
File("a.text").let{
? ? it.absoluteFile ?//let把file對象賦給了it
}
這個特性可以稍微擴展一下,比如增加?檢查
getVaraiable()?.let{
? ? it.length? ? // when not null
}
這樣可以先檢查返回值是否為空,不為空才繼續進行
3.apply
apply可以操作一個對象的任意函數,再結合let返回該對象,例如
ints.apply{//拿到一個arraylist對象
? ? add(0,3) ?//操作該對象的函數
}.let{ it.size} // 返回該對象(已被修改),繼續處理
4.run
apply是操作一個對象,run則是操作一塊兒代碼
apply返回操作的對象,run的返回則是最后一行代碼的對象
ints.run(){ //操作一個集合
add(0,3) //操作該集合
var a=Activity()
a //會返回最后一行的對象
}.let{ it.actionBar}
5.with
with有點兒像apply,也是操作一個對象,不過它是用函數方式,把對象作為參數傳入with函數,然后在代碼塊中操作,例如
with(ints){ //傳入一個集合
? ? add(0,3) //操作該集合
? ? var a=Activity()
? ? a //會返回最后一行的對象
}.let{ it.actionBar}
但是返回像run,也是最后一行
6.inline
inline內聯函數,其實相當于對代碼塊的一個標記,這個代碼塊將在編譯時被放進代碼的內部,相當于說,內聯函數在編譯后就被打散到調用它的函數里的,目的是得到一些性能上的優勢。
Kotlin的潛在問題
Kotlin也有一些潛在的問題是我們需要注意的,主要是開發時容易遇到的一些問題。
思維方式的問題
我們已經知道Kotlin的核心在于函數式編程,問題在于函數式編程的核心不是語法的問題,而是思維方式的問題,語法容易轉變,思維卻很難,所以沒有函數式編程經驗的話,切換到Kotlin其實會相當困難。
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的選項。
增加了Kotlin Activity
創建類/文件:增加了Kotlin文件/類的選項,同上圖。
Kotlin的文件類型在右下角都有個“K”字形的角標。
Kotlin文件
初次創建時會提示需要進行配置,實際就是告訴編譯器,這個module用kotlin編譯還是用java編譯。
提示配置Kotlin
Kotlin和Java可以無縫兼容,但是需要你通過配置,說明哪些module是Kotlin的,哪些module是Java的。
選擇哪些module是Kotlin的
在project的gradule里增加了kotlin version和dependencies的引用
project的gradule設置
在app的gradule里增加了關于Kotlin的app plugin和dependencies
app的gradule設置
針對已經存在的Java文件,可以轉換為Kotlin文件
轉換文件
Kotlin文件的后綴名不再是.java,而是.kt
文件擴展名為kt。
現在,我們可以編寫Kotlin代碼了。
參考
Using Project Kotlin for Android
用 Kotlin 寫 Android ,難道只有環境搭建這么簡單?
使用Kotlin&Anko, 扔掉XML開發Android應用
NullPointException 利器 Kotlin 可選型
Data Classes in Kotlin: save a good bunch of lines of code (KAD 10)