简单来说一下 Kotlin ,Kotlin 是 JetBrains 在2011年推出的一门全新的编程语言,可运行在 JVM (Java 虚拟机)上。Kotlin 可以与现有的 Java 语言保持 100% 的兼容性,而且 Kotlin 代码比 Java代码更简洁、更富有表现力。另外,Kotlin 还可以直接编程成 JavaScript 代码实现前端编程和后端编程。
学习 Kotlin 的另一个重要的原因是因为,Google 已推荐 Kotlin 最为 Android 开发的一等语言和 Java 并肩使用,这将意味着 Kotlin 将会在 Android 开发中大放异彩。接下来,开始学习吧!
Kotlin 的属性
Kotlin 的 属性 不同于 Java 中的 成员变量(field)。Kotlin 中的属性相当于 Java 的字段(field)再加上 getter 和 setter 方法,而且 Kotlin 默认已经实现了 getter 和 setter 方法。
1.1 属性
Kotlin 使用 val 定义只读属性,使用 var 定义读写属性,系统会为只读属性生成 getter 方法,会为读写属性生成 getter 和 setter 方法。在定义 Kotlin 的普通属性时,需要程序员显式指定初始值:要么在定义时指定初始值,要么在构造器中指定初始值。
class Student {
var name = "" //可以推断出属性的类型
var id:Int = 0
var major:String = ""
val gender:Boolean = true
}
fun main(args: Array<String>) {
var stu = Student()
stu.name = "Tomdog"
stu.id = 123
stu.major = "计算机"
// stu.gender = "false" 不能赋值,只读属性
print( stu.name+"\n"+stu.id+"\n"+stu.major+"\n"+stu.gender)
}
在定义属性时,如果系统可以根据属性推断出属性的类型,那么属性就可以不显式指定属性的类型。Kotlin 定义一个属性,就相当于一个 Java 类的 private 修饰的 field ,以及 public 、final 修饰的 getter 和 setter 方法。需要提及是是,虽然 Kotlin 确实会为属性生成 getter 、setter 方法,但是由于源程序中并未真正定义这些 getter、setter 方法,因此 Kotlin 程序并不允许直接调用 Student 对象的 getter 、setter 方法。但是如果 Java 程序来调用 Student 类,由于该 Student 类中各属性对应的 field 都用 private 修饰,因此不能用点语法直接访问这些 field,所以只能用 getter 、setter 方法来访问属性。
1.2 Java成员变量
在 Java 语言中,根据定位位置的不同,可以将变量分成两大类:成员变量和局部变量。成员变量和局部变量的运行机制存在较大差异:
成员变量:在系统加载类或创建该类的实例时,系统自动为成员变量分配内存空间,并在分配内存空间后,自动为成员变量指定初始值(Kotlin 的普通属性需要赋值)
。
局部变量:布局变量定义后,必须经过显式初始化后才能使用,系统不会为局部变量执行初始化。这意味着定义局部变量后,系统并未这个变量分配内存空间,直到等到程序为这个变量赋值初始化时,系统才会为局部变量分配内存,并初始化值保持到这块内存中。
局部变量与成员变量不同的是,局部变量不属于任何类或实例,因此它总是保存在其所在方法的栈内存
中。如果局部变量是基本类型的变量,则直接把这个变量的值保持在该变量对应的内存中;如果局部变量是一个引用类型的变量,则整个变量里存放的是地址,通过该地址引用到该变量实际引用的对象或数组。
栈内存中的变量无须系统垃圾回收,往往随方法或地址块的运行结束而结束。因此,局部变量的作用域是从初始化该变量开始,直到该方法或代码块运行完成而结束。因为局部变量只保存基本类型的值或对象的引用,因此局部变量所占的内存区通常比较小。
自定义getter 和 setter
在定义属性时可指定自定义的 getter 和 setter 方法,这些方法可加入自己的控制逻辑,其中 getter 是一个形如 get(){} 方法(也可使用单表达式方法体),getter 应该是无参数、带一个返回值的方法;setter 是一个形如 set(value) {} 的方法(也可使用单表达式方法体),setter 应该是带一个参数、无返回值的方法。
对于只读属性来说,由于该属性只有 getter 方法,因此只能重写 getter方法;对于读写属性来说,由于该属性既有 getter 方法,也有 setter 方法,当属性有默认值时,开发者可以更新需要重写其中之一;当属性没有默认值时,必须重写两个方法。
class User(first:String,last:String) {
var first:String = first
var last :String = last
var fullName
get() = "${first}.${last}"
set(value) {
println("执行fullname 的 setter 方法")
if("." !in value || value.indexOf(".") != value.lastIndexOf(".")){
println("你输入的fullName 不合法")
}else{
var token = value.split(".")
first = token[0]
last = token[1]
}
}
}
fun main(args: Array<String>) {
var user = User("小明","李")
user.fullName = "小刚.刘"
//实际上调用的就是 gettter 方法
println(user.fullName)
println(user.last + user.first)
}
输出:
执行fullname 的 setter 方法
小刚.刘
刘小刚
从上面的输出可以看出,程序访问 user.fullName 时实际上调用了 fullName 的 getter 方法。虽然 fullName 是一个读写属性,但其实该属性并不保存状态,因此 Kotlin 依然不需要维该属性生成 field(Java中叫成员变量) ,故该属性不能指定初始值。
幕后字段
在 Kotlin 中定义一个普通属性时,Kotlin 会为该属性生成一个 field (字段)、getter 和 setter 方法(只读属性没有 setter 方法)。Kotlin 为该属性所生成的 field 就被称为幕后字段(backing field)。需要强调的是:如果 Kotlin 类的属性有幕后字段,则 Kotlin 要求为该属性显式指定初始值——要么在定义时指定,要么在构造器中指定;如果 Kotlin 类的属性没有幕后字段,则Kotlin 不允许为该属性指定初始值(由于没有 field,即使指定了初始值也没有地方保存)。
只要满足以下条件,系统就会为属性生成幕后字段:
- 该属性使用 Kotlin 自动生成的 getter 和 setter 方法或其中之一。换句话说,对于只读属性,必须重写 getter 方法;对于读写属性,必须重写 getter 、setter 方法;否则总会为该属性生成幕后字段。
- 重写 getter 、setter 方法时,使用 field 关键字显式引用了幕后字段。
class Person(name:String,age:Int) {
var name = name
set(value) {
if(value.length > 5 || value.length < 2){
println("你设置的人名不符合要求")
}else{
field = value
// this.name = value //这是Java中常用的方式,但是在 Kotlin 中是错误(java.lang.StackOverflowError)的会造成无限递归
}
}
var age = age
set(value) {
if(value > 100 || value < 0){
println("你设置的年龄不合法")
}else{
field = value
}
}
}
fun main(args: Array<String>) {
var person = Person("tomdog",25)
person.name = "tom"
println("${person.name} ${person.age}")
person.age = 130
println("${person.name} ${person.age}")
}
输出:
tom 25
你设置的年龄不合法
tom 25
上面的程序定义 name 、age 两个属性。并重写了两个属性的 setter 方法。可以发现,当程序重写getter 和 setter 方法时,在方法内不能通过点语法对 name、age赋值。原因是:加入在 name 的setter 方法中使用点语法对 name 赋值,由于点语法的本质是调用 setter 方法,这样就会造成在 setter 方法中再次调用 setter 方法,从而形成无限递归。因此,上面程序重写 name、age 属性的setter 方法时,实际上是通过 field引用幕后字段,从而实现对幕后字段的赋值。
幕后属性
如果开发中希望自己定义 field ,并为该 field 提供 setter 、getter 方法,就像 Java 所使用的方法,此时可使用 Kotlin 的幕后属性。幕后属性就是用 private 修饰的属性。
Kotlin 不会为幕后属性生成任何 getter 、setter 方法。因此程序不能直接访问幕后属性,必须有开发者为幕后属性提供 getter 、setter 方法。
class Person2(name:String) {
private var backingName:String = name
var name
get() = backingName
set(value) {
if(value.length > 5 || value.length < 2){
println("你设置的人名不符合要求")
}else{
backingName = value
}
}
}
fun main(args: Array<String>) {
var p = Person2("tomdog")
println(p.name)
p.name = "tom"
println(p.name)
}
上面程序中定义了一个用 private 修饰 backingName 属性,该属性就是一个幕后属性,Kotlin 不会为该属性生成 getter 、setter 方法,因此程序无法直接访问 Person2 对象的 backingName 属性。接下来程序定义了一个 name 属性,并重写了 name 属性的 getter 、setter 方法,重写的 getter、setter 方法实际上访问的是 backingName 幕后属性的值。上面的这种做法就是 Java 的做法:先定义一个 private 修饰的字段,然后再为该字段定义 public 修饰的 getter 、setter 方法。这种方式比较繁琐,通常没有必要这样做。
延迟初始化属性
正如前面所介绍的,Kotlin 要求所有属性必须有开发者显式初始化——要么在定义该属性时赋值初始化;要么在构造器中对该属性赋初始值。但是在某些时候,这不是必须的。比如,我们可能通过依赖注入为属性设置初始值,或者在单元测试的 setUp 方法中初始化该属性等等。总之,并不需要在定义属性时或在构造器中对属性执行初始化。
Kotlin 提供了 lateinit修饰符来解决属性的延迟初始化。使用 lateinit 修饰的属性,可以在定义该属性时和在构造器中都不指定初始化。对 lateinit 修饰符有一下限制:
- lateinit 只能修饰在类体中可变属性(使用 val 声明的属性不行,在主构造器中声明的属性也不行)
- lateinit 修饰的属性不能有自定义的 getter 和 setter 方法
- lateinit 修饰的属性必须是非空类型
- lateinit 修饰的属性不能是原生类型(即 Java 中的8种基本类型)
Kotlin 不会为延迟属性执行默认初始化。因此,如果在 lateinit 属性赋初始值之前访问它,程序将会引发异常。
class Person3 {
lateinit var name:String
lateinit var birth:Date
// lateinit var gender:Boolean //Java 原始基本类型不能这样修饰
// lateinit var id:Int
}
fun main(args: Array<String>) {
var per = Person3()
// println("${per.name} ${per.birth}") //引发lateinit property name has not been initialized异常
per.name = "tomdog"
per.birth = Date()
println("${per.name} ${per.birth}")
}
内联属性
从 Kotlin 1.1 开始,inline修饰符可修饰没有幕后字段的属性的 getter 个 setter 方法,即可单独修饰属性的 getter 或 setter 方法;也可以修饰属性本身,这相当于同时修饰该属性的 getter 和 setter 方法。对于使用 inline 修饰的 getter 和 setter 方法,就像内联函数一样,程序在调用 getter 和 setter 方法时也会执行内联化。
class Address(province:String,city:String){
var province = province
var city = city
}
class Person3 {
var address:String? = null
val personProvince:Address
inline get() = Address("河南","")
var personCity:Address
get() = Address("","郑州")
inline set(value) {
this.address = value.city
}
//inline 修饰属性本身,表明读取和设置属性都会内联化
inline var fullAddress:Address
get() = Address("广东","广州")
set(value) {
this.address = value.city+value.province
}
}
站在巨人的肩膀上
官方文档
疯狂 Kotlin 讲义 —— 李刚