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 : Ricky
initializer blocks 1 print name : Ricky
initializer blocks 1 print Upper name : RICKY
Second print name : Ricky
initializer blocks 2 print name : Ricky
由输出结果可以发现, init
初始化块和属性初始化器交织在一起。他们按照在代码中出现的顺序执行。
将上面的代码转换为 Java 代码:
public final class Human {
@NotNull
private final String upperName;
@NotNull
private final String printName1;
@NotNull
private final String printName2;
@NotNull
public final String getUpperName() {
return this.upperName;
}
@NotNull
public final String getPrintName1() {
return this.printName1;
}
@NotNull
public 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 {
@NotNull
private String name;
@NotNull
public 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 {
@NotNull
private final String name;
@NotNull
public final String getName() {
return this.name;
}
public Customer(@NotNull String name) {
Intrinsics.checkParameterIsNotNull(name, "name");
super();
this.name = name;
}
// $FF: synthetic method
public 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 block
Secondary 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 Animal
class 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:jack
Argument for Animal:Jack
Animal init block
Initializing property nameSize in Animal :4
Dog init block
Initializing 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.borderColor
override fun draw() {
super.draw()
println("Rectangle draw")
}
}
如果存在内部类,则内部类可以通过 super@外部类名
调用外部类。
class Rectangle : Shape() {
val fillColor: String get() = super.borderColor
override 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 = 0
var y: Int = 0
}
println(operand.x + operand.y)
}
或者在类中这样写:
class ObjectSample {
object Operand {
var x: Int = 2
var 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.abc
val 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 = 1
val 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 method
public 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
注解,我们可以将伴生对象的成员生成为真正的静态方法和字段。