Kotlin基础——类、对象和接口

文章目录

  • 1 定义类继承结构
    • 1.1 接口
      • 1.1.1 接口概述
      • 1.1.2 接口中的默认方法
      • 1.1.3 接口方法重复
      • 1.1.4 Kotlin接口中静态方法实现原理
    • 1.2 修饰符
      • 1.2.1 类继承修饰
      • 1.2.2 方法重写修饰
      • 1.2.3 抽象类
      • 1.2.4 接口的修饰符
    • 1.3 可见性修饰符
      • 1.3.1 Kotlin中的可见性修饰符
      • 1.3.2 Kotlin中的可见性修饰符和Java的对应关系
    • 1.4 内部类和嵌套类
    • 1.5 密封类
  • 2 声明一个带非默认构造方法或属性的类
    • 2.1 主构造方法和初始化语句块
      • 2.1.1 主构造方法的简化流程
      • 2.1.2 默认构造方法
      • 2.1.3 主构造私有
    • 2.2 构造方法:初始化父类
    • 2.3 实现在接口中声明的属性
    • 2.4 通过getter或setter访问支持字段
    • 2.5 修改访问器的可见性
  • 3 数据类和类委托
    • 3.1 通用对象方法
    • 3.2 数据类
    • 3.3 类委托
  • 4 objet关键字
    • 4.1 对象声明
    • 4.2 伴生对象
    • 4.3 作为普通对象使用的伴生对象
    • 4.4 对象表达式:改变写法的匿名内部类

1 定义类继承结构

1.1 接口

1.1.1 接口概述

kotlin中同样使用interface关键字来定义接口,接口的含义和Java中类似,用于定义抽象

fun main() {
    Button().click()
}

interface Clickable {
    fun click()
}

class Button : Clickable {
    override fun click() {
        println("Button clicked")
    }
}

使用interface关键字定义接口。实现一个接口跟Java中不同,Kotlin中继承类和实现接口都是使用冒号。
重写接口中的方法,使用override关键字,与Java中不同的是,override是必须写的,如果不写则会报错。这会避免先写出实现方法再添加抽象方法造成的意外重写。
和Java一样,Kotlin中的类只能继承一个类,但可以实现多个方法。

1.1.2 接口中的默认方法

Kotlin接口中的默认方法也是与Java中有区别,Java中需要使用default关键字修饰,而Kotlin中不用。Kotlin接口中,如果方法有实现,则是一个默认方法,如果没有实现,则是纯接口。

fun main() {
    Button().click()
}

interface Clickable {
    fun click() {
        println("default click")
    }
}

class Button : Clickable

对于拥有默认实现的接口方法,可以使用默认实现,也可以重写

1.1.3 接口方法重复

由于可以实现多个接口,如果两个接口出现同样的方法,那么实现类应该怎么处理?在Java中,处理方式是必须重写其中的方法,Kotlin中也是一样的要求。

fun main() {
    Button().click()
}

interface Clickable1 {
    fun click() {
        println("default click 1")
    }
}

interface Clickable2 {
    fun click() {
        println("default click 2")
    }
}

class Button : Clickable1, Clickable2 {
    override fun click() {
        super<Clickable1>.click()
    }
}

必须显示的重写重复的方法,否则会报错。这里就是重写click方法,具体实现则调用父类Clickable1的click方法。
注意这里super的用法与Java中有区别,Kotlin中,super后面通过尖括号限定是哪个父接口,而Java中则是如下使用

class Button2 implements Clickable1, Clickable2 {
    @Override
    public void click() {
        Clickable1.super.click();
    }
}

1.1.4 Kotlin接口中静态方法实现原理

Kotlin以jdk1.6为目标进行涉及,当时还没有默认方法。Kotlin会把每个带默认方法的接口编译成一个普通接口和一个将方法体作为静态函数的类的结合体

interface Clickable1 {
    fun click() {
        println("default click 1")
    }
}

对于以上包含默认实现方法的Kotlin接口,使用kotlinc编译后,会产生两个class文件
在这里插入图片描述
内容分别为:
Clickable1.class

