阅读 1078

Kotlin教程(三)类、对象和接口

写在开头:本人打算开始写一个Kotlin系列的教程,一是使自己记忆和理解的更加深刻,二是可以分享给同样想学习Kotlin的同学。系列文章的知识点会以《Kotlin实战》这本书中顺序编写,在将书中知识点展示出来同时,我也会添加对应的Java代码用于对比学习和更好的理解。

Kotlin教程(一)基础
Kotlin教程(二)函数
Kotlin教程(三)类、对象和接口
Kotlin教程(四)可空性
Kotlin教程(五)类型
Kotlin教程(六)Lambda编程
Kotlin教程(七)运算符重载及其他约定
Kotlin教程(八)高阶函数
Kotlin教程(九)泛型


定义类继承结构

Kotlin中的接口

Kotlin的接口与Java 8 中的相似:它们可以包含抽象方法(方法=函数)的定义以及非抽象方法的实现(与Java 8 中的默认方法类似),但它们不能包含任何状态。 使用interface 关键字定义接口:

interface Clickable {
    fun click()
}
复制代码

我们声明了一个拥有名为click的抽象方法的接口。所有实现这个接口的非抽象类都需要提供这个方法的一个实现。我们来实现以下这个接口:

class Button : Clickable {
    override fun click() = println("i was clicked")
}
复制代码

Kotlin在类名后面使用冒号来代替了Java中的extendsimplements 关键字。和Java一样,一个类可以实现任意多个接口,但是只能继承一个类。 与Java中的@Override 注解类似,Kotlin中使用override 修饰符来标注被重写的父类或者接口的方法和属性,使用override 修饰符是强制要求的,不标注将不能编译, 这会避免先写出实现方法在添加抽象方法造成的意外重写。 接口的方法可以有一个默认实现。Java 8中需要你在这样的实现上标注default 关键字。而Kotlin不需要特殊的标识,只需要提供一个方法体:

interface Clickable {
    fun click()
    fun showOff() = println("i'm Clickable!") //默认实现的方法
}

class Button : Clickable {
    override fun click() = println("i was clicked")
}
复制代码

在Kotlin中实现这个接口时,有默认实现的方法就不一定要实现了。 但是注意了,如果你在Java代码中实现这个Kotlin接口时,所有的方法都要实现,并没有默认实现的说法。

class Abc implements Clickable {

    @Override
    public void click() {
    }

    @Override
    public void showOff() { //必须实现
    }
}
复制代码

这和Kotlin默认方法实现的方式有关系,先来看下实现方式就知道为什么在Java中所有方法都要实现了。我们将上面的接口和实现类转换成Java代码:

public interface Clickable {
   void click();

   void showOff();

   public static final class DefaultImpls {
      public static void showOff(Clickable $this) {
         String var1 = "i'm Clickable!";
         System.out.println(var1);
      }
   }
}

public final class Button implements Clickable {
   public void click() {
      String var1 = "i was clicked";
      System.out.println(var1);
   }

   public void showOff() {
      Clickable.DefaultImpls.showOff(this);
   }
}
复制代码

可以看到Kotlin实现接口默认方法的方式是:定义了一个静态内部类DefaultImpls,在这个类中实现了默认方法,并且参数是Clickable对象,然后给每个实现类(Button)默认加上了实现和调用Clickable.DefaultImpls.showOff(this); 。Kotlin需要兼容到Java 6,因此并没有使用Java 8的接口特性。 有没有发现这种实现方式其实与上一章的扩展函数非常类似?

有一种特殊情况:如果你的类实现了两个接口,并且这两个接口中分别定了同名的默认实现的方法,那这个时候这个类会采用那个接口的默认实现那? 答案是:任何一个都不会使用。取而代之的时,如果你没有显示实现这个同名接口,会得到编译错误的提示。

interface Clickable {
    fun click()
    fun showOff() = println("i'm Clickable!")
}

interface Focusable {
    fun showOff() = println("i'm Focusable!")
}

class Button : Clickable, Focusable {
    override fun showOff() {
        super<Clickable>.showOff()
        super<Focusable>.showOff()
    }

