Kotlin基础-有趣的面向对象(上)

461 阅读13分钟

15.png

1、类与对象的基本概念

是一个的数据类型,是对象的模板,它是对现实世界中某个对象的一种抽象,是一组具有相似属性和方法的对象的集合

对象是类的实例,是对类这种抽象数据类型的一种具体、现实的描述,具有相似的属性和方法。类定义了对象的结构行为,对象是由类派生而来。

在Kotlin中,一切皆为对象,但不要误会,Kotlin是一门多范式语言,只不过面向对象这种编程思想非常优秀。而像Java这种典型的面向对象式的语言,就是为了达到上层抽象,这也是Java可以实现跨平台的关键。

关于类与对象的理解,我们可以从以下学习。

2、类的基本结构

类的定义使用class关键字,而对象的实例化往往是通过类名(实参列表)的形式,注意,这与Java的new关键字实例化有所不同

在Kotlin中类的定义方式如下

 class Person {}

在Kotlin类中可以包括类名、构造函数、初始化块、属性、函数、内部类和嵌套类、伴生对象、对象表达式和对象声明

16.png

主次构造函数

构造函数在对象被实例化时调用。与Java不同的是,在Kotlin中可以有一个主构造函数,有一个或者多个次级构造函数,如下为一个主构造函数的例子,name和age是Person对象的属性

 class Person constructor(name: String, age: Int) // 不写结构体内容可以省略掉{}

如果主构造函数没有任何的修饰符或者注解修饰的话,就可以将constructor关键字省略,是不是感觉特别简洁

 class Person (name: String, age: Int)

当然,如果有特殊的修饰符或者注解,您的constructor关键字不能省,如下

 class Person private constructor(name: String, age: Int) 

说完主构造函数,再来简单了解一下次级构造函数,我们已经知道,次级构造函数可以有一个或者多个。它在类主体内使用constructor关键字声明,如下是一个简单的例子,我们在实例化Person的时候将name和age初始化,并且输出构造函数传入的两个参数

 class Person {
     val name: String
     val age: Int
 ​
     constructor(name: String, age: Int) {
         this.name = name
         this.age = age
         println("$name $age")
     }
 }
 ​
 fun main() {
     Person("Jack", 18)
 }

当然初始化我们完全可以交给主构造函数,虽然主构造函数不能包含任何代码块,但这个println我们也完全可以放在init {}代码块中完成,如下

 class Person (name: String, age: Int) {
     init {
         println("$name $age")
     }
 }
 ​
 fun main() {
     val person = Person("Jack", 18)
 }

那下面我们就应该去解两个问题,首先就是次构造函数有什么作用呢?其次就是多个构造函数之间怎么区分?

在Kotlin中,次构造函数之间可以相互委托,但最终次构造函数都要委托主构造函数,委托的形式是: this(实际参数)像我们上面次构造函数的例子,在没有主构造函数的时候它可以不委托,如果有主构造函数的话次构造函数必须要委托给主构造函数(只要有主构造函数,即便是无参也要委托),可以是直接或者间接委托。我们换一个示例,我们还是通过构造函数去初始化Person实例,但是我们可以选择直接传入age或者传入一个出生日期可以推算出age。以下示例是通过传入age或者birth两种形式去初始化age(getAgeByBirth只是简单自定义的通过birth获取年龄的一个函数,您无需关注它的逻辑)

17.png

 import java.text.SimpleDateFormat
 import java.util.Date
 ​
 class Person(val name: String, val age: Int) {
     constructor(name: String, birth: String) : this(name, getAgeByBirth(birth)) // 委托给主构造函数
 }
 ​
 fun getAgeByBirth(birth: String): Int {
     val nowTime = SimpleDateFormat("yyyy-MM-dd").format(Date()).split('-')
     val year = birth.split('-')[0].toInt()
     return if (nowTime[0].toInt() > year) nowTime[0].toInt() - year else 0
 }
 ​
 fun main() {
     val person1 = Person("Jack" ,"2003-01-15") // 通过次构造函数初始化
     println("${person1.name}'s age ${person1.age}")
     val person2 = Person("Tom", 18) // 主构造函数初始化
     println("${person2.name}'s age ${person2.age}")
 }

以上代码中Person类的主构造函数的参数age如果没有关键字val的话,它只作为参数传入,Person类中并没有age这个属性,而有val这个关键字声明的话就代表Person类中有这个属性,并且接受一个名为age的参数为age属性初始化