public interface Clickable1 {
    void click();
}

Clickable1$DefaultImpls.class

public final class Clickable1$DefaultImpls {
    public static void click(@NotNull Clickable1 $this) {
        System.out.println((Object) "default click 1");
    }
}

接口中只包含了声明,类中包含了一个静态方法,该方法有实现

1.2 修饰符

Kotlin中常用的修饰符有open、final、abstract
Java中有关键字final用于修饰方法,被修饰的方法在其子类中不能够被重写。Kotlin中的final也是有此用途,Kotlin中的open与final对应,表示可以被重写。
Kotlin与Java设计理念不同,Java中,默认是open的,子类可以重写父类中的方法,这会导致脆弱的基类问题。对基类的修改可能会导致子类无法正常运行。在Java中,也建议将子类不需要重写的方法在基类中设置成final。
Kotlin依据此,从语言级别上,将默认的修饰符设置成final,只有需要重写的方法,才需要显示的使用open修饰,允许子类进行重写。
open和final的修饰不止应用于方法级别,对于类的继承也是一样。

修饰符 相关成员 评注
final 不能被重写 类中成员默认使用
open 可以被重写 需要明确地表示
abstract 必须被重写 只能在抽象类中使用,抽象成员不能有实现
override 重写父类或接口中的成员 如果没有使用final表明,重写的成员默认是open的

1.2.1 类继承修饰

Kotlin基础——类、对象和接口_第1张图片
类默认是final的,无法继承,只能将基类设置成open的才能够继承

open class Father
class Son : Father()

继承父类,并调用父类的构造方法

1.2.2 方法重写修饰

Kotlin基础——类、对象和接口_第2张图片
方法默认是final修饰的,需要定义成open才能够重写

open class Father {
    open fun getName() {
        println("Father")
    }
}
class Son : Father() {
    override fun getName() {
        println("Son")
    }
}

定义成open后可以被子类重写

open class Son : Father() {
    override fun getName() {
        println("Son")
    }
}

class Grandson : Son() {
    override fun getName() {
        println("GrandSon")
    }
}

子类重写后子类的方法也是open的,如果不需要让当前子类的子类重写,则需要重新显示的定义final修饰

1.2.3 抽象类

一个类如果被abstract修饰,那么就是抽象类。

abstract class Animated {
    abstract fun animate();

    open fun stopAnimating() {}

    fun animateTwice() {}
}

抽象类中的抽象方法也需要使用abstract修饰,没有使用abstract修饰的方法是普通方法。抽象方法是默认open的,而普通方法则是默认final的。

1.2.4 接口的修饰符

接口中不使用open、final、abstract修饰符。
其中open是默认的,加不加都不影响。
不能使用final修饰,因为接口中的方法就是用来重写的。
如果接口中的方法没有实现,则默认是abstract的,有实现则不是。

1.3 可见性修饰符

1.3.1 Kotlin中的可见性修饰符

Kotlin中的可见性修饰符有private、protected和public,与Java相比,少了default。
Kotlin中的默认可见性修饰符是public的。这与Java不同,Java中是包私有的。
Kotlin中的包和Java中的包含义不同,Kotlin中的包只是代码组织的一种形式,没有用作可见性控制。
Kotlin引入了一个新的可见性修饰符,internal,表示模块内可见。internal可见性的优势在于它提供了对模块实现细节的真正封装。
Kotlin允许在顶层声明中使用private可见性,包括类、函数和属性。这些声明就会只在声明它们的文件中可见。

修饰符 类成员 顶层声明
public 所有地方可见 所有地方可见
internal 模块中可见 模块中可见
protected 子类中可见
private 类中可见 文件中可见

Kotlin基础——类、对象和接口_第3张图片
类的基础类型和类型参数列表中用到的所有类,或者函数的签名都有与这个类或者函数本身相同的可见性

