1

博客主页

定义类继承结构

1.kotlin中的接口

kotlin的接口可以包含抽象方法的定义和非抽象方法的实现,但不能包含任何状态。

// 使用interface关键字声明一个kotlin接口
interface Clickable {
    fun click()
}

// kotlin 在类名后面使用冒号(:)代替java中的extends和implements关键字
// 与java一样,一个类可以实现任意多个接口,但只能继承一个类
class Button : Clickable {
    // override 修饰符用来标注被重写的父类或者接口的方法和属性,与java中的@Override注解类似
    // kotlin中override 修饰符是强制要求的
    override fun click() {
        println(" i was clicked.")
    }
}

接口中的方法可以有一个默认实现,只需要提供一个方法体。

interface Clickable {
    fun click()
    // 带默认实现的方法,在子类中也可以重新定义该方法的行为
    fun showOff() = println("I'm clickable.")
}

如果在类中同时实现了两个接口,而这两个接口都包含了带默认实现的showOff方法,如果没有显式实现showOff,编译器报错,强制要求提供实现。

interface Focusable {
    fun showOff() = println("I'm focusable.")

    fun setFocus(b: Boolean) =
        println("i ${if (b) "got" else "lost"}")
}

class Button : Clickable, Focusable {
    // 如果实现的两个接口中有相同的默认的实现方法,必须显式的实现一个
    override fun showOff() {
        // 使用尖括号加上父类型名字的super表明了想要调用哪一个父类的方法
        // java中把基类的名字放在super关键字的前面:Clickable.super.showOff()
        super<Clickable>.showOff()
        super<Focusable>.showOff()
    }

    override fun click() {
        println(" i was clicked.")
    }
}

2. open、final、abstract修饰符:默认为final

java的类和方法默认是open的,而kotlin中默认都是final的。

// 声明一个带一个open方法的open类,其他的类可以继承它
open class RichButton : Clickable {
    // 这个函数重写一个open函数且它本身同样是open的
    // public void click()
    override fun click() {}

    // 这个函数时final的,不能在子类中重写它
    // public final void disable()
    fun disable() {}

    // 这个函数是open的,可以在子类中重写它,如果不想子类重写它,可以显式的标注为final
    // public void animate()
    open fun animate() {}
}

kotlin中声明一个抽象类使用 abstract 修饰符,这种类不能实例化。

abstract class Animated {
    // 这个函数时抽象的,它没有实现必须被子类重写。抽象成员始终是open的
    // public abstract void animate();
    abstract fun animate()

    // 抽象类中的非抽象函数并不是默认open的,但可以标注为open的
    // public void stopAnimating()
    open fun stopAnimating() {}

    // 抽象类中的非抽象函数并不是默认open的
    // public final void animateTwice()
    fun animateTwice() {}
}

类中访问修饰符的意义,在接口中,不能使用final、open或者abstract,接口中的成员始终是open的,不能将其声明为final的。

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

3.可见性修饰符:默认为public

kotlin中的可见性修饰符与java类似,public、private、protected,但默认的可见性不一样,kotlin中如果省略了修饰符,声明就是public的,java中默认可见性是包私有,kotlin中并没有使用,kotlin只把包作为在命名空间里组织代码的一种方式使用,没有将其作可见性控制。

kotlin提供一个新的修饰符,internal,表示 只在模块内部可见internal 优势在于它提供了对模块实现细节的真正封装。java中外部代码可以将类定义到与你代码相同的包中,从而得到访问包私有声明的权限。

kotlin也允许在顶层声明中使用private可见性,包括类、函数、属性。这些声明就会只在声明它们的文件中可见。

修饰符 类成员 顶层声明
public(默认) 所有地方可见 所有地方可见
internal 模块内部可见 模块内部可见
protected 子类中可见 ---
private 类中可见 文件中可见

4.内部类和嵌套类:默认是嵌套类

