类、对象和接口

定义类的继承结构

Kotlin 中的接口

Kotlin 的接口可以包含抽象方法的定义以及非抽象方法的实现( 与 Java 8 中的默认方法类似),但它们不能包含任何状态。跟 Java 一样,用 interface 来声明一个接口:

interface Clickable{
    fun click()
}

让我们看看如何实现这个接口:

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

Kotlin 在类名后面使用冒号来代替 Java 中的 extendsimplements 关键字,跟 Java 一样,也是单继承和多实现。在 Kotlin 中,用 override 修饰符来标注被重写的父类或接口的方法和属性,且是强制性的,而不用 Java 可选的 @Override 注解,这样可以避免方法的意外重写。

接口的方法可以有一个默认的实现,在 Java 8 需要你在这样的方法上标注 default 关键字,而 Kotlin 不需要特殊的标识,只需要提供一个方法体:

// 定义一个接口
interface Clickable {
    fun click()

    // 接口带默认实现
    fun showOff() = println("I am a clickable!")
}

// 接口实现类
class Button : Clickable {
    override fun click() = println("I was clicked")
}

实现上述接口时,有默认实现的方法可以不重写,也可以重写实现自己的逻辑,没有默认实现的方法必须重写实现。

注意:在 Java 中,并不支持 Kotlin 的这种接口默认实现,必须重写所有的方法,因为 Kotlin 是以 Java6 为目标设计的,Java 8 及以上版本才支持接口默认实现。

那么 Kotlin 的接口默认方法是怎样实现的呢?其实,Kotlin 的接口默认实现,会把每个带默认方法的接口编译成一个普通接口和一个将方法体作为静态函数的类的结合体,我们来看一下生成的 Java 代码:

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);
   }
}

public interface Clickable {
   // 普通接口方法
   void click();
   void showOff();

    // 静态内部类
   public static final class DefaultImpls {
      // 默认方法体实现对应的静态函数
      public static void showOff(Clickable $this) {
         String var1 = "I am a clickable!";
         System.out.println(var1);
      }
   }
}

代码可知,接口实际上并没有默认实现,会把默认实现转换成对应的静态函数实现。

假设,有两个不同的接口,但有同名的默认实现方法,如果某个类实现了这两个接口,那么调用的是哪个接口的默认实现呢?实际上,会编译错误,需要显式重写自己的实现,因为编译器无法知道要调用哪个,示例代码如下:

// 接口实现类
class Button : Clickable, Focusable {
    override fun click() = println("I was clicked")

    override fun showOff() {
        super<Clickable>.showOff()
        super<Focusable>.showOff()
    }

}

interface Focusable {
    // 接口带默认实现
    fun showOff() = println("I am a Focusable!")
}

因为有两个同名的 showOff 默认实现,所以需要重写 showOff 函数,这里通过 super 来调用接口或基类的实现,只是语法跟 Java 有点不同,super<Clickable>.showOff() 类似于 Clickable.super.showOff()

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

Java 中,允许你创建任意类的子类并重写任意方法,除非显式使用 final 关键字进行标注,这通常很方便,但子类如果出现预期之外的方法重写,就会出问题。所以,Kotlin 采用了默认是 final 的思想,如果你想允许创建一个类的子类,需要使用 open 修饰符来标注,还要给每个可以被重写的属性和函数添加 open 标注符。

open class RichButton : Clickable{  // 这个类是 open 的,可以被其他类继承

    fun disable(){}     //这个函数是 final 的,不能被子类重写

    open fun animate(){}    //这个函数是 open 的,可以被子类重写

    override fun click() {} //这个函数重写了一个 open 函数并且本身也是 open 的

}

注意:如果重写了基类或接口的成员,默认还是 open 的,想要阻止子类继承重写,可以显式地指定重写成员为 final 的。

final override fun click() {} //显式标记final,阻止子类重写

Kotlin 中 ,也可以将类声明为抽象(abstract)的 ,这种类不能被实例化,跟接口一样,默认是 open 的。跟接口的区别是:接口里面的方法默认都是 open 的,无论方法有没有默认实现。而抽象类里面只有抽象方法是 open ,非抽象方法默认不是 open 的,但是可以标注为 open

// 抽象类,默认为 open
abstract class Animated{

    abstract fun animate()      // 抽象方法,默认为 open

    fun animateTwice() {}       // 非抽象方法,默认为 final

    open fun stopAnimating(){}  // 非抽象方法,显式指定是 open

} 