1.3.2 Kotlin中的可见性修饰符和Java的对应关系

  • Kotlin中的public、protected和private修饰符在编译成Java字节码时会被保留。
  • 唯一的例外是private类:在这种情况下它会被编译成包私有声明(在Java中你不能把类声明为private。
  • internal修饰符在字节码中会变成public。

1.4 内部类和嵌套类

Kotlin中也是可以在一个类中定义类,但是与Java不同的是,Kotlin中定义的类默认不是成员内部类,并没有持有外部类的引用,而是类似于Java中的static修饰的静态内部类。如果要定义类似Java中的成员内部类,需要使用inner修饰。
Kotlin基础——类、对象和接口_第4张图片
嵌套类不持有父类的引用,无法直接访问父类的成员。

class Outer {
    val num = 10

    inner class Inner {
        fun printTest() {
            println(num)
        }
    }
}

使用inner修饰,嵌套类会持有父类的引用,可以直接访问父类的成员,包括私有成员,类似于成员内部类。

class Outer {
    val num = 10

    inner class Inner {
        fun getOuterInstance() : Outer{
            return this@Outer
        }
    }
}

访问外部类对象,使用this@访问。
Kotlin对于嵌套类的设计可以防止Java中的以下问题:当内部类可序列化而外部类不可序列化时,如果序列化内部类对象,会报异常,因为内部类对象存储了外部类的引用,而外部类是不可序列化的。

1.5 密封类

密封类使用sealed关键字修饰,对其可能创建的子类做出严格的限制,所有的直接子类必须嵌套在父类中。

interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr

fun eval(e: Expr): Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.left) + eval(e.right)
        else
        -> throw IllegalArgumentException("Unknown expression")
    }

考虑上述场景,当使用when结构来执行表达式的时候,Kotlin编译器会强制检查默认选项。如果新增一个子类,就得新增分支,如果忘了新增分支,则会走默认分支,可能导致潜在的bug。
如果使用密封类,就可以解决上述问题。

sealed class Expr {
    class Num(val value: Int) : Expr()
    class Sum(val left: Expr, val right: Expr) : Expr()
}

fun eval(e: Expr): Int =
    when (e) {
        is Expr.Num -> e.value
        is Expr.Sum -> eval(e.left) + eval(e.right)
    }

外部类无法继承Expr类,所有继承了Expr类的子类都在其内部。

2 声明一个带非默认构造方法或属性的类

Java中,一个类可以声明多个构造方法,Kotlin中也可以,Kotlin中只是做了一些区分,区分主构造方法和从构造方法。
主构造方法:通常是简洁的类初始化方法,声明在类定义的时候
从构造方法:在类内部声明

2.1 主构造方法和初始化语句块

2.1.1 主构造方法的简化流程

class User(val nickName: String)

定义了一个类User,并声明了主构造方法,其中nickName是主构造方法的参数。这里是一种简化后的写法,下面是简化流程

//以下就是完整的定义,使用constructor声明一个构造方法,init关键字用来引入一个初始化语句块
//初始化语句块会在类创建的时候执行,并与主构造方法一起使用。由于主构造方法有语法限制,不能包含初始化代码,所以用初始化语句块
//属性的nickName和构造方法参数中的可以定义为相同的,通过this进行区分
class User constructor(_nickName: String) {
    val nickName: String

    init {
        nickName = _nickName
    }
}

//属性声明与初始化语句块中的初始化代码相结合,所以可以省略初始化语句块
class User constructor(_nickName: String) {
    val nickName: String
}

//如果主构造方法没有注解或者可见性修饰符,可以去掉constructor关键字
class User(_nickName: String) {
    val nickName: String
}

//如果属性用相应的构造方法参数来初始化,可以将val关键字放到参数前,并省略属性定义来简化
//这就简化成了最终的定义
class User(val nickName: String)

2.1.2 默认构造方法

如果一个类没有声明任何构造法方法,在继承的时候,必须显示调用父类构造

open class Button
class RadioButton: Button()

Button类会生成一个不带任何参数的默认构造方法,继承必须调用父类的构造方法。

2.1.3 主构造私有

如果想确保类不被其他代码实例化,必须把构造方法标记为private

class Secretive private constructor(){}

Java中,通常将构造方法私有来实现静态工具类或者是单例类。这在Kotlin中有替代方案,可以使用顶层函数来代替静态工具类,使用对象声明来代替单例类。