那看清上面的例子之后呢我们再来讨论,多个不同的构造函数之间可以通过传入不同的参数来区分,如果您的两个构造函数传入两个相同长度、相同类型的参数的话,这是不合法的,在Intellij IDEA也会给出您错误提示

最后要注意一下主构造函数、次构造函数以及Init代码块的执行顺序,简单结论,主构造函数>init>次构造函数

限定修饰符

接下来再稍微拓宽一下修饰符,在Kotlin中,修饰符可用于类、方法、函数、枚举、接口等。

接下来了解三个比较基本的限定修饰符publicprotectedinternalprivate,默认为public

修饰符可见范围
public所有地方可见
protected本类或子类中可见,不可在顶层中声明
private本类中可见
internal模块中可见

以上是Kotlin中限定修饰符的可见范围,它与Java是有挺大区别的

  • 首先,Java默认是default,而Kotlin默认是public,在Java中,我们所用到的大部分类都会添加一个public修饰符,而Kotlin就为我们简化了这种代码
  • Kotlin没有包内可见的概念,默认为public修饰符并且Kotlin中没有default这个修饰符,而Kotlin引入了模块内可见的概念,因此添加了internal修饰符,关于模块内可见的概念我们后面还会涉及到
  • protected修饰符在Java中是说在同一个包内的类和所有的子类可见,而Kotlin中就没有包内可见的概念,所以就是同一类以及子类中可见,还有一点需要注意,就是不可以在顶层声明,它不能修饰类
  • private修饰符在本类中可见,子类中都不可见

那下面就列举几个例子,我们前面写的都是public全局可见的,现在我们简单看一下protected和private

 fun main() {
     val person = Person()
     println(person.i)
 }
 ​
 class Person {
     val i = 1
 }

以上代码是没有问题的,但是当我们将Person类的i属性的修饰符改为privateprotected就会有错误提示

 fun main() {
     val person = Person()
     println(person.i)
 }
 ​
 class Person {
     private val i = 1
 }

接下来我们看一下privateprotected有什么区别

 open class Person {
     protected val i = 1
 }
 ​
 class Men : Person() {
     fun println() {
         println(i)
     }
 }

以上代码是没有问题的,因为Men类继承了Person类,它是Person的子类(别急,继承的内容我们待会就会详细解说),因此它可以访问到父类的i属性,但是当我们将Person类的i属性的修饰符改为private之后,IDE就会给出您错误提示

internal的模块内可见我们后面会提及

内部类和嵌套类

在Kotlin中内部类使用inner关键字来声明,以下是一个内部类的示例

 class Outer {
     val name = "Outer"
     inner class Inner {
         fun print() {
             println("Inner class: $name") // 内部类可以访问外部的成员
         }
     }
 }
 ​
 fun main() {
     Outer().Inner().print()
 }

以下是一个嵌套类的示例

 class Outer {
     val name = "Outer"
     class Nested {
         fun print() {
             println("Nested class")
         }
     }
 }
 ​
 fun main() {
     Outer.Nested().print() // 注意现在是Outer.Nested(),并没有实例化Outer,因此嵌套类是静态的
 }

类与接口之间可以嵌套组合,后面我们会详细提及

嵌套类是静态的,不可以访问外部类中的非公共成员,而内部类是可以访问外部类中的非公共成员。

简单总结:

Kotlin类中可以包括类名、构造函数、初始化块、属性、函数、内部类和嵌套类、伴生对象、对象表达式和对象声明

主次构造函数不仅可以实现构造函数重载,还可以让您用不同的初始化逻辑来初始化类的成员变量,比如初始化块、属性初始化器和委托等方式

Kotlin中的限定修饰符。嵌套类和内部类的不同

3、类的继承

首先,Kotlin中有一个超类叫做Any,所有类都隐式继承它,它有三个默认方法equals(), hashCode(), toString()。学过Java的小伙伴是不是听起来特别的熟悉,感觉很像Java中的Object类。

18.png

我们先来看一下类的继承结构

 open class Base 
 ​
 class Derived : Base() 

与Java不同的是,Kotlin中类的继承不使用extends关键字,而是使用:来替换,并且Kotlin中的类默认都是被final修饰符修饰,即不可被继承和覆盖,因此我们如果希望一个类可以被其他类继承,我们就需要添加一个open关键字

