写给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处
这种特性令人联想起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)