2.2 构造方法:初始化父类

Kotlin中由于区分了主构造方法和从构造方法,对于主构造方法来说,一个类中只能有一个,而可以拥有多个从构造方法。
继承时,主构造方法在继承后面调用父类的构造方法。从构造方法使用super调用。

//拥有主构造方法的类继承父类
open class Person
class Student : Person()

//只有从构造方法的类继承父类
open class View {
    constructor(ctx: Context) {}
    constructor(ctx: Context, attr: AttributeSet) {}
}

class MyButton : View {
    //constructor(ctx: Context) : super(ctx)
    constructor(ctx: Context) : this(ctx, MY_STYKLE)
    constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr)
}

可以直接使用super调用父类构造,也可以通过自身调用了父类构造的其他构造方法来调用父类构造

2.3 实现在接口中声明的属性

Kotlin接口与Java接口对于属性有所区别。Java,可以在接口中定义常量,而Kotlin中的常量使用const修饰,只能放在顶层或者object修饰的域中。
Kotlin中,接口可以包含抽象属性说明,让实现类来实现属性声明。

interface User {
    val nickName: String
}

class PrivateUser(override val nickName: String) : User
class SubscribingUser(val email: String) : User {
    override val nickName: String
        get() = email.substringBefore('@')
}

class FacebookUser(val accountId: Int) : User {
    override val nickName = getFacebookName(accountId)

    private fun getFacebookName(accountId: Int): String {
        return "test"
    }
}

接口中定义抽象属性说明,在实现类中用字段去存储,可以表达不同的含义。
第一个PrivateUser使用主构造方法中定义的属性字段进行存储。第二个SubscribingUser通过主构造方法中的属性email来提供nickName这个属性的一个get方法。第三个通过一个函数返回。

除了抽象属性声明外,接口还可以包含具有getter和setter的属性,只要它们没有引用一个存储字段

interface User {
    val email: String
    val nickName: String
        get() = email.substringBefore('@')
}

2.4 通过getter或setter访问支持字段

目前有两种属性的例子:一种是存储值的属性,一种是具有自定义访问器在每次访问时计算值的属性。
如何在自定义访问其中访问支持字段呢?

class User(val name: String) {
    var address: String = "Beijing"
        set(value) {
            println("""Address was changed for $name: "$field" -> "$value".""".trimIndent())
        }
}

fun main() {
    val user = User("Alice")
    user.address = "Shanghai"
}

这里有一个关键字field用于访问支持字段

2.5 修改访问器的可见性

访问器的可见性默认与属性的可见性相同,但是可以修改其可见性

class LengthCounter {
    var counter: Int = 0
        private set

    fun addWord(word: String) {
        counter += word.length
    }
}

默认属性的可见性是public的,将其修改为private,在外部无法修改counter的值。

3 数据类和类委托

3.1 通用对象方法

在编写Java类时,有些方法是常常要重写的,比如toString、equals和hashCode这三个方法。
toString方法用来打印对象实例,如果想输出具体属性值,需要重写该方法。
equals方法用于比较两个对象是否相等,重写之后可用于比较对象中属性是否相等。这里有双等号和equals的区别,双等号在Java中对于基本类型,比较值,对于引用,比较对象地址。在Java的实践中,通常总是调用equals比较对象内容。在Kotlin中,双等号是比较两个对象的默认方式:本质上说是通过equals比较两个值的,所以重写equals后,可以直接使用双等号比较。
hashCode方法用于实现hash运算,便于使用hash算法相关的容器。hash算法的约定是:如果两个对象相等,它们必须有相同的hash值。所以重写equals,必须重写hashCode,以维护这个约定。

3.2 数据类

Kotlin提供了一个新的关键字——data,用于定义数据类。数据类自动重写了equals、hashCode和toString方法。

data class Client(val name: String, val postalCode: Int)

fun main() {
    val client = Client("test", 32)
    println(client)
}
输出:
Client(name=test, postalCode=32)