被继承的类被称为基类(父类),继承的类被称为派生类(子类)

当子类继承一个父类时,需要调用父类的构造函数来初始化父类的属性和方法。值得注意的是,如果父类有主构造函数,而子类没有,它需要在次构造函数后使用super关键字去初始化基类型或者委托给其他次构造函数,如下

 open class Base(id: Int) {
 ​
 }
 ​
 class Derived : Base{
     constructor(id: Int) : super(id)
 }

但是这样的话更推荐写成子类有主构造函数的形式(您的Intellij IDEA也会给出推荐)

 open class Base(id: Int) {
 }
 ​
 class Derived(id: Int) : Base(id) {
 }

下面要看一下Kotlin继承类重写方法和属性

方法重写(Override methods)

以下代码摘自Kotlin官方文档

 open class Shape {
     open fun draw() { /*...*/ }
     fun fill() { /*...*/ }
 }
 ​
 class Circle() : Shape() {
     override fun draw() { /*...*/ }
 }

因为重写的成员本身默认为open,因此,如果想禁止在子类中被重写的话,还需要添加final关键字修饰

属性重写

以下代码摘自Kotlin官方文档

 open class Shape {
     open val vertexCount: Int = 0
 }
 ​
 class Rectangle : Shape() {
     override val vertexCount = 4
 }

派生类与基类的初始化顺序

派生类的初始化顺序是在基类的初始化顺序之后

可以看以下的示例来理解一下(官网内容简化版)

 fun main() {
     Derived("hello", "world")
 }
 ​
 open class Base(val name: String) {
     init {
         println("Base init")
     }
     open val size: Int = name.length.also {
         println("Base size is $it")
     }
 }
 ​
 class Derived(name: String, val lastName: String)
 : Base(name.uppercase().also { println("Derived for Base argument is $it") }) {
     init {
         println("Derived init")
     }
 ​
     override val size: Int = (name.length + lastName.length).also {
         println("Derived size is $it")
     }
 }

以下为输出结果

 Derived for Base argument is HELLO
 Base init
 Base size is 5
 Derived init
 Derived size is 10

首先我们发现我们传入参数的时候通过uppercase()函数将hello转为大写输出,然后依次执行了基类的init、基类size的初始化、派生类的init、派生类size的初始化,也就是验证了我们的结论派生类的初始化顺序是在基类的初始化顺序之后

在Kotlin中,我们可以通过super关键字调用超类的属性或者函数

 open class Base {
     val baseName = "Base"
     fun printBase() {
         println(baseName)
     }
 }
 ​
 class Derived : Base() {
     val derivedName = super.baseName
     fun printDerived() {
         super.printBase()
         println("Derived")
     }
 }
 ​
 fun main() {
     val derived = Derived()
     println(derived.derivedName)
     derived.printDerived()
 }

如果是内部类,可以通过super@外部类的方式访问外部类的父类属性或函数,我们可以看以下示例来理解

 open class Animal {
     val kind = "Animal"
     open fun action() {
         println("activity")
     }
 }
 ​
 class Reptile : Animal() {
     val name = "Reptile"
     override fun action() {
         val tiger = Tiger()
         tiger.operation()
     }
 ​
     inner class Tiger {
         private fun action() {
             println("crawl")
         }
 ​
         fun operation() {
             super@Reptile.action() // 如果直接写action(),相当于调用外部类的action方法,递归进入死循环
             action()
             println("super Reptile kind is ${super@Reptile.kind}")
         }
     }
 }
 ​
 fun main() {
     Reptile().Tiger().operation()
 }

简单总结:

继承是面向对象中非常重要的一环,派生类可以继承基类的属性和方法,并且可以重写基类的属性和方法

4、类的属性

(1)属性的声明

在Kotlin中,类中的属性的声明与我们的变量声明是一样的,使用valvar关键字即可声明类中的属性。而我们前面还了解过,如果在主构造函数的形参中通过添加这两个关键字即可省去在类中声明在init块中初始化的写法,写法更加灵活优雅

(2)属性的getter和setter方法

写过Java的小伙伴肯定不陌生,在处理一些业务逻辑的时候封装实体类就是需要一些getter和setter方法,但是在Java中的getter和setter方法是比较繁琐的,但不错的是有像Lombok这样的工具帮我们省去一些代码。