可见性修饰符: 默认为 public

  1. Kotlin 中,如果省略了修饰符,默认就是 public ,而 Java 默认是包私有。

  2. Kotlin 提供了一个新的修饰符 internal ,表示只在模块内部可见,提供了对模块实现细节的真正封装。使用 Java 时,这种封装很容易被破坏,因为外部代码可以将类定义到与你代码相同的包中,从而得到访问你包私有声明的权限。

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

  4. Kotlin 不允许在包中访问 protected 的成员,protected 成员只在类和子类中可见,这是跟 Java 的重要区别。

  5. 类的扩展函数不能访问它的 privateprotected 成员属性和函数。

  6. Kotlin 中的 private 类在 Java 中会被编译成包私有声明,因为 Java 中不能把类声明为 private 的,而 internal 修饰符会在 Java 字节码中变成 public

  7. Kotlin 中的外部类不能看到其内部类或嵌套类中的 private 成员。

第一个例子:

internal open class TalkativeButton : Focusable {

    private fun yell() = println("Hey!")

    protected fun whisper() = println("Let is talk !")
}

fun TalkativeButton.giveSpeech() {   // 这里报错,需要加上 internal
    yell()     // 这里报错,无法调用 private 方法

    whisper()   // 这里报错,无法调用 protected 方法
}

第二个例子:

// Java 代码
public class ObjectPraticeJava {

    private static InnerClass innerClass = new InnerClass();

    public static void main(String[] args) {
        // 可以访问嵌套类的 private 成员
        System.out.println(innerClass.age);
    }

    static class InnerClass {
        private int age = 18;
    }
}

// kotlin 代码
class ObjectPratice {

    private val innerClass: MyInnerClass = MyInnerClass()

    fun main() {
        // 不能访问嵌套类的 private 成员,会报错
        println(innerClass.age)
    }

    class MyInnerClass {
        private val age = 18
    }
}

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

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

// Java 代码
class Outer {

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

    static class Nested {
        //嵌套类,不持有外部类
    }
}

// kotlin 代码
class OuterClass{

    // 这是嵌套类,不持有外部类引用
    class NestedClass{

    }

    // 这是内部类,持有外部类引用
    inner class innerClass{ //声明  inner

    }

}

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

// 这是内部类,持有外部类引用
inner class innerClass{ //声明  inner
    fun getOuter() = this@OuterClass
}

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

Kotlin 提供了一个 sealed 修饰符来对可能创建的子类做出严格的限制,sealed 修饰的类默认就是 open 的,所以无需显式添加 open 修饰符。在 Kotlin 1.0 中 , sealed 修饰的类的子类必需嵌套到父类里面,而在 1.1 版本以后,允许在同一文件的任何位置定义 sealed 类的子类。

当文件中只存在一个 sealed 类时,我们分别看一下对应的 Kotlin 和 Java 代码:

// Kotlin 代码
sealed class Expr

// Java 代码,变成了抽象类
public abstract class Expr {
    // 私有构造
   private Expr() {
   }
}

从上面的代码得知 sealed 类会变成抽象类,且私有构造函数,所以在其他文件中继承 sealed 类是不可能的,因为不能初始化父类,但是在同一文件可以实现子类

// 简单的值对象,实现 Expr 接口
class Num(val value: Int) : Expr()

// Sum 运算,需要左右两个简单值对象,运用多态,声明成基类
class Sum(val left: Expr, val right: Expr) : Expr()

sealed 类所在文件中添加了子类后的 Java 代码如下:

public abstract class Expr {
   private Expr() {
   }

   // 多了这个公有构造函数
   // $FF: synthetic method
   public Expr(DefaultConstructorMarker $constructor_marker) {
      this();
   }
}

子类就是同通过上述这个公有构造函数来初始化父类的,但是不同文件中创建子类时,会提示私有构造,无法创建。如果你在 when 表达式中处理了所有 sealed 类的子类,你就不再需要提供默认分支了:

// 如果表达式是 Num 类型,直接返回值,如果是 Sum 类型,先计算左右表达式的值再求和
fun eval(e: Expr): Int =
    when (e) {
        // 用 is 判断实例,不需要再强转
        is Num -> e.value
        is Sum -> eval(e.left) + eval(e.right)
//        else -> throw Exception("error")  // 不需要了,已经覆盖所有的情况
    }

如果添加了新的 sealed 类的子类,when 表达式会报错,避免忘记添加新分支(会走默认分支选项)导致的潜在问题。

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

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;
   }
}

我们来看一下不用简洁方式实现的代码:

class User constructor(nickName: String, age: Int) {
    val nickName: String
    val age: Int

    // 初始化语句块
    init {
        this.nickName = nickName
    }

    init {
        this.age = 18
    }
}