数据类的属性并没有要求一定是val,但是建议使用val,让数据类的实例不可变。这样做有很多好处,一是容器中作为键使用,不用担心被修改。二是在多线程环境下不担心被修改。
由于建议数据类尽量不被修改,所以提供了copy()方法,可以用于copy类的实例,并在copy的同时修改某些属性的值,这个是编译器自动生成的。

val client2 = client.copy("hello", 23)
println(client2)
输出:
Client(name=hello, postalCode=23)

3.3 类委托

考虑Java中实现装饰器模式,创建一个新类,实现与原始类一样的接口并将原来的类的实例作为一个字段保存,与原始类拥有同样行为的接口不用修改,直接转发到原始类

class DelegatingCollection<T> : Collection<T> {
    private val innerList = arrayListOf<T>();
    override val size: Int
        get() = innerList.size
    override fun isEmpty(): Boolean = innerList.isEmpty()
    override fun iterator(): Iterator<T> = innerList.iterator()
    override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)
    override fun contains(element: T): Boolean = innerList.contains(element)
}

这里就有很多都是模板代码。Kotlin中可以通过委托来实现上述功能,使用by关键字将接口的实现委托到另一个对象。

class DelegatingCollection<T>(innerList: Collection<T> = ArrayList<T>()) : Collection<T> by innerList {
}

这里使用类委托,上述的模板代码都消失了

class CountingSet<T>(val innerSet: MutableCollection<T> = HashSet<T>()) : MutableCollection<T> by innerSet {
    var objectsAdded = 0;

    override fun add(element: T): Boolean {
        objectsAdded++;
        return innerSet.add(element)
    }

    override fun addAll(elements: Collection<T>): Boolean {
        objectsAdded += elements.size
        return innerSet.addAll(elements)
    }
}

可以只重写关注的方法,其他的方法委托给父类对象。

4 objet关键字

objet关键字的实质是定义一个类并同时创建一个对象,常使用的场景有:

  • 对象声明是定义单例的一种方式
  • 伴生对象可以持有工厂方法和其他与这个类相关,但在调用时并不依赖类实例的方法。它们的成员可以通过类名来访问
  • 对象表达式可以用来替代Java的匿名内部类

4.1 对象声明

对象声明通过object关键字引入

object CaseInsensitiveFileComparator : Comparator<File> {
    override fun compare(file1: File, file2: File): Int {
        return file1.path.compareTo(file2.path, ignoreCase = true)
    }
}

对象声明,声明一个类并创建一个对象,是天生的单例类。
与类一样,一个对象声明可以包含属性、方法、初始化语句块等声明,唯一不允许的是构造方法(包括主构造和从构造)
对象声明的对象,在对象声明定义时就创建了,不需要在代码其他地方调用构造方法。
但是在大型系统中,由于对象声明不提供构造,无法对对象实例化进行控制,并且不能通过构造方法指定特定的参数,所以大型系统中还是需要将依赖注入的框架与普通的Kotlin类一起使用。
Kotlin中的对象声明被编译成了通过静态字段来持有它的单一实例的类,这个字段的名字始终是INSTANCE,Java中可以通过如果下调用

CaseInsensitiveFileComparator.INSTANCE.compare(file1, file2);

4.2 伴生对象

Kotlin中的类不能拥有静态成员,Java中的static关键字并不是Kotlin语言的一部分。作为替代,Kotlin依赖包级别函数和对象声明。
在大多数情况下,使用顶层函数可以替代,但是顶层函数不能访问类中的private成员。
如果需要写一个可以在没有类实例的情况下调用但是需要访问类内部的函数,可以将其写成类中的对象声明的成员。
在类中定义的对象之一可以使用一个特殊的关键字来标记:companion。如果这样做,就获得了直接通过容器类名称来访问这个对象的方法和属性的能力,不在需要显式地指明对象的名称。

class A {
    companion object {
        val a = 120
        fun bar() {
            println("Companion object called")
            println("c is ${A().c}")
        }
    }
}

fun main() {
    A.bar()
    println(A.a)
}
输出:
Companion object called
c is 20
120

这里companion object就是声明了一个伴生对象,伴生对象可以访问类中的所有private成员,包括private构造方法,它是实现工厂模式的理想选择。