    override fun click() = println("i was clicked")
}
复制代码

这里我们实现同名的showOff ,并且调用父类型的实现。我们使用了与Java相同的关键字super 。但是语法略有不同,Java中可以把基类的名字放在super关键字的前面,就像Clickable.super.showOff() ,在Kotlin中需要把基类的名字放在尖括号中:super<Clickable>.showOff()

open、final和abstract修饰符:默认为final

Java中默认类都是可以被继承和复写方法的,除非显示地使用final 关键字,这通常很方便,但也造成了一些问题。对基类进行修改会导致不正确的行为,这就是所谓的脆弱的基类问题。《Effective Java》中也建议:要么为继承做好设计并记录文档,要么禁止这么做。所以Kotlin采用了这样的思想,默认都是final的。如果你想允许创建一个类的子类,需要使用open 修饰符来标识这个类,还要给每一个可以被重写的属性或方法添加open 修饰符。

open class RichButton : Clickable { //open修饰表示可以有子类
    fun disable() {} //这个函数是final的,不能被子类重写
    
    open fun animate() {} //函数是open的,可以被子类重写
    
    override fun click() {} //这个函数是重写了一个open函数,因此也是open的
}
复制代码

如果你重写一个基类或者接口的成员,重写的成员同样默认是open的,如果你想改变这一行为,阻止子类继续重写,可以显示地将重写的成员标注为final:

open class RichButton : Clickable { 
    final override fun click() {} //显示标记final,阻止子类重写
}
复制代码

在Kotlin中也有abstract 类,除了默认是final以外基本与Java相同:

abstract class Animated { //抽象类,不能创建实例
    abstract fun animate()//抽象方法,必须被子类重写

    open fun stopAnimating() {}//显示修饰open

    fun animateTwice() {}//普通方法默认还是final
}
复制代码

个人建议虽然接口可以默认实现,但我们还是按照Java的习惯来使用,不在接口中定义默认实现,有默认实现的定义成abstract 类即可。

类中范文修饰符的意义

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

可见性修饰符:默认为public

总体来说Kotlin中的可见性修饰符与Java中类似。同样可以使用publicprotectedprivate 修饰符。但是默认的可见性是不一样的,如果省略了修饰符,声明就是public 的。 Java中默认可见性——包私有。在Kotlin中并没有使用。Kotlin只把包作为在命名空间里组织代码的一种方式使用,并没有将其用作可见性控制。 作为替代方案,Kotlin提供了一个新的修饰符:internal ,表示只在模块内部可见。一个模块就是一组一起编译的Kotlin文件,这可能是一个Intellij IDEA模块、一个Eclipse项目、一个Maven或Gradle项目或者一组使用调用Ant任务进行编译的文件。 internal 可见性的优势在于它提供了对模块实现细节的真正封装。使用Java时,这种封装很容易被破坏,因为外部代码可以将类定义到与你代码相同的包中,从而得到访问你包私有声明的权限。 Kotlin中有特有的顶层声明,如果在顶层声明中使用private 可见性,包括类、函数和属性,那么这些声明是会在声明他们的文件中可见。

Kotlin的可见性修饰符

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

注意,protected 修饰符在Java和Kotlin中不同的行为。在Java中,可以从同一个包中访问一个protected 成员,但是在Kotlin中protected 成员只在类和它的子类中可见,即同一个包是不可见的。 同时还要注意类的扩展函数不能访问类的protected 成员。

Kotlin中的public、protected和private修饰符在编译成Java字节码时会被保留。你从Java代码使用这些Kotlin声明时就如同他们在Java中声明了同样的可见性。唯一的例外是private类会被编译成包私有声明(在Java中你不能把类声明为private)。 但是你可能会问,internal修饰符会发生什么?Java中并没有直接与之类似的东西。包私有可见性是一个完全不同的东西,一个模块通常会由多个包组成,并且不同模块可能会包含来自同一个包的声明。因此internal修饰符在字节码中会变成public。 这些Kotlin声明和它们Java翻版(或者说它们的字节码呈现)的对应关系解释了为什么有时你能从Java代码中访问internal类或顶层声明,抑或从同一个包的Java代码中访问一个protected的成员(与你在Java中做的相似)。但是你应该尽量避免这种情况的出现来打破可见性的约束。