这里出现了两个关键字: constructorinitconstructor 关键字用来声明一个主构造或从构造方法,init 关键字引入一个初始化语句块,在类被创建时执行,配合主构造方法来完成初始化 ,init 语句块可以有多个。转换成 Java 代码是这样的:

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

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

   public final int getAge() {
      return this.age;
   }

   public User(@NotNull String nickName, int age) {
      this.nickName = nickName;
      this.age = 18;
   }
}

其实 init 初始化语句可以与属性相结合,并且如果主构造方法没有注解或可见性修饰符,可以去掉 constructor 关键字,那么会变成这样:

// 省略 constructor,属性与初始化语句结合
class User(_nickName: String, _age: Int) {
    val nickName: String = _nickName
    val age: Int = _age
}

由于命名问题,我们把主构造的参数前面加上 _ 来表示区分,通过类型推导可以省略掉类型,变成这样:

// 省略 constructor,属性与初始化语句结合
class User(_nickName: String, _age: Int) {
    val nickName = _nickName
    val age = _age
}

如果属性用构造方法参数来初始化,可以通过把 val 关键字加在参数前来进行简化,参数的名称就变成属性名称了,而且该属性使用这些参数初始化。

class User(val nickName: String,val age: Int) 

注意:没有说一定是加上 val ,加上 var 也是一样的,只是 val 和 var 的区别罢了。

// kotlin
class Son(var grade: Int)

// Java
public final class Son {
    // 没有 final
   private int grade;

   public final int getGrade() {
      return this.grade;
   }

    // 多了 set 方法
   public final void setGrade(int var1) {
      this.grade = var1;
   }

   public Son(int grade) {
      this.grade = grade;
   }
}

跟其他函数一样,构造方法也可以设置默认值:

// 构造方法有默认值
class User(val nickName: String = "fanda",val age: Int = 18)

注意,方法参数的类型是不能省略的,比如,不能这样写:

class User(val nickName = "fanda",val age = 18)

为了能让 Java 也能享受默认参数的好处,可以用 @JvmOverloads 注解修饰构造器,这时候,constructor 不能省略了:

// 构造方法有默认值
class UserSuper @JvmOverloads constructor(val nickName: String = "fanda",val age: Int = 18)

这里的构造方法的使用,跟普通带有默认值的函数的使用完全一样,可以通过命名参数来指定参数。如果你的类具有一个父类,主构造方法同样需要初始化父类,可以通过在基类列表的父类引用中提供父类构造方法参数的方式来做到:

// 构造方法有默认值
open class UserSuper(val nickName: String = "fanda", val age: Int = 18)

// 有父类,需要把参数传递给父类来初始化
class User(val myName: String, nickName: String, age: Int) : UserSuper(nickName, age)

注意: val myName: String 表示这个是 User 的属性,并利用该参数赋值,而 nickNameage 并不是 User 的属性,仅仅只是参数而已,只要是用来传递给父类 UserSuper 的属性进行赋值,我们来看一下生成的 Java 代码:

public final class User extends UserSuper {
   // 只有这个是 User 的属性
   @NotNull
   private final String myName;

   @NotNull
   public final String getMyName() {
      return this.myName;
   }

   public User(@NotNull String myName, @NotNull String nickName, int age) {
      // nickname 和 age 传递给父类
      super(nickName, age);
      this.myName = myName;
   }
}

再重申一下: var 和 val 加在主构造方法的参数前面,是用来简化属性和初始化操作的,参数与对应的属性才能关联上,如果构造方法前面没有关键字 var 和 val ,那仅仅是参数的传递,不会生成属性。

如果没有给一个类声明任何构造方法,将会生成一个无参默认构造方法,继承该类的子类需要显式调用父类的无参构造:

interface Run
open class Car
class Bus : Car(), Run

注意到Car() 后面的 () 了吧,这也是与接口的区别,接口没有构造,因此没有 ()

如果你不想类在外部被实例化,比如提供工厂方法来实例化对象,那么需要私有化构造方法,在 constructor 前面加上 private 即可:

class Bus private constructor(): Car(), Run

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

参数默认值和参数命名的语法涵盖了重载函数的场景了,但是如果你一定要声明多个构造方法,也是可以的(实际上没有必要)。

open class View {
    constructor(ctx: Context)
    constructor(ctx: Context, attrs: String)
}

class MyView : View {

    val mContext: Context
    val mAttrs: String

    //    constructor(ctx: Context):super(ctx)
    constructor(ctx: Context) : this(ctx, "default")    // 从构造方法

    // super 用于父类的初始化
    constructor(ctx: Context, attrs: String) : super(ctx, attrs) {// 从构造方法
        mContext = ctx
        mAttrs = attrs
    }
}