在Kotlin中,getter和setter方法的写法没有Java那么繁琐,它的写法相当灵活,而且它跟Java的getter和setter方法的实现方式还不一样,熟悉Java的小伙伴可能清楚Java中一般是封装实体类的时候将属性改为私有来保证隐藏信息实现细节,还要采用特定的命名规范以及返回值,以及调用的时候要用属性名.get()等,而Kotlin却与Java的实现方式不一样。

首先,Kotlin中有默认的getter和setter方法,比如下面几个例子

 class Person {
     val name = "Jack"
     var age = 18
 }
 ​
 fun main() {
     val person = Person()
     println(person.name)
     println(person.age)
     person.age = 19
     println(person.age)
 }

上面的例子中,Person类的name属性只有getter方法,因为它是用val关键字声明,age属性既有默认getter又有默认setter,因为它是用var关键字声明。

那既然都有默认实现getter和setter方法还有什么作用呢?

我们可以自定义getter和setter(以下均摘自Kotlin官网文档)

比如getter我们可以自定义一个计算属性,定义getter为一个长方形的面积

 class Rectangle(private val width: Int, private val height: Int) {
     val area: Int
         get() = width * height
 }
 ​
 fun main() {
     val rec = Rectangle(50, 50)
     println("矩形的面积是 ${rec.area}")
 }

或者我们自定义setter函数,选择性的进行setter,比如下面的例子就是说不能将counter属性修改为负值。(顺便说一下,在setter函数中如果您选择使用counter = value的话它又会触发counter属性的setter函数,进入死循环,因此Kotlin团队就引进field这个关键字)

 class Counter {
     var counter: Int = 0
         set(value) {
             if (value >= 0) field = value
         }
 }
 ​
 fun main() {
     val counter = Counter()
     println(counter.counter) // 0
     counter.counter = 1
     println(counter.counter) // 1
     counter.counter = -1
     println(counter.counter) // -1
 }

(3)常量

如果一个只读属性的值是在编译期是已知的,那就可以通过const关键字来进行修饰,然后编译器会内联该常数,并且替换为该常数的实际值,而且在反射时该字段可以拿到这个字段(反射后面会详细说到)。那么常量有以下的要求

  • 必须声明在全局或者伴生对象内(伴生对象后面会提及)
  • 必须被初始化为Kotlin中的基本数据类型
  • 只能通过const val来声明,没有自定义getter
 const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"

(4)属性和变量的延期初始

在Java中一个类的所有属性都会有一个默认的初始值,当然也可以通过构造方法或者自己默认初始值去初始化。这么做的目的就是为了避免NullPointerException,而在Kotlin中,可以通过lateinit关键字来延期初始化一个属性或者变量。

我们可以看下面的例子

 ​
 class Person(private val name: String) {
     private lateinit var signature: Signature
 ​
     init {
         if ( ::signature.isInitialized ) { // ::signature.isInitialized来判断signature属性有没有被初始化
             println("$name's signatrue is $signature")
         }
 ​
         signature = Signature("I'm a coder")
     }
 ​
     fun printMessage() {
         println("$name's signature is "$signature"")
     }
 }
 ​
 class Signature(private val content: String) {
     override fun toString(): String {
         return content
     }
 }
 ​
 fun main() {
     val person = Person("Tom")
     person.printMessage()
 }

在上面我们使用了lateinit关键字来延期初始化,在init代码块中进行延期初始化,也许您会疑问这样做跟直接初始化有什么区别,我们不仅可以在init代码块中进行后期初始化,我们也可以在很多场景用到,就比如说依赖注入(Dependency Injection),这些我们后面会提及。

使用lateinit关键字进行延期初始化要注意以下几点

  • 变量或属性需要为非空类型
  • 变量或属性不能是基本数据类型,比如Int、Long、String等等,因为它们都有默认值

简单总结:

类的属性可用关键字valvar声明

使用val声明的属性有getter方法,使用var声明的属性有getter和setter方法,getter和setter方法有默认的实现

常量可以使用const val来声明

属性可以使用lateinit来延期初始化

5、抽象类

在Kotlin中,抽象类用关键字abstract声明。抽象类中既可以有自己的属性、构造函数以及方法,还可以有抽象函数和抽象属性。

如果一个类中有抽象属性和抽象方法,那么这个类一定会是抽象类