kotlin的嵌套类不能访问外部类的实例。在java中,在另一个类中声明一个类时,它会默认变成内部类,隐式地存储了它的外部类的引用,如果内部类上加上static修饰符,静态的嵌套类会从这个类中删除包围它的类的隐式引用。

interface State : Serializable

interface View {
    fun restoreState(state: State)
    fun getState(): State
}

class Button : View {
    override fun getState(): State {
        return ButtonState()
    }

    override fun restoreState(state: State) {
    }

    // kotlin中没有显式修饰符的嵌套类与java中的static嵌套类一样
    // 如果要把它变成一个内部类来持有一个外部类的应用,需要使用inner修饰符
    class ButtonState : State {}
}

嵌套类和内部类在java与kotlin中对应关系

类A在另一个类B中声明 在java中 在kotlin中
嵌套类(不存储外部类的引用) static class A class A
内部类(存储外部类的引用) class A inner class A

在kotlin中,引用外部类实例的语法与java不同,需要使用 this@Outer 从Inner类去访问Outer类

class Outer {
    inner class Inner {
        fun getOuterReference() : Outer = this@Outer
    }
}

5.密封类:定义受限的类继承结构

先来看下作为接口实现的表达式,必须检查else分支

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分支
        else -> throw IllegalArgumentException("Unknown")
    }

kotlin提供了一个解决方案:sealed类。为父类添加一个sealed修饰符,对可能创建的子类做出严格的限制,所有直接子类必须嵌套在父类中。

// 将基类标记为密封类
// sealed修饰符隐含这个类是一个open类,不需显式的添加open修饰符
sealed class Expr {
    // 将所有可能的类作为嵌套类列出
    class Num(val value: Int) : Expr()
    class Sum(val left: Expr, val right: Expr) : Expr()
}

// when表达式处理所有的sealed类的子类,不在需要提供默认的else分支
fun eval(e: Expr): Int = when (e) {
    is Expr.Num -> e.value
    is Expr.Sum -> eval(e.left) + eval(e.right)
}

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

1.初始化类:主构造方法和初始化语句块

// 这段被括号围起来的语句块叫作主构造方法,表明构造方法的参数,以及定义使用这些参数初始化的属性
// val 意味着相应的属性会用构造方法的参数来初始化
class User(val nickname: String)

// constructor 关键字用来开始一个主构造方法或者从构造方法的声明
class User constructor(_nickname: String) {
    val nickname: String
    
    // init关键字用来引入一个初始化语句块,包含了在类被创建时执行的代码,与主构造方法一起使用
    // 可以在一个类中声明多个初始化语句块
    init {
        nickname = _nickname
    }
}

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

可以像函数参数一样为构造方法参数声明一个默认值

// 为构造方法参数提供一个默认值
class User(
    val _nickname: String,
    val isSubscribed: Boolean = true
)


val kerwin = User("kerwin", false)
println(kerwin.isSubscribed) // false

val blob = User("blob") // isSubscribed参数使用默认值true
println(blob.isSubscribed) // true

// 可以显式的为某些构造方法参数标明名称
val alice = User("Alice", isSubscribed = false)
println(alice.isSubscribed) // false

如果一个类有父类,且该类的主构造方法也需要初始化父类,可以通过该类的主构造方法参数引用提供给父类构造方法参数

open class User(
    val nickname: String,
    val isSubscribed: Boolean = true
)

class TwitterUser(nickname: String) : User(nickname)

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

// 因为Person类只有一个private的构造方法,这个类外部代码不能实例化它
class Person private constructor() {}

2. 构造方法:用不同的方式来初始化父类

// 这个类没有主构造方法,声明了两个从构造方法
// 从构造方法可以声明多个
open class View {
    constructor(ctx: Context) {}

    constructor(ctx: Context, attr: AttributeSet?) {}
}

class MyButton : View {
    // 可以使用this关键字,调用自己类的另一个构造方法
    constructor(ctx: Context) : this(ctx, null) {}