这个类没有声明主构造方法,声明两个从构造方法,从构造方法必须使用 constructor 来声明,子类使用 super 关键字调用对应的父类构造方法,也可以使用 this 关键字,从一个构造方法调用另一个构造方法。

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

open class OtherView(){
//    constructor(ctx: Context) : this()// 从构造方法
    constructor(ctx: Context) : this(ctx, "default")// 从构造方法
    constructor(ctx: Context, attrs: String) :this()     // 从构造方法
}

open class OtherView(count: Int) {
    //    constructor(ctx: Context) : this()// 从构造方法
    constructor(ctx: Context) : this(ctx, "default")// 从构造方法

    constructor(ctx: Context, attrs: String) : this(count = 10)      // 从构造方法
}

我们再来分析一下同时有主构造和从构造的情况:

class OtherClient(val name: String) {
    var postalCode: Int = 0

    constructor(_postalCode: Int) : this("") {
        this.postalCode = _postalCode
    }
}

上述类 OtherClient 会有两个构造方法,第一个是只传入 name ,第二个是只传入 postalCode 。这里的 postalCode 不能声明为 val 的,因为如果调用的是只传入 name 的构造,假设 postalCodeval 的,那么 postalCode 必须先进行初始化。但是如果属性被初始化了,第二个只传入 postalCode 的构造又能够重新赋值,这是 val 不允许,因此互相矛盾了。最终 postalCode 只能声明为 var 的并进行初始化,确保调用第一个只传入 name 的构造时,postalCode 有值,而且能够被只传入 postalCode 的构造重新赋值。

重申一下:从构造函数必须直接或间接调用主构造方法,因为如果调用了只传入 postalCode 的构造方法,val 修饰和 name 属性必须要有值,上述代码传入了 “” ,就算主构造方法没有参数,也要调用主构造的无参构造。

实现在接口中声明的属性

Kotlin 中 ,接口可以包含抽象属性声明,但接口本身不包含任何状态,所以并没有说明这个值应该存储到一个支持字段还是通过 getter 来获取。

interface IUser{
    // 声明一个抽象属性
    val nickName: String
}

上面其实定义的不是字段,而是 val 代表了 getter 方法,对应的 Java 代码:

public interface IUser {
   @NotNull
   String getNickName();
}

第一种实现方式:

// 直接用最简语法,实现接口抽象属性,需要加上 override
class privateUser(override val nickName: String) : IUser

// Java 代码
public final class privateUser implements IUser {
   @NotNull
   private final String nickName;

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

   public privateUser(@NotNull String nickName) {
      this.nickName = nickName;
   }
}

可以看到,接口的抽象属性在实现类中变成字段了,在构造方法中初始化,并有 getter 方法。

第二种实现方式:

class CallUser(val email: String) : IUser {
    override val nickName: String
        get() = email.substringBefore("@")  //只有 getter 方法,不是字段
}

这种实现方式中,nickName 不是字段,只提供了一个 getter 方法而已,每次获取 nickName 时,都会调用 getter 方法。

第三种实现方式:

class BookUser(val accountId: Int) : IUser {
    override val nickName = getBookName(accountId)

    fun getBookName(accountId: Int) = "$accountId@name" //有字段支持

}

这种实现方式和第二种有些类似,实际上完全不一样,这里的 nickName 是有字段支持的,并且和属性 accountId 相关联,即在 accountId 属性进行初始化时,nickName 也会进行初始化进行存储,每次获取的是初始化存储好的值,想象一下,如果 getBookName 方法开销非常大,这种方式非常有优势。

我们来看一下对应的 Java 代码,会容易理解:

public final class BookUser implements IUser {
   @NotNull
   private final String nickName;
   private final int accountId;

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

   @NotNull
   public final String getBookName(int accountId) {
      return accountId + "@name";
   }

   public final int getAccountId() {
      return this.accountId;
   }

   public BookUser(int accountId) {
      this.accountId = accountId;
      this.nickName = this.getBookName(this.accountId);
   }
}

注意:除了抽象属性声明外,接口还可以包含具有 gettersetter 的属性,只要它们没有一个支持字段(支持字段需要在接口中存储状态,这是不允许的)。

interface ITeacher {
    //这是抽象属性,需要重写
    val email: String   
    // 这个属性可以被继承,但是没有字段支持,每次会调用 getter 方法
    val nickName: String   
        get() = email.substringBefore("@")
}

通过 getter 或 setter 访问支持字段

现在我们结合有字段支持的属性和具有自定义访问器的例子:

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

输出
Address was changed for fanda: "default" -> "xilichaguang".

上述的类在对属性 address 进行修改时都会输出日志,因为自定义了 setter 访问器来处理了一些额外的逻辑。在 setter 的函数体中,使用了特殊的标识符 field 来访问支持字段的值。在 getter 中,只能读取值,而在 setter 中既能读取值也能修改值。这里没有自定义 getter 方法,直接返回字段的值即可,无需额外逻辑。