抽象类的子类如果不是抽象类,必须重写父类中所有的抽象属性和函数。如下

 abstract class Animal {
     abstract val species: String
     abstract fun action()
 ​
     var survive: Boolean = true
 }
 ​
 class Tiger : Animal() {
     override val species: String
         get() = "terrestrial animal"
     override fun action() {
         println("爬行")
     }
 }
 ​
 class Whale : Animal() {
     override val species: String
         get() = "aquatic animal"
     override fun action() {
         println("游行")
     }
 }
 ​
 fun main() {
     val tiger = Tiger()
     print("tiger species=${tiger.species} survive=${tiger.survive} action=")
     tiger.action()
 ​
     val whale = Whale()
     print("whale species=${whale.species} survive=${whale.survive} action=")
     whale.action()
 }

输出

 tiger species=terrestrial animal survive=true action=爬行
 whale species=aquatic animal survive=true action=游行

其实也比较容易理解,何为抽象?不是具体的事物,它只定义一个模板,不能被实例化,继承抽象类的子类就要重写它的抽象函数和属性进行具体的实现。就像上面的例子,抽象类Animal有了Tiger和Whale两个子类,它们都属于动物,但是在种类和行为上却不一样因此我们需要去重写。

简单总结:

抽象类使用关键字abstract定义,抽象类不能被实例化

抽象类可以有自己的抽象函数和抽象属性,非抽象子类必须重写抽象函数和抽象属性

6、接口

在Kotlin中,接口用关键字interface来声明,Kotlin中类可以实现接口,同样是用:,只是接口没有构造函数不加括号,并且一个类可以实现多个接口,接口之间用逗号分隔。

明白过抽象类之后接口就比较好理解了,还是抽象类中的例子

 interface Species {
     val species: String
 }
 ​
 interface Action {
     fun action()
 }
 ​
 interface Survive {
     var survive: Boolean
 }
 ​
 class Tiger : Species, Action, Survive {
     override val species: String
         get() = "terrestrial animal"
     override fun action() {
         println("爬行")
     }
 ​
     override var survive: Boolean = true
 }
 ​
 class Whale : Species, Action, Survive {
     override val species: String
         get() = "aquatic animal"
     override fun action() {
         println("游行")
     }
 ​
     override var survive: Boolean = true
 }
 ​
 fun main() {
     val tiger = Tiger()
     print("tiger species=${tiger.species} survive=${tiger.survive} action=")
     tiger.action()
 ​
     val whale = Whale()
     print("whale species=${whale.species} survive=${whale.survive} action=")
     whale.action()
 }

输出

 tiger species=terrestrial animal survive=true action=爬行
 whale species=aquatic animal survive=true action=游行

接口与抽象类的区别

  • 抽象类可以被继承,而接口不能被继承,只能被实现,可以看到同样是通过:符号定义,继承是Animal(),实现是:Animal,没有括号

  • 抽象类中的抽象方法用关键字abstract声明,抽象类中可以自己的属性、方法和构造函数,还可以有抽象函数和抽象属性,抽象函数和抽象属性都需要子类去重写;接口中属性和属性不用关键字abstract声明,可以写函数和属性签名,并且子类需要重写全部的函数和属性签名,接口中可以有自己的属性和方法,接口中的属性默认是不可变的,不能用var

  • 使用上的区别:可以理解为抽象类是一个抽象的事物,而接口是一个事物具有的功能。要弄清继承和实现的本质区别,Kotlin与Java一样,不支持多继承,这就好比一个具体的事物,比如Tiger,它可以是继承自抽象类Animal,因为它属于动物,但是它不能再去继承Plant,因为它不能即是动物又是植物;而接口不一样,接口代表功能,你可以为了增强这个类有多个功能(实现多个接口)。所以我们选择的时候可以以这个为参考,到底选用抽象类还是接口。

    很多时候还是接口使用较多,因为它可以用作定义一些规范,比如在Java中要驱动数据库,Sun公司最开始会去制定一些接口,而各个数据库厂商只需要去实现这个接口就可以让Java去操作MySQL、Oracle等等数据库。所以接口很多时候也会用作一些提前定义好的标准。

简单总结:

接口使用关键字interface定义,接口不能被实例化

一个类可以实现多个接口

文章若有错误或不足之处,欢迎大家评论区指正,谢谢大家!

另外,欢迎大家来了解一下济南KUG(Jinan Kotlin User Group),如果您对Kotlin技术分享抱有热情,不妨加入济南KUG,济南KUG官网:济南KUG