class User {
    val nickName: String

    constructor(email: String) {
        nickName = email.substringBefore('@')
    }

    constructor(facebookAccountId: Int) {
        nickName = getFacebookName(facebookAccountId)
    }

    private fun getFacebookName(accountId: Int): String {
        return "test"
    }
}

之前的User类可以通过伴生对象来实现成工厂模式

class User private constructor(val nickName: String) {
    companion object {
        fun newSubscribingUser(email: String) = User(email.substringBefore('@'))
        fun newFacebookUser(accountId: Int) = User(getFacebookName(accountId))
        private fun getFacebookName(accountId: Int): String {
            return "test"
        }
    }
}

工厂方法很有用,可以通过用途来命名来创建对应的子类对象。

4.3 作为普通对象使用的伴生对象

伴生对象是一个声明在类中的普通对象,可以有名字,实现一个接口或者有扩展函数或属性。

class Person(val name: String) {
    companion object Loader {
        fun fromJSON(jsonText: String): Person {
            return Person("test")
        }
    }
}

伴生对象可以有自己的名字,如果没有提供名字,则会使用默认的名字Companion

interface JSONFactory<T> {
    fun fromJSON(jsonText: String): T
}
class Person(val name: String) {
    companion object : JSONFactory<Person> {
        override fun fromJSON(jsonText: String): Person {
            return Person("test")
        }
    }
}

伴生对象可以实现接口
类的伴生对象会同样被编译成常规对象:类中的一个引用了它的实例的静态字段,Java中访问伴生对象的方法如下

public static void main(String[] args) {
    Person.Companion.fromJSON("");
}

如果伴生对象有名字,则Companion这个默认名字会被替换。
可以在对应的成员上使用@JvmStatic注解来将类中成员设置成静态的,如果想声明一个static的字段,可以再在一个顶层属性或者声明在object中的属性上使用@JvmField注解。

class Person(val name: String) {
    companion object : JSONFactory<Person> {
        @JvmStatic
        var test: String = "hello"
        @JvmStatic
        override fun fromJSON(jsonText: String): Person {
            return Person("test")
        }
    }
}

对于属性来说,会生成静态的getter和setter方法。对于方法来说,会生成静态的方法。
Java中调用如下:

ystem.out.println(Person.getTest());
System.out.println(Person.fromJSON(""));

对于@JvmField注解

class Person(val name: String) {
    companion object : JSONFactory<Person> {
        @JvmField
        var test: String = "hello"
        override fun fromJSON(jsonText: String): Person {
            return Person("test")
        }
    }
}

会生成静态的字段,Java中访问如下:

System.out.println(Person.test);

伴生对象可以定义扩展函数,调用就像是该函数定义在伴生对象中定义的函数一样。

class Person(val name: String) {
    companion object {

    }
}

fun Person.Companion.fromJSON(jsonText: String): Person {
    return Person("test")
}

fun main() {
    val p = Person.fromJSON("")
}

4.4 对象表达式:改变写法的匿名内部类

object关键字不仅仅能用来声明单例式的对象,还能用来声明匿名对象。匿名对象替代了Java中匿名内部类的用法。

open class Person(val name: String) {
    open fun testFun() {
        println("Person testFun")
    }
}

fun main() {
    object : Person("test") {
        override fun testFun() {
            println("object testFun")
        }
    }.testFun()
}

使用object创建了一个匿名内部类
与Java匿名内部类只能扩展一个类或实现一个接口不同,Kotlin的匿名对象可以实现多个接口或者不识闲接口。
注意与对象声明不同,匿名对象不是单例的。每次对象表达式被执行都会创建一个新的对象实例。
与Java的匿名类一样,可以访问创建它的函数中的变量,但是不同的是Java只能访问final类型的变量,而Kotlin中没有限制,还可以在对象表达式中修改变量的值。

fun main() {
    var num = 20
    object : Person("test") {
        override fun testFun() {
            println("object testFun")
            num++
        }
    }.testFun()
}

你可能感兴趣的:(Kotlin语言,kotlin,python,开发语言)