此外,Kotlin与Java之间可见性规则的另一个区别:Kotlin中的一个外部类不能看到其内部(或嵌套)类中private成员。

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

如果你对Java的内部类和嵌套类的定义不是很清楚,或者忘了细节,可以看下这篇博客:深入理解java嵌套类和内部类、匿名类

class Outer {

    class Inner {
        //内部类,持有外部类的应用
    }

    static class Nested {
        //嵌套类,不持有外部类
    }
}
复制代码

Java中内部类会持有外部类引用,这层引用关系通常很容易忽略而造成内存泄露和意料之外的问题。因此Kotlin中默认是嵌套类,如果想声明成内部类,需要使用inner 修饰符。

嵌套类和内部类在Java与Kotlin中的对应关系

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

在Java中内部类通过Outer.this 来获取外部类的对象,而在Kotlin中则是通过this@Outer 获得外部类对象。

class Outer {
    inner class Inner {
        fun getOuter(): Outer = this@Outer
    }
}
复制代码

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

Kotlin提供了一个sealed 修饰符用于修饰类,来限制子类必须嵌套在父类中。

sealed class Father {
    class ChildA : Father()

    class ChildB : Father()
}
复制代码

sealed 修饰符隐含这个类是一个open 的类,你不再需要显示得添加open 修饰符了。

这有什么好处那?当你在when 表达式处理所有sealed 类的子类时,你就不再需要提供默认分支了:

fun a(c: Father): Int =
            when (c) {
                is ChildA -> 1
                is ChildB -> 2
//                else -> 3  //覆盖了所有可能的情况,所以不再需要了
            }
复制代码

声明了sealed 修饰符的类只能在内部调用private构造方法,也不能声明一个sealed 的接口。为什么呢?还记得转换成Java字节码时可见性的规则吗?如果不这样做,Kotlin编译器不能保证在Java代码中实现这个接口。

在Kotlin 1.0 中,sealed功能是相当严格的。所有子类必须是嵌套的,并且子类不能创建为data类(后面会提到)。Kotlin 1.1 解除了这些限制并允许在同一文件的任何位置定义sealed类的子类。

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

Java中可以声明一个或多个构造方法,Kotlin也是类似的,只是做了一点修改:区分了主构造方法(通常是主要而简洁的初始化类的方法,并且在类体外部声明)和从构造方法(在类体内部声明)。同样也允许在初始化语句块中添加额外的初始化逻辑。

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

在这之前我们已经见过怎么声明一个简单的类了:

class User (val nickName: String)
复制代码

这里括号围起来的语句块(val nickName: String) 叫做主构造方法。主要有两个目的:标明构造方法的参数,以及定义使用这些参数初始化的属性。查看转换后的Java代码可以了解它的工作机制:

public final class User {
   @NotNull
   private final String nickName;

   @NotNull
   public final String getNickName() {
      return this.nickName;
   }

   public User(@NotNull String nickName) {
      this.nickName = nickName;
   }
}
复制代码

我们也可以按照Java的这种逻辑在Kotlin中实现(事实上完全没有必要,仅仅是学习关键字的例子,这样写与上面完全相同):

class User constructor(_nickName: String) {
    val nickName: String

    init {
        nickName = _nickName
    }
}
复制代码

这里出现了两个新的关键字:constructor 用来开始一个主构造方法或者从构造方法的声明(与类名一起定义主构造方法时可以省略);init 关键字用来引入一个初始化语句块,与Java中的构造代码块非常类似。 这种写法与class User (val nickName: String) 完全一致,有没有注意到简单的写法中多了val 关键字,这意味着相应的属性会使用构造方法的参数来初始化。

构造方法也可以像函数参数一样设置默认值:

class User @JvmOverloads constructor(val nickName: String, val isSubscribed: Boolean = true)
复制代码

默认参数有效减少了定义重载构造,@JvmOverloads 支持Java代码创建实例时也能享受默认参数。