修改访问器的可见性

访问器的可见性默认与属性的可见性相同,但是可以通过在访问器前添加可见性修饰符来修改它:

class LengthCounter{
    var counter: Int = 0
    private set     // 变成私有的,不能在类外部修改这个属性

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

上述代码把属性 counterset 方法变成 private 的,因此 setter 方法是不会再自动生成了,因为不能在类外部修改这个属性了,没有必要生成。但是 getter 方法还是有的,这里提供了一个 addWord 方法来对属性 counter 来进行修改,而不再使用默认的 setter ,我们看一下对应生成的 Java 代码:

public final class LengthCounter {
   private int counter;

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

   public final void addWord(@NotNull String word) {
      this.counter += word.length();
   }
}

注意:如果直接在属性前设置 private,那么对应的 set 和 get 访问器都不会生成,等同于分别在对应的 set 或 get 访问器前放置 private 。

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

通用对象方法

我们先来看看 Java 中常见的 toStringequalshasCode 方法在 kotlin 中是如何重写的:

toString

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

// 输出 Client(name=fanda,postalCode=123456)
println(Client("fanda", 123456))

equals

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

class Client(val name: String, val postalCode: Int) {
    override fun equals(other: Any?): Boolean {
        if (other == null || other !is Client) {
            return false
        }
        // 因为上面判断了类型,所以这里编译器会对 other 进行智能转换为 Client
        return name == other.name && postalCode == other.postalCode
    }
}

Anyjava.lang.Object 的模拟: Kotlin 中所有类的父类。可空类型 Any? 意味着 other 有可能为 null 。在 Kotlin 中所有可能为 null 的情况都需要显式标明,即在类型后面加上

hasCode

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

class Client(val name: String, val postalCode: Int) {
    override fun hashCode(): Int {
        return name.hashCode() * 31 + postalCode
    }
}

我们来测试一下上述三个方法是否被重写时,下面代码的输出是怎样的:

val client = Client("fanda", 123456)
val client2 = Client("fanda", 123456)
val hashSet = hashSetOf(client)

println(client.toString())
println(client == client2)
println(hashSet.contains(client2))

//没被重写时,会输出
zeng.fanda.com.kotlinpratice.object.Client@372f7a8d
false
false

//被重写时,会输出
Client(name=fanda,postalCode=123456)
true
true

上述的三个方法在数据容器 bean 通过都是要被重写的,并且基本上都是用工具自动生成的,而现在 kotlin 编译器能够帮助自动生成这些方法,而不需要显式地手动生成。

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

只需要在类前面加上 data 修饰符,上述三个方法将会自动生成,非常方便。

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

我们来测试一下:

fun testClient() {
    val client = NewClient("fanda", 123456)
    val client2 = NewClient("fanda", 123456)
    val hashSet = hashSetOf(client)

    println(client.toString())
    println(client == client2)
    println(hashSet.contains(client2))
}

// 输出
NewClient(name=fanda, postalCode=123456)
true
true

这种数据类,有两点需要注意的地方:

  1. data 修饰的类,必须要有主构造方法,并且至少要有一个属性。

  2. 不在主构造方法里面的属性,不会加入到上述三个方法的处理逻辑中去。

  3. 属性最好使用 val 来修饰,让数据类的实例不可变,避免多线程等情况属性值被改变导致的各种问题。

我们来看一下例子:

data class OtherClient(val name: String) {
    var postalCode: Int = 0

    constructor(_postalCode: Int) : this("") {
        this.postalCode = _postalCode
    }
}

上述 OtherClient 类的 postalCode 属性将不会加入到上述三个方法的处理逻辑中去。我们来测试一下:

val client = OtherClient("fanda")
client.postalCode = 123456
val client2 = OtherClient("fanda")
client2.postalCode = 123456

println(client.toString())

//输出,并没有 postalCode 属性
OtherClient(name=fanda)

具体生成的对应的方法逻辑是没有 postalCode 属性的,可以研究一下生成的 Java 代码。

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

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

从上述方法可知,copy 方法的默认参数值就是属性的值,也可以单独修改一些属性的值。

注意: 使用 data 类自动生成的 copy 方法,copy 的属性只包括主构造的属性,跟上述自动生成的三个方法的一样。

val client = OtherClient("fanda")
client.postalCode = 123456

println(client.copy("liuhang").name)
println(client.copy("liuhang").postalCode)    // 这里将输出 0 ,而不是 123456

//输出
liuhang
0

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

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

// 装饰类
class DelegationCollection<T> : Collection<T> { // 实现与原始类同样的接口
    // 原始类保存为字段
    private val innerList = arrayListOf<T>()