    // 调用父类构造方法,使用super关键字调用父类对应的构造方法
    constructor(ctx: Context, attr: AttributeSet?) : super(ctx, attr) {}
}

3. 实现在接口中声明的属性

kotlin中,接口可以包含抽象属性声明。

// 接口没有说明这个属性存储到一个支持字段还是通过getter来获取,本身并不包含任何状态
interface User {
    val nickname: String
}

// 直接在主构造方法声明一个来自实现了User抽象属性
class PrivateUser(override val nickname: String) : User

// 通过自定义getter,每次访问需要计算
class SubscribingUser(val email: String) : User {
    override val nickname: String
        get() = email.substringBefore("@")
}

// 属性初始化,一个支持字段来存储
class FacebookUser(val accountId: Int) : User {
    override val nickname = accountId.toString()
}

接口还可以包含具有getter和setter属性,只要它们没有引用一个支持字段

interface User2 {
    // 必须在子类中重写
    val email: String
    // 可以被继承,属性没有支持字段,结果值在每次访问时通过计算得到
    val nickname: String
        get() = email.substringBefore("@")
}

4. 通过getter或者setter访问支持字段

// 在setter中访问支持字段
class User(val name: String) {
    var address: String = "Unknown"
        set(value) {
            println("Addressn was changed for $name: $field -> $value")
            // 使用特殊的标识符field 来访问支持字段的值
            // getter中只能读取值,setter中,既能读取它也能修改它
            field = value
        }
}

// 修改属性的值,在底层调用了setter
>>> val user = User("kerwin")
>>> user.address = "shanghai"

5. 修改访问器的可见性

访问器的可见性默认与属性的可见性相同。

// 声明一个具有private setter的属性
class LengthCounter {
    // 不能在类外部修改这个属性,确保只能在类中被修改
    var counter: Int = 0
        private set

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

编译器生成的方法:数据类和类委托

1. 通用对象方法

字符串表示:toString()

class Client(
    val name: String,
    val postalCode: Int
) {
    // 重写toString
    override fun toString() = "Client{name:$name, postalCode:$postalCode}"
}

>>> val client = Client("kerwin", 1234)
>>> println(client)
Client{name:kerwin, postalCode:1234}

对象相等性:equals()

class Client(
    val name: String,
    val postalCode: Int
) {
    override fun toString() = "Client{name:$name, postalCode:$postalCode}"
 
    // 重写equals
    override fun equals(other: Any?): Boolean {
        // kotlin中is检查一个值是否为一个指定的类型
        if (other == null || other !is Client) {
            return false
        }
        return name == other.name && postalCode == other.postalCode
    }
}

// 在kotlin中,==检查对象是否相等,而不是比较引用
>>> val client1 = Client("kerwin", 1234)
>>> val client2 = Client("kerwin", 1234)
>>> println(client1 == client2)
true
在kotlin中,==运算符是比较两个对象的默认方式:本质上就是通过调用equals来比较两个值的。如果进行引用比较,可以使用 ===运算符

Hash容器:hashCode()
hashCod方法通常与equals一起被重写。

// 什么情况?返回了false
// 原因就是Client缺少了hashCode方法,如果两个对象相等,必须有这相同的hash值
// HashSet中的值首先比较它们的hash值,相等才会比较值
>>> val set = hashSetOf(Client("kerwin", 1234))
>>> println(set.contains(Client("kerwin", 1234)))
false


class Client(
    val name: String,
    val postalCode: Int
) {
    // ...

    // 重写hashCode     
    override fun hashCode(): Int = name.hashCode() * 31 + postalCode
}

2. 数据类:自动生成通用方法的实现

在kotlin中,在类添加data修饰符,自动生成通用的方法(toString、equals、hashCode)实现。

// 数据类Client
// equals和hashCode方法会将所有在主构造方法中的声明的属性纳入考虑。
// 注意:没有在主构造方法中声明的属性将不会加入到相等性检查和哈希值计算中去
data class Client(
    val name: String,
    val postalCode: Int
)

数据类和不可变性:copy()方法
数据类的属性推荐只使用只读属性(val),让数据类的实例不可变。
为了让使用不可变对象的数据类变得更容易,kotlin编译器为它们多生成了一个方法,一个允许copy类的实例的方法,并在copy的同时修改某些属性的值。

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

