1.5 为什么要学Kotlin
现在的编程语言已经足够多了,为什么我们还需要更多的语言?Java已经足够强大了,为什么我们还需要Kotlin、Scala这样的语言呢?
其实,如果我们仔细想想,会发现这个问题本身的逻辑就不成立。例如,我们能这样说吗——煎鸡排已经足够好吃了,为什么我们还要去吃煎牛排呢?
从最早的机器语言(01机器码,汇编语言)到高级语言(LISP、BASIC、Pascal、C、C++、Java、Haskell等),再到现代编程语言(Go、Swift、Scala、Kotlin等),编程语言的演化过程可谓是百花齐放、百家争鸣。
最早的编程语言是01机器码(Machine Code),那个时候的程序员要会用0和1表示一切!
后来人们可以把一些常用的指令操作单独抽象出来,用特定的关键字来映射01机器码序列,而这就是汇编语言,这可以算作是编程语言过程中的第一次抽象封装。也许汇编语言的主要意义不在于它与机器语言之间并不显著的差别,而是这样的一种思路:程序完全可以在不同的层次上编制!人们可以用机器语言写一个“翻译程序”,从而可以在一个更高层次上进行编程。
后来汇编语言用久了,人们也逐渐发现了使用汇编语言的缺点:可移植性差。汇编代码中是大量的字节指令码,而且必须一步步地告诉计算机每一步要怎么做,如果其中的一个步骤出错,那么执行结果将是程序员们意想不到的!使用汇编语言编程,极易在子程序调用过程中导致寄存器内容错误,而且调试程序也很困难。程序在正常运行时,我们基本不会过多地关心和想象它的活动结构和层次空间。只有当程序出现bug或者崩溃的时候,我们才会在不同层次上思考和想象程序运行的具体细节。而这其中的出错信息将变得至关重要。例如,一个除法操作,遇到除数为0的情况程序将暂停运行,并把错误抛给程序员。下面是不同层次上的debug信息:
机器语言层:程序运行异常终止于11110000010001001地址;
汇编语言层:程序运行异常终止于DIV指令;
编译语言层:程序运行异常终止于代码行256: (a+b)/c处。
上面的信息中,显而易见的是层次越高,越容易被人类大脑所理解。
在高级语言中,所有参数都必须严格匹配其类型,这样就不会出现寄存器内容错误的情况。高级语言就是为了解决汇编语言的这些问题进行的更高一层的抽象与封装。这层封装就是编译器。编译器所要解决的问题就是如何构造一个系统,使它可以接收当前层次的描述,然后从中生成另一个层次上的描述。通常来说,设计一门语言相对容易,而实现这门语言的编译器则是比较复杂的。编译器制定了一系列的协议规范、语法规则等,只要程序员按照这个协议规范来编程,编译器就可以将高级语言的源代码翻译成对应CPU指令集上的汇编语言代码。高级语言不要求程序员掌握计算机的硬件运行原理,只要写好上层代码即可。著名的高级语言有BASIC、Fortran(公式翻译)、COBOL(通用商业语言)、C、Pascal(结构化编程语言)、ADA(通用程序设计语言)等。
尽管C语言(1972, Dennis MacAlistair Ritchie,启发语言有B语言、汇编、ALGOL68等)已经足够普及且非常强大,但是之后还是出现了针对C语言进行改进和功能扩展的新语言——C++语言(1979, Bjarne Stroustrup)。C++语言集成了C语言的特性,然后加入了面向对象程序设计的特性支持。和汇编语言不同的是,C语言的语句和机器语言的指令之间不再是简单的一一对应关系,不过毫无疑问的是,仍然存在从C语言代码到机器语言代码的映射关系,但是这种关系要比从汇编语言到机器语言之间的关系复杂多了。而完成这个映射过程翻译的程序,我们就称之为“编译器”。
而C/C++语言最大的一个问题就是“一切都会尖叫着停止”,因为它们使用了直接操纵内存的指针。一旦因为使用指针而出现了内存错误,系统核心就会崩溃。
有没有一种语言可以控制这样的风险呢?
后来的James Gosling在1995年开发出的Java语言继承了C和C++语言的优点,摒弃了C++语言里的指针操作、手动管理内存、多继承等诸多复杂而并不实用的功能特性,引入了划时代的Java虚拟机(Java Virtual Machine, JVM)。JVM是一种虚拟的计算机,从结构上看,它与实际的计算机架构相似,JVM的作用是使得一台实际的机器能够运行Java字节码(bytecode)。引用Java之父James Gosling的话就是
“大部分人大谈特谈Java语言,这对于我来说也许听起来很奇怪,但是我无法不去在意。JVM才是Java生态系统的核心啊。我真正关心的是Java虚拟机的概念,因为是它把所有的东西都联系在了一起;是它造就了Java语言;是它使得事物能在所有的异构平台上得到运行;也还是它使得所有类型的语言能够共存。”
首先,JVM实现了Java的可移植性。另外,JVM里面实现了一个垃圾收集器(Garbage Collector, GC)来管理内存,GC对保证系统的可靠性和安全性非常实用有益。同时,JVM还奠定了一个庞大的语言生态的基础。
Java是互联网时代当之无愧的最流行的开发语言。经过20多年的积累和沉淀,Java生态拥有了很多优秀的开源社区,如Apache和Spring。有了这些框架,我们可以更加专注业务的实现。
Java语言也有不好的一面,简单列举如下。
1. 检查异常(Checked Exceptions)
检查异常会在编译时强制执行try catch处理,同时还需要进行某种排序处理。检查异常是一个失败的实践,几乎所有的主要API提供者都反对可检查异常。Kotlin中摒弃了检查异常。
2. 基本类型和数组
Java的这个设计保留了字节码的底层细节,违反了“凡事皆为对象”的原则,如泛型无法包容基本类型就是一个经典的例子。这也使得Java的类型系统显得不是那么地简单统一。比较好的方案是,源代码不用直接使用基本类型或者数组,由编译器(或者JVM)来决定是否可以帮你对其进行优化,而Kotlin正是这么做的。
3. 静态变量(Static)
静态方法经常会导致需要显式地定义接口,从而使得API更加复杂。一个更好的办法就是采用单例对象,单例对象在大多数情况下的表现与静态对象差不多,但是可以像一个对象一样被传递。Kotlin中提供了object单例对象。
4. 泛型
Java泛型本身就很复杂,当使用?exends和?super等变种句型时就变得尤为复杂,非常容易搞错。这个问题在Effective Java一书中提出了PECS(Producer Extends Consumer Super)的建议,Kotlin直接使用了这个方案。
5. 空指针异常(NPE)
在Java中我们不得不写一堆防御代码来避免令人头疼的NPE。Kotlin中引入了可空类型与安全调用符、Elvis操作符等特性来实现空安全,这部分内容将在第3章中介绍。
6. 一堆getter/setter单调冗长的样板代码
例如,下面的Person Bean类:
在Kotlin中我们可以使用数据类,代码如下:
关于数据类的内容,将在第4章中介绍。
7. 不容易传递函数
Java中没有提供一等函数类型,函数式编程(FP)只能通过使用接口类型以及多态特性“曲线”来实现。Java会将每一个算法(方法)都放入类中,这种限制会出现这样的“荒唐”事:我们只是想要实现一个函数算法,而这个时候必须还要给出一个类来放置这个方法;同样,如果在其他地方要调用这个方法,必须通过创建该类来实现调用。在Kotlin中直接提供了一等函数类型(First-Class Function Type),其跟普通类型一样,函数类型可以作为值来传递,也可以作为返回值。
此外,还有其他的经验教训,上面所述只是其中的一部分。
不可否认的是,C、C++和Java语言都是非常优秀的编程语言。但是事物总是不断发展变化的。就像C++语言是对C语言的继承与发展,Java语言是对C++语言的继承与改造,而Kotlin语言也是对Java语言的继承与变革。