关于现代开发语言的思考
像 Java, Objective-C 这些上个世纪的开发语言越来越没法满足现代移动开发的需要,所以要有一些更现代化的语言和编译器,它能够够更像人类的语言,更符合人类的表达习惯,更加简洁,更加富有语义性,能够根据上下文环境判断我们想表达东西并理解我们的简述,能够察觉或者纠正我们的语法错误,能够有一些语法之内或之外的一些约定俗成的表达习惯,能够很好地支持面向对象编程、函数式编程以及响应式编程,最重要的还是在简洁高效的同时依然能保证很高的可读性。这些要求其实对于高级语言来说大多只是针对编译器的,而编译器作为桥梁也是高级语言的一部分,所以我们迫切地需要更现代化的高级语言和更现代化的编译器。
JetBrains 2011 年开发了更现代化的语言 Kotlin 用来弥补 Java 的不足。
Apple 2014 年开发了更现代化的语言 Swift,用来弥补 Objective-C 的不足。
Google 2011 年开发了更现代化的语言 Dart,一种可以构建Web, 服务器和移动应用的通用开发语言。
这些语言都很大程度上满足了上面所说的一些要求,但 Kotlin 要与 Java 互操作,所以有很多限制,Swift 也同样会受 Objective-C 相关的限制,而 Dart 可以更自由地成长,语法风格也是我比较喜欢的风格。Kotlin 虽然目前的使用率很低,但是毕竟相对于 Java 来说还是有特别多的优点的,所以即使现在不使用或者之后也不想使用,那至少能读懂 Kotlin 代码也是很有用处的。
Kotlin 语法
Elvis 运算符
在 Java 中,三目运算符 a > b ? a : b
可以简化这样的 if 语句:
if (a > b) {
return a;
} else {
return b;
}
复制代码
但是如果用来判断并设置默认值就显得乏力了,如:
person.getAvater() != null ? person.getAvatar() : DEFAULT_AVATAR;
复制代码
如果你不想函数执行两次或者执行两次后会有副作用那就无法使用这个运算符了,只能通过判断语句实现,但是这种缺省赋值的情况还是有很多的,我们为什么不能通过一个运算符就指定为空时的缺省值呢?类似这样:
person.getAvater() ?: DEFAULT_AVATAR;
复制代码
这种 ?:
二目运算符也被称为 Elvis operator,因为和 Elvis Presley 的表情符号 一样。在 Kotlin 中利用 ?:
可以简化大部分情况下的缺省赋值或者条件控制语句,而且 ?:
右边也可以选择直接抛出异常。而三目运算符可以使用 if else
表达式替代。
空类型检查
在 Java 中,规定某个参数是否可以为空只能通过额外的注解标注,而在 Kotlin 中只需要用一个 ?
修饰可以为空的程序元素即可,如 val s: String? = null
,Kotlin 的类型系统会检测程序中可以为空或不可以为空的值以在编译时消除 NullPointerException
,也就是说如果不显式地使用 ?
标记那么常见类型默认就是不能为空的。
安全调用运算符 ?.
可以将空检查和方法调用合并成一个操作,如 s?.toUpperCase()
等同于 if(s != null) s.toUpperCase() else null
,而这个表达式的结果是 String?
类型的。这个操作符还可以用来访问属性。!!
可以对值进行非空断言 val sNotNull: String = s!!
如果 s 为空会显式地在这一行抛出异常。
安全调用 let
只在表达式不为空时执行 lambda email?.let { sendEmailTo(it) }
可以定义一些扩展函数简化一些空类型检查,如 fun String?.isNullOrBlank(): Boolean = this == null || this.isBlank()
可以这样被使用 input.isNullOrBlank()
,而 input
是 String?
类型的。
类型转化
在 Java 中,对象转化时不要求先判断对象类型再强制转化,所以很难避免 ClassCastException
,而 Kotlin 将检查和转化合并成一次操作,这样一旦检查通过就不需要额外的转化操作,也就不会出现未经检查的类型转换了:
if (value is String)
println(value.toUpperCase())
复制代码
as?
运算符也可以尝试把值转换成指定的类型,不合适的话就返回 null
,可以结合 Elvis 运算符一起使用 val otherPerson = o as? Person ?: return false
。
表达式体
在 Java 中,所有控制结构都是语句,赋值操作是表达式,而在 Kotlin 中,除了 for
、while
循环外的大多数控制结构都是表达式,也就是说即便是 if
结构也是表达式,而赋值操作则是语句。表达式是有值的,可以作为另一个表达式的一部分使用,所以区分 expression 和 statement 依旧很重要。
如果函数体写在花括号中,我们说这个函数有 代码块体(block body),如果函数直接返回了一个表达式,那么我们说这个函数有 表达式体(expression body),而在 Kotlin 中表达式体函数可以简化,省略花括号和 return
,改为赋值语句:
fun max(a: Int, b: Int): Int = if (a > b) a else b
复制代码
类型推导
在 Java 中,每个变量必须显式并精确地指定类型,而 Kotlin 使用了类型推导机制,很多情况下你不需要显式或精确地指定类型。比如表达式体函数的返回类型可以忽略,因为编译器会分析表达式体并把它的类型作为函数的返回类型:
fun max(a: Int, b: Int) = if (a > b) a else b
复制代码
也正是因为省略,声明变量时必须以 val
或 var
关键字开始,val
是 value 的缩写,用来声明不可变引用,也就是说 val
声明的变量不能在初始化后再次赋值,即只读/只能赋值一次。var
是 variable 的缩写,用来声明可变引用。应该尽可能使用 val
,这样有利于函数式编程。val
引用自身是不可变的,但它指向的对象可能是可变的。即使 var
允许变量改变自己的值,但是类型却是改变不了的。如果变量没有在声明时初始化,就必须在声明时显式地指定类型:
val answer: Int
answer = 42
复制代码
属性
在 Java 中,private 字段和对应的 getter/setter
方法(也叫访问器)的组合通常被称为属性,而 Kotlin 简化了属性的声明,只需要使用 val
或 var
关键字像声明普通变量一样即可:
class Person {
val name: String,
var isMarried: Boolean
}
复制代码
在 Java 中使用的话可以使用 person.getName()
,person.isMarried()
,person.setMarried()
方法,而在 Kotlin 中使用的话就可以简化为 person.name
,person.isMarried
,虽然本质还是调用 getter/setter
方法。
when
在 Java 中,使用 switch
结构进行条件判断处理有很多限制,在 Java 7 之前只支持 byte
、short
、char
、int
、enum
类型,之后才支持 String
,而且 case
子句只能是常量,default
子句虽然可省但一般为了安全必须要写。而在 Kotlin 中 when
表达式比 switch
结构更强大。when
表达式中多个值合并到一个分支时用逗号隔开,when
可以没有参数,分支条件就是任意布尔表达式。使用 when
加 is
进行类型判断转换,when
加 in
进行区间检查更加方便。
命名参数和参数默认值
常规调用函数时必须根据参数顺序赋值,可以省略的只有排在末尾的有默认值的参数,但命名参数调用时就可以随便省略了。
顶层函数与顶层属性,扩展函数与扩展属性
在 Kotlin 中,函数和属性可以在类外面定义,也就不需要去写只包含静态方法的 util 工具类了。join.kt
文件对应于 Java 中的 JoinKt
类和其静态方法,可以使用注解改变这个默认类名: @file:JvmName("StringFunctions")
。顶层属性使用 const
修饰等同于 Java 中的 public static final
。
扩展函数可以在类外扩展某个类的功能,是一种特殊的顶层函数,只需要在函数名前加上 接收者类型.
即可,如 fun String.lastChar(): Char = this.get(this.length - 1)
,String
就是接收者类型,可以随便使用它的可访问的属性和方法,this
就是接收者对象,可以省略不写。使用 import
导入扩展函数,使用 as
重命名导入后的类名或函数名。如果扩展函数和成员函数签名一样,成员函数会被优先使用,而且扩展函数是不能被继承的,也就不能被重写。扩展属性必须定义 getter
函数,因为没有字段支持。
可变参数
在 Java 中,不确定数量的参数声明用 ...
表示,而在 Kotlin 中要使用 vararg
关键字,而且赋值时要使用展开运算符 *
主动将数组展开,如 listOf("args: ", *args)
。
中缀调用
使用 infix
关键字修饰的函数可以使用中缀符号进行函数调用,也就是说可以省略 .
和括号,函数名称直接放在目标对象和参数之间,如 val (number, name) = 1 to "one"
。这就要求可以进行中缀调用的函数必须只能有一个参数且不能是 vararg
可变参数,必须是成员函数或扩展函数。
类与继承
接口和抽象类
Kotlin 中的继承采用的是单继承的方式,一个类只能继承一个类,但可以同时实现多个接口,接口不但可以包含抽象方法,还可以包含非抽象方法的默认实现,接口和抽象类的区别在于接口不能存储状态,接口的属性必须是抽象的或者提供访问器的实现。实现接口和继承父类只需要冒号 :
即可,多个接口使用逗号 ,
分隔。
如果一个类同时实现了带默认实现的相同方法的多个接口,那么它就必须显示地实现这个方法,并可以通过类似 super
的形式调用某个父类的实现。
对于接口中声明的属性,要么是抽象的 val email: String
,要么虽然提供了自定义 getter/setter
方法但是没有引用支持字段,在自定义访问器中可以使用 field
来访问支持字段的值,如果你显式地引用或使用默认的访问器实现,编译器会为属性生成支持字段。访问器的可见性和属性一样,不过可以在 get
或 set
关键字前加可见性修饰符修改。
继承限制和可见性限制
Kotlin 中的类和方法默认是 final
的,也就是说禁止被继承,可以通过 open
修饰符修改。
抽象类的成员默认是 open
的,因为抽象类肯定是希望被继承重写的,所以不需要显式地写 open
修饰符。在接口中不能使用 final
、open
或者 abstract
。
Kotlin 可见性默认是 public
的,protected
成员只在类和它的子类中可见,类的扩展函数不能访问它的 private
和 protected
成员。
内部类,嵌套类,密封类
对于类内部定义的类,在 Java 中 非静态的内部类是会隐式持有外部类的引用的,可以直接访问外部类的任何成员方法和变量,而静态内部类就不会持有外部类的引用,也无法直接访问外部类的成员。在 Kotlin 中将非静态的内部类叫做内部类(Inner classes),并必须使用 inner
修饰符修饰,否则的话默认就是不持有外部类引用的静态的内部类,也叫嵌套类(Nested classes)。内部类由于持有外部类的引用,会有内存泄漏的风险,而且内部类序列化也是个问题。内部类访问外部类需要使用 this@Outer
。
不管还是 Java 中的 switch
还是 Kotlin 中的 when
,我们通常都会提供一个默认的分支,以便能够处理任何其它分支都不匹配的情况,但是如果逻辑发生了更改,比如新增了一个分支,编译器无法察觉,我们自己也可能忘了处理,那么程序就会走到默认的分支,产生无法预料的 Bug,尤其是当 when
表达式是检测类的子类型的时候,而在 Kotlin 中可以通过 sealed
关键字修饰一个类以限制类的继承。这种类也被叫做密封类,其实也是枚举类的拓展,密封类的所有子类必须在同个文件中声明,由于密封类肯定是希望被继承的,所以 open
是不需要的。
构造器
如果主构造器没有注解或可见性修饰,那么 constructor
关键词可以省略。如果子类没有提供任何构造器,那么必须显式调用父类构造器。
让编译器自动生成模板代码
对于只用来存储数据的数据类,在 Kotlin 中成为数据类(Data Classes),可以使用 data
修饰,data class User(val name: String, val age: Int)
,然后 IDE/编译器 就会帮你自动生成 equals()/hashCode()
函数比较主构造器中的值并生成唯一的 hashCode,自动生成 toString()
函数格式类似于 "User(name=John, age=42)"
,自动生成 componentN()
函数用于解构声明,自动生成 copy()
函数用于创建副本。
对于 final
类,通常使用装饰者模式进行扩展,而这些模板代码也可以让 IDE/编译器 自动生成,只需要通过 by
关键字委托对象即可。
单例
创建类和该类的唯一实例可以放在一起声明,使用 object
关键字,和声明普通类不同的是不能声明构造器。也可以在类里面声明一个单例对象。虽然这种单例类的声明方式更简洁,但是却无法控制构造的参数和过程,所以依赖注入还是最好的选择。
伴生对象
在 Kotlin 中没有 static
关键字也没有静态成员的概念,虽然 Kotlin 的包级别函数和顶层函数可以满足大部分需求,但是它们还是无法访问 private
成员,所以需要一个工厂方法,也就是使用 companion
关键字在类中定义对象,这样使用的时候就不需要显式地指定对象的名字,而是直接通过类名加方法名就可以调用了,与此同时,这个对象完全可以访问类中的 private
成员,包括 private
构造器。伴生对象可以不指定名字,默认名字是 Companion
,伴生对象可以实现一个接口,可以有扩展函数和属性,也就是说,声明一个空的伴生对象并在其他地方声明这个伴生对象的扩展函数有时候是个不错的选择。
对象表达式
可以利用 object
关键字声明匿名对象,匿名对象可以不实现接口也可以实现多个接口,匿名对象不是单例的,每次使用对象表达式都会创建新的对象,可以把表达式的值赋值给一个变量,那么匿名对象也就有名字了。
Lambda 表达式
λ演算 是一个数学逻辑中的形式系统,以变量绑定和替换的规则,来研究函数如何抽象化定义,函数如何被应用及递归。它是一种通用的计算模型,可用于模拟任何图灵机。Lambda 表达式是函数式编程中非常重要的一个概念,通常是指使用特殊的语法所书写的匿名函数,函数可以像普通参数一样赋值给其它变量,也可以作为其它函数的返回值。
Kotlin 中声明 Lambda 表达式的语法类似于 { x: Int, y: Int -> x+y }
,用花括号包裹,用箭头把实参列表和 Lambda 函数体隔开。像函数内声明的匿名内部类一样,函数内使用的 lambda 表达式也可以访问函数的参数以及在 lambda 之前定义的局部变量,而且可以是非 final
的变量,非 final
的变量的值是被封装在一个特殊的包装器中的,这个包装器的引用会和 lambda 代码一起存储,也就是说,如果 lambda 被用作事件处理或者其他异步执行的情况,对局部变量的修改只会在 lambda 中的代码真正执行时发生,这个时候不使用局部变量而是使用类成员变量是个不错的选择。
成员引用
把一个已经定义好的函数作为值传递,只需要使用 ::
运算符来转换 val getAge = Person::age
,这种表达式成为成员引用,双冒号把类名和成员(方法或属性)名隔开。可以引用顶层函数,也就可以不写类名。可以引用扩展函数就像实例成员一样。可以引用构造器,只需要在双冒号后指定类名 val createPerson = ::Person
。
函数式接口
只有一个抽象方法的接口被称为函数式接口,或 SAM(Single Abstract Method)接口,像 OnClickListener
、Runnable
和 Callable
等这些都是函数式接口,函数式接口作为参数的方法在调用时可以使用 lambda 简化,而且如果 lambda 表达式没有访问任何来自定义它的函数的变量时,相应的匿名类实例可以在多次调用时重用:
button.setOnClickListener { view -> doSomething(view) }
复制代码
编译器可以帮助生成 lambda 转换成函数式接口所需的 SAM 构造方法,这个方法只需要一个参数,就是一个被用作函数式接口中唯一的方法的方法体:
fun createAllDoneRunnable(): Runnable {
return Runnable { println("All done!") }
}
复制代码
"with" 和 "apply" 函数
库函数 with
会把第一个参数转换成作为第二个参数传给它的 lambda 的接受者,返回值是执行 lambda 代码的结果,而 apply
函数始终会返回作为实参传给它的对象。
注解
声明注解需要额外的关键字 annotation
来修饰类 annotation class Fancy
,元注解包括:@Target
表明该注解类型可以注解哪些程序元素,如果注解类型不使用 @Target
描述那么表明可以注解所有程序元素,值是 Array
类型:
AnnotationTarget.CLASS
(类、接口、对象,包括注解类)AnnotationTarget.PROPERTY
(属性)AnnotationTarget.FIELD
(字段,包括属性的支持字段)AnnotationTarget.LOCAL_VARIABLE
(本地变量)AnnotationTarget.VALUE_PARAMETER
(函数的实参或构造器)AnnotationTarget.CONSTRUCTOR
(主构造器或从构造器)AnnotationTarget.FUNCTION
(函数,不包括构造函数)AnnotationTarget.PROPERTY_GETTER
(属性的 getter)AnnotationTarget.PROPERTY_SETTER
(属性的 setter)
@Retention
表明该注解是否保留到编译完的 class 文件中,是否可以在运行时通过反射访问,默认都是可以的,也就是说 AnnotationRetention.RUNTIME
:
AnnotationRetention.SOURCE
(注解不会保留二进制输出文件中,即 class 文件中)AnnotationRetention.BINARY
(注解会保留到二进制输出文件中,但对反射不可见)AnnotationRetention.RUNTIME
(注解会保留到二进制输出文件中,并对反射可见,默认是这个策略)
@Repeatable
表明该注解是否能同时注解同一个元素多次。@MustBeDocumented
表明这个注解是公共 API 的一部分,应该包含在自动生成的 API 文档中类或方法签名的地方。
注解可以有构造器和参数,只能拥有以下类型的参数: 基本数据类型、字符串、枚举、类引用、其他的注解类,以及前面这些类型的数组。
如果要把一个类指定为注解的实参,类名后要加 ::class
,如 @MyAnnotataion(MyClass::class)
。
如果把另一个注解指定为注解的实参,那么需要去掉 @
。
如果把一个数组指定为注解的实参,那需要使用数组字面值或者 arrayOf
函数。
如果把属性指定为注解的实参,那么这个属性必须是编译时常量,也就是说用 const
修饰。
由于 Kotlin 中一个简单的程序元素可能会自动生成多个程序元素和其字节码,比如构造器声明中的变量声明可能还隐式包含了属性声明和相应的 getter/setter
方法,所以为了精确地表示你想注解的程序元素,可以使用将目标放在 @
和注解名之间,并用 :
将目标和注解名隔开,如 @get:Ann val bar
表明注解 bar
的 getter
方法。
如果多个注解同时作用于一个元素,可以使用方括号包裹:
class Example {
@set:[Inject VisibleForTesting]
var collaborator: Collaborator
}
复制代码
注解也可以用在 lambda 表达式中,它们将被应用于自动生成 lambda 体的 invoke()
方法。
反射
Kotlin 反射主要依靠 KClass
、KCallable
、KFunction
和 KProperty
。
使用 val kClass = person.javaClass.kotlin
可以返回一个 KClass
的实例,然后就可以通过这个实例获取 kClass.memberProperties
所有属性等信息。
通过 ::
语法可以获取 KFunction
或 KProperty
的实例。
Tips
- Kotlin 不需要方法去 throws 异常,
try
像if
一样也是表达式。 - 函数可以多层嵌套,也就是定义局部函数,以便符合 DRY 原则,减少重复代码。
- 冒号
:
后面一定要加空格。 - 冒号
:
只有在用来分隔子类型和父类型、委托给父类构造器或同类其它构造器以及object
关键词之后这三种情况时冒号前面才加空格。 - 在 Kotlin 中
==
其实是调用equals
方法,===
比较的才是引用。 Any
是 Java 中java.lang.Object
的模拟,is
是 Java 中instanceOf
的模拟。Unit
是 Java 中void
的模拟。- 如果 lambda 表达式是函数调用的最后一个实参,它可以放在括号的外边。如果 lambda 表达式是函数唯一的实参,可以去掉调用代码中的空括号对。
- 如果 lambda 参数的类型可以推导出来,就可以不用写参数类型,如果不显式地指定这个唯一的实参名字那么默认名就是
it
。