    override fun toString() = "Client{name:$name, postalCode:$postalCode}"

    fun copy(
        name: String = this.name,
        postalCode: Int = this.postalCode
    ) = Client(name, postalCode)
}

>>> val client = Client("kerwin", 1234)
>>> println(client.copy(postalCode = 4321))
>>> println(client)
Client{name:kerwin, postalCode:4321}
Client{name:kerwin, postalCode:1234}

3. 类委托:使用by关键字

class DelegatingCollection<T> : Collection<T> {
    private val innerList = arrayListOf<T>()

    override val size: Int
        get() = innerList.size

    override fun contains(element: T): Boolean = innerList.contains(element)

    override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)

    override fun isEmpty(): Boolean = innerList.isEmpty()

    override fun iterator(): Iterator<T> = innerList.iterator()
}

在kotlin中,无论什么时候实现一个接口,都可以使用by关键字将接口的实现委托到另一个对象。

// Collection接口的实现通过by关键字委托给innerList
// 编译器会自动生成方法的实现
class DelegatingCollection<T>(
    val innerList: Collection<T> = arrayListOf()
) : Collection<T> by innerList

object关键字:将声明一个类与创建一个实例结合起来

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

1. 对象声明:创建单例易如反掌

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

// 与类一样,一个对象声明也可以包括属性、方法、初始化语句块等。
// 唯一不允许的有构造方法(包括主构造方法和从构造方法)
// 与普通类的实例不同,对象声明在定义的时就立即创建
object Payroll {
    val allEmployees = arrayListOf<Person>()

    fun calculateSalary() {
        for (person in allEmployees) {
        }
    }
}

// kotlin中的对象声明被编译成了通过静态字段来持有它的单一实例的类,这个字段名字始终都是INSTANCE
// kotlin代码转换为java代码
public final class Payroll {
   @NotNull
   private static final ArrayList allEmployees;
   public static final Payroll INSTANCE;

   @NotNull
   public final ArrayList getAllEmployees() {
      return allEmployees;
   }

   public final void calculateSalary() {
      Person var1;
      for(Iterator var2 = allEmployees.iterator(); var2.hasNext(); var1 = (Person)var2.next()) {
      }

   }

   private Payroll() {
   }

   static {
      Payroll var0 = new Payroll();
      INSTANCE = var0;
      boolean var1 = false;
      allEmployees = new ArrayList();
   }
}

对象声明可以继承类和实现接口。对于实现并不包含任何状态的时候很有用,如:实现java.util.Comparator接口,只需一个单独的Comparator实例比较对象。

object FileComparator : Comparator<File>{
    override fun compare(o1: File, o2: File): Int {
        return o1.path.compareTo(o2.parent, ignoreCase = true)
    }
}

可以在类中声明对象,这样对象也同样只有一个单一实例

// 使用嵌套类实现Comparator
data class Person(val name: String) {
    object NameComparator : Comparator<Person> {
        override fun compare(o1: Person, o2: Person): Int {
            return o1.name.compareTo(o2.name)
        }
    }
}

>>> val list = listOf(Person("Kerwin"), Person("Bob"))
>>> println(list.sortedWith(Person.NameComparator))
    // [Person(name=Bob), Person(name=Kerwin)]

2. 伴生对象:工厂方法和静态成员的地盘

kotlin中的类不能拥有静态成员,java的static关键字并不是kotlin语言的一部分。

kotlin对象声明在其他情况下替代java的静态方法,同时还包括静态字段
kotlin依赖包级别函数在大多数情况下能够替代java的静态方法
推荐使用顶层函数,但顶层函数不能访问类的private成员。