    // 分别转发原始类对应的方法
    override val size = innerList.size
    override fun contains(element: T) = innerList.contains(element)
    override fun containsAll(elements: Collection<T>) = innerList.containsAll(elements)
    override fun isEmpty() = innerList.isEmpty()
    override fun iterator() = innerList.iterator()
}

观察上述代码,典型的装饰类处理,里面有相当多的模板代码,而 Kotlin 对这种委托做了语言级别的支持。无论什么时候实现一个接口,你都可以使用 by 关键字将接口的实现委托到另一个对象,我们利用这个特征来重构上述代码:

class DelegationCollection<T>(private val innerList: Collection<T> = ArrayList()) : Collection<T> by innerList

类中所有的方法都不需要写了,编译器会生成它们,如果需要改变某些方法的逻辑,重写那些方法即可:

class DelegationCollection<T>(private val innerList: Collection<T> = ArrayList()) : Collection<T> by innerList{
    var count = 0

    override fun isEmpty(): Boolean {
        count++
        return innerList.isEmpty()
    }
}

上述代码重写了 isEmpty 函数,其他函数实现还是委托给对应的内部实例 innerList

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

Kotlinobject 关键字在多种情况下出现,但是他们都遵循同样的核心理念:这个关键字定义一个类并同时创建一个实例对象,有以下使用场景:

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

对象声明:创建单例

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

class Person

// 对象声明,不需要 class ,将类声明与该类的单一实例声明结合到了一起
object Payroll{
    val allEmplayees = arrayListOf<Person>()

    fun calculateSalary() {
        for (person in allEmplayees) {
            //...
        }
    }
}

对象声明用 object 来替代 class 来声明类,与普通类一样,一个对象声明也可以包含属性,方法,初始化语句块等声明。

注意: 对象声明不允许有构造函数,无论是主构造还是从构造,对象声明在定义的时候就立即创建了,不需要在代码的其他地方调用构造方法。

与变量一样,对象声明允许你使用对象名加 . 字符的方式来调用方法和访问属性:

Payroll.allEmplayees.add(Person())
Payroll.calculateSalary()

对象声明跟普通类一起,也可以继承类和实现接口,让我们实现一个忽略大小写比较文件路径的比较器:

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

println(FileComparator.compare(File("/User"), File("/user")))

也可以在任何使用普通对象的地方使用单例对象,例如:

val files = listOf(File("/Z"), File("/a"))
println(files.sortedWith(FileComparator))

注意:单例对象名就是对象本身,可以看作普通对象一样使用,只不过这个对象是一个单例的而已,因为是对象本身,所以用 . 来访问方法和属性是很正常的。

同样的,也可以在类内部声明对象,就像声明普通类一样:

data class Man(val name: String) {
    object NameComparator : Comparator<Man> {
        override fun compare(o1: Man, o2: Man): Int {
            return o1.name.compareTo(o2.name)
        }
    }
}

val manList = listOf(Man("fanda"), Man("hehe"))
println(manList.sortedWith(Man.NameComparator))

那么,在 Java 中如何使用 Kotlin 声明的单例对象呢?我们先来看一下 FileComparator 转换成 Java 的代码是怎样的:

public final class FileComparator implements Comparator {
   // 静态字段持有单一实例,该字段名字始终是 INSTANCE
   public static final FileComparator INSTANCE;

   public int compare(@NotNull File o1, @NotNull File o2) {
        ...
   }

   // 私有化构造方法,不让外部创建实例
   private FileComparator() {
   }

   // 静态代码块,初始化实例,只有在类加载时调用一次
   static {
      FileComparator var0 = new FileComparator();
      INSTANCE = var0;
   }
}

答案很明显了,要从 Java 代码使用 kotlin 单例对象,可以通过访问静态的 INSTANCE 字段,比如:

System.out.println(FileComparator.INSTANCE.compare(new File("/User"), new File("/user")));

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

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

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

// 伴生对象
class Dog{
    companion object ObjectDog{
         val age = 5
        fun run() {
            println("I am running")
        }
    }
}

调用的时候是这样的:

println(Dog.age)
println(Dog.ObjectDog.run())
println(Dog.run())    // 省略 ObjectDog

ObjectDog 是可以省略掉的,直接调用对应的属性和方法即可,是不是非常类似于 Java 类内声明的静态变量和方法。我们来看一下转化为 Java 时的代码:

public final class Dog {
    // 伴生对象里面声明的属性会变成静态属性
   private static final int age = 5;
    // 生成一个 public static 的伴生对象类
   public static final Dog.ObjectDog ObjectDog = new Dog.ObjectDog((DefaultConstructorMarker)null);

