Kotlin 使用 class 关键字声明类。类声明由 类修饰符、类名、类头(指定其类型参数、主题构造函数等)以及花括号的类体构成。修饰符、类头与类体都是可选的。
类修饰符 class 类名 主构造函数 {init{}次构造函数(参数列表)/******/}// 简单的类可以像这样的声明class Person{/******/}
注意:
- 同样的参数列表,主构造函数(primary constructor)和次构造函数(secondary constructors)不能同时存在;
- 次构造函数可以有多个,但是参数列表不能一样;
- 直接使用
class声明,没有使用任何修饰符的类,默认为final的,不能被继承。
构造函数
主构造函数
主构造函数是类头的一个部分,它跟在类名的后面。如果没有特殊指定其可见性,则表示该主构造函数是 public 的。
class Human constructor(name: String){/** 类体 **/}// 或者class Human private constructor(name: String){/** 类体 **/}
如果主构造函数没有任何的注解或者可见性修饰符(public,private,protected,internal),则 constructor 可以省略。主构造函数默认为 public 修饰的。如果你不想让这个类有一个公有主构造函数,可以想下面的声明方式一样,将主构造函数声明为 pivate 的。
主构造函数内不能有任何的代码。初始化的代码可以放到 init 关键字作为前缀的初始化块(initializer blocks)中;也可以在类体的属性初始化器中使用。
class Human(name: String) {// 在类体里,可以直接使用主构造函数传入的变量,这里叫做属性初始化器val upperName = name.toUpperCase()val printName1 = "First print name : $name".also(::println)init{println("initializer blocks 1 print name : $name")println("initializer blocks 1 print Upper name : $upperName")}val printName2 = "Second print name : $name".also(::println)init {println("initializer blocks 2 print name : $name")}}
由上面的代码可以看出来, init 初始化块可以多次使用。上面的代码会按照出现在类体中的顺序执行,其输出结果为:
First print name : Rickyinitializer blocks 1 print name : Rickyinitializer blocks 1 print Upper name : RICKYSecond print name : Rickyinitializer blocks 2 print name : Ricky
由输出结果可以发现, init 初始化块和属性初始化器交织在一起。他们按照在代码中出现的顺序执行。
将上面的代码转换为 Java 代码:
public final class Human {@NotNullprivate final String upperName;@NotNullprivate final String printName1;@NotNullprivate final String printName2;@NotNullpublic final String getUpperName() {return this.upperName;}@NotNullpublic final String getPrintName1() {return this.printName1;}@NotNullpublic final String getPrintName2() {return this.printName2;}public Human(@NotNull String name) {Intrinsics.checkParameterIsNotNull(name, "name");super();String var10000 = name.toUpperCase();Intrinsics.checkExpressionValueIsNotNull(var10000, "(this as java.lang.String).toUpperCase()");String var7 = var10000;this.upperName = var7;String var2 = "First print name : " + name;System.out.println(var2);this.printName1 = var2;var2 = "initializer blocks 1 print name : " + name;System.out.println(var2);var2 = "initializer blocks 1 print Upper name : " + this.upperName;System.out.println(var2);var2 = "Second print name : " + name;System.out.println(var2);this.printName2 = var2;var2 = "initializer blocks 2 print name : " + name;System.out.println(var2);}}
通过 Java 代码,我们可以看出来,无论是在 init 初始化块还是属性初始化器,最终都会到同一个构造函数里,它们按照代码出现的顺序执行。
在 Kotlin 声明类时,还有一个简洁的语法,可以在主构造函数中声明属性。
class Person(val firstName: String, val lastName: String, var age: Int) { /*……*/ }
与普通属性一样,主构造函数中声明的属性可以是可变的(var)或只读的(val)。
接下来我们根据转换来的 Java 代码来看。首先我们不在主构造函数中声明变量:
class Dog(name: String) {init {println(name)}}
转换成 Java 代码:
public final class Dog {public Dog(@NotNull String name) {Intrinsics.checkParameterIsNotNull(name, "name");super();boolean var2 = false;System.out.println(name);}}
可以看到,并没有一个 叫做 name 的属性。
然后我们在主构造函数中把 name 声明成一个属性 :
class Dog(var name: String) {init {println(name)}}
然后转换成 Java 代码:
public final class Dog {@NotNullprivate String name;@NotNullpublic final String getName() {return this.name;}public final void setName(@NotNull String var1) {Intrinsics.checkParameterIsNotNull(var1, "<set-?>");this.name = var1;}public Dog(@NotNull String name) {Intrinsics.checkParameterIsNotNull(name, "name");super();this.name = name;String var2 = this.name;boolean var3 = false;System.out.println(var2);}}
可以很明显的看到,Java 代码中多出了一个 name 的全局变量,并且有 getter 和 setter 方法。
在 JVM 上,如果主构造函数的所有的参数都有默认值,编译器会生成一个额外的无参构造函数,它将使用默认值。比如下面的 Kotlin 类:
// 这里的 name 的默认值为 ""class Customer(val name: String = "") {}
将其转换为 Java 代码:
public final class Customer {@NotNullprivate final String name;@NotNullpublic final String getName() {return this.name;}public Customer(@NotNull String name) {Intrinsics.checkParameterIsNotNull(name, "name");super();this.name = name;}// $FF: synthetic methodpublic Customer(String var1, int var2, DefaultConstructorMarker var3) {if ((var2 & 1) != 0) {// 编译器生成了一个额外的无参构造函数,它使用默认值var1 = "";}this(var1);}// 编译器生成了一个额外的无参构造函数,它使用默认值public Customer() {this((String)null, 1, (DefaultConstructorMarker)null);}}
如果没有显示的声明主构造函数,默认的也会有一个空的主构造函数。就像 Java 中的类,如果没有声明构造函数,其实它也是会默认有一个空的无参构造。
次构造函数
除了上面的主构造函数,Kotlin 还提供了 次构造函数。使用 constructor 关键字修饰。
class Human {val children: MutableList<Human> = mutableListOf()constructor(parent: Human){parent.children.add(this)}}
如果类有一个主构造函数,每个次构造函数都需要委托给主构造函数,可以直接委托或者通过别的次构造函数间接委托。委托到同一个类的另一个构造函数使用 this 关键字:
class Human(name: String) {val children: MutableList<Human> = mutableListOf()// 通过 :this(name) 调用主构造函数,将主构造函数委托给次构造函数constructor(name: String, parent: Human) : this(name) {parent.children.add(this)}}
这样我们在调用的时候,Human 类就有了两个构造函数,一个要传入一个 name ,另一个需要传入 name 和 Human ,我们可以这样调用:
fun main() {val human = Human("Ricky")val human2 = Human("Jim", human)}
注意:
- 如果主构造函数中有参数,次构造函数的个数必须大于等于主构造函数中的参数个数
- 次构造函数必须包含主构造函数的参数
- 如果同时存在主构造函数和次构造函数,那么主构造函数必须直接或者间接的委托给次构造函数
关于初始化块
- 初始化块中的代码实际上会成为主构造函数的一部分;
- 初始化块委托给主构造函数会作为次构造函数的第一条语句;
- 所有的初始化块中的代码都会在次构造函数体之前执行;
- 即使该类没有主构造函数,这种委托仍然会隐式发生,并且仍然会执行初始化块。
class Constructors{init {println("Init block")}constructor(i:Int){println("Secondary constructor")}}
上面的代码的输出结果为:
Init blockSecondary constructor
可以看出,初始化块在次构造函数前执行,这里我们证明了第3点:所有的初始化块中的代码都会在次构造函数体之前执行 。然后我们将其转换成 Java 代码:
public final class Constructors {public Constructors(int i) {// 1. 初始化块中的代码实际上会成为主构造函数的一部分;// 2. 初始化块委托给主构造函数会作为次构造函数的第一条语句;// 4. 即使该类没有主构造函数,这种委托仍然会隐式发生,并且仍然会执行初始化块。String var2 = "Init block";boolean var3 = false;System.out.println(var2);var2 = "Secondary constructor";var3 = false;System.out.println(var2);}}
通过 Java 代码就可以很直观的看到, 初始化块中的代码跟次构造函数中的代码都在 Java 类Constructors 的构造函数中,初始化块的代码在其第一行,我们并没有声明主构造函数,但是初始化块中的代码仍然会次构造函数之前。
关于 构造函数 的总结
- 主构造函数没有函数体,它是类统一的参数的入口,它用来接收参数,我们可以属性初始化时,使用传入的变量,也可以在
init初始化块中使用传入的变量。主构造函数时可选的,但是如果你写了,并且还需要次构造函数,那么就需要在次构造函数调用主构造函数。因为我们需要在创建实例的时候,初始化入口传入的参数。 - 主构造函数会参入到所有次构造函数的初始化过程中。如果存在多个构造函数,我们一般把包含最基本,最通用的参数作为主构造函数。
创建类的实例
Kotlin 中没有 new 关键字,我们如果要创建一个类的实例,可以像普通的函数调用一个样调用构造函数:
val constructors = Constructors(1)val human = Human("Ricky")val human2 = Human("Ricky", human)
继承
在 Kotlin 中,所有的类都一个共同的父类 Any ,这里的 Any 类似于 Java 中的 Object 类 。如果没有显式的去继承 Any ,它就会隐式继承 Any。
class Example // 它的父类就是 Any,这里是隐式的继承 Any
Any 有三个方法:equals() 、hashCode() 、toString() 。因此,所有的 Kotlin 类都默认定义了这些方法。
如果要继承一个类,只需要在类头中把父类放到冒号 : 的后面即可。作为父类的类,在声明时必须使用 open 修饰,因为使用 class 声明的类,默认为 final 的,是不能被继承的。
如果父类有主构造函数,对应的子类也必须有对应的主构造函数,并且在继承时必须带上对应的参数。
open class Animal(name: String)class Dog(name: String):Animal(name)
子类在继承父类时,父类必须使用主构造函数就地初始化,即在子类的类头中初始化。在 Kotlin 中,如果显示的声明主构造函数,它会默认生成一个空的无参的主构造函数。
open class Animal// 子类 Dog 中有一个主构造,但是父类并没有,这父类还是需要初始化class Dog(name: String) : Animal() {}
上面的代码中子类 Dog 有一个主构造函数,但是父类 Animal 没有。但是当 Dog 继承 Animal 时,还是需要在采用默认的空的构造函数对 Animal 进行初始化。
如果子类没有主构造函数,父类也没有主构造函数,可以像下面一样进行声明:
open class Animalclass Dog : Animal()
如果子类和父类都不存在主构造函数,而存在次构造函数,那么每个次构造函数必须使用 super 关键字调用父类的构造,对父类进行初始化。这里不要求参数的一致,不同的次构造函数可以调用父类的不同的构造函数:
open class Animal {constructor(name: String)constructor(name: String, age: Int)}class Dog : Animal {constructor(name: String) : super(name)constructor(name: String, age: Int) : super(name, age)// 也可以这样调用// constructor(name: String, age: Int) : super(name)}
如果父类同时存在主构造函数和次构造函数,那么子类只能通过其中的一个父类进行初始化。因为在 Kotlin 类如果同时存在主构造函数和次构造函数,次构造函数必须通过 this 调用主构造函数。这个时候如果子类在次构造函数中再次通过 super 对父类进行初始化,就会造成重复调用。
覆盖(重写)方法
只有使用了 open 修饰的方法才可以被覆盖。子类使用 override 修饰符来修饰覆盖的函数。
open class Animal {open fun run() {println("Animal run ...")}}open class Dog : Animal() {override fun run() {println("Dog run ...")}}fun main() {val dog = Dog()dog.run()}
上面的代码输出:
Dog run ...
我们在 Dog 类中覆盖了父类 Animal 中的 run 方法。
override 的方法默认是 open 的,如果不想让该方法再次覆盖,则需要在前面添加 final 关键字。
open class Animal {open fun run() {println("Animal run ...")}}open class Dog : Animal() {// 这里使用final修饰,则其子类不可覆盖该方法final override fun run() {println("Dog run ...")}}class Corgi : Dog(){// Dog 的 run方法使用了 final 修饰,这里如果再次覆盖,就会报错。override fun run() {println("Corgi run ...")}}
覆盖属性
属性覆盖与方法覆盖类似;只有使用了 open 修饰的属性才可以被覆盖;子类通过 override 修饰重新声明的属性。当然,如果覆盖和被覆盖的类型必须是兼容的。可以使用 var 属性覆盖 val 属性,但是反过来却不行。因为一个 val 属性本质上声明了一个 get 方法, 而将其覆盖为 var 只是在子类中额外声明一个 set 方法。
open class Animal {open val age: Int? = 10}open class Dog : Animal() {override var age: Int? = 20}class Corgi : Dog() {override var age: Int? = 30}
在主构造函数中override 关键字也可以作为属性声明的一部分。
open class Animal {open val age: Int? = 10}open class Dog(override var age: Int? = 20) : Animal() {}class Corgi : Dog(){override var age: Int? = 30}
子类的初始化顺序
子类实例的构建过程分以下几步:
- 对父类构造函数的参数求值
- 执行父类的构造函数
- 初始化父类的属性
- 对子类构造函数的参数求值
- 执行子类的构造函数
- 初始化子类的属性
// 父类open class Animal(name: String) {// 2.初始化父类的构造init {println("Animal init block")}// 3.初始化父类的属性open val nameSize: Int? = name.length.also {println("Initializing property nameSize in Animal :$it")}}// 子类open class Dog(name: String) :// 1.继承父类,对父类构造函数的参数求值Animal(name.capitalize().also { println("Argument for Animal:$it") }) {// 5.初始化子类的构造init {println("Dog init block")}// 6.初始化子类的属性override val nameSize: Int? = name.length.also {println("Initializing property nameSize in Dog :$it")}}fun main() {// 4.对子类构造函数的参数求值val dog = Dog("jack".also { println("Argument for Dog:$it") })}
上面代码的输出结果为:
Argument for Dog:jackArgument for Animal:JackAnimal init blockInitializing property nameSize in Animal :4Dog init blockInitializing property nameSize in Dog :4
从上面的代码中,可以看出来,子类在初始化时会首先去找到父类,对父类进行初始化。然后才会对子类进行初始化。
调用父类实现
子类可以通过 super 关键字调用父类的函数与属性访问器的实现。
open class Shape {val borderColor: String get() = "black"open fun draw() {println("Shape draw")}}class Rectangle : Shape() {val fillColor: String get() = super.borderColoroverride fun draw() {super.draw()println("Rectangle draw")}}
如果存在内部类,则内部类可以通过 super@外部类名 调用外部类。
class Rectangle : Shape() {val fillColor: String get() = super.borderColoroverride fun draw() {super.draw()println("Rectangle draw")}inner class Filler {fun fill() {}fun drawAndFill() {// 内部类可以通过 `super@外部类名` 调用外部类。super@Rectangle.draw()fill()}}}
抽象类
使用 abstract 声明的类被称为抽象类 ,使用 abstract 声明的方法被称为抽象方法。抽象类有以下几个特点:
- 抽象方法在抽象类中可以不用实现,其默认为
open的; - 抽象方法必须存在于抽象类中;
- 抽象类中可以存在已经实现的方法;
- 子类如果不是抽象类,去继承抽象类,必须实现抽象类的所有的抽象方法;
- 抽象方法去覆盖非抽象的方法。
abstract class Bird {// 抽象方法在抽象类中可以不用实现,其默认为 `open` 的;abstract fun fly()// 抽象类中可以存在已经实现的方法;open fun chirp() {println("abstract Bird chirp")}}abstract class Blackbird : Bird() {override fun fly() {println("Blackbird fly")}// 抽象方法去覆盖非抽象的方法。abstract override fun chirp()}
对象
这里对象是指 object 关键字,object 的作用是在声明一个类的同时创建这个类实例。
有时候我们不想通过继承的方式来对一个类的功能进行微调,那么我们就可以通过 object 来实现。
对象表达式
通过 object 关键字我们可以定义匿名类,通过继承的方式来实现一些特定功能:
window.addMouseListener(object : MouseAdapter() {override fun mouseClicked(e: MouseEvent?) {/* ... */}override fun mouseEntered(e: MouseEvent?) {/* ... */}})
上面的代码使用 object 创建了一个抽象类 MouseAdapter 的实例,并实现了其中的两个方法。将其转换成 Java 代码:
window.addMouseListener((MouseListener)(new MouseAdapter() {public void mouseClicked(@Nullable MouseEvent e) {}public void mouseEntered(@Nullable MouseEvent e) {}}));
如果父类有一个构造函数,则必须传递指定类型的参数给它。如果有父类和多个接口可以在使用逗号分隔。
open class A(x: Int) {public open val y: Int = x}interface B { /*……*/ }val ab: A = object : A(1), B {override val y = 15}
有些时候,我们并不想创建一个类,只想要一个对象,这个对象不需要特殊的类型,那么我们可以这样写:
fun foo() {val operand = object {var x: Int = 0var y: Int = 0}println(operand.x + operand.y)}
或者在类中这样写:
class ObjectSample {object Operand {var x: Int = 2var y: Int = 4}}fun main() {println(ObjectSample.Operand.x + ObjectSample.Operand.y)}
当 object 生成的对象作用在私有属性或者方法上,它的返回类型是匿名对象的类型;如果作用在在公有的方法或者属性上,它返回的内容是Any 类型,我们没办法在外边访问它的内部。
class ObjectSample {// 私有属性,返回类型为匿名的对象的类型,可以在外边使用private val privateProperty = object {val abc = "abc"}// 公有属性,返回类型为 Any ,不能在外边使用val publicProperty = object {val abc = "abc"}// 私有方法,返回类型为匿名的对象的类型,可以在外边使用private fun privateMethod() = object {val x = "X"}// 公有方法,返回类型为 Any,不能在外边使用fun publicMethod() = object {val x = "X"}fun ownedMethod() {// 在方法内部,返回类型为匿名的对象的类型,可以在方法内部使用val o = object {val x = "X"}val xo = o.x}fun bar() {val p1 = privateProperty.abc// 公有属性,返回类型为 Any ,不能在外边使用// val p2 = publicProperty.abcval m1 = privateMethod().x// 公有方法,返回类型为 Any,不能在外边使用// val m2 = publicMethod().x}}
对象声明
如果在 `object` 的后面加上一个名称,就像变量声明一样,我们就声明了一个类,同时用创建了一个类的引用了;这就是对象声明。我们可以通过类名直接调用里面的方法。我们不能自己新建其它的实例,有且只有当前一个实例。这就是 Kotin 中单例。
object DataProvider{fun provide(){println("provide datas")}}fun main() {DataProvider.provide()}
对象声明的初始化过程是线程安全的。
当然声明对象时,也可以给它指定父类:
abstract class Provider {abstract fun provide()}object DataProvider : Provider() {override fun provide() {println("provide datas")}}fun main() {DataProvider.provide()}
对象声明不能嵌套在函数内部,但是可以嵌套到其它对象声明或者非内部类中。
class DatabaseProvider{fun provide(){// 函数内部是不能进行对象声明的object InnerProvider{}}}
伴生对象
在类的内部使用 companion 关键字标记的对象声明叫做半生对象。比如下面的代码:
class DatabaseProvider {companion object Type {val type1 = 1val type2 = 2}}
该伴生对象的成员可通过只使用类名作为限定符来调用,就像 java 类中静态成员,如下:
fun getDataType(type: Int) {when (type) {DatabaseProvider.type1 -> {println("data is type1")}DatabaseProvider.type2 -> {println("data is type1")}else -> {println("data is unknown")}}}
如果半生对象没有定义名字,我们可以使用名称 Companion 来调用;比如:
class MyClass {companion object { }}val x = MyClass.Companion
虽然像是 Java 的静态成员,但是在运行时伴生对象仍然是真实的实例成员,我们将其转换成 Java 代码:
public final class DatabaseProvider {private static int type1 = 1;private static int type2 = 2;public static final DatabaseProvider.Type Type = new DatabaseProvider.Type((DefaultConstructorMarker)null);//半生对象会被转换成静态的final类public static final class Type {public final int getType1() {return DatabaseProvider.type1;}public final void setType1(int var1) {DatabaseProvider.type1 = var1;}public final int getType2() {return DatabaseProvider.type2;}public final void setType2(int var1) {DatabaseProvider.type2 = var1;}private Type() {}// $FF: synthetic methodpublic Type(DefaultConstructorMarker $constructor_marker) {this();}}}
在 JVM 平台,如果使用 @JvmStatic 注解,我们可以将伴生对象的成员生成为真正的静态方法和字段。
对象表达式和对象声明的区别
- 对象表达式是在使用它们的地方立即执行的;
- 对象声明是在第一次被访问到时延迟初始化的;
- 半生对象的初始化是在相应的类被加载时,与Java静态初始化器的语义匹配。
总结
- Kotlin 的类有主构造和次构造之分
- 主构造和次构造同时存在时,次构造需要通过
this调用主构造 - Kotlin 类的初始化顺序:
- 主构造参数的表达式
- 主构造函数
- 属性初始化器跟
init初始化块是按出现的顺序执行的 - 次构造参数的表达式
- 次构造函数
- Kotlin 中不存在
new,只需要像声明变量一样声明对象的引用val p = Person() - Kotlin 所有的类的最顶层父类是
Any - 只有使用了
open修饰的类才可以被继承 - 只有使用了
open修饰的方法才可以被覆盖(重写) - 只有使用了
open修饰的属性才可以被覆盖 override的方法默认是open的,如果不想被再次覆盖(重写),需要加final- 子类可以使用
super调用父类的实现 - 如果存在内部类,则内部类可以通过
super@外部类名调用外部类。 - 在继承时需要以下的规则:
- 如果父类有主构造函数,子类也一定也要有对应的主构造,并且在继承时必须带上对应的参数
- 子类在继承父类时,父类必须使用主构造函数就地初始化,即在子类的类头中初始化
- 如果子类和父类都不存在主构造函数,而存在次构造函数,那么每个次构造函数必须使用
super关键字调用父类的构造,对父类进行初始化 - 如果父类同时存在主构造函数和次构造函数,那么子类只能通过其中的一个父类进行初始化
- 以上内容不需要记住,编译器会告诉你。。。。
- 抽象类默认是
open的 - 抽象方法可以去覆盖非抽象的方法
- 在Kotlin
object指的就是对象本身,它可以在声明一个类的同时创建这个类实例 - 虽然
companion object修饰的伴生对象很像 java 类中静态成员,但是它本质还是一个类 - 在 JVM 平台,如果使用
@JvmStatic注解,我们可以将伴生对象的成员生成为真正的静态方法和字段。