在类中定义的对象之一可以使用一个特殊的关键字来标记:companion

class A {
    // 伴生对象
    companion object {
        fun bar() {
            println("companion object called.")
        }
    }
}

// 可以直接通过类名来访问这个伴生对象的方法和属性
// 这种调用方式有点像java中静态方法的调用
>>> A.bar()

伴生对象可以访问类中的所有的private成员,包括private构造方法。使用工厂方法来创建类实例,下面User实例就是通过工厂方法来创建,而不是通过多个构造方法。伴生对象成员在子类中不能被重写。

// 将主构造方法标记为私有的,该类不被其他代码实例化
class User private constructor(
    val nickname: String
) {
 
    // 声明伴生对象 
    companion object {
        fun newSubscribingUser(email: String) = User(email.substringBefore("@"))

        // 用工厂方法创建一个新用户
        fun newFacebookUser(accountId: Int) = User(accountId.toString())
    }
}

// 通过类名调用 companion object 的方法
>>>   val user = User.newSubscribingUser("kerwin0618@163.com")
>>>   println(user.nickname)
kerwin0618

3. 作为普通对象使用的伴生对象

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

class Person(
    val name: String
) {
   
    // 伴生对象可以有名字,如果省略伴生对象名字,默认名字为Companion
    // 伴生对象没有命名,在java中,可以通过Person.Companion.fromJSON("");访问
    companion object Loader {
        fun fromJson(jsonText: String) : Person {
            return Person(jsonText)
        }
    }
}

// 可以通过两种方式来调用fromJson
>>>    Person.Loader.fromJson("{name: 'kerwin'}")
>>>    Person.fromJson("{name: 'kerwin'}")

在伴生对象中实现接口:

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(jsonText)
        }
    }
}

fun <T> loadFromJSON(factory: JSONFactory<T>) : T {
    return factory.fromJSON("{name: 'kerwin'}")
}

// 将伴生对象实例传入函数中。Person类的名字被当作JSONFactory的实例
>>>    loadFromJSON(Person)

如果要和java代码一起工作,需要类中的成员是静态的,可以在对应的成员上使用 @JvmStatic 注解达到目的。如果想声明一个static字段,可以在一个顶层属性或者声明在object中的属性上使用 @JvmField 注解。

伴生对象扩展:扩展函数定义通过类的实例调用方法 ,但需要定义可以通过类调用方法怎么办呢?

// 为伴生对象定义一个扩展函数
data class Person(
    val firstName: String,
    val lastName: String
) {
    // 声明一个空的伴生对象
    companion object {

    }
}

// 声明一个扩展函数
fun Person.Companion.fromJSON(jsonText: String) : Person {
    return Person("xu", "jinbing")
}

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

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

    // 使用匿名对象来实现事件监听器
    val button = Button()
    button.setOnClickListener(
        // 声明一个实现OnClickListener的匿名对象
        object : OnClickListener {
           // 重写OnClickListener的方法
            override fun onClick(view: View) {

            }
        }
    )

   // 给对象分配一个名字
   val listener = object : OnClickListener {
        override fun onClick(view: View) {
        }
    }

kotlin的匿名对象可以实现多个接口或者不实现接口。

与对象声明不同,匿名对象不是单例的,每次对象表达式被执行都会创建一个新的对象实例。

对象表达式中的代码可以访问创建它的函数中的变量,与java不同,访问并没有被限制在final变量,还可以在对象表达式中修改变量的值

// 匿名对象访问局部变量
fun countClicks(button: Button) {
    // 声明局部变量
    var clickCount = 0
    button.setOnClickListener(object : OnClickListener {
        override fun onClick(view: View) {
            // 更新变量的值
            clickCount++
        }
    })
}

如果我的文章对您有帮助,不妨点个赞鼓励一下(^_^)


小兵兵同学
56 声望23 粉丝

Android技术分享平台,每个工作日都有优质技术文章分享。从技术角度,分享生活工作的点滴。