   public static final class ObjectDog {
      public final int getAge() {
         return Dog.age;
      }

      public final void run() {
         String var1 = "I am running";
         boolean var2 = false;
         System.out.println(var1);
      }

      private ObjectDog() {
      }

      // $FF: synthetic method
      public ObjectDog(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

因此,在 Java 中如果要使用伴生对象的话,需要这样使用:

// Java 使用时,ObjectDog 不能省略
System.out.println( Dog.ObjectDog.getAge());
Dog.ObjectDog.run();

Kotlin 中,因为伴生对象的类名在调用时是可以省略的,可以在声明上也可以省略掉,如下:

// 伴生对象
class Dog{
    companion object {    // 省略掉伴生对象名称
         val age = 5
        fun run() {
            println("I am running")
        }
    }
}

如果是这种情况的话, Java 该如何调用呢?其实,如果省略掉伴生对象名称,编译器也会生成一个默认的类名,叫做 Companion ,如下:

public final class Dog {
   private static final int age = 5;
    // 现在用的是默认生成的名字, Companion
   public static final Dog.Companion Companion = new Dog.Companion((DefaultConstructorMarker)null);

    // 类名为 Companion
   public static final class Companion {
      public final int getAge() {
         return Dog.age;
      }

      public final void run() {
         String var1 = "I am running";
         boolean var2 = false;
         System.out.println(var1);
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

那么在调用的时候,只需要把对应的名称换成 Companion 即可。

那么在类内声明对象和声明伴生对象,到底有什么区别呢?我们为声明一个类似的类内对象:

class Cat{
     object ObjectCat{
        val age = 5
        fun run() {
            println("I am running")
        }
    }
}

首先有第一个区别,类内声明对象一定要有类名,不能省略掉,因为属性和方法的调用是通过类名进行的,调用的时候就像在 Java 中调用伴生对象一样:

// ObjectCat 不能省略
println(Cat.ObjectCat.age)
println(Cat.ObjectCat.run())

这是调用上的区别,还有就是生成的 Java 代码其实完全不一样,我们看一下:

public final class Cat {

   public static final class ObjectCat {
      // 对象里面声明的属性会变成静态属性,但在单例对象里层
      private static final int age = 5;
      public static final Cat.ObjectCat INSTANCE;

      public final int getAge() {
         return age;
      }

      public final void run() {
         String var1 = "I am running";
         boolean var2 = false;
         System.out.println(var1);
      }

      private ObjectCat() {
      }

      static {
         Cat.ObjectCat var0 = new Cat.ObjectCat();
         INSTANCE = var0;
         age = 5;
      }
   }
}

首先静态属性是属于单例对象的(伴生对象是属于外部类的),外部类也不持有内部声明的对象,这也说明了为什么不能省略对象名称。

总结一下:

  1. object 声明的对象是单例对象,可以直接通过对象名称对调用方法和访问属性,仅此而已,就算该对象是声明在某个类内,但是这个对象是独立的,其实跟外部类没有多大关系,是一个嵌套类罢了。

  2. companion object 声明的是伴生对象,从叫法上就能知道,肯定跟外部类是有关联的,伴生对象的属性会变成外部类的静态属性,外部类会持有伴生对象,可以通过持有的伴生对象调用对于的方法,语法上是可以省略掉的,即可以通过外部类来直接调用伴生对象的属性和方法(跟 Java 的静态变量和方法调用变得一致了),所以如果想实现 Java 类内的静态变量和静态方法,完全可以通过伴生对象来实现。

  3. 伴生对象是可以访问到外部类的私有构造方法的。

Java 中通过私有化构造方法,然后提供工厂方法来提供该类的实例,而在 Kotlin 中的实现方式就是通过伴生对象,因为伴生对象是可以访问到外部类的私有构造方法的:

data class User private constructor(val nickName: String) {   // 有一个私有的构造方法的数据类

    companion object {
        // 工厂方法生成 User 对象
        fun newCallUser(email: String) = User(email.substringBefore("@"))
        fun newBookUser(accountId: Int) = User(getBookName(accountId))

        private fun getBookName(accountId: Int) = "$accountId@name" //有字段支持
    }
}

//调用
println(User.newCallUser("2543533434@qq.com"))
println(User.newBookUser(435730))

如果想让 Java 调用伴生对象时,也有一致的体验,可以利用注解来实现,用 @JvmField 注解表示声明一个 static 字段 ,用 @JvmStatic 表示声明一个 static 方法,例如:

// 伴生对象
class Dog {

    companion object {
        @JvmField
        val age = 5
        @JvmStatic
        fun run() {
            println("I am running")
        }
    }
}

此时,在 Java 中调用就有一致的体验了:

System.out.println( Dog.age);
Dog.run();

注意: 伴生对象其实也跟普通类一样,也可以继承其他类和实现接口,如果继承或实现接口了,就等同于外部类也一样继承或实现接口了,当有一个函数使用抽象方法来加载实体,可以传入外部类。

interface IEat {
    fun eat()
}

open class Base

// 伴生对象
class Dog {
    companion object : IEat, Base() {
        override fun eat() {
            println("I am eating")
        }

        @JvmField
        val age = 5

        @JvmStatic
        fun run() {
            println("I am running")
        }
    }
}

最后说明一点,伴生对象也可以声明扩展函数,如果类 C 有一个伴生对象,并且在C.Companion 上定义了一个扩展函数 func ,可以通过 C.func() 来调用它:

// 伴生对象的扩展
class C {
    companion object 
}

fun C.Companion.func() {
    println("我是扩展函数")
}

// 调用
C.func()

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

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

// Java 代码
interface Listener{
    void onClick();
}

static class TestView{
    private Listener listener;

    public void setListener(Listener listener) {
        this.listener = listener;
    }
}

new TestView().setListener(new Listener() {
    @Override
    public void onClick() {

    }
});

Kotlin 中调用是这样的:

ObjectPraticeJava.TestView().setListener(object : ObjectPraticeJava.Listener {
    override fun onClick() {

    }
})

注意:与对象声明不同,匿名对象不是单例的,每次对象表达式被执行都会创建一个新的对象实例。并且访问创建匿名内部类的函数中的变量是没有限制在final变量,还可以在对象表达式中修改变量的值,而 Java 不行。

Java 示例代码如下:

// 必须声明为 final
final int clickCount = 0 ;
new TestView().setListener(new Listener() {

    @Override
    public void onClick() {
        // 这里只能读取,不能做修改,因为是 final 类型
        System.out.println(clickCount);
    }
});

Kotlin 示例代码如下:

// 不用定义为 val
var clickCount = 0
ObjectPraticeJava.TestView().setListener(object : ObjectPraticeJava.Listener {
    override fun onClick() {
        // 可以任意修改
        clickCount++
        println(clickCount)
    }
})

为什么会这样呢?我们来查看一下转化为 Java 的代码:

public static final void testNoneNameInnerClass() {
   final IntRef clickCount = new IntRef();
   clickCount.element = 0;
   (new TestView()).setListener((Listener)(new Listener() {
      public void onClick() {
         int var1 = clickCount.element++;
      }
   }));
}

原来是生成了一个包装类来包装了我们定义的 clickCountfinal 属性也声明在了该包装类中。

注意:与 Java 匿名内部类只能扩展一个类或实现一个接口不同,Kotlin 的匿名对象可以实现多个接口或不实现接口。可以把实现多个接口的匿名对象保存在变量中,然后在需要不同接口实现的匿名对象的方法中都传入同一个匿名对象,这非常方便

示例如下:

// 将实现多个接口的匿名对象保存到属性 listener 中
val listener = object : ObjectPraticeJava.Listener, ObjectPraticeJava.LongClickListener {
    override fun onClick() {
    }

    override fun onLongClick() {
    }
}
// 在需要不同接口实现的匿名对象的方法中都传入同一个匿名对象
ObjectPraticeJava.TestView().setListener(listener)
ObjectPraticeJava.TestView().setLongClickListener(listener)

小结

  • Kotlin 的接口与 Java 相似,但是可以包含默认实现(Java 8 开始支持)和属性。
  • 所有的声明默认都是 finalpublic 的。
  • 要想使声明不是 final ,将其标记为 open
  • internal 声明在同一模块中可见。
  • 嵌套类默认不是内部类。使用 inner 关键字来存储外部类的引用。
  • 类委托帮助避免在代码中出现许多相似的委托方法。
  • 对象声明是 Kotlin 中定义单例类的方法。
  • 伴生对象(与包级别函数和属性一起)替代了 Java 静态方法和字段定义。
  • 伴生对象与其他对象一一样,可以实现接口,也可以拥有扩展函数和属性。
  • 对象表达式是 Kotlin 中针对 Java 匿名内部类的替代品,并增加了诸如实现多个接口的能力和修改在创建对象的作用域中定义的变量的能力等功能。
  • 使用 field 标识符在访问器方法体中引用属性的支持字段。
  • 数据类提供了编译器生成的 equalshashCodetoStringcopy 和其他方法。
  • sealed 类的子类只能嵌套在自身的声明中(kotlin 1.1 允许将子类放置在同一文件的任意地方)。