方法
Kotlin 中方法和函数其实是统一的,但是我们这么理解区别:
函数:直接定义在文件中的 fun
。
方法:定义在 class
中的 fun
。
方法和函数一样,也是可以赋值给其他对象,也使用双冒号::
运算符。
函数:::函数名
。
方法:类名::方法名
。
fun main() {
// 以下两种函数类型等价
val a1: (Int) -> Int = ::test
val a2: Function1 = ::test
// 以下三种函数类型等价
val b1: (Dog, String) -> String = Dog::eat
val b2: Dog.(String) -> String = Dog::eat
val b3: Function2 = Dog::eat
// 注:FunctionN:N为参数加返回值个数,<>中依次写入 类名,参数类型(0~n)个,返回值类型(0~1)个
println(a1.invoke(2)) //4
println(a2(3)) //9
println(b1.invoke(Dog(), "bone1")) //Dog is eating bone1
println(b2(Dog(), "bone2")) //Dog is eating bone2
println(b3.invoke(Dog(), "bone3")) //Dog is eating bone3
}
// 定义函数
fun test(a: Int) = a * a
// 定义类
class Dog {
// 定义方法
fun eat(food: String) = "Dog is eating $food"
}
注:函数类型均可省略,因为编译器可以自动推断出来。
中缀表示法
Koltin 中的方法也可以使用 infix
来修饰,写法就和双目运算符一样。
需要注意的是:infix
方法只能有一个参数,因为双目运算符的后面只能带一个参数。
componentN 方法与解构
Kotlin 允许将一个对象的 N 个属性 “解构” 给多个变量,写法如下:
var (name, age) = user
Kotlin 实际会执行的代码:
var name = user.component1()
var age = user.component2()
如果希望将对象解构成多个变量的话,那就需要定义多个 componentN()
方法,且该方法需要使用 operator
修饰。
class User(private var name: String, private var age: Int) {
operator fun component1() = this.name
operator fun component2() = this.age
}
fun main() {
// 解构 User
val (name, age) = User("张三", 28)
println("$name 今年的年龄是 $age 岁") //张三 今年的年龄是 28 岁
// 只解构 User 的部分属性,如果想忽略前面属性,可使用 _ 来代替
val (name2) = User("李四", 29)
println("用户的名字是 $name2") //用户的名字是 李四
val (_, age2) = User("王五", 30)
println("用户的年龄是 $age2 岁") //用户的年龄是 30 岁
}
如果只想解构部分属性,那忽略前面属性可以使用 _
来占位。
数据类 data class
数据类专门用来封装数据,由于简化 Java 中某些只定义field
,getter
和setter
方法的类。
数据类型支持解构,从而可以实现返回多个值的函数。
数据类需要满足如下要求:
- 主构造器中至少需要一个参数;
- 主构造器的所有参数需要使用
val
或var
声明为属性; - 不能使用
abstract
、open
、sealed
修饰,也不能定义成内部类; - 可以实现接口,也可继承其他类。
数据类会自动生成如下内容:
- 生成 equals()/hashCode() 方法;
- 自动重写 toString() 方法;
- 为每个属性自动生成 operator 修饰的 componentN() 方法;
- 生成 copy() 方法,用于完成对象复制。
数据类定义
data class DataClass(
val result: Int,
val status: Int
)
数据类使用
fun main() {
val (result, status) = DataClass(1, 2)
println("result: $result, status: $status") //result: 1, status: 2
val d1 = DataClass(1, 1)
val d2 = d1.copy()
println(d1) //DataClass(result=1, status=1)
println(d2) //DataClass(result=1, status=1)
println(d1 === d2) //false,说明 copy() 出来的不是同一个的对象
}
Kotlin 标准库中提供了
Pair
(支持两个任意类型的属性) 和Triple
(支持三个任意类型的属性) 两个数据类。
Lambda 表达式中解构
如果 Lambda 表达式的参数是支持解构的类型,那么就可以在括号中引入多个新参数来替代单个参数。
map.mapValues { entry -> "${entry.value}" }
可以写成
map.mapValues { (_, value) -> "$value" }
Lambda 表达式参数与解构的区别:
{ a -> ... }
:一个参数
{ a, b -> ... }
:两个参数
{ (a, b) -> ... }
:一个参数,并解构成了两个变量
{ (a, b), c -> ... }
:两个参数,第一次参数解构成了两个变量
注意:Lambda 表达式中的参数是不需要圆括号的,如果出现圆括号,那就是使用解构
属性和字段
属性是 Kotlin 中的一个重要特色,相当于 Java 中的field
加上getter
和setter
方法(只读属性没有setter
方法),且开发者不需要自己实现getter
和setter
。
在定义 Kotlin 的普通属性时,必须显示指定初始值,要么在定义时指定,要么在构造器中指定。
// 属性a需要在构造器中初始化,属性b,c直接在定义时初始化
// a,b是只读属性,只有getter方法
// c是读写属性,有getter和setter方法
class Item(val a: String) {
val b = "bbb"
var c = "ccc"
}
在 Kotlin 类中定义属性后,Kotlin 中只能使用点语法来访问属性,Java 中只能使用 getter 和 setter 方法来访问属性。
自定义 getter 和 setter
虽然定义了属性之后,系统会自动生成getter
和setter
方法,但是你还可以自定义这两个方法。无须使用 fun
关键字。
getter
:get() {}
(可使用单表达式),应该是一个无参,有返回值的方法;
setter
:set(value) {}
(可使用单表达式),应该带一个参数,无返回值。
class UserInfo(var first: String, var last: String) {
// 自定义getter和setter
var fullName: String
// 由于fullName是通过first和last计算出来的,所以不需要生成field,所以就不能设置初始值
get() = "$first.$last"
set(value) {
if ("." !in value && value.indexOf(".") != value.lastIndexOf(".")) {
throw IllegalArgumentException("您输入的名称不合法")
} else {
val names = value.split(".")
first = names[0]
last = names[1]
}
}
// 可直接在getter和setter方法名前修改可见性和添加注解,并不修改默认实现
var school = "清华大学"
private set
@Inject get
}
val user1 = UserInfo("张", "三")
println("first: ${user1.first}, last: ${user1.last}, full: ${user1.fullName}") //first: 张, last: 三, full: 张.三
val user2 = UserInfo("张", "三")
user2.fullName = "李.四"
println("first: ${user2.first}, last: ${user2.last}, full: ${user2.fullName}") //first: 李, last: 四, full: 李.四
幕后字段
当定义完属性后,系统自动为属性生成的field
字段就成为幕后字段(backing field)。
只要满足以下条件,系统就会为属性生成幕后字段:
- 该属性使用系统自动生成的
getter
和setter
。 - 重写
getter
和setter
时,使用field
显式引用了幕后字段。
class BackingField(name: String, age: Int) {
var name = name
set(value) {
if (value.length < 2 || value.length > 6) {
println("您输入的姓名不合法!")
} else {
field = value
}
}
var age = age
set(value) {
if (value < 0 || value > 100) {
println("您输入的年龄不合法!")
} else {
field = value
}
}
}
val backingField = BackingField("张三", 29)
backingField.name = "张三三三三三三"
println(backingField.name) //张三
backingField.name = "李四"
println(backingField.name) //李四
在getter
和setter
方法中,需要通过field
关键字来引用幕后字段。
幕后属性
如果需要自定义field,并自定义getter和setter,就可以使用幕后属性(backing property)。
幕后属性就是用private修饰的属性,Kotlin不会为幕后属性提供getter和setter方法。
class BackingProperty(name: String) {
// 定义幕后属性 _name
private var _name: String = name
// name赋值取值都是通过幕后属性 _name 进行的
var name
get() = _name
set(value) {
_name = value
}
}
val backingProperty = BackingProperty("Kotlin")
println(backingProperty.name) //Kotlin
backingProperty.name = "Java"
println(backingProperty.name) //Java
延迟初始化属性
设置延迟初始化后,就可以在定义时和构造方法里不设置初始值了。使用lateinit
关键字修饰。
对lateinit
修饰符有以下限制:
- 只能修饰在类体中声明的可变属性,即
lateinit var
是固定搭配; - 修饰的属性不能有自定义的
getter
和setter
方法; - 修饰的属性必须是非空类型;
- 修饰的属性不能是原生类型(即Java的8种类型对应的类型)。
注意:使用
lateinit
修饰后,Kotlin不会为属性执行默认初始化,如果在赋值之前调用,则会引发lateinit property name has not been initialized
异常。
class LateInit {
lateinit var a: String
lateinit var b: String
}
val lateInit = LateInit()
lateInit.a = "aaa"
lateInit.b = "bbb"
println("${lateInit.a}, ${lateInit.b}") //aaa, bbb
内联属性
可以使用inline
修饰符修饰没有幕后字段的属性的getter
或setter
方法,也可以修饰属性本身,这就相当于同时修饰该属性的getter
和setter
。
被修饰的getter
或setter
方法在调用时会执行内联化。
class InlineProp {
// 定义普通属性,由于有幕后字段,故不能被inline修饰
var name: String = ""
// inline get,不能有幕后字段field
val firstName: String
inline get() = name.split(".")[0]
// inline set,不能有幕后字段field
var lastName: String
inline set(value) {
name = "${name.split(".")[0]}.$value"
}
get() = name.split(".")[1]
// inline prop <=> inline get & set,不能有幕后字段field
inline var userName: String
get() = name
set(value) {
if ("." !in value && value.indexOf(".") != value.lastIndexOf(".")) {
println("您输入的名称不合法")
return
}
name = value
}
}
深入构造器
Kotlin 类可以定义0 ~ 1个主构造器和0 ~ N个次构造器。
如果主构造器没有任何注解或可见性修饰符,则可以省略constructor
关键字
主构造器和初始化块
主构造器的作用:
初始化块可以使用主构造器定义的形参;
在声明属性时可以使用主构造器定义的形参;
class ConstructorTest(name: String) {
// 初始化块中可以直接调用主构造器中定义的参数
init {
println(name)
}
}
// 定义一个private的主构造器,不可省略constructor关键字
class ConstructorTest private constructor(name: String) {
// 初始化块中可以直接调用主构造器中定义的参数
init {
println(name)
}
}
次构造器和构造器重载
初始化块必定在每个构造器之前调用,因为次构造器是委托的主构造器,即委托调用初始化块。
: this()
// 定义一个无参的主构造器
class ConstructorOverload() {
var a: String = ""
var b: String = ""
init {
println("这是初始化块")
}
// 构造器重载,定义有一个参数的次构造器,委托主构造器,即委托调用初始化块,使用 : this()
constructor(a: String): this() {
println("有一个参数的构造器:$a")
this.a = a
}
// 构造器重载,定义有两个参数的次构造器,委托主构造器,即委托调用初始化块,使用 : this()
constructor(a: String, b: String): this() {
println("有两个参数的构造器:a = $a,b = $b")
this.a = a
this.b = b
}
}
val constructorOverload = ConstructorOverload()
//这是初始化块
val constructorOverload1 = ConstructorOverload("param1")
//这是初始化块
//有一个参数的构造器:param1
val constructorOverload2 = ConstructorOverload("param1", "param2")
//这是初始化块
//有两个参数的构造器:a = param1,b = param2
主构造器声明属性
在主构造器参数上直接加上 var 或 val 即可声明属性,也可为参数设上默认值
// 在主构造器参数上直接加上 var 或 val 即可声明属性,也可为参数设上默认值
class ConstructorParam(var p1: String = "p1", var p2: String = "p2") {}
继承
修饰符 class SubClass: SuperClass { ... }
Kotlin 的类默认是final
的,不能派生子类,所以如果需要让一个类能派生子类,需要使用open
修饰该类。
open class SuperClass(name: String) {
constructor(): this("nnnn")
init {
println(name)
}
}
// 定义一个无参子类,必须立即调用父类构造器
class SubClass1: SuperClass("foo") {
}
// 定义一个参数的子类,必须立即调用父类构造器
class SubClass2(name: String): SuperClass(name) {
}
class SubClass3: SuperClass {
// 定义一个次构造器,隐式委托调用父类无参的构造器
constructor()
// 定义一个参数的次构造器,显式委托父类带参数的构造器
constructor(name: String): super(name)
// 定义两个参数的次构造器,显式委托本类带参数的构造器
constructor(name: String, name1: String): this(name)
}
重写父类的方法
方法也需要使用open
来修饰,才可以被子类重写。
子类重写父类的方法必须使用override
来修饰;
open class Bird(name: String) {
init {
println(name)
}
// 该方法可被子类重写,故需要使用 open 修饰
open fun fly() {
println("我能飞")
}
}
// 麻雀
class Sparrow : Bird("麻雀")
// 鸵鸟
class Ostrich : Bird("鸵鸟") {
// 重写父类的 fly 方法
override fun fly() {
println("我不能飞")
}
}
val sparrow = Sparrow() //麻雀
sparrow.fly() //我能飞
val ostrich = Ostrich() //鸵鸟
ostrich.fly() //我不能飞
重写父类的属性
父类中需要被重写的属性,也需要用open
修饰;
子类重写属性必须用override
修饰;
属性重写有两个限制:
- 类型要兼容;
- 访问权限要更大或相等;只读属性
val
可被读写属性var
重写;读写属性var
不可被只读属性val
重写;
open class Book {
internal open var price: Double = 10.9
open var publisher: String = ""
open val name: String = ""
}
class KotlinBook: Book() {
// 重写父类price属性,可扩大访问权限
public override var price: Double = 20.9
// 重写父类publisher属性,不能将var改成val,读写属性不能被只读属性重写
override var publisher: String = "机械工业出版社"
// 重写父类name属性,能将val改成var,只读属性能被读写属性重写
override val name: String = "Kotlin编程实践"
}
val kotlinBook = KotlinBook()
println("《${kotlinBook.name}》是由“${kotlinBook.publisher}”出版,售价${kotlinBook.price}元") //《Kotlin编程实践》是由“机械工业出版社”出版,售价20.9元
super
访问被子类重写的属性或调用被子类重写的方法时,默认会访问子类中的属性或执行子类中的方法,若仍想访问父类中被重写的属性或调用父类中被重写的方法,需要使用super
。
open class Book {
internal open var price: Double = 10.9
open var publisher: String = "出版社"
open val name: String = "书名"
open fun test1() {
println("Book test1")
}
open fun test2() {
println("Book test2")
}
}
class KotlinBook: Book() {
// 重写父类price属性,可扩大访问权限
public override var price: Double = 20.9
// 重写父类publisher属性,不能将var改成val,读写属性不能被只读属性重写
override var publisher: String = "机械工业出版社"
// 重写父类name属性,能将val改成var,只读属性能被读写属性重写
override val name: String = "Kotlin编程实践"
// 访问被重写的属性,默认只会访问当前子类中定义的属性
fun getSelfName() = name
// 如果想访问父类中的属性,使用super
fun getParentName() = super.name
override fun test2() {
println("KotlinBook test2")
}
fun test3() {
// test2 被当前子类重写,默认子类的test2
test2() //KotlinBook test2
// 使用super来调用父类的test2
super.test2() //Book test2
// 由于当前子类未重写test1,故会调用父类的test1
test1() //Book test1
}
}
强制重写
如果子类从多个直接超类型(接口或类)继承了同名成员,那么Kotlin要求子类必须重写该成员;
如果要访问超类中的成员,可使用super<超类型名>
来进行引用。
open class MandatoryOverride {
open fun test() {
println("class MandatoryOverride test")
}
}
interface IMandatoryOverride {
fun test() {
println("interface IMandatoryOverride test")
}
}
class Mandatory: MandatoryOverride(), IMandatoryOverride {
// 由于MandatoryOverride和IMandatoryOverride中都有test方法,所以子类必须强制重写该方法
override fun test() {
// 可使用super<超类型名>来引用指定超类的test方法
super.test() //class MandatoryOverride test
super.test() //interface IMandatoryOverride test
}
}
多态
把一个子类对象赋值给父类变量,这就是多态。
变量在编译阶段只能调用其编译时类型所具有的方法,但在运行时则执行其运行时所具有的方法。
open class BaseClass {
open var book = 6
fun base() {
println("父类中的普通方法")
}
open fun test() {
println("父类中可以被覆盖的方法")
}
}
class SubClass: BaseClass() {
override var book = 60
fun sub() {
println("子类中的普通方法")
}
override fun test() {
println("子类的覆盖父类的方法")
}
}
//把一个子类对象赋值给父类变量,这就是多态
val baseClass: BaseClass = SubClass()
println(baseClass.book) //60
baseClass.base() //父类中的普通方法
baseClass.test() //子类的覆盖父类的方法
// baseClass.sub() 由于声明的是BaseClass,没有sub方法,故此处使用会编译错误
使用is检查类型
Kotlin提供了类型检查运算符is
和!is
,来保证类型转换不会出错。
is
运算符前面操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则编译时程序就会报错。
val hello: Any = "Hello"
//由于Any是所有类的基类,所以可以使用 is String,is Date
println("hello 是String:${hello is String}") //hello 是String:true
println("hello 是Date:${hello is Date}") //hello 是Date:false
val str = "Hello"
// println(str is Date) 编译错误,因为String与Date没有继承关系
Kotlin的is
和!is
是非常智能的,只要你对其进行了判断,变量就会自动转换为目标类型。
val hello: Any = "Hello"
if (hello is String) {
println(hello.length) //hello自动转换为String,可直接调用String相关的方法。
}
when
分支也可进行智能转换。
fun isTest(x: Any) {
when (x) {
is String -> println(x.length)
is Int -> println(x.toDouble())
}
}
isTest(3) //3.0
isTest("abcd") //4
使用as运算符转型
除了使用is
进行类型检查,还可使用as
或as?
进行强制转型。
as
:不安全的强制转型运算符,若转换失败,会引发ClassCastException
异常;
as?
:安全的强制转型运算符,若转换失败,不会引发异常,而是返回null
。
val obj: Any = "Hello"
//使用as强制转型,成功
val objStr = obj as String
println(objStr)
// val objInt = obj as Int 转型失败,会引发ClassCastException异常
// val num: Number = objStr as Number 由于objStr是String,String和Number没有继承关系,所以编译器会提示转换不可能成功
val objStrNullable = obj as? String
println(objStrNullable?.length) //5
val objIntNullable = obj as? Int
println(objIntNullable?.toDouble()) //null