目录
2.5 面向对象编程
和很多现代高级语言一样,Kotlin 也是面向对象的,因此理解什么是面向对象编程对我们来说就非常重要了。关于面向对象编程的解释,你可以去看很多标准化、概念化的定义,但是我觉得 那些定义只有本来就懂的人才能看得懂,而不了解面向对象的人,即使看了那些定义还是不明白什么才是面向对象编程。
因此,这里我想用自己的理解来向你解释什么是面向对象编程。不同于面向过程的语言(比如 C 语言),面向对象的语言是可以创建类的。类就是对事物的一种封装,比如说人、汽车、房 屋、书等任何事物,我们都可以将它封装一个类,类名通常是名词。而类中又可以拥有自己的字段和函数,字段表示该类所拥有的属性,比如说人可以有姓名和年龄,汽车可以有品牌和价格,这些就属于类中的字段,字段名通常也是名词。而函数则表示该类可以有哪些行为,比如说人可以吃饭和睡觉,汽车可以驾驶和保养等,函数名通常是动词。
通过这种类的封装,我们就可以在适当的时候创建该类的对象,然后调用对象中的字段和函数来满足实际编程的需求,这就是面向对象编程最基本的思想。当然,面向对象编程还有很多其 他特性,如继承、多态等,但是这些特性都是建立在基本的思想之上的,理解了基本思想之后,其他的特性我们可以在后面慢慢学习。
2.5.1 类与对象
现在我们就按照刚才所学的基本思想来尝试进行面向对象编程。首先创建一个 Person 类。右击 com.example.helloworld 包 →New→Kotlin File/Class,在弹出的对话框中输入“Person” 。 对话框在默认情况下自动选中的是创建一个 File,File通常是用于编写 Kotlin 顶层函数和扩展函数的,我们可以点击展开下拉列表进行切换,如 图2.17 所示。
这里选中 Class 表示创建一个类,按下回车键完成创建,会生成如下所示的代码:
class Person {
}
这是一个空的类实现,可以看到,Kotlin中也是使用 class 关键字来声明一个类的,这一点和 Java 一致。现在我们可以在这个类中加入字段和函数来丰富它的功能,这里我准备加入 name 和age 字段,以及一个 eat() 函数,因为任何一个人都有名字和年龄,也都需要吃饭。
class Person {
var name = ""
var age = ""
fun eat(){
println(name + " is eating. He is " + age + " years old.")
}
}
简单解释一下,这里使用 var 关键字创建了 name 和 age 这两个字段,这是因为我们需要在创建对象之后再指定具体的姓名和年龄,而如果使用 val 关键字的话,初始化之后就不能再重新赋值了。接下来定义了一个 eat() 函数,并在函数中打印了一句话,非常简单。
Person类已经定义好了,接下来我们看一下如何对这个类进行实例化,代码如下所示:
val p = Person()
Kotlin 中实例化一个类的方式和 Java 是基本类似的,只是去掉了 new 关键字而已。之所以这么设计,是因为当你调用了某个类的构造函数时,你的意图只可能是对这个类进行实例化,因此即使没有 new 关键字,也能清晰表达出你的意图。Kotlin 本着最简化的设计原则,将诸如 new、行尾分号这种不必要的语法结构都取消了。
上述代码将实例化后的类赋值到了 p 这个变量上面,p 就可以称为 Person 类的一个实例,也可以称为一个对象。
下面我们开始在 main() 函数中对 p 对象进行一些操作:
fun main(){
val p = Person()
p.name = "Jack"
p.age = "19"
p.eat()
}
这里将 p 对象的姓名赋值为 Jack,年龄赋值为 19 ,然后调用它的 eat() 函数,运行结果如 图 2.18 所示。
这就是面向对象编程最基本的用法了,简单概括一下,就是要先将事物封装成具体的类,然后将事物所拥有的属性和能力分别定义成类中的字段和函数,接下来对类进行实例化,再根据具 体的编程需求调用类中的字段和方法即可。
小贴士:
class 关键字来声明一个类。
2.5.2 继承与构造函数
现在我们开始学习面向对象编程中另一个极其重要的特性——继承。继承也是基于现实场景总结出来的一个概念,其实非常好理解。比如现在我们要定义一个 Student 类,每个学生都有自 己的学号和年级,因此我们可以在 Student 类中加入 sno 和 grade 字段。但同时学生也是人呀,学生也会有姓名和年龄,也需要吃饭,如果我们在 Student 类中重复定义 name、age字段和eat()函数的话就显得太过冗余了。这个时候就可以让 Student 类去继承 Person 类,这样 Student 就自动拥有了 Person 中的字段和函数,另外还可以定义自己独有的字段和函数。
这就是面向对象编程中继承的思想,很好理解吧?接下来我们尝试用 Kotlin 语言实现上述功能。 右击 com.example.helloworld 包 →New→Kotlin File/Class,在弹出的对话框中输入“Student” ,并选择创建一个 Class,你可以通过上下按键快速切换创建类型。
按下回车完成创建,并在 Student 类中加入学号和年级这两个字段,代码如下所示:
class Student {
var sno = ""
var grade = 0
}
现在 Student 和 Person 这两个类之间是没有任何继承关系的,想要让 Student 类继承Person 类,我们得做两件事才行。
第一件事,使 Person 类可以被继承。可能很多人会觉得奇怪,尤其是有 Java 编程经验的人。一 个类本身不就是可以被继承的吗?为什么还要使 Person 类可以被继承呢?这就是 Kotlin 不同的地方,在 Kotlin中 任何一个非抽象类默认都是不可以被继承的,相当于 Java 中给类声明了final 关键字。之所以这么设计,其实和 val 关键字的原因是差不多的,因为类和变量一样,最好都是不可变的,而一个类允许被继承的话,它无法预知子类会如何实现,因此可能就会存在一些未 知的风险。Effective Java 这本书中明确提到,如果一个类不是专门为继承而设计的,那么就应该主动将它加上 final 声明,禁止它可以被继承。
很明显,Kotlin 在设计的时候遵循了这条编程规范,默认所有非抽象类都是不可以被继承的。之所以这里一直在说非抽象类,是因为抽象类本身是无法创建实例的,一定要由子类去继承它才 能创建实例,因此抽象类必须可以被继承才行,要不然就没有意义了。由于 Kotlin 中的抽象类和 Java 中并无区别,这里我就不再多讲了。
既然现在 Person 类是无法被继承的,我们得让它可以被继承才行,方法也很简单,在Person 类的前面加上 open 关键字就可以了,如下所示:
open class Person {
var name = ""
var age = ""
fun eat(){
println(name + " is eating. He is " + age + " years old.")
}
}
加上 open 关键字之后,我们就是在主动告诉 Kotlin 编译器,Person 这个类是专门为继承而设计的,这样 Person 类就允许被继承了。
第二件事,要让 Student 类继承 Person 类。在 Java 中继承的关键字是 extends,而在Kotlin 中变成了一个冒号,写法如下:
class Student : Person(){
var sno = ""
var grade = 0
}
继承的写法如果只是替换一下关键字倒也挺简单的,但是为什么 Person 类的后面要加上一对括号呢?Java 中继承的时候好像并不需要括号。对于初学 Kotlin 的人来讲,这对括号确实挺难理解的,也可能是 Kotlin 在这方面设计得太复杂了,因为它还涉及主构造函数、次构造函数等方面的知识,这里我尽量尝试用最简单易懂的讲述来让你理解这对括号的意义和作用,同时顺便学习 一下 Kotlin 中的主构造函数和次构造函数。
任何一个面向对象的编程语言都会有构造函数的概念,Kotlin 中也有,但是 Kotlin 将构造函数分成了两种:主构造函数和次构造函数。
主构造函数将会是你最常用的构造函数,每个类默认都会有一个不带参数的主构造函数,当然你也可以显式地给它指明参数。主构造函数的特点是没有函数体,直接定义在类名的后面即 可。比如下面这种写法:
class Student(val sno:String,val grade:Int) : Person(){
}
这里我们将学号和年级这两个字段都放到了主构造函数当中,这就表明在对 Student 类进行实例化的时候,必须传入构造函数中要求的所有参数。比如:
val student = Student("a123",5)
这样我们就创建了一个 Student 的对象,同时指定该学生的学号是 a123 ,年级是 5。另外,由于构造函数中的参数是在创建实例的时候传入的,不像之前的写法那样还得重新赋值,因此我 们可以将参数全部声明成 val。
你可能会问,主构造函数没有函数体,如果我想在主构造函数中编写一些逻辑,该怎么办呢? Kotlin 给我们提供了一个 init 结构体,所有主构造函数中的逻辑都可以写在里面:
class Student(val sno:String,val grade:Int) : Person(){
init {
println("son is " + sno)
println("grade is " + grade)
}
}
这里我只是简单打印了一下学号和年级的值,现在如果你再去创建一个 Student 类的实例,一 定会将构造函数中传入的值打印出来。
到这里为止都还挺好理解的吧?但是这和那对括号又有什么关系呢?这就涉及了Java 继承特性中的一个规定,子类中的构造函数必须调用父类中的构造函数,这个规定在 Kotlin 中也要遵守。
那么回头看一下 Student 类,现在我们声明了一个主构造函数,根据继承特性的规定,子类的构造函数必须调用父类的构造函数,可是主构造函数并没有函数体,我们怎样去调用父类的构造函数呢?你可能会说,在 init 结构体中去调用不就好了。这或许是一种办法,但绝对不是一 种好办法,因为在绝大多数的场景下,我们是不需要编写 init 结构体的。
Kotlin 当然没有采用这种设计,而是用了另外一种简单但是可能不太好理解的设计方式:括号。 子类的主构造函数调用父类中的哪个构造函数,在继承的时候通过括号来指定。因此再来看一遍这段代码,你应该就能理解了吧。
class Student(val sno:String,val grade:Int) : Person(){
}
在这里,Person 类后面的一对空括号表示 Student 类的主构造函数在初始化的时候会调用 Person 类的无参数构造函数,即使在无参数的情况下,这对括号也不能省略。
而如果我们将 Person 改造一下,将姓名和年龄都放到主构造函数当中,如下所示:
open class Person (val name:String,val age:Int){
fun eat(){
println(name + " is eating. He is " + age + " years old.")
}
}
此时你的 Student 类一定会报错,当然,如果你的 main() 函数还保留着之前创建 Person 实例的代码,那么这里也会报错,但是它和我们接下来要讲的内容无关,你可以自己修正一下,或 者干脆直接删掉这部分代码。
现在回到 Student 类当中,它一定会提示如 图2.19 所示的错误。
这里出现错误的原因也很明显,Person 类后面的空括号表示要去调用 Person 类中无参的构造函数,但是 Person 类现在已经没有无参的构造函数了,所以就提示了上述错误。
如果我们想解决这个错误的话,就必须给 Person 类的构造函数传入 name 和 age 字段,可是 Student 类中也没有这两个字段呀。很简单,没有就加呗。我们可以在 Student 类的主构造函 数中加上 name 和 age 这两个参数,再将这两个参数传给 Person 类的构造函数,代码如下所示:
class Student(val sno:String,val grade:Int,name:String,age:Int) :
Person(name,age){
init {
println("son is " + sno)
println("grade is " + grade)
}
}
注意,我们在 Student 类的主构造函数中增加 name 和 age 这两个字段时,不能再将它们声明成 val,因为在主构造函数中声明成 val 或者 var 的参数将自动成为该类的字段,这就会导致和父类中同名的 name 和 age 字段造成冲突。因此,这里的 name 和 age 参数前面我们不用加任何关键字,让它的作用域仅限定在主构造函数当中即可。
现在就可以通过如下代码来创建一个 Student 类的实例:
fun main(){
val student = Student("a123",5,"Jack",19)
}
学到这里,我们就将 Kotlin的 主构造函数基本掌握了,是不是觉得继承时的这对括号问题也不是那么难以理解?但是,Kotlin 在括号这个问题上的复杂度并不仅限于此,因为我们还没涉及 Kotlin 构造函数中的另一个组成部分——次构造函数。
其实你几乎是用不到次构造函数的,Kotlin 提供了一个给函数设定参数默认值的功能,基本上可以替代次构造函数的作用,我们会在本章最后学习这部分内容。但是考虑到知识结构的完整性,我决定还是介绍一下次构造函数的相关知识,顺便探讨一下括号问题在次构造函数上的区别。
你要知道,任何一个类只能有一个主构造函数,但是可以有多个次构造函数。次构造函数也可以用于实例化一个类,这一点和主构造函数没有什么不同,只不过它是有函数体的。
Kotlin 规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)。这里我通过一个具体的例子就能简单阐明,代码如下:
class Student (val sno:String,val grade:Int,name:String,age:Int):
Person(name,age){
constructor(name: String,age: Int) : this("",0,name,age){
}
constructor():this("",0){
}
}
次构造函数是通过 constructor 关键字来定义的,这里我们定义了两个次构造函数:第一个次构造函数接收 name 和 age 参数,然后它又通过 this 关键字调用了主构造函数,并将 sno 和 grade 这两个参数赋值成初始值;第二个次构造函数不接收任何参数,它通过 this 关键字调用了我们刚才定义的第一个次构造函数,并将 name 和 age 参数也赋值成初始值,由于第二个次构造函数间接调用了主构造函数,因此这仍然是合法的。
那么现在我们就拥有了 3 种方式来对 Student 类进行实体化,分别是通过不带参数的构造函数、 通过带两个参数的构造函数和通过带 4 个参数的构造函数,对应代码如下所示:
fun main(){
val student1 = Student()
val student2 = Student("Jack",19)
val student = Student("a123",5,"Jack",19)
}
这样我们就将次构造函数的用法掌握得差不多了,但是到目前为止,继承时的括号问题还没有进一步延伸,暂时和之前学过的场景是一样的。
那么接下来我们就再来看一种非常特殊的情况:类中只有次构造函数,没有主构造函数。这种情况真的十分少见,但在 Kotlin 中是允许的。当一个类没有显式地定义主构造函数且定义了次构 造函数时,它就是没有主构造函数的。我们结合代码来看一下:
class Student : Person{
constructor(name:String,age:Int):super(name, age){
}
}
注意这里的代码变化,首先 Student 类的后面没有显式地定义主构造函数,同时又因为定义了次构造函数,所以现在 Student 类是没有主构造函数的。那么既然没有主构造函数,继承 Person 类的时候也就不需要再加上括号了。其实原因就是这么简单,只是很多人在刚开始学习 Kotlin 的时候没能理解这对括号的意义和规则,因此总感觉继承的写法有时候要加上括号,有时候又不要加,搞得晕头转向的,而在你真正理解了规则之后,就会发现其实还是很好懂的。
另外,由于没有主构造函数,次构造函数只能直接调用父类的构造函数,上述代码也是将 this 关键字换成了 super 关键字,这部分就很好理解了,因为和Java 比较像,我也就不再多说了。
这一小节我们对 Kotlin 的继承和构造函数的问题探究得比较深,同时这也是很多人新上手Kotlin 时比较难理解的部分,希望你能好好掌握这部分内容。
小贴士:
open 关键字 使类可以被继承
:关键字 继承
init 结构体 所有主构造函数中的逻辑都可以写在里面
constructor 关键字 定义次构造函数
this 关键字 调用
super 关键字
2.5.3 接口
上一小节的内容比较长,也偏复杂一些,可能学起来有些辛苦。本小节的内容就简单多了,因 为 Kotlin 中的接口部分和 Java 几乎是完全一致的。
接口是用于实现多态编程的重要组成部分。我们都知道,Java 是单继承结构的语言,任何一个类最多只能继承一个父类,但是却可以实现任意多个接口,Kotlin 也是如此。
我们可以在接口中定义一系列的抽象行为,然后由具体的类去实现。下面还是通过具体的代码来学习一下,首先创建一个 Study 接口,并在其中定义几个学习行为。右击 com.example.helloworld 包→New→Kotlin File/Class,在弹出的对话框中输入“Study” ,创建类型选择“Interface” 。
然后在 Study 接口中添加几个学习相关的函数,注意接口中的函数不要求有函数体,代码如下所示:
interface Study {
fun readBooks()
fun doHomework()
}
接下来就可以让 Student 类去实现 Study 接口了,这里我将 Student 类原有的代码调整了一 下,以突出继承父类和实现接口的区别:
class Student (name:String,age:Int):Person(name,age),Study{
override fun readBooks() {
println(name + " is reading.")
}
override fun doHomework() {
println(name + " is doing homework.")
}
}
熟悉 Java 的人一定知道,Java 中继承使用的关键字是 extends,实现接口使用的关键字是 implements,而 Kotlin 中统一使用冒号,中间用逗号进行分隔。上述代码就表示 Student 类继承了 Person 类,同时还实现了 Study 接口。另外接口的后面不用加上括号,因为它没有构造函数可以去调用。
Study 接口中定义了 readBooks() 和 doHomework() 这两个待实现函数,因此 Student 类必须实现这两个函数。Kotlin 中使用 override关键字来重写父类或者实现接口中的函数,这里我 们只是简单地在实现的函数中打印了一行日志。
现在我们可以在 main() 函数中编写如下代码来调用这两个接口中的函数:
fun main(){
val student = Student("Jack",19)
doStudy(student)
}
fun doStudy(study: Study) {
study.readBooks()
study.doHomework()
}
这里为了向你演示一下多态编程的特性,我故意将代码写得复杂了一点。首先创建了一个 Student 类的实例,本来是可以直接调用该实例的 readBooks() 和 doHomework() 函数的,但是我没有这么做,而是将它传入到了 doStudy() 函数中。doStudy() 函数接收一个 Study 类型的参数,由于 Student 类实现了 Study 接口,因此 Student 类的实例是可以传递给 doStudy() 函数的,接下来我们调用了 Study 接口的 readBooks() 和 doHomework() 函数, 这种就叫作面向接口编程,也可以称为多态。
现在运行一下代码,结果如 图2.20 所示。
这样我们就将 Kotlin 中接口的用法基本学完了,是不是很简单?不过为了让接口的功能更加灵活,Kotlin 还增加了一个额外的功能:允许对接口中定义的函数进行默认实现。其实 Java 在JDK 1.8 之后也开始支持这个功能了,因此总体来说,Kotlin 和 Java 在接口方面的功能仍然是一模一样的。
下面我们学习一下如何对接口中的函数进行默认实现,修改 Study 接口中的代码,如下所示:
interface Study {
fun readBooks()
fun doHomework(){
println("do homework default implementation.")
}
}
可以看到,我们给 doHomework() 函数加上了函数体,并且在里面打印了一行日志。如果接口中的一个函数拥有了函数体,这个函数体中的内容就是它的默认实现。现在当一个类去实现Study 接口时,只会强制要求实现 readBooks() 函数,而 doHomework() 函数则可以自由选择 实现或者不实现,不实现时就会自动使用默认的实现逻辑。
现在回到 Student 类当中,你会发现如果我们删除了 doHomework() 函数,代码是不会提示错误的,而删除 readBooks() 函数则不行。当删除了 doHomework() 函数之后,重新运行 main()函数,结果如 图2.21 所示。可以看到,程序正如我们所预期的那样运行了。
现在你已经掌握了 Kotlin 面向对象编程中最主要的一些内容,接下来我们再学习一个和 Java 相比变化比较大的部分——函数的可见性修饰符。
熟悉 Java 的人一定知道,Java 中有 public、private、protected 和 default(什么都不写)这 4 种函数可见性修饰符。Kotlin 中也有 4 种,分别是public、private、protected 和 internal,需要使用哪种修饰符时,直接定义在 fun 关键字的前面即可。下面我详细介绍一下 Java 和 Kotlin 中这些函数可见性修饰符的异同。
首先 private 修饰符在两种语言中的作用是一模一样的,都表示只对当前类内部可见。public 修饰符的作用虽然也是一致的,表示对所有类都可见,但是在 Kotlin 中 public 修饰符是默认项,而在 Java 中 default 才是默认项。前面我们定义了那么多的函数,都没有加任何的修饰符,所以它们默认都是 public 的。protected 关键字在 Java 中表示对当前类、子类和同一包路径下的类可见,在 Kotlin 中则表示只对当前类和子类可见。Kotlin 抛弃了Java 中的 default 可见性(同一包路径下的类可见),引入了一种新的可见性概念,只对同一模块中的类可见,使用的是 internal 修饰符。比如我们开发了一个模块给别人使用,但是有一些函数只允许在模块内部调用,不想暴露给外部,就可以将这些函数声明成 internal。关于模块开发的内容,我们会在本书的最后一章学习。
表2.2 更直观地对比了Java 和 Kotlin 中函数可见性修饰符之间的区别。
修饰符 | Java | Kotlin |
---|---|---|
public | 所有类可见 | 所有类可见(默认) |
private | 当前类可见 | 当前类可见 |
protected | 当前类、子类、同一路径下的类可见 | 当前类、子类可见 |
default | 同一包路径下的类可见(默认) | 无 |
internal | 无 | 同一模块中的类可见 |
小贴士:
override关键字 来重写父类或者实现接口中的函数
private 修饰符 表示只对当前类内部可见
public 修饰符 (默认项)表示对所有类都可见
protected 关键字 表示只对当前类和子类可见
internal修饰符 表示只对同一模块中的类可见
2.5.4 数据类与单列类
在面向对象编程这一节,我们已经学习了很多的知识,那么在本节的最后我们再来了解几个 Kotlin 中特有的知识点,从而圆满完成本节的学习任务。
在一个规范的系统架构中,数据类通常占据着非常重要的角色,它们用于将服务器端或数据库中的数据映射到内存中,为编程逻辑提供数据模型的支持。或许你听说过MVC、MVP、MVVM 之类的架构模式,不管是哪一种架构模式,其中的M指的就是数据类。
数据类通常需要重写 equals()、hashCode()、toString() 这几个方法。其中,equals() 方法用于判断两个数据类是否相等。hashCode() 方法作为 equals() 的配套方法,也需要一起重写,否则会导致 HashMap、HashSet 等 hash 相关的系统类无法正常工作。toString()方法用于提供更清晰的输入日志,否则一个数据类默认打印出来的就是一行内存地址。
这里我们新构建一个手机数据类,字段就简单一点,只有品牌和价格这两个字段。如果使用 Java 来实现这样一个数据类,代码就需要这样写:
public class Cellphone {
String brand;
double price;
public Cellphone(String brand, double price) {
this.brand = brand;
this.price = price;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Cellphone) {
Cellphone other = (Cellphone) obj;
return other.brand.equals(brand) && other.price == price;
}
return false;
}
@Override
public int hashCode() {
return brand.hashCode() + (int) price;
}
@Override
public String toString() {
return "Cellphone(brand=" + brand + ", price=" + price + ")";
}
}
看上去挺复杂的吧?关键是这些代码还是一些没有实际逻辑意义的代码,只是为了让它拥有数据类的功能而已。而同样的功能使用 Kotlin 来实现就会变得极其简单,右击 com.example.helloworld 包→New→Kotlin File/Class,在弹出的对话框中输 入“Cellphone” ,创建类型选择 “Class” 。然后在创建的类中编写如下代码:
data class Cellphone (val brand:String,val price:Double)
你没看错,只需要一行代码就可以实现了!神奇的地方就在于 data 这个关键字,当在一个类前面声明了data 关键字时,就表明你希望这个类是一个数据类,Kotlin 会根据主构造函数中的参 数帮你将 equals()、hashCode()、toString() 等固定且无实际逻辑意义的方法自动生成,从而大大减少了开发的工作量。
另外,当一个类中没有任何代码时,还可以将尾部的大括号省略。
下面我们来测试一下这个数据类,在main()函数中编写如下代码:
fun main(){
val cellphone1 = Cellphone("Samsung",1299.99)
val cellphone2 = Cellphone("Samsung",1299.99)
println(cellphone1)
println("cellphone1 equals cellphone2 " + (cellphone1 == cellphone2))
}
这里我们创建了两个 Cellphone 对象,首先直接将第一个对象打印出来,然后判断这两个对象是否相等。运行一下程序,结果如 图2.22 所示。
很明显,Cellphone 数据类已经正常工作了。而如果 Cellphone 类前面没有 data 这个关键 字,得到的会是截然不同的结果。如果感兴趣的话,你可以自己动手尝试一下。
掌握了数据类的使用技巧之后,接下来我们再来看另外一个 Kotlin 中特有的功能——单例类。
想必你一定听说过单例模式吧,这是最常用、最基础的设计模式之一,它可以用于避免创建重复的对象。比如我们希望某个类在全局最多只能拥有一个实例,这时就可以使用单例模式。当然单例模式也有很多种写法,这里就演示一种最常见的 Java 写法吧:
public class Singleton {
private static Singleton instance;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public void singletonTest() {
System.out.println("singletonTest is called.");
}
}
这段代码其实很好理解,首先为了禁止外部创建 Singleton 的实例,我们需要用 private 关键字将 Singleton 的构造函数私有化,然后给外部提供了一个 getInstance() 静态方法用于获取Singleton 的实例。在 getInstance() 方法中,我们判断如果当前缓存的 Singleton 实例为 null,就创建一个新的实例,否则直接返回缓存的实例即可,这就是单例模式的工作机制。
而如果我们想调用单例类中的方法,也很简单,比如想调用上述的 singletonTest() 方法, 就可以这样写:
Singleton singleton = Singleton.getInstance();
singleton.singletonTest();
虽然 Java 中的单例实现并不复杂,但是 Kotlin 明显做得更好,它同样是将一些固定的、重复的逻辑实现隐藏了起来,只暴露给我们最简单方便的用法。
在 Kotlin 中创建一个单例类的方式极其简单,只需要将 class 关键字改成 object 关键字即可。 现在我们尝试创建一个 Kotlin 版的 Singleton 单例类,右击com.example.helloworld 包 →New→Kotlin File/Class,在弹出的对话框中输入“Singleton” ,创建类型选择“Object” ,点击“OK” 完成创建,初始代码如下所示:
object Singleton {
}
现在 Singleton 就已经是一个单例类了,我们可以直接在这个类中编写需要的函数,比如加入 一个 singletonTest() 函数:
object Singleton {
fun singletonTest(){
println("singletonTest is called.")
}
}
可以看到,在 Kotlin 中我们不需要私有化构造函数,也不需要提供 getInstance() 这样的静态 方法,只需要把 class 关键字改成 object 关键字,一个单例类就创建完成了。而调用单例类中的函数也很简单,比较类似于 Java 中静态方法的调用方式:
fun main(){
Singleton.singletonTest()
}
这种写法虽然看上去像是静态方法的调用,但其实 Kotlin 在背后自动帮我们创建了一个 Singleton 类的实例,并且保证全局只会存在一个 Singleton 实例。
这样我们就将 Kotlin 面向对象编程最主要的知识掌握了,这也是非常充实的一节内容,希望你能好好掌握和消化。要知道,你往后的编程工作基本上是建立在面向对象编程的基础之上的。
标签:面向对象编程,关键字,Kotlin,函数,Person,Student,Android,构造函数 From: https://blog.csdn.net/fhbxts/article/details/136765273小贴士:
data 关键字 ,当在一个类前面声明了data 关键字时,就表明你希望这个类是一个数据类,Kotlin 会根据主构造函数中的参 数帮你将 equals()、hashCode()、toString() 等固定且无实际逻辑意义的方法自动生成,从而大大减少了开发的工作量。
object 关键字 ,创建一个单例类。