类与对象
类是对象的抽象,用于描述一组对象的共同特征和行为。
类中可以定义成员变量和成员函数,其中:
成员变量用于描述对象的特征,也被称作属性;
成员函数用于描述对象的行为,可以简称为函数或方法。
同时顶级属性、顶级方法,指的是该属性或方法放在类的最外层,和java中的全局变量类似。
构造函数
在实例化对象的同时,就为这个对象的属性进行赋值,可以通过构造函数来实现,因为构造函数是类的一个特殊成员,他会在类实例化的时候被自动调用。
Kotlin的构造函数分为两种:主构函数、次构函数。构造函数使用constructor关键字进行修饰,一个类可以有一个主构函数,多个次构函数。
主构函数
主构函数位于类头,在类名之后。若主构函数没有任何注解,或可见性修饰符(如public),constructor关键字可省略。主构函数的定义语法如下:
class 类名 constructor ([形参1,形参2,形参3]){}
若在定义类时没有指定主构函数,Kotilin将会与java一样,自动生成一个无参主构函数。
class 类名 constructor(){} //第一种写法
class 类名 (){} //第二种写法,省略constructor,即普通类的定义形式
在主构函数赋值时,通常使用init{}初始化代码块。
class Clerk constructor(username: String) {
var name: String
init {
name = username
println("My name is $name")
}
}
fun main() {
var clerk = Clerk("bob")
}
次构函数
在实际赋值时,有可能出现多种情况,比如只给name赋值,或者同时给name和age赋值,这时候只有主构函数还不够,就需要次构函数来完成。
而一个次构函数,必须要调用主构函数,或者其他的次构函数,调用方法为“次构函数:this(参数列表)”。
需要注意的是,新定义的次构函数,在调用主构函数或者其他次构函数时,被调用的函数,参数顺序必须与新定义的顺序一致,同时参数个数必须小于新定义的次构函数。
class Workers constructor(name: String) {
var name: String
init {
this.name = name
println("My name is $name")
}
constructor(name: String, age: Int) : this(name) {//继承了主构函数
println("My name is $name, I am $age years old")
}
constructor(name: String, age: Int, sex: String) : this(name, age) {//继承了次构函数
println("My name is $name, I am $age years old, I am a $sex")
}
}
fun main() {
var workers = Workers("bob", 10, "male")
}
在编写构造函数时,通过this关键字不难看出,第一个次构函数,调用的是主构函数,而第二个次构函数,调用的则是第一个次构函数。
在main函数中,在初始化workers变量时,传入了三个变量,此时将进入第二个次构函数。而因为上述的调用关系,在初始化时,三个构造函数都将被投入使用。
类的继承
类的继承
Kotlin中,所有类都默认使用final关键字修饰,所以想要继承某个类时,需要在这个类前面加上open关键字。在继承时,Kotlin使用英文冒号来修饰。
open class Father() {
fun sayHello() {
println("Hello")
}
}
class Son() : Father() {}
fun main() {
var son = Son()
son.sayHello()
}
在示例中,类Father使用了open关键字修饰,使得可以被其他类继承;而类Son继承了Father,虽然在类Son中没有任何内容,但是可以使用类Father中的方法sayHello(),说明在继承时,子类会继承父类的所有方法。
在Kotlin中,继承有以下几种情况:
- 一个类只能继承一个父类。
- 一个类可以拥有多个子类。
- 多层继承是允许的。比如b继承了a,但b同时可以是c的父类,此时c也可以称作是a的子类。
方法重写(override)
在子类中,重写的属性需要用override来修饰。
同样的,在父类中,被重写的属性需要用open来修饰。
open class Father() {
open var name = "bob"
open var age = 35
open fun sayHello() {
println("My name is $name, I am $age years old")
}
}
class Son() : Father() {
override var name = "bobby"
override var age = 5
override fun sayHello() {
println("My name is $name, I am son of ${super.name}, I am $age years old")
}
}
fun main() {
var father = Father()
father.sayHello()
var son = Son()
son.sayHello()
}
运行结果中,子类虽然重写了父类中的sayHello方法,但是在运行时,只会走各自类中的sayHello方法,子类并不会调用父类的方法。
super关键字
当子类重写父类方法后,子类对象无法访问父类被重写的方法。为此,Kotlin中可以使用super关键字,让子类对象来调用父类方法。
open class Father() {
open var name = "bob"
open var age = 35
open fun sayHello() {
println("My name is $name, I am $age years old")
}
}
class Son() : Father() {
override var name = "bobby"
override var age = 5
override fun sayHello() {
super.sayHello()
println("My name is $name, I am son of ${super.name}, I am $age years old")
}
}
fun main() {
var son = Son()
son.sayHello()
}
Any类与Object类
Kotlin中,所有类都继承Any类,他是所有列的父类。若一个类在声明时没有指定父类,则默认其父类为Any类。
在运行时,Any类会自动映射为java中的java.lang.Object类。
fun main(){
println(Any().javaClass)//输出结果为:class java.lang.Object
}
在Java中,有8种基本类型和引用类型。在Kotlin中,所有类型都是引用类型,这些引用类型统一继承父类Any。Any类中提供了3个方法,分别如下:
方法名 | 方法作用 |
equals() | 检测两个对象是否相等 |
hashCode() | 返回一个对象的哈希码值 |
toString() | 返回一个对象的字符串形式 |
相对应的,在Java中,Object类是所有类的父类,但不包括8种基本类型(如Int、Long
、Double等),Object中提供了11个方法,分别如下:
方法名 | 方法作用 |
equals() | 检测两个对象是否相等 |
hashCode() | 返回一个对象的哈希码值 |
toString() | 返回一个对象的字符串形式 |
getClass() | 返回一个Class类型的对象 |
clone() | 创建并返回一个对象的副本,也就是复制该对象 |
finalize() | Object类的子类可以覆盖该方法以实现资源清理工作,垃圾收集时,由对象上的垃圾收集器调用该方法 |
notify() | 唤醒一个等待该对象的线程 |
notifyAll() | 唤醒在这个对象监视器上等待的所有线程 |
wait() | 使当前线程等待,直到另一个线程被调用 |
wait(long) | 使当前线程等待,直到另一个线程被调用,该方法中的参数是等待的最大时间,单位是毫秒 |
wait(long,int) | 使当前线程等待,直到另一个线程被调用,该方法中的第一个参数是等待的最大时间,单位是毫秒;第二个参数是额外时间,以毫微秒为单位,范围是0~999999 |
抽象类和接口(interface)
抽象类
定义一个类时,通常需要定义一些方法,来描述该类的行为特征。
但有时这些方法的实现方法无法确定,此时可以将其定义为抽象方法,使用abstract关键字来修饰,当要使用这个抽象方法时,需要实现该方法体;
而当一个类中包含了抽象方法,这个类就必须被定义为抽象类,同样使用的是abstract关键字。
//定义抽象类和抽象方法的语法格式
abstract class Animal{
abstract fun eat()
}
虽然包含抽象方法的类必须被声明为抽象类,但抽象类可以不包含任何抽象方法。
同时,抽象类不能被实例化,因为其中可能含有抽象方法,而抽象方法是不包含方法体的,所以不能被调用。如果要调用抽象类中的抽象方法,需要创建一个子类,在子类中将其实现。
abstract class Animal() {
abstract fun eat()
}
class Monkey(food: String) : Animal {
var food = food
override fun eat() {
println("Monkey is eating $food")
}
}
fun main() {
var monkey = Monkey("banana")
monkey.eat()//运行结果:Monkey is eating banana
}
当子类实现了父类的抽象方法,可以正常进行实例化,并通过该实例化对象调用实现的方法。
接口
若一个抽象类中的所有方法都是抽象的,则可以将这个类用接口来定义。
因此,接口也是一种特殊的抽象类。
interface Animal{
fun eat()//定义抽象方法
}
在使用接口时,并不像抽象方法那样,需要使用abstract关键字来修饰,这是因为接口中已经默认包含该关键字。
由于接口中的所有方法都是抽象方法,所以不能简单地通过实例化对象来调用接口中的方法,而是要定义一个类来实现接口中的所有方法。
interface Animal {
fun eat()
}
class Monkey(food: String) : Animal {
var food = food
override fun eat() {
println("Monkey is eating $food")
}
}
fun main() {
var monkey = Monkey("banana")
monkey.eat()//运行结果:Monkey is eating banana
}
在程序中,还可以定义一个接口去继承另一个接口,通过冒号(:)实现。
interface Animal {
fun eat()
}
interface Monkey : Animal {
fun sleep()
}
class GoldenMonkey(food: String) : Monkey {
var food = food
override fun eat() {
println("I am golden monkey, I love to eat $food")
}
override fun sleep() {
println("I am golden monkey, I love to sleep")
}
}
fun main() {
var goldenMonkey = GoldenMonkey("banana")
goldenMonkey.eat()
goldenMonkey.sleep()
}
//运行结果:
//I am golden monkey, I love to eat banana
//I am golden monkey, I love to sleep
有几点需要注意:
- 当一个类实现接口,如果这个类是抽象类,则不用实现该接口中的全部方法,否则将要全部实现;
- 当一个类使用冒号(:)实现接口,可以实现多个接口,被实现的多个接口使用逗号(,)隔开。一个接口在继承多个接口时,也是使用以上的格式;
- 一个类在继承另一个类的同时,也可以实现接口,被继承的类名和实现的接口名字,都放在冒号(:)后面,也是使用逗号(,)隔开。
常见类
嵌套类
嵌套在其它类中的类,该类不能访问外部类的成员。
在没有任何修饰的情况下,定义在一个类内部的类,被默认成为嵌套类。
class Outer {
var name = "bob"
var age = 35
class Inner {
fun sayHello() {
println("My name is $name, I am $age years old")//会报错,因为无法访问外部类成员
}
}
}
内部类(inner)
同样是定义在其他类中的类。
与嵌套类的区别在于,被inner关键字修饰,以此修饰的内部类可以访问外部类成员。
class Outer {
var name = "bob"
var age = 35
inner class Inner {
fun sayHello() {
println("My name is $name, I am $age years old")
}
}
}
fun main() {
Outer().Inner().sayHello()
}
在main方法中访问内部类的成员时,需要先实例化外部的类,在实例化内部的类,最终调用内部类的方法。
与java的区别是,在java中,将一个类定义在另一个类的内部,则将这个类称为成员内部类;如果加上static修饰,则是静态内部类。java 的成员内部类可以访问外部类的所有成员。
枚举类(enum)
顾名思义就是一一例举,每个枚举常量都是一个对象,使用逗号分隔,用enum关键字修饰。
由于每个枚举常量都是枚举类的实例,因此这些实例也可以初始化,同时枚举支持构造函数。
enum class Week1{
星期一,星期二,星期三,星期四,星期五,星期六,星期日
}
enum class Week2{
MONDAY("星期一","上班"),
TUESDAY("星期二","聚会"),
WEDNEWSDAY("星期三","上班"),
THURSDAY("星期四","上班"),
FRIDAY("星期五","上班"),
SATURDAY("星期六","加班"),
SUNDAY("星期日","休息")
}
密封类(sealed)
当一个值只能在一个集合中取值,而不能取其他值时,此时可以使用密封类。
在某种意义上,密封类是枚举类的拓展。每个枚举类只能存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。
由于密封类的构造函数是私有的,因此密封类的子类只能定义在密封类的内部,或同一个文件中。
sealed class Stark {
class RobStark : Stark() {} //密封类的子类
class SansaStark : Stark() {} //密封类的子类
class AryaStark : Stark() {} //密封类的子类
class BrandonStark() {} //密封类的嵌套类
}
class JonSnow : Stark() {} //不在嵌套类中的嵌套类子类
密封类用于子类可数的情况,而枚举类用于实例可数的情况。
数据类(data)
专门用来保存数据或者对象状态的类,被称为数据类。
在java中,这种类一般会称为bean类、entity类或model类。
data class 类名([形参1,形参2……])//语法格式
需要注意以下几点:
- 数据类的主构函数至少有一个参数,如果需要一个无参的构造函数,可以将其中的参数直接设置成默认值;
- 数据类中的主构函数中传递的参数必须用val或var修饰;
- 数据类不可以用abstract、open、sealed或inner关键字修饰。
data class Man(var name: String, var age: Int) {}
fun main() {
var man = Man("bob", 20)
println("man : $man")//运行结果:man : Man(name=bob, age=20)
}
根据运行结果可知,man中的数据已存储在数据类Man中。
单例模式(object)
指的是在程序运行期间,针对该类只能存在一个实例。
就好比世界只有一个太阳,现在要设计一个太阳类,这个类就只能有一个实例对象。
object Singleton {
var name = "单例模式"
fun sayHello() {
println("Hello! I am the $name, I love this world")
}
}
fun main() {
Singleton.name = "Sun"
Singleton.sayHello()//运行结果:Hello! I am the Sun, I love this world
}
从上述代码不难看出,在使用单例类时,不需要再去创建一个该类的实例化对象,而是直接使用“类名.成员名”的形式调用类中的方法与参数。
这是因为通过object关键字创建单例类时,默认创建了该类的单例对象。
伴生对象(companion)
在Kotlin中没有静态变量,因此使用了伴生对象来替代。
伴生对象在类加载时初始化,与类的生命周期相同,每个类有且仅有一个伴生对象,因此可以不指定伴生对象的名称,并且其他类可以共享伴生对象。
companion object 伴生对象名称(可省略){//伴生对象语法
代码……
}
由于伴生对象可以不指定名称,因此在调用时同样有两种方式。
class Company {
companion object Factory{
fun sayHello(){
println("I am a companion")
}
}
}
fun main() {
Company.Factory.sayHello() //第1种调用方式:类名.伴生对象名.成员函数名
Company.sayHello() //第2种调用方式:类名,成员函数名
}
委托(by)
委托模式也称代理模式,简而言之是将a的工作交给b来做。
类委托
委托是有两个对象完成的,因此类委托也包含两个对象:委托类和被委托类。
在委托类中,没有真正的功能方法,该类的功能是通过被委托类中的方法实现的。
第一种委托方式:定义了一个接口Wash以及两个实现接口的类Child和Parent,但是在Parent中没有实现方法,它需要实现的功能交给了Child来完成。
//第一种委托方式
interface Wash {
fun washDishes()
}
class Child : Wash {
override fun washDishes() {
println("委托儿子洗碗")
}
}
class Parent : Wash by Child() {}
fun main() {
var parent = Parent()
parent.washDishes()
}
第二种委托方式:在委托类继承接口时,传入一个被委托类的实例,通过被委托类调用其中的方法。当在main方法中调用时,初始化一个实例并传入即可。
//第二种委托方式
interface Wash {
fun washDishes()
}
class Child : Wash {
override fun washDishes() {
println("委托儿子洗碗")
}
}
class Parent(washer: Wash) : Wash by washer {}//传入一个被委托类实例
fun main() {
var child = Child()//初始化一个被委托类实例,用于传入委托类
Parent(child).washDishes()
}
属性委托
指一个类中的属性不是在类中直接定义,而是委托给一个代理类,在其中对所有属性统一管理。
val/var <属性名>: <类型> by <委托类>//语法格式
由于属性对应的get和set方法会被委托给getValue和setValue方法,因此属性的委托不必实现任何接口。对于val类型的属性,只需提供getValue方法。
以下代码的实例,参照的是过年小朋友收到红包,将压岁钱委托给家长。
class Parent() {
var money: Int = 0
operator fun getValue(child: Child, property: KProperty<*>): Int {
println("getValue()方法被调用,修改的属性:${property.name}")
return money
}
operator fun setValue(child: Child, property: KProperty<*>, value: Int) {
println("setValue()方法被调用,修改的属性:${property.name}、属性值:${value}")
money = value
}
}
class Child() {
var money: Int by Parent()//将压岁钱委托给家长
}
fun main() {
val child = Child()
println("(1)父母给孩子100元压岁钱")
child.money = 100
//运行结果:setValue()方法被调用,修改的属性:money、属性值:100
println("(2)买玩具花了50")
child.money -= 50
//运行结果:
//getValue()方法被调用,修改的属性:money
//setValue()方法被调用,修改的属性:money、属性值:50
//getValue()方法被调用,修改的属性:money
println("(3)自己还剩${child.money}")
}
若Child类没有委托给Parent类,将使用自己的get和set方法,但是委托后,通过输出可知使用的是Parent类的getValue和setValue方法。
注意以下几点:
- getValue和setValue方法前必须使用operator关键字;
- getValue方法的返回类型必须与委托属性相同,或者是其子类。
延迟加载(by lazy)
在Kotlin中,在声明变量或者属性的同时,需要对其初始化,否则会报错。
能不能在变量被使用的时候,再初始化?
为此Kotlin提供了延迟加载(又称懒加载),当变量被访问时,才会初始化。
这样的好处显而易见,因为不是把变量一股脑全员初始化,可以提高效率,使得程序启动更快。
延迟加载的变量必须被声明为val,即不可变变量,相当于java中的final。
val 变量: 变量类型 by lazy{
变量初始化代码
}
延迟加载的变量,在第一次初始化时输出所有内容,但是在之后,只会输出最后一行。
fun main() {
val content by lazy {
println("Hello")
"World"
}
println(content)
//运行结果:
//Hello
//World
println(content)
//运行结果:
//World
}