如果你的类具有与一个父类,主构造方法同样需要初始化父类。可以通过在基类列表的父类引用中提供父类构造方法参数的方式做到这一点:

open class User(val nickName: String)

class TwitterUser(nickName: String) : User(nickName)
复制代码

如果没有给一个类声明任何的构造方法,将会生成一个不做任何事情的默认构造方法,继承了该类的的类也必须显示的调用的父类的构造方法:

open class Button

class RadioButton : Button()
复制代码

注意到Button() 后面的() 了吗?这也是与接口的区别,接口没有构造方法,因此接口后面没有()

interface Clickable

class RadioButton : Button(), Clickable
复制代码

如果你想要确保类不被其他代码实例化,那就加上private

class Secretive private constrauctor()
复制代码

在Java中可以通过使用private构造方法禁止实例化这个类来表示一个更通用的意思:这个类是一个静态实用工具类的容器或者单例的。Kotlin针对这种目的具有內建的语言级别的功能。可以使用顶层函数作为静态实用工具。想要表示单例,可以使用对象声明,将会在之后的章节中见到。

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

默认参数已经可以避免构造方法的重载了。但是如果你一定要声明多个构造参数,也是可以的。

open class View {
    constructor(context: Context)

    constructor(context: Context, attributes: Attributes)
} 
复制代码

这个类没有声明主构造方法,但是声明了两个从构造方法,从构造方法必须使用constructor 关键字引出。 如果想要扩展这个类,可以声明同样的构造方法,使用super 关键字调用对应的父类构造方法:

class Button : View {
    constructor(context: Context) : super(context)

    constructor(context: Context, attributes: Attributes) : super(context, attributes)
}
复制代码

就像在Java中一样,也可以使用this 关键字,从一个构造方法调用类中另一个构造方法。

class Button : View {
    constructor(context: Context) : super(context)

    constructor(context: Context, attributes: Attributes) : this(context)
}
复制代码

注意,如果定义了主构造方法,所有的从构造方法都必须直接或者间接的调用主构造方法:

open class View() {
    constructor(context: Context) : this()

    constructor(context: Context, attributes: Attributes) : this(context)
}
复制代码

实现在接口中声明的属性

在Kotlin中,接口可以包含抽象属性声明:

interface User {
    val nickName: String
}
复制代码

其实这里的属性,并不是变量(字段),而是val 代表了getter方法,相应的Java代码:

public interface User {
   @NotNull
   String getNickName();
}
复制代码

我们用几种方式来实现这个接口:

class PrivateUser(override val nickName: String) : User

class SubscribingUser(val email: String) : User {
    override val nickName: String 
        get() = email.substringBefore("@")   //只有getter方法
}

class FacebookUser(val accountId: Int) : User {
    override val nickName = getFacebookName(accountId) //字段支持
}
复制代码

PrivateUser类使用了简洁的语法在主构造方法中声明了一个属性,这个属性实现了来自于User的抽象属性,所以需要标记override。 SubscribingUser类,nickName属性通过一个自定义getter实现,这个属性没有一个支持存储它的值,它只有一个getter在每次调用时从email中得到昵称。 FacebookUser类在初始化时将nickName属性与值关联。getFacebookName 方法通过与Facebook关联获取用户信息,代价较大,因此只在初始化阶段调用一次。 除了抽象属性声明外,接口还可以包含具有getter和setter的属性,只要它们没有引用一个支持字段(支持字段需要在接口中存储状态,这是不允许的):

interface User {
    val email: String
    val nickName: String 
          get() = email.substringBefore("@")
}
复制代码

通过getter或setter访问支持字段

之前说的属性其实有两种:一种是字段或者说变量,Kotlin中声明这种字段会生成默认的getter和setter方法。而另一个种即没有字段,仅仅只有getter和setter方法,因为在Kotlin的表现形式相同,因此都叫做属性。而相应的Java代码可以较清楚地表现两者的区别:

class Student {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSurname() {
        return name.length() > 0 ? name.substring(0, 1) : "";
    }
}
复制代码

name 属性是字段支持的,而Surname 属性仅仅只有get方法,这两个属性定义在Kotlin中是这样的:

class Student {
    var name: String = ""
    val surname: String
        get() = if (name.isNotEmpty()) name.substring(0, 1) else ""
}
复制代码

Kotlin中声明的字段属性会生成默认的getter和setter方法,也可以改变这种默认的生成:

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

在字段的下方也可以像定义自定义访问器那样定义getter和setter方法,在方法中使用field 标识符来表示支持字段。是否发现在Kotlin中这两种属性的区别很小:是否初始化:= "unspecified" ,是否使用field 字段。

修改访问器的可见性

访问器的可见性与属性的可见性相同。但是如果需要可以通过在get和set关键字前放置可见性修饰符的来修改它:

class LengthCounter {
    var counter: Int = 0
        private set

    private var other: Int = 0
}
复制代码

直接在属性前放置private 和在set或者get访问器前放置有什么区别那?看看转换后的Java代码:

public final class LengthCounter {
   private int counter;
   private int other;

   public final int getCounter() {
      return this.counter;
   }

   private final void setCounter(int var1) {
      this.counter = var1;
   }
}
复制代码

private直接修饰属性将不会生成getter和setter方法。而修饰set会生成private的setter方法。

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

通用对象方法

我们先来看看Java中常见的toStringequalshashCode 方法在Kotlin中是如何复写的。

toString()

class Client(val name: String, val postalCode: Int) {
    override fun toString(): String = "Client(name=$name, postalCode=$postalCode)"
}
复制代码

equals()

在Java中== 运算符,如果应用在基本数据类型上比较的是值,而在引用类型上比较的是引用。因此,在Java中通常总是调用equals。 而在Kotlin中== 就是Java中的equals ,如果在Kotlin中想要比较引用,可以使用=== 运算符。


class Client(val name: String, val postalCode: Int) {
    override fun equals(other: Any?): Boolean {
        if (other == null || other !is Client) { //检查是不是一个Client
            return false
        }
        return name == other.name && postalCode == other.postalCode
    }
}
复制代码

Any 是java.lang.Object的模拟:Kotlin中所有类的父类。可空类型Any? 意味着other有可能为null。在Kotlin中所有可能为null的情况都需要显示标明,即在类型后面加上 ,后续章节会详细说明。

hashCode()

hashCode方法通常与equals一起被重写,因为通用的hashCode契约:如果两个对象相等,他们必须有着相同的hash值。

class Client(val name: String, val postalCode: Int) {
    override fun hashCode(): Int = name.hashCode() * 31 + postalCode
}
复制代码

这三个方法在数据容器bean通常都是被重写的,并且基本都是工具自动生成的,而现在Kotlin编译器就可以帮我们做这些工作。

数据类:自动生成通用方法的实践

只需要在class 前加上data 关键字就能定义一个实现了toStringequalshashCode 方法的类——数据类:

data class Client(val name: String, val postalCode: Int) 
复制代码

虽然数据类的属性并没有要求是val ,但还是强烈推荐只使用只读属性,让数据类的实例不可变。为了让不可变对象的数据类的使用变得更容易,Kotlin编译器为它们多生成了一个方法,一个允许copy类的实例的方法,并在copy的同时修改某些属性的值。下面是手动实现copy方法后看起来是的样子:

data class Client(val name: String, val postalCode: Int) {
    fun copy(name:String = this, postalCode:Int = this.postalCode) = Client(name, postalCode)
}
复制代码

类委托:使用“by”关键字

Java中通常采用装饰器模式来向其他类添加一些行为。这种模式的本质就是创建一个新类,实现与原始类一样的接口并将原来的类的实例作为一个字段保存,与原始类拥有同样行为的方法不用修改,只需要直接转发到原始类的实例。 这种方式的一个缺点是需要相当多的模板代码。例如我们来实现一个Collection的接口的装饰器,即使你不需要修改任何的行为:

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

    override val size: Int = 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 关键字将接口的实现委托到另一个对象。下面就是怎样通过推荐的方式来重写前面的例子:

class DelegatingCollection<T>(val innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList
复制代码

类中所有的方法实现都消失了,编译器会生成它们,并实现与DelegatingCollection的例子是相似的。这样的话仅仅只需要重写我们需要改变行为的方法就可以了:

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

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

    override fun addAll(elements: Collection<T>): Boolean {
        objectAdded++
        return innerSet.addAll(elements)
    }
}
复制代码

这个例子通过重写add和addAll方法计数,并将MutableCollection接口剩下的实现委托给被包装的容器。

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

Kotlin中object关键字在多种情况下出现,但是他们都遵循同样的核心理念:这个关键字定义一个类并同时创建一个实例(对象)。让我们来看看使用它的不同场景:

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

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

单例模式是Java中最常用的一种设计模式。Kotlin通过使用对象声明功能为这一切提供了最高级的语言支持。对象声明将类声明与该类的单一实例声明结合到了一起。

object Payroll {
    val allEmployees = arrayListOf<Person>()

    fun calculateSalary() {
        for (person in allEmployees) {
            ...
        }
    }
}
复制代码

与类一样,一个对象声明也可以包含属性、方法、初始化语句块等的声明。唯一不允许的就是构造方法。与普通类的实例不同,对象声明在定义的时候就立即创建了,不需要再代码的其他地方调用构造方法。 与变量一样,对象声明允许你使用对象名.字符 的方式来调用方法和访问属性:

Payroll.allEmployees.add(Person(...))
Payroll.calculateSallary()
复制代码

想知道它是如何工作的?同样来看转换后的Java代码吧:

public final class Payroll {
   @NotNull
   private static final ArrayList allEmployees;
   public static final Payroll INSTANCE;

    private Payroll(){
    }

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

   public final void calculateSalary() {
       ...
   }

   static {
      Payroll var0 = new Payroll();
      INSTANCE = var0;
      allEmployees = new ArrayList();
   }
}
复制代码

可以看到私有化了构造方法,并且通过静态代码块初始化了Payroll实例,保存在INSTANCE字段,这也是为什么在Java中是使用需要这种方式:

Payroll.INSTANCE.calculateSalary()
复制代码

该INSTANCE是在Payroll类加载进内存中就会创建的实例,因此,不建议将依赖太多或者开销太大的类使用object声明成单例。

同样可以在类使用对象声明创建单例,并且该对象声明可以访问外部类中的private属性:

data class Person(val name: String) {
    //定义
    object NameComparator : Comparator<Person> {
        override fun compare(o1: Person, o2: Person): Int = o1.name.compareTo(o2.name)
    }
}

val persons = listOf(Person("Bob"), Person("Alice"))
persons.sortedWith(Person.NameComparator) //调用
复制代码

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

Kotlin中的类不能拥有静态成员:Java的static关键字并不是Kotlin语言的一部分。作为代替,Kotlin依赖包级别函数(在大多数情况下能够替代Java的静态方法)和对象声明(在其他情况下替代Java的静态方法,同时还包括静态字段)。在大多数情况下,还是推荐使用顶层函数,但是顶层函数不能访问类的private成员。 特别是Java中常见的工厂方法和类中需要使用的static成员该如何定义那?就像这样的:

static class B {
        public static final String tag = "tag";
        
        private B() {
        }

        public static B newInstance() {
            return new B();
        }
    }
复制代码

这时候就要使用伴生对象了。伴生对象是在类中定义的对象前添加一个特殊的关键字来标记:companion 。这样做,就获得了直接通过容器类名称来访问这个这个对象的方法和属性的能力,不再需要显示得指明对象的名称,最终的语法看起来非常像Java中的静态方法调用:

class A private constructor() {
    companion object {
        fun newInstance() = A()
        val tag = "tag"
    }
}

A.newInstance()
A.tag
复制代码

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

伴生对象本质也是一个普通对象,普通对象可以做的一切伴生对象都可以,例如实现接口。 之所以看上去奇怪,是因为之前我们只是省略它的类名,也可以给它加上类名:

class A private constructor() {
    companion object C{
        val tag = "tag"
        fun newInstance() = A()
    }
}

A.C.newInstance() //两种使用方式效果相同
A.newInstance()
复制代码

如果省略了伴生对象的名字,默认的名字将会是Companion。这点在将代码转换成Java代码后就出现了:

public final class A {
   @NotNull
   private static final String tag = "tag";
   public static final A.Companion Companion = new A.Companion();

   private A() {
   }

   public static final class Companion {
      @NotNull
      public final String getTag() {
         return A.tag;
      }

      @NotNull
      public final A newInstance() {
         return new A((DefaultConstructorMarker)null);
      }

      private Companion() {
      }
   }
}
复制代码

所以,你应该理解在Java中调用伴生对象的属性是这样的了:A. Companion.newInstance() 。 为了让Java中调用也有一致的体验,可以在对应的成员上使用@JvmStatic注解来达到这个目的。如果你想声明一个static字段,可以在一个顶层属性或者声明在object中的属性上使用@JvmField注解。

class A private constructor() {
    companion object{
        @JvmField
        val tag = "tag"
        @JvmStatic
        fun newInstance() = A()
    }
}
复制代码

既然伴生对象就是一个普通类,当然也是可以声明扩展函数:

fun A.Companion.getFlag() = "flag"

A.getFlag()
复制代码

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

object关键字不仅仅能用来声明单例式的对象,还能用来声明匿名对象。我们来翻写下Java中如下使用匿名内部类的代码:

public static void main(String[] args) {
        new B().setListener(new Listener() {
            @Override
            public void onClick() {

            }
        });
    }

    interface Listener {
        void onClick();
    }

    static class B {

        private Listener listener;

        public void setListener(Listener listener) {
            this.listener = listener;
        }
    }
复制代码

Kotlin中使用匿名内部类:

fun main(args: Array<String>) {
    B().setListener(object : Listener {
        override fun onClick() {
        }
    })
}
复制代码

除了去掉了对象的名字外,语法时与对象声明相同的。对象表达式声明了一个类并创建了该类的一个实例,但是并没有给这个类或是实例分配一个名字。通常来说它们都不需要名字,应为你会将这个对象用作一个函数调用的参数。如果你需要给对象分配一个名字,可以将其存储到一个变量中。 与Java匿名内部类只能扩展一个类或实现一个接口不同,Kotlin的匿名对象可以实现多个接口。并且访问创建匿名内部类的函数中的变量是没有限制在final变量,还可以在对象表达式中修改变量的值:

fun main(args: Array<String>) {
    var clickCount = 0 
    B().setListener(object : Listener {
        override fun onClick() {
            clickCount++ //修改变量
        }
    })
}
复制代码

同样的,我们通过查看转换的Java代码还研究为什么可以做到这些区别:

public static final void main(@NotNull String[] args) {
      final IntRef clickCount = new IntRef();
      clickCount.element = 0;
      (new B()).setListener((Listener)(new Listener() {
         public void onClick() {
            int var1 = clickCount.element++;
         }
      }));
   }
复制代码

可以看到这里通过IntRef包装了我们定义的clickCount,因此,final属性声明在了包装类上。 那Kotlin的匿名对象可以实现多个接口,又是如何做的那?我又新定义了一个接口,让匿名内部类同时实现两个接口:

fun main(args: Array<String>) {
    var clickCount = 0
    val niming = object : Listener, OnLongClickListener {
        override fun onLongClick() {
        }

        override fun onClick() {
            clickCount++
        }
    }
    B().setListener(niming)
    View().onLongClickListener = niming
}

interface OnLongClickListener {
    fun onLongClick()
}

class View {
    var onLongClickListener: OnLongClickListener? = null
}
复制代码
public static final void main(@NotNull String[] args) {
      final IntRef clickCount = new IntRef();
      clickCount.element = 0;
      <undefinedtype> niming = new Listener() {
         public void onLongClick() {
         }

         public void onClick() {
            int var1 = clickCount.element++;
         }
      };
      (new B()).setListener((Listener)niming);
      (new View()).setOnLongClickListener((OnLongClickListener)niming);
   }
复制代码

出现了一个新东西<undefinedtype> 根据字面理解应该是一个未确定的类型,并且可以强转成对应的接口,这个可能就不是Java的内容了,不清楚具体的实现是怎样的。

关注下面的标签,发现更多相似文章
评论