类和对象
-
定义类的语法
[修饰符] class 类名 [constructor 主构造器]{ 0...n 个次构造器 0...n 个属性 0...n 个方法 } 修饰符: public internal private protected 只能出现一个 final open abstract
-
修饰符
- 类声明默认是public final的
- open是final的反义词,用于修饰类、方法或属性,表示类可派生子类,方法或属性可被重写
包和导入
-
一旦在Kotlin程序中使用了package关键字,就意味着该源程序中定义的所有类、函数都属于这个包。位于包中的每个类的完整类名都应该是包名和类名的组合。包中的类在文件系统中也必须有与包名相同的目录结构(这点与java相同),Java 和 Kotlin 的 package 语句指定源文件编译之后的路径,与源文件的路径无关,一般情况下我们最好把 package 语句与源文件路径写成一致
-
如果没有指明包,该文件的内容属于无名字的默认包
-
Kotlin中的import相当于java中的import和import static(静态导入)的合并。不仅可以导入类,还可以导入
- 顶层函数以及属性
- 在对象声明中声明的函数和属性
- 枚举常量
-
如果在同一源文件中导入不同包中的类,在java中很难处理,必须有一个类使用全限定名(即放弃导入包)。而Kotlin使用更简单的处理方式。即import语句后面增加as关键字,指定导入类的别名
import java.util.* import java.sql.Date as SqlDate var d = Date() var s = SqlDate(System.currentTimeMillis()) println(d) println(s)
-
Kotlin的默认导入
kotlin.* kotlin.annotation.* kotlin.collections.* kotlin.comparisons.* kotlin.io.* kotlin.ranges.* kotlin.sequence.* kotlin.text.*
-
对于JVM平台,还会自动导入
java.lang.* kotlin.jvm.*
-
对于JavaScript平台,额外导入
kotlin.js.*
访问控制
-
Kotlin提供四种访问控制符
- private: 与java的private类似,private成员只能在该类的内部或文件的内部被访问,如果顶层声明是 private 的,它是声明它的文件所私有的
- internal:internal成员可以在该类的内部或文件的内部或同一模块内访问
- protected:protected成员可以在该类的内部或文件的内部或者子类中被访问
- public:public成员可以在任意地方被访问
-
Kotlin与java访问控制符的区别
- Kotlin默认的访问控制符是public。java默认访问权限是包访问权限
- Kotlin取消了protected的包访问权限
-
Kotlin使用internal代替java的包访问权限控制符是很好的改进。java的包访问控制符有时不方便。比如在同一个项目组开发,同一个模块内不同类型的应用组件肯定放在不同的子包中。此时如果同一模块中的不同成员使用的是包访问控制符,那么他们之间就不能互相访问,只能改为使用public修饰符。
构造方法
-
Kotlin通过构造器返回类的对象(不用使用new)
-
一个Kotlin类可以有0~1个主构造器,0~n个次构造器。主构造器是类头的一部分,如果主构造器没有任何注解或修饰符,则可以省略constructor关键字,如果构造函数有注解或可见性修饰符,constructor不可省略。如果没有显示的为非抽象类定义任何构造器,系统会自动提供一个无参主构造器(public修饰),一旦为一个类提供了构造器,系统将不再为该类提供构造器
class User constructor(username: String) { } 等价 class User(username: String) { }
-
主构造器作为类头的一部分,只有形参,没有执行体(初始化代码可以放到初始化块中)。主构造器形参的作用
- 初始化块(使用关键字init)可以使用主构造器定义的形参。
- 在声明属性时可以使用主构造器定义的形参
- 从以上两点可以看出,Kotlin的主构造器与Java的构造器不相同,它更像java初始化块的增强(java的初始化块不能传入参数)。
-
案例:从以下输出可以看出,当程序通过主构造器创建对象时,系统其实就是调用该类里定义的初始化块。如果一个类中有多个普通初始化块,则按照定义的顺序依次执行(一般情况下,会把多个初始化块合并为一个,可读性更强)。如果在Kotlin中想为对象的属性显示指定初始化值,则可以通过初始化块来指定
fun main() { /** * 局部变量a>3 * 初始化(1)块执行,jannal... * 初始化(2)块执行,jannal... * jannal */ var h = HelloWorld("jannal", 1) println(h.name) } class HelloWorld(name: String, count: Int) { var name: String var count: Int init { var a = 6 if (a > 3) { println("局部变量a>3") } println("初始化(1)块执行,${name}...") } init { println("初始化(2)块执行,${name}...") } init { this.name = name this.count = count } }
-
Kotlin要求所有的次构造器都必须先调用主构造器(执行初始化块中的代码),即每个次构造器函数需要委托主构造函数。这种设计与java类似,java会让每个构造器(java的构造器相当于Kotlin中的次构造器)先调用初始化块(java的初始化块相当于Kotlin的主构造器)。Kotlin使用
:this(参数)
来委托另一个构造器,到底委托哪个构造器取决于传入的参数。class Man(name: String) { var name: String var age: Int var info: String? = null init { println("Man init") this.name = name this.age = 0 } //次构造器委托主构造器,java中委托this(name),kotlin中使用:this(name) constructor(name: String, age: Int) : this(name) { this.age = age } //次构造器委托次构造器。系统会根据传入的参数推断出委托了哪个构造器 constructor(name: String, age: Int, info: String) : this(name, age) { this.info = info } } fun main() { /** * Man init * jack,0,null */ var man = Man("jack") println("${man.name},${man.age},${man.info}") /** * Man init * jack,18,null */ man = Man("jack", 18) println("${man.name},${man.age},${man.info}") /** * Man init * jack,18,hello world! */ man = Man("jack", 18, "hello world!") println("${man.name},${man.age},${man.info}") }
-
Kotlin允许主构造器上声明属性。直接在参数之前使用var或者val即可声明属性(读写或者只读属性)。
- 当程序调用这种方式声明的主构造器创建对象时,传给构造器的参数将会赋值给对象的属性
- 如果主构造的所有参数都有默认值,程序能以构造参数的默认值来调用该构造器(即不需要为构造参数传入值),此时看上去像调用无参的构造器。
class Customer(val name: String = "jannal") { var city: String = "天津" } fun main() { //天津,jack var cus = Customer("jack") println("${cus.city},${cus.name}") //天津,jannal cus = Customer() println("${cus.city},${cus.name}") }
类中的方法
-
分离类中的方法单独使用
class Cat { fun run() { println("run...") } fun eat(food: String) { print("eat ") } } fun main(args: Array<String>) { //将Cat的run方法赋值给变量,获取类中方法的引用,需要在方法前加类的名字. var runFunction: (Cat) -> Unit = Cat::run val cat = Cat() //当程序将run方法独立成函数时,调用run方法的调用者(Cat对象)将作为第一个参数传入 runFunction(cat) //将Car变量的类型赋值给et变量,没有指定,系统自动推断出et的类型是(Cat,String)->Unit var et = Cat::eat et(cat, "老鼠") }
-
方法调用中缀表示法:使用
infix
修饰,这样该方法就可以通过中缀表示法调用,就像此方法是双目运算符一样。需要指出的是,infix修饰的方法只能有一个参数(因为双目运算符后面只能有一个参数)class Cat { fun run() { println("run...") } fun eat(food: String) { println("eat ") } } class Calc(value: Int) { var value = value infix fun add(calc: Calc): Int { return this.value + calc.value } } fun main(args: Array<String>) { var left = Calc(3) var right = Calc(5) //普通方式调用,输出8 println(left.add(right)) //中缀表达式方式调用,输出8 println(left add right) }
componetN
-
Kotlin允许将一个对象的N个属性解构给多个变量,通过
componentN
方法,程序需要解构几个变量,就必须为该对象的类定义几个componentN
方法,并且该方法需要使用operator
修饰//将一个对象的N个属性解构给多个变量,这里是将user的component1和component2解构 var (name, pass) = user class User constructor(username: String, password: String) { var username: String = username var password: String = password /** * componentN方法与解构 */ operator fun component1(): String { return this.username } operator fun component2(): String { return this.password } } fun main(args: Array<String>) { var user = User("jannal", "123456") //将一个对象的N个属性解构给多个变量,这里是将user的component1和component2解构 var (name, pass) = user //使用_来占位,忽略相关的componentN返回值 var (name2, _) = user var (_, pass2) = user //jannal println(name) //jannal println(name2) //123456 println(pass) //123456 println(pass2) }
-
遍历Map的语法解析
//遍历map val mapp = mutableMapOf<String, Any>("key" to 24, "name" to "zhangsan", "age" to 25) // 普通方式 for (entry in mapp){ println("${entry.key}:${entry.value}") } //因为Map.Entry提供operator修饰的componentN方法,所以程序可执行如下解构 // var (key,value) = Map.Entry for ((key, value) in mapp) { println("$key:$value") } //对于具有多个参数的 lambda 表达式,可以使用_字符替换不使用的参数的名称 mapp.mapValues { (_, value) -> "${value}!" }
数据类与多返回值
-
Kotlin本身并不支持定义返回多个值的函数或方法。可以通过让Kotlin返回一个支持解构的对象来达到返回多个值的目的。为了简化解构的实现,Kotlin提供了专门用于封装数据的类(java中的POJO DTO VO等)——数据类
-
数据类需要满足的要求
- data修饰,参数必须有var或者val
- 主构造器至少需要有一个参数
- 数据类不能用abstract、open 、sealed、inner修饰,也不能定义成内部类
- kotlin1.1之前,数据类只能实现接口,之后数据类可继承其他类
-
定义数据类,系统会自动给数据类生成如下内容:
- 生成equals()/hashcode()
- 自动重写toString()
- 为每个属性自动生成operator修饰的componentN()方法
- 生成copy()方法,用于完成对象复制
-
案例
//定义一个数据类 data class Result(val status: Int, val message: String) fun status(): Result { return Result(200, "success") } //通过结构获取函数多返回值 var (status, message) = status()
-
通过IDEA查看Tools->Kotlin->Show Kotlin Bytecode
可以看到数据类反编译为java之后 @Metadata( mv = {1, 1, 15}, bv = {1, 0, 3}, k = 1, d1 = {"\u0000 \n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\b\n\u0000\n\u0002\u0010\u000e\n\u0002\b\t\n\u0002\u0010\u000b\n\u0002\b\u0004\b\u0086\b\u0018\u00002\u00020\u0001B\u0015\u0012\u0006\u0010\u0002\u001a\u00020\u0003\u0012\u0006\u0010\u0004\u001a\u00020\u0005¢\u0006\u0002\u0010\u0006J\t\u0010\u000b\u001a\u00020\u0003HÆ\u0003J\t\u0010\f\u001a\u00020\u0005HÆ\u0003J\u001d\u0010\r\u001a\u00020\u00002\b\b\u0002\u0010\u0002\u001a\u00020\u00032\b\b\u0002\u0010\u0004\u001a\u00020\u0005HÆ\u0001J\u0013\u0010\u000e\u001a\u00020\u000f2\b\u0010\u0010\u001a\u0004\u0018\u00010\u0001HÖ\u0003J\t\u0010\u0011\u001a\u00020\u0003HÖ\u0001J\t\u0010\u0012\u001a\u00020\u0005HÖ\u0001R\u0011\u0010\u0004\u001a\u00020\u0005¢\u0006\b\n\u0000\u001a\u0004\b\u0007\u0010\bR\u0011\u0010\u0002\u001a\u00020\u0003¢\u0006\b\n\u0000\u001a\u0004\b\t\u0010\n¨\u0006\u0013"}, d2 = {"Lcn/jannal/kotlin/Result;", "", "status", "", "message", "", "(ILjava/lang/String;)V", "getMessage", "()Ljava/lang/String;", "getStatus", "()I", "component1", "component2", "copy", "equals", "", "other", "hashCode", "toString", "kotlin-object"} ) public final class Result { private final int status; @NotNull private final String message; public final int getStatus() { return this.status; } @NotNull public final String getMessage() { return this.message; } public Result(int status, @NotNull String message) { Intrinsics.checkParameterIsNotNull(message, "message"); super(); this.status = status; this.message = message; } public final int component1() { return this.status; } @NotNull public final String component2() { return this.message; } @NotNull public final Result copy(int status, @NotNull String message) { Intrinsics.checkParameterIsNotNull(message, "message"); return new Result(status, message); } // $FF: synthetic method public static Result copy$default(Result var0, int var1, String var2, int var3, Object var4) { if ((var3 & 1) != 0) { var1 = var0.status; } if ((var3 & 2) != 0) { var2 = var0.message; } return var0.copy(var1, var2); } @NotNull public String toString() { return "Result(status=" + this.status + ", message=" + this.message + ")"; } public int hashCode() { int var10000 = this.status * 31; String var10001 = this.message; return var10000 + (var10001 != null ? var10001.hashCode() : 0); } public boolean equals(@Nullable Object var1) { if (this != var1) { if (var1 instanceof Result) { Result var2 = (Result)var1; if (this.status == var2.status && Intrinsics.areEqual(this.message, var2.message)) { return true; } } return false; } else { return true; } } }
-
@Metadata 信息存在于由 Kotlin 编译器生成的所有类文件中 , 并由编译器和反射读取
-
对Lambda表达式的解构: Kotlin允许对Lambda表达式使用解构,如果Lambda表达式的参数是支持解构的类型(如Pair、Map.Entry等),它们都具有operator修饰的componentN()方法,那么即可通过将它们放在括号中引入多个新参数来代替带个参数
mapp.mapValues { entry -> "${entry.value}!" } mapp.mapValues { (key,value) -> "${key},${value}!" } mapp.mapValues { (_,value) -> "${value}!" }
-
Lambda表达式包含两个参数和使用解构的区别: Kotlin中Lambda表达式多个参数是不需要括号的,如果使用括号就是使用解构
{a -> ...} 一个参数 {a,b -> ...} 两个参数 {(a,b)->...} 一个解构对 {(a,b),c -> ...} 一个解构和第三个参数
Kotlin提供的数据类
-
Pair(二元组)与Triple(三元组)
public data class Pair<out A, out B>( public val first: A, public val second: B ) : Serializable { public override fun toString(): String = "($first, $second)" } //二元组的中缀表达式 public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that) public fun <T> Pair<T, T>.toList(): List<T> = listOf(first, second) public data class Triple<out A, out B, out C>( public val first: A, public val second: B, public val third: C ) : Serializable { public override fun toString(): String = "($first, $second, $third)" } public fun <T> Triple<T, T, T>.toList(): List<T> = listOf(first, second, third)
-
使用Pair来初始化一个Map
val map= mapOf(1 to "A", 2 to "B", 3 to "C") println(map)
属性和字段
-
Kotlin中的属性相当于java的字段(field)再加上该字段的getter和setter方法(如果是只读字段,则没有)
-
Kotlin中var定义读写属性,val定义只读属性,系统会为只读属性生成getter方法,为读写属性生成getter和setter方法。在定义普通属性时,需要显示指定初始化值,在定义时指定或者在构造器中指定。Kotlin虽然确实会为属性生成getter和setter方法,但是由于源程序中并未真正定义这些getter和setter方法,所以Kotlin程序不允许直接调用Address对象的getter和setter方法。但是如果Java程序来调用Address类,只能通过getter和setter方法访问属性
/** * 在kotlin类中定义属性后,被Kotlin程序使用时只能使用点语法访问属性 * 被java程序使用时只能通过getter和setter方法访问属性 */ class Address { var street: String = "" var provice = "" var postCode: String? = null var city = "" } var address = Address() address.city = "信阳" address.provice = "河南" println(address.city)
-
自定义getter和setter方法
-
定义getter或setter方法无需使用fun关键字
class Person(first: String, last: String) { var first: String = first var last: String = last /**1. 自定义getter和setter方法不需要fun关键字 * 2. fullName是只读属性,因此只能重写get方法不能重写setter方法 * 3. 对于fullName,Kotlin不需要为该属性生成对应的field(字段) */ val fullName: String get() { println("执行fullName的getter方法") return "${first}.${last}" } }
-
-
如果仅仅是改变getter或者setter方法的可见性或者对其添加注解,但不需要修改默认的行为,可以只定义getter或setter方法名,不需要重新定义代码实现
var city: String = "北京" private set var food: String = "苹果" @Inject set
幕后字段
-
在Kotlin中定义一个普通属性时,Kotlin会为属性生成一个field(字段)、getter和setter方法。Kotlin为属性生成的field被称为幕后字段(backing field)。
-
如果Kotlin类的属性有幕后字段,则Kotlin要求为该属性显示指定初始化(定义时或者构造器中指定)。如果Kotlin类的属性没有幕后字段,则Kotlin不允许为该属性值指定初始化值(因为没有field)
-
满足以下条件为属性生成幕后字段
-
该属性使用Kotlin自动生成的getter或setter方法
-
重写getter或setter方法时,使用field关键字显示引用了幕后字段
class Person(first: String, last: String) { /** * 1. kotlin中定义一个普通属性时,kotlin会为该属性生成一个field(字段)、getter、setter(只读属性没有) * kotlin为该属性所生成的field就被称为幕后字段(backing field). * 2. kotlin要求幕后字段必须显示指定初始值,定义的时候或者构造器中指定 * 3. 对于只读属性,如果重写getter方法,对于读写属性,如果不重写getter、setter方法 * kotlin总会为该属性生成幕后字段 * 4. 重写getter、setter方法时,使用field关键字显示引用了幕后字段 */ var first: String = first var last: String = last /**1. 自定义getter和setter方法不需要fun关键字 * 2. fullName是只读属性,因此只能重写get方法不能重写setter方法 * 3. 对于fullName,Kotlin不需要为该属性生成对应的field(字段) */ val fullName: String get() { println("执行fullName的getter方法") return "${first}.${last}" } var fullName2: String get() { return "${first}.${last}" } set(value) { val split = value.split(".") this.first = split[0] this.last = split[1] } /** * 可以省略{} */ val fullName3: String get() = this.first + this.last /** * 1. 重写getter setter方法,使用field显示引用幕后字段 * 2. 当程序重写getter、setter方法时,不能通过点语法来对password赋值,因为点语法 * 本质是调用getter和setter方法,这样就形成了无限递归 * 3. 通过field引用幕后字段,从而实现对幕后字段赋值 */ var password: String = "123456" set(newPassword) { field = newPassword } var city: String = "北京" private set /* var food: String = "苹果" @Inject set*/ }
-
幕后属性
-
幕后属性就是使用private修饰的属性。此种方式比较繁琐,通常没必要采用这种方式
fun main() { var bp = BackProperty("jannal") //jannal println(bp.name) bp.name = "孙悟空" //孙悟空 println(bp.name) //无法访问 //println(bp.username) } class BackProperty(name: String) { //幕后属性 private var username: String = name var name get() = username set(newName) { if (newName.length > 8) { println("用户名必须大于8位") } else { username = newName } } }
延迟初始化属性
-
Kotlin提供了lateinit修饰符来解决属性的延迟初始化,使用lateinit修饰的属性,可以在定义该属性时和在构造器中都不指定初始值
-
对lateinit的限制
- lateinit只能修饰在类体中声明的可变属性(使用val声明的属性不行,主构造器中声明的属性也不行)
- lateinit修饰的属性不能有自定义getter或setter方法
- lateinit修饰的属性必须是非空类型
- lateinit修饰的属性不能是原生类型
-
与java不同的是,kotlin不会为属性执行默认初始化。如果在lateinit属性赋值初始化之前,程序将引发
lateinit property name has not been initialized
异常fun main(args: Array<String>) { var user = UserInfo() //由于name和password两个属性都没有初始化值,因此在创建User //对象后不能立即访问它的name和password属性,否则将会引发异常 // 以下程序运行时会报错lateinit property name has not been initialized //println(user.name) //初始化后就不会抛异常 user.name = "jannal" println(user.name) } class UserInfo { lateinit var name: String lateinit var password: String }
内联属性
- 从Kotlin1.1开始,inline修饰符可以修饰没有幕后字段的属性的getter或setter方法。inline如果直接修饰属性本身,相当于同时修饰属性的getter和setter方法。inline修饰属性与内联函数一样,程序在调用getter或setter方法时也会执行内联化