面向对象(四)

嵌套类和内部类

大部分时候,类被定义成一个独立的程序单元。在某些情况下,也会把一个类放在另一个类的内部定义,这个定义在其他类内部的类就被称为嵌套类(有的地方也叫寄生类),包含嵌套类的类被称为外部类(有的地方也叫宿主类) 。
Java 的内部类可分为两种:静态内部类 (有 static 修饰)和非静态内部类(无 static 修饰)。而 Kotlin 则完全取消了 static 修饰符。但实际上 Kotlin 也需要有静态内部类和非静态内部类之分,所以 Kotlin 只得为它们换了个“马甲”。

  • 嵌套类(相当于静态内部类):只要将一个类放在另一个类中定义,这个类就变成了嵌套类,相当于 Java 中有 static修饰的静态内部类。
  • 内部类(非静态内部类):使用 inner修饰的嵌套类叫内部类,相当于Java 中无 static 修饰的非静态内部类 。

嵌套类主要有如下作用:

  • 嵌套类提供了更好的封装,可以把嵌套类隐藏在外部类之内,不允许同一个包中的其他类访问该类 。 假设需要创建 Cow 类, Cow 类需要组合一个 CowLeg对象, CowLeg类只有在Cow 类中才有效,离开了 Cow 类之后没有任何意义。在这种情况下,就可以把 CowLeg 定义成 Cow 的嵌套类,不允许其他类访问CowLeg。
  • 内部类(相当于 Java 的非静态内部类)成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同一个类的成员之间可以互相访问。但外部类不能访问内部类的实现细节,例如内部类的属性。

从语法的角度来看,定义嵌套类(内部类)与定义外部类的语法大致相同,只是嵌套类(内部类)可使用 protected修饰,用于表示该嵌套类(内部类)可在其外部类的子类中被访问。 定义嵌套类(内部类)非常简单,只要把一个类放在另一个类的类内部嵌套定义即可。此处的“类内部”包括类中的任何位置,甚至在方法中也可以定义嵌套类(方法中定义的嵌套类被称为局部嵌套类) 。 嵌套类(内部类)定义的语法格式如下:

class OuterClass{
//此处可以定义嵌套类、内部类
}

内部类

内部类(等同于Java 的非静态内部类)相当于外部类的实例成员,因此它可以直接访问外部类的所有成员 。
下面程序在 Cow类中定义了 一个 CowLeg 内部类,并在 CowLeg类的方法中直接访问 Cow 的 private 属性。

//通过主构造器为外部类定义属性
class Cow(var weight: Double = 0.0) {
    //定义一个内部类(用 inner 修饰,相当于 Java 的非静态内部类)
    //通过主构造器为内部类定义属性
    private inner class CowLeg(var length: Double = 0.0, var color: String = "") {
        //内部类的方法
        fun info() {
            :("当前牛腿颜色是:${color},高:${length}")
            //直接访问外部类的 private 修饰的 foo()方法
            foo()
        }
    }

    fun test(){
        val cl = CowLeg(1.12,"黑白相间")
        cl.info()

    }
    private fun foo() {
        println("Cow的foo方法")
    }
}

fun main(args: Array<String>) {
    val cow = Cow(378.9)
    cow.test()
}

上面代码是一个普通的类定义,但因为把这个类定义放在了另一个类的内部,且使用了inner修饰,所以它就成了一个内部类,可以使用 private修饰符来修饰这个类。外部类 Cow 中包含了 一个 test()方法,该方法中创建了一个 CowLeg 对象,并调用了该对象的 info()方法。不难发现,在外部类中使用内部类时,与平时使用普通类并没有太大的区别。

编译上面程序,将看到在文件所在路径下生成了两个 class 文件,其中一个是 Cow.class, 另 一个是 Cow$CowLeg.class,前者是外部类 Cow 的 class 文件,后者是内部类 CowLeg 的 class 文件,即成员嵌套类和成员内部类的 class 文件总是这种形式: OuterClass$InnerClass.class。

前面提到过,在内部类中可以直接访问外部类的 private 成员,上面代码就有在CowLeg类的方法内直接访问其外部类的private方法。这是因为 在内部类对象中 ,保存了一个它所寄生的外部类对象的引用 (当调用内部类的方法时,必须使用内部类对象作为调用者,内部类实例必须寄生在外部类实例中)。

当在内部类的方法内访问某个属性时,系统优先在该方法内查找是否存在该名字的局部变量,如果存在就使用该变量;如果不存在,则到该方法所在的内部类中查找是否存在该名字的属性,如果存在则使用该属性 : 如果不存在,则到该内部类所在的外部类中查找是否存在该名字的属性,如果存在则使用该属性:如果依然不存在,系统将出现编译错误,提示找不到该属性。

因此,如果外部类属性、内部类属性与内部类中方法的局部变量同名,则可通过使用this、 带标签的 this进行限定来区分。

class DiscernVariable { //隐式标签@ DiscernVariable
    private val prop = "外部类的属性"

    inner class InClass { //隐式标签@ InClass
        private val prop = "内部类的属性"

        fun info() {
            val prop = "局部变量"
            //通过外部类类名.this.varName 访问外部类的属性
            println("外部类的属性值:${this@DiscernVariable.prop}")
            //通过this.varName 访问内部类的属性
            println("内部类的属性值:${this.prop}")
            //直接访问局部变盘
            println("局部变茧的值:${prop}")
        }
    }

    fun test() {
        val ic = InClass()
        ic.info()

    }
}

fun main(args: Array<String>) {
    DiscernVariable().test()
}

上面代码分别访问外部类的属性、内部类的属性。通过带标签的 this 前缀可显式指定访问哪个类的属性。不带标签的 this 前缀默认访问内部类的属性。

Kotlin的 this,比 Java 的 this 更强大,通过这种带标签的 this, Kotlin 可以进行非常细致的区分。 Kotlin关于this的处理规则如下。

  • 在类的方法或属性中,this代表调用该方法或属性的对象。
  • 类的构造器中, this代表该构造器即将返回的对象。
  • 扩展函数或带接收者的函数字面值中,this 表示点(.)左边的“接收者”。
  • 如果 this 没有限定符,那么它优先代表包含该this的最内层的接收者,并且会自动向外搜索。如果要让 this 明确引用特定的接收者,则可使用标签限定符。

如下程序示范了一个比较复杂的this示例,读者可以参考该程序和注释来理解Kotlin对this 的处理 。

class A { //隐式标签@ A
   inner class B { //隐式标签@B
       //为 Int 扩展 foo ()方法
       fun Int.foo() {  //隐式标签@ foo
           val a = this@A //A 的 this
           val b = this@B //B 的 this
           val c = this //不带标签的 this,默认代表该方法所属对象: Int对象
           val c1 = this@foo //显式指定@foo标签,与c代表的对象相同

           println(a)
           println(b)
           println(c)
           println(c1)

           //为String扩展funLit()方法
           val funLit = lambda@ fun String.() {
               val d = this //不带标签的 this,默认代表该方法所属对象: String对象
               val d1 = this@lambda //显式指定@lambda标签,与 d代表的对象相同
               println(d)
               println(d1)
           }

           "fkit".funLit()

           //直接定义一个 Lambda 表达式,没有接收者
           val funLit2 = {
               //该 this 所在的 Lambda 表达式没有接收者 ,因此当前范围没有 this
               //系统会继续向该 Lambda 表达式所在范围搜索 this
               //故此处 this 将代表 foo ()方法的接收者: Int 对象
               val e = this
               val e1 = this@foo ////显式指定@foo标签,与 e 代表的对象相同
               println("foo ()方法中 Lambda 表达式的 this: ${e}")
               println("e1的this:${e1}")
           }

           funLit2()
       }

       fun testB() {
           //调用2 (Int值)的foo()方法
           2.foo()
       }

   }

   fun testA() {
       var bObj = B()
       println("程序创建的B对象: ${bObj}")
       bObj.testB()

   }
}

fun main(args: Array<String>) {
   var aObj =A()
   println("程序创建的A对象: ${aObj}")
   aObj.testA()
}

内部类的成员可以访问外部类的 private 成员,但反过来就不成立了。内部类的成员只在内部类范围内是可知的,并不能被外部类直接使用。如果外部类需要访问内部类的成员,则必须显式创建内部类对象来调用访问其成员。

嵌套类

嵌套类相当于 Java 的静态内部类,因此嵌套类直接属于外部类的类本身,而不是外部类实例相关 。
Java语法有一条规则:静态成员不可访问非静态成员。而 Kotlin彻底取消了static修饰符,因此Kotlin 类中的成员除嵌套类之外,全部都是非静态成员,因此嵌套类不可访问外部类的其他任何成员(只能访问其他嵌套类)。

class NestedClassTest {
    var prop1 = 5
    fun test() {
        println("外部类的test()")
    }

    //没有 inner 修饰符,是嵌套类(相当于 Java 的静态内部类)
    class NestedClass {
        fun accessOuterMember() {
            //访问另一个嵌套类是允许的
            val a = A()
            //下面两行代码都会出现错误
            //println(prop1)
            //test()

        }
    }

    class A
}

上面代码定义了一个属性和一个方法,NestedClass类中定义了 一个accessOuterMember()方法,但它不能访问外部类的 propl 属性和 test()方法。 嵌套类唯一可访问的是外部类的其他嵌套类。 嵌套类相当于外部类的静态成员,因此外部类的所有方法、属性、初始化块都可以使用嵌套类来定义变量 、创建对象等。 外部类依然不能直接访问嵌套类的成员,但可以使用嵌套类的对象作为调用者来访问嵌套类的成员。

除此之外, Kotlin 还允许在接口中定义嵌套类,但不允许在接口中定义内部类(即不允许定义使用 inner 修饰的内部类)。如果为接口中的嵌套类指定访问控制符,则只能指定 public 或 private; 如果定义接口中的嵌套类时省略访问控制符,则该嵌套类默认是 public 访问权限。

在外部类以外使用内部类

定义类的主要作用就是定义变量、创建对象和派生子类。定义内部类的主要作用也如此。正如前面所看到的,在外部类内部使用嵌套类或内部类时,与平常使用普通类没有太大的区别,一样可以直接通过嵌套类或内部类的类名来定义变量,调用嵌套类或内部类的构造器来创建实例。

唯一要牢记的一点:嵌套类只能访问外部的其他嵌套类,不能访问外部的其他任何成员。
如果希望在外部类以外的地方使用内部类或嵌套类,一定要注意访问权限的限制,比如使用 private修饰的嵌套类或内部类只能在外部类之内使用。

在外部类以外的地方定义内部类变量的语法格式如下

var | val varName: OuterClass.InnerClass

从上面语法格式可以看出,在外部类以外的地方使用内部类时,内部类完整的类名应该是
OuterClass.InnerClass。如果外部类有包名,则还应该增加包名前缀。
由于内部类的对象必须寄生在外部类的对象中,因此在创建内部类对象之前,必须先创建
其外部类对象。 在外部类以外的地方创建内部类实例的语法格式如下

Outerinstance.InnerConstructor()

从上面语法格式可以看出,在外部类以外的地方创建内部类实例时必须使用外部类实例来调用内部类的构造器。下面程序示范了如何在外部类以外的地方创建内部类对象,并把它赋值给内部类类型的变量。

class Out {
    //定义一个内部类,不使用访问控制符,默认是 public
    inner class In(msg: String) {
        init {
            println(msg)
        }

    }
}

fun main(args: Array<String>) {
    var oi :Out.In = Out().In("测试信息")
}

从上面代码可以看出,内部类的构造器必须使用外部类对象来调用。

在外部类以外使用嵌套类

因为嵌套类是属于外部类的类本身的,因此创建嵌套类对象时无须创建外部类对象,所以嵌套类用起来非常方便。在外部类以外的地方创建嵌套类实例的语法格式如下:

OuterClass. NestedConstructor()

class NestedOut {
    //定义一个嵌套类 , 不使用访问控制符,默认是 public
    open class Nested {
        init {
            println("嵌套类的构造器")
        }

    }
}

fun main(args: Array<String>) {
    var nn :NestedOut.Nested = NestedOut.Nested()
}

从上面代码可以看出,不管是内部类还是嵌套类,其声明变量的语法完全一样。区别只是在创建对象时,嵌套类只需使用外部类即可调用构造器,而内部类必须使用外部类对象来调用构造器。

因为在调用嵌套类的构造器时无须使用外部类对象,所以创建嵌套类的子类也比较简单。 下面代码就为嵌套类 NestedSubClass 定义了一个空的子类 。

class NestedSubClass :NestedOut.Nested()

相比之下,使用嵌套类比使用内部类要简单很多,只要把外部类当作嵌套类的包空间即可。 因此程序应该优先考虑使用嵌套类。

局部嵌套类

如果把一个嵌套类放在方法或函数中定义,则这个嵌套类就是一个局部嵌套类,局部嵌套类仅在该方法或函数中有效。由于局部嵌套类不能在方法或函数以外的地方使用,因此局部嵌套类也不能使用访问控制符修饰。
如果需要用局部嵌套类定义变量、创建实例或派生子类,那么都只能在局部嵌套类所在的方法(或函数)内进行。

class LocalNestedClass {
    fun info() {
        //定义局部嵌套类
        open class NestedBase(var a: Int = 0) {

        }

        //定义用部嵌套类的子类
        class NestedSub(var b: Int = 0) : NestedBase() {

        }

        //创建局部嵌套类的对象
        val ns = NestedSub()
        ns.a = 5
        ns.b = 8
        println("NestedSub对象的 a和 b属性是:${ns.a},${ns.b}")
    }
}

fun main(args: Array<String>) {
    LocalNestedClass().info()
}

编译上面程序,可以看到生成了三 class 文件:LocalNestedClass.class、 LocalNestedClass$1NestedBase.class和 LocalNestedClass$1NestedSub.class,这表明局部嵌套类的class文件总是遵循如下命名格式: OuterClass$NNestedClass.class。注意到局部嵌套类的 class 文件的文件名比嵌套类、内部类的class文件的文件名多了一个数字,这是因为同一个类中不可能有两个同名的嵌套类、内部类,而同一个类中则可能有两个以上同名的局部嵌套类(处于不同的方法中), 所以 Kotlin 为局部嵌套类的 class 文件名增加了一个数字,用于区分。

局部嵌套类是一个非常“鸡肋”的语法,在实际开发中很少定义局部嵌套类,这是因为局部嵌套类的作用域太小了,只能在当前方法中使用。

匿名内部类

Java 有一个非常实用的功能:匿名内部类, Kotlin 则彻底抛弃了这个功能。不过读者不用担心, Kotlin 提供了一个更加强大的语法 : 对象表达式。

此外,如果对象是函数式接口(只包含一个抽象方法的接口)的实例,则可使用带接口类型前缀的 Lambda 表达式创建它。 例如,如下代码创建了一个 Runnable 实例来启动线程。

fun main(args: Array<String>) {
    //使用 Lambda 表达式创建 Runnable 实例
    var t = Runnable {
        for (i in 1..100) {
            println("${Thread.currentThread().getName()} ,i: ${i}")
        }
    }

    //启动新线程
    Thread(t).start()

    //主线程的循环
    for (i in 1..100) {
        println("${Thread.currentThread().getName()},i: ${i}")
    }
}

上面程序中代码使用 Lambda 表达式创建了一个 Runnable 实例,通过 Runnable 实例可启动多个线程 。

对象表达式其实就是增强版的匿名内部类,下面会详细介绍。

对象表达式和对象声明

Kotlin 提供了比匿名内部类更加强大的语法:对象表达式。它们的主要区别在于:匿名内部类只能指定一个父类型(接口或父类),但对象表达式可指定 0~N个父类型(接口或父类)。

对象表达式

对象表达式的语法格式如下:

object[: 0~N 个父类型] {
//对象表达式的类体部分
}

从上面语法格式可以看出,对象表达式可以指定0~N个父类型(类或接口),对象表达式的本质就是增强版的匿名内部类,因此编译器处理对象表达式时也会像匿名内部类一样生成对应的class文件。
关于对象表达式还有如下规则。

  • 对象表达式不能是抽象类,因为系统在创建对象表达式时会立即创建对象。因此不允许将对象表达式定义成抽象类。
  • 对象表达式不能定义构造器。但对象表达式可以定义初始化块,可以通过初始化块来完成构造器需要完成的事情。
  • 对象表达式可以包含内部类(有 inner修饰的内部类),不能包含嵌套类。

下面程序示范了几种对象表达式。

interface Outputable {
    fun output(msg: String)
}

abstract class Product(var price: Double) {
    abstract val name: String
    abstract fun printinfo()
}

fun main(args: Array<String>) {
    //指定一个父类型(接口)的对象表达式
    var ob1 = object : Outputable {
        override fun output(msg: String) {
            //重写父接口中的抽象方法
            for (i in 1..6) {
                println("<h${i}>${msg}</h${i}>")

            }
        }
    }

    ob1.output("java")
    println("------------")
    //指定0个父类型的对象表达式
    var ob2 = object {
        //初始化块
        init {
            println("初始化块")
        }

        //属性
        var name = "kotlin"
        //方法
        fun test(){
            println("test()方法")
        }

        //只能包含内部类,不能包含嵌套类
        inner class Foo{}
    }

    println(ob2.name)
    ob2.test()
    println("------------")
    //指定两个父类型的对象表达式
    //由于 Product 只有一个带参数的构造器,因此需要传入构造器参数
    var ob3 = object :Outputable,Product(28.8){
        override fun output(msg: String) {
            println("输出信息:${msg}")
        }

        override val name: String
            get() = "激光打印机"

        override fun printinfo() {
            println("”高速激光打印机,支持自动双面打印!")
        }
    }

    println(ob3.name)
    ob3.output("Kotlin 真不错!")
    ob3.printinfo()
}

上面代码的第一个对象表达式只有一个父类型(接口),这个对象表达式与Java 的匿名内部类实现太像了,除了 Kotlin 的对象表达式需要使用 object 关键字,该对象表达式定义的匿名类实现了 Outputable接口,因此在类体部分实现了该接口中的抽象方法。

从上面介绍可以看出, Kotlin 的对象表达式可以完全取代 Java 的匿名内部类 。
上面代码的第二个对象表达式则没有指定父类型,这就是对象表达式的增强之处。我们在该对象表达式的类体部分定义了初始化块(程序创建对象时会自动执行)、属性、方法、内部类,由此可见,对象表达式的本质其实就是增强版的匿名内部类,因此在类体部 除不能定义构造器、嵌套类之外,完全可以定义其他成员。

上面代码的第三个对象表达式指定了两个父类型,一个是接口,一个是抽象类,这也是对象表达式的增强之处。此处需要说明的是,就像 Java 的匿名内部类继承抽象类时,必须调用抽象类的指定构造器一样, Kotlin也是如此,所以我们使用了Product(28.8),表 明调用Product 抽象类的带一个 Double 参数的构造器 。该对象表达式的类体部分没有任何特别之处,它只是重写了接口、抽象父类的抽象成员。

运行上面代码,仔细查看输出,应该会发现一个特征: ob2 对象没有继承任何父类型,程序为该对象(相当于匿名内部类)定义了 name 属性和 test()方法,接下来程序居然可以直接调用该对象的 name 属性和 test()方法一一这对 Java 的匿名内部类来说是不可想象的 : Java 为匿名内部类增加的方法几乎无法直接访问,因为编译器只会把匿名内部类当成它所继承的父类或所实现的接口处理。
但 Kotlin 的对象表达式不同, Kotlin 的对象表达式可分为两种情形 :

  • 对象表达式在方法(或函数)的局部范围内,或使用 private修饰的对象表达式, Kotlin编译器可识别该对象表达式的真实类型,就像上面的代码所示:程序为 ob2 增加了方法和属性,在 main()函数的局部范围内, Kotlin编译器完全可以识别ob2的真实类型, 因此 ob2可调用对象表达式增加的属性和方法。
  • 非private修饰的对象表达式与Java的匿名内部类相似,编译器只会把对象表达式当成它所继承的父类或所实现的接口处理。如果它没有父类型,系统当它是 Any 类型。

如下程序示范了 Kotlin编译器处理对象表达式类型的两种情形:

class ObjectExprType {
    private val ob1 = object {
        val name: String = "java"
    }

    internal val ob2 = object {
        val name: String = "java"
    }

    private fun privateBar() = object {
        val name: String = "kotlin"
    }

    fun publicBar() = object {
        val name: String = "kotlin"
    }

    fun test(){
        //ob1是private对象表达式,编译器可识别它的真实类型
        //下面代码正确
        println(ob1.name)

        //ob2是非private对象表达式,编译器当它是Any类型
        //下面代码错误
        //println(ob2.name)

        //privateBar private 函数,编译器可识别它返回的对象表达式的真实类型
        //下面代码正确
        println(privateBar().name)

        //publicBar是非private函数,编译器将它返回的对象表达式当成Any类型
        //下面代码错误
        //println(publicBar().name)
    }
}

fun main(args: Array<String>) {
    ObjectExprType().test()
}

从上面代码可以看出, Kotlin 编译器可识别 private 对象表达式的真实类型。
此外, Kotlin 的对象表达式可访问或修改其作用域内的局部变量(java只能访问其所在范围内的 effectively final局部变量)。
例如如下程序 :

fun main(args: Array<String>) {
    var a = 10
    var obj = object {
        fun change(){
            a++
        }
    }
    obj.change()
    println("a的值:${a}")
}

总结起来, Kotlin的对象表达式比 Java的匿名内部类增强了三个方面。

  • 对象表达式可指定多个父类型。
  • Kotlin编译器能更准确地识别局部范围内或 private对象表达式的类型。
  • 对象表达式可访问或修改其所在范围内的局部变量。

对象声明和单例模式

对象声明的语法格式如下:

object ObjectName[: 0~N个父类型]{
//对象声明的类体部分
}

从上面语法格式可以看出,对象声明与对象表达式的语法非常相似,似乎它们之间唯一的区别是对象表达式在object关键字后没有名字 ;而对象声明需要在 object关键字后指定名字。
实际上,对象声明和对象表达式还存在如下区别:

  • 对象表达式是一个表达式,因此它可以被赋值给变量 ; 而对象声明不是表达式,因此它不能用于赋值。
  • 对象声明可包含嵌套类,不能包含内部类 ; 而对象表达式可包含内部类,不能包含嵌套类。
  • 对象声明不能定义在函数和方法内 ; 但对象表达式可嵌套在其他对象声明或非内部类中。

下面我们用对象声明来改写上面的代码:

interface Outputable {
    fun output(msg: String)
}

abstract class Product(var price: Double) {
    abstract val name: String
    abstract fun printinfo()
}

//指定一个父类型(接口)的对象声明
object MyObject1 : Outputable {
    override fun output(msg: String) {
        //重写父接口中的抽象方法
        for (i in 1..6) {
            println("<h${i}>${msg}</h${i}>")

        }
    }
}

//指定0个父类型的对象声明
object MyObject2 {
    //初始化块
    init {
        println("初始化块")
    }

    //属性
    var name = "kotlin"

    //方法
    fun test() {
        println("test()方法")
    }

    //只能包含嵌套类,不能包含内部类
    class Foo
}

//指定两个父类型的对象声明
//由于 Product 只有一个带参数的构造器,因此需要传入构造器参数
object MyObject3 : Outputable, Product(28.8) {
    override fun output(msg: String) {
        println("输出信息:${msg}")
    }

    override val name: String
        get() = "激光打印机"

    override fun printinfo() {
        println("”高速激光打印机,支持自动双面打印!")
    }
}

fun main(args: Array<String>) {


    MyObject1.output("java")
    println("------------")


    println(MyObject2.name)
    MyObject2.test()
    println("------------")

    println(MyObject3.name)
    MyObject3.output("Kotlin 真不错!")
    MyObject3.printinfo()
}

将上面程序与上面的对象表达式的第一个程序进行对比,即可发现对象声明不能用于赋值,对象声明本身己有名称,因此可以使用对象声明的名称访问该对象。此外,程序还将对象声明移到 main()函数之外,这是因为对象声明不允许放在函数或方法内。

对象声明专门用于实现单例模式,对象声明所定义的对象也就是该类的唯一实例,程序可通过对象声明的名称直接访问该类的唯一实例。

伴生对象和静态成员

在类中定义的对象声明,可使用 companion修饰,这样该对象就变成了伴生对象。
每个类最多只能定义一个伴生对象,伴生对象相当于外部类的对象,程序可通过外部类直接调用伴生对象的成员。例如如下代码:

interface Outputable {
    fun output(msg: String)
}

class MyClass {
    //使用 companion 修饰的伴生对象
    companion object MyObject1 : Outputable {

        val name = "name属性值"
        override fun output(msg: String) {
            for (i in 1..6) {
                println("<h${i}>${msg}</h${i}>")
            }
        }

    }
}

fun main(args: Array<String>) {
    //使用伴生对象所在的类调用伴生对象的方法
    MyClass.output("kotlin")
    println(MyClass.name)
}

上面代码使用 companion修饰了对象声明,因此该对象变成了伴生对象。接下来在主程序中即可使用伴生对象所在的类调用伴生对象的成员。

从上面的 MyClass.output("kotlin")代码不难发现: 这不就是 Java使用类访问静态成员的语法吗? 实际上确实如此,由于Kotlin取消了static关键字,因此Kotlin引入伴生对象来弥补没有静态成员的不足。可见,伴生对象的主要作用就是为其所在的外部类模拟静态成员。

从代码可以看出,伴生对象的名称并不重要,因此伴生对象可以省略名称。省略名称之后,如果程序真的要访问伴生对象,则可通过 Companion 名称进行访问。例如如下代码。

interface Outputable {
    fun output(msg: String)
}

class MyClass {
    //省略名字的伴生对象
    companion object : Outputable {

        val name = "name属性值"
        override fun output(msg: String) {
            for (i in 1..6) {
                println("<h${i}>${msg}</h${i}>")
            }
        }

    }
}

fun main(args: Array<String>) {
    //使用伴生对象所在的类调用伴生对象的方法
    MyClass.output("kotlin")
    //使用 Companion 名称访问伴生对象
    println(MyClass.Companion)
}

虽然伴生对象的主要作用就是为它所在的类模拟静态成员,但只是模拟,伴生对象的成员依然是伴生对象本身的实例成员,并不属于伴生对象所在的外部类。

在JVM平台上,可通过@JvmStatic注解让系统根据伴生对象的成员为其所在的外部类生成真正的静态成员。具体信息可参考后面的Kotlin与Java互相调用。

伴生对象的扩展

伴生对象也可以被扩展。如果一个类具有伴生对象,则Kotlin允许为伴生对象扩展方法和属性。为伴生对象扩展的方法和属性,就相当于为伴生对象所在的外部类扩展了静态成员,可通过外部类的类名访问这些扩展成员。
如下程序示范了为伴生对象扩展成员:

interface Outputable {
    fun output(msg: String)
}

class MyClass {
    //省略名字的伴生对象
    companion object : Outputable {

        val name = "name属性值"
        override fun output(msg: String) {
            for (i in 1..6) {
                println("<h${i}>${msg}</h${i}>")
            }
        }

    }
}

//为伴生对象扩展方法
fun MyClass.Companion.test(){
    println("为伴生对象扩展的方法")
}

val MyClass.Companion.foo
get() = "为伴生对象扩展的属性"


fun main(args: Array<String>) {
    //使用伴生对象所在的类调用伴生对象的方法
    MyClass.output("kotlin")
    //使用 Companion 名称访问伴生对象
    println(MyClass.Companion)
    //通过伴生对象所在的类调用为伴生对象扩展的成员
    MyClass.test()
    println(MyClass.foo)
}

上面代码示范了为伴生对象扩展方法和属性,正如前面所提到的,Kotlin 可通过Companion名称访问伴生对象,因此代码显式指定为 MyClass.Companion 扩展方法和属性,这就是为伴生对象扩展方法和属性。

枚举类

Kotlin 当然也支持枚举类, Kotlin 的枚举类与 Java 差别不大。

枚举类入门

Kotlin使用 enum class关键字组合定义枚举类。枚举类是一种特殊的类,它一样可以有自己的属性、方法,可以实现一个或多个接口,也可以定义自己的构造器。
但枚举类与普通类有如下简单区别:

  • 枚举类可以实现一个或多个接口,使用enum定义的枚举类默认继承 kotlin.Enum 类,而不是默认继承Any类,因此枚举类不能显式继承其他父类。其中Enum类实现了kotlin.Comparable 接口 。
  • 使用 enum 定义的非抽象的枚举类不能使用 open修饰,因此枚举类不能派生子类。
  • 枚举类的构造器只能使用private访问控制符,如果省略了构造器的访问控制符,则默认使用 private 修饰;如果强制指定访问控制符,则只能指定 private 修饰符。
  • 枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远都不能产生实例。列出枚举实例后最好用分号结尾。

枚举类默认提供了如下两个方法 。

  • EnumClass.valueOf(value: String): EnumClass:类似于Java 枚举类的 valueOf()方法,用于根据枚举的字符串名获取实际的枚举值。如果传入的名称参数与类中定义的任何枚举常量均不匹配, valueOf()方法将抛出 IllegalArgumentException 异常。
  • EnumClass.values(): Array<EnumClass>:类似于 Java 枚举类的 values()方法,用于获取该枚举的所有枚举值组成的数组。

下面程序定义了一个 Season 枚举类

enum class Season{
    //在第一行列出 4 个枚举实例
    SPRING, SUMMER, FALL , WINTER
}

编译上面的Kotlin 程序,将生成一个Season.class 文件,这表明枚举类是一种特殊的类 。 由此可见, enum class 关键字组合和 class、 interface 关键字的作用大致相似 。
在定义枚举类时,需要显式列出所有的枚举值,如上面的 SPRING, SUMMER, FALL, WINTER 所示,所有的枚举值之间以英文逗号(,)隔开。这些枚举值代表了该枚举类的所有可能的实例。
如果需要使用该枚举类的某个实例,则可使用 EnumClass.variable 的形式,如Season.SPRING 。

enum class Season{
    //在第一行列出 4 个枚举实例
    SPRING, SUMMER, FALL , WINTER
}

fun main(args: Array<String>) {
    //枚举类默认有一个 values ()方法,返回该枚举类的所有实例
    for (s in Season.values()){
        println(s)
    }

    val seasonName = "SUMMER"
    val s = Season.valueOf(seasonName)
    println(s)
    //直接访问枚举值
    println(Season.WINTER)
}

上面程序测试了 Season 枚举类的用法,该类通过 values()方法返回了 Season 枚举类的所有实例,并通过循环迭代输出了 Season 枚举类的所有实例;还使用了 Season 的 valueOf()方法根据字符串参数来获取枚举值。
前面己经介绍过,所有的枚举类都继承了 kotlin.Enum类,所以枚举类可以直接使用该类中所包含的属性和方法。 kotlin.Enum 类中提供了如下属性和方法。

  • name 属性: 返回此枚举实例的名称,这个名称就是定义枚举类时列出的所有枚举值之一。与此属性相比,应该优先考虑使用 toString()方法,因为 toString() 方法可返回对用户更加友好的名称。
  • ordinal 属性: 返回枚举值在枚举类中的索引值(就是枚举值在枚举声明中的位置,第一个枚举值的索引值为 0) 。
  • int compareTo(E o): 该方法用于与指定的枚举对象比较顺序,同 一个枚举实例只能与相同类型的枚举实例进行比较。如果该枚举对象位于指定的枚举对象之后,则返回正整数;如果该枚举对象位于指定的枚举对象之前,则返回负整数 ; 否则返回 0。
  • String toString(): 返回枚举常量的名称,与 name 属性相似,但 toString()方法更常用。

正如前面所看到的,当程序使用 println(s)语句来打印枚举值时,实际上输出的是该枚举值toString()方法的返回值,也就是输出该枚举值的名字。

枚举类的属性、方法和构造器

枚举类也是一种类,只是一种比较特殊的类,因此它一样可以定义属性、方法和构造器。由于枚举类应该设计成不可变类,因此它的属性值不允许改变,这样会更安全。 Kotlin禁止开发者对属性赋值,并推荐使用 val 为枚举声明只读属性。 由于枚举的属性都是只读属性,枚举必须在构造器中为这些属性指定初始值(或在初始化块中指定初始值, 一般不会在定义时指定初始值,因为这样会导致所有枚举值的该属性值总是相同的),因此应该为枚举类显式定义带参数的构造器。
一旦为枚举类显式定义了带参数的构造器,在列出枚举值时就必须对应地传入参数。例如如下程序。

//使用主构造器声明 cnName 只读属性
enum class Gender(val cnName: String) {
    MALE("男"), FEMALE("女");

    //定义方法
    fun info() {
        when (this) {
            MALE -> println("男的")
            FEMALE -> println("女的")
        }
    }
}

从上面程序中可以看出,当为 Gender枚举类定义了一个 Gender(String)构造器之后,列出枚举值时就必须对应地传入参数。也就是说,在枚举类中列出枚举值时,实际上就是调用构造器创建枚举类对象,只是这里无须显式调用构造器 。前面列出枚举值时无须传入参数,甚至无须使用括号,仅仅是因为前面的枚举类包含无参数的构造器。
不难看出,上面程序中的粗体字代码实际上等同于如下代码:

MALE = new Gender ("男" ) , FEMALE = new Gender ("女");

此外,由于上面的枚举类需要在列出枚举值之后定义额外的枚举成员(如枚举方法),因此上面程序需要用分号表示枚举值列表结束 。
下面程序示范了 Gender枚举类的用法。

//使用主构造器声明 cnName 只读属性
enum class Gender(val cnName: String) {
    MALE("男"), FEMALE("女");

    //定义方法
    fun info() {
        when (this) {
            MALE -> println("男的")
            FEMALE -> println("女的")
        }
    }
}

fun main(args: Array<String>) {
    //通过 Gender 的 valueOf ()方法根据枚举名获取枚举值
    val g = Gender.valueOf("FEMALE")
    //访问枚举值的 enName 属性
    println("${g}代表:${g.cnName}")
    //调用 info 方法
    g.info()
}

实现接口的枚举类

枚举类也可以实现一个或多个接口 。 与普通类实现一个或多个接口完全一样,枚举类实现一个或多个接口时,也需要实现该接口所包含的方法 。下面程序定义了一个 GenderDesc 接口。

interface GenderDesc{
    fun info()
}
//使用主构造器声明 cnName 只读属性
enum class Gender(val cnName: String) :GenderDesc{
    MALE("男"), FEMALE("女");

    //定义方法
    override fun info() {
        when (this) {
            MALE -> println("男的")
            FEMALE -> println("女的")
        }
    }
}

枚举类实现接口不过如此,与普通类实现接口完全一样:同样要将被实现的接口放在英文冒号后面,并实现接口中包含的抽象方法 。

包含抽象方法的抽象枚举类

假设有一个 Operation枚举类,它的 4个枚举值 PLUS,MINUS,TIMES, DIVIDE分别代表加、减、乘、除 4 种运算,该枚举类需要定义一个 eval()方法来完成计算。 从上面描述可以看出Operation 需要让PLUS,MINUS,TIMES, DIVIDE这 4 个值对 eval()方法各有不同的实现。此时可考虑为 Operation枚举类定义一个eval()抽象方法,然后让4个枚举值分别为 eval()提供不同的实现。例如如下代码。

enum class Operation {
    PLUS {
        override fun eval(x: Double, y: Double): Double {
            return x + y
        }

    },
    MINUS {
        override fun eval(x: Double, y: Double): Double {
            return x - y
        }
    },
    TIMES {
        override fun eval(x: Double, y: Double): Double {
            return x * y
        }
    },
    DIVIDE {
        override fun eval(x: Double, y: Double): Double = x / y
    };

    //为枚举类定义一个抽象方法
    //这个抽象方法由不同的枚举值提供不同的实现
    abstract fun eval(x: Double, y: Double): Double
}

fun main(args: Array<String>) {
    println(Operation.PLUS.eval(5.6,6.7))
}

编译上面程序,会生成 5 个 class 文件,其中 Operation 对应一个 class 文件,它的4个匿名内部子类各对应一个 class文件。
枚举类中包含抽象方法时依然不能使用 abstract 修饰枚举类(因为系统自动会为它添加 abstract 关键字),但因为枚举类需要显式创建枚举值,而不是作为父类,所以在定义每个枚举值时必须为抽象成员提供实现,否则将出现编译错误。
上面程序中的代码看起来有些奇怪:当创建 PLUS,MINUS,TIMES, DIVIDE 枚举值时,后面又紧跟了一对花括号,这对花括号中包含了一个 eval()方法定义。如果读者还记得对象表达式(或匿名内部类)语法的话,则可能就熟悉这样的语法了,花括号部分实际上 就是一个类体部分,在这种情况下,当创建PLUS,MINUS,TIMES, DIVIDE枚举值时 , 并不是直接创建 Operation 枚举类的实例的,而是相当于创建 Operation的匿名子类的实例。因为代码的括号部分实际上是对象表达式(匿名内部类)的类体部分,所以这个部分的代码语法与前面介绍的对象表达式(匿名内部类)的语法大致相似。

类委托和属性委托

委托是 Kotlin 的另一个特色功能,也是Java原本所不具备的功能。Kotlin的委托可分为类委托和属性委托。

类委托

类委托是代理模式的应用,而代理模式可作为继承的一个不错的替代。类委托的本质就是将本类需要实现的部分方法委托给其他对象,相当于借用其他对象的方法作为自己的实现 。
例如,下面定义一个类,该类实现了一个接口,但该类并不实现该接口中的抽象方法,而是借用其他对象中的方法来实现,被借用的对象被称为被委托对象。

interface Outputable{
    fun output(msg:String)
    var type:String
}

//定义一个 DefaultOutput 类实现 Outputable 接口
class DefaultOutput :Outputable{
    override fun output(msg: String) {
        for (i in 1..6){
            println("${msg} ${i}")
        }
    }

    override var type: String = "输出设备"
}

//定义 Printer类,指定构造参数b作为委托对象
class Printer(b:DefaultOutput) :Outputable by b

//定义 Projector 类,指定新建的对象作为委托对象
class Projector():Outputable by DefaultOutput(){
    override fun output(msg: String) {
        javax.swing.JOptionPane.showMessageDialog(null,msg)
    }

}

fun main(args: Array<String>) {
    val output =DefaultOutput()
    //Printer 对象的委托对象是 output
    var printer = Printer(output)
    //其实就是调用委托对象的 output ()方法
    printer.output("xq")
    //Projector 对象的委托对象也是 output
    var p = Projector()
    //Projector 本身重写了 output ()方法,所以此处是调用本类重写的方法
    p.output("xy")
    //其实就是调用委托对象的 type 属性方法
    println(p.type)
}

上面程序定义了 Printer 和 Projector 两个类,这两个类都实现了 Ouputable 接口,因此需要实现该接口中的抽象方法和抽象属性。程序通过 by关键字为这两个类指定了委托对象,这 意味着这两个类可直接“借用”被委托对象所实现的方法和属性。
上面程序中 Printer类虽然没有实现 output()方法和 type属性,但它的实例 一样可以在 main() 函数中调用 output()方法和 type 属性 。 Projector类虽然指定了 DefaultOutput对象作为委托,但 由于该类本身也实现了 output()方法,因此当 Projector 对象调用 output()方法时,它不再需要调用委托对象的 output()方法,而是直接使用自己实现的方法。
由此可见,当一个类重写了委托对象所包含的方法之后, Kotlin 将优先使用该类自己实现的方法。
上面程序示范了为类指定委托对象的两种方式,第一种方式是通过构造器参数指定委托对 象;第二种方式是直接在类定义的 by后新建对象。通常会采用第一种方式, 因为这样可以让多个对象共享同一个委托对象。

属性委托

Kotlin 也支持属性委托,属性委托可以将多个类的类似属性统一交给委托对象集中实现,这样就可避免每个类都需要单独实现这些属性。
对于指定了委托对象的属性而言,由于它的实现逻辑己经交给委托对象处理,因此开发者不能为委托属性提供 getter 和 setter 方法, Kotlin也不会为委托属性提供 getter、 setter方法的默认实现。
一旦将某个对象指定为属性的委托对象,该对象就会全面接管该属性的读取( getter)和写入( setter)操作,因此属性的委托对象无须实现任何接口,但一定要提供一个 getValue()方法和 setValue()方法(val 属性无须提供 setValue方法)。
对于只读属性(用 val 声明的属性)而言,由于程序无须设置属性值,只要能读取属性值即可,因此只读属性的委托对象只需提供使用 operator修饰的 getValue()方法。该方法的参数和返回值要求如下。

  • thisRef:该参数代表属性所属的对象,因此该参数的类型必须是属性所属对象的类型 (对于扩展属性,指被扩展的类型)或其超类型。
  • property:该参数代表目标属性,该参数的类型必须是 KPropetry<*>或其超类型 。
  • 返回值: 该方法必须返回与目标属性相同的类型(或其子类型)。

对于读写属性(用 var声明的属性)而言,属性的委托对象必须额外提供一个使用 operator修饰的 setValue()方法,该方法不需要返回值,但必须指定如下参数。

  • thisRef: 与 getValue()的 thisRef参数的作用相同。
  • property: 与 getValue()的property参数的作用相同。
  • newValue: 该参数代表目标属性新设置的属性值,该参数的类型必须具有和目标属性相同的类型或其超类型 。

属性的委托对象的 getValue()、setValue()方法既可以是成员方法,也可以是扩展方法。总之,只要是符合规则的两个方法均可。

Kotlin标准库在kotlin.properties下提供了 ReadOnlyProperty和 ReadWriteProperty两个接口,其中ReadOnlyProperty接口定义了一个符合只读属性委托标准的getValue()抽象方法,因此该接口的实现类可作为只读属性的委托对象;而ReadWriteProperty接口定义了符合读写属性委托标准的 getValue()和 setValue()抽象方法,因此该接口的实现类可作为读写属性的委托对象。

import kotlin.reflect.KProperty

class PropertyDelegation {
    //该属性的委托对象是 MyDelegation
    var name: String by MyDelegation()
}

class MyDelegation {
    private var _backValue = "默认值"

    operator fun getValue(thisRef: PropertyDelegation,
                          property: KProperty<*>): String {
        println("${thisRef}的${property.name}属性执行getter方法")
        return _backValue
    }

    operator fun setValue(thisRef: PropertyDelegation,
                          property: KProperty<*>, newValue: String) {
        println("${thisRef}的${property.name}属性执行 setter 方法,传入参数值为:${newValue}")
        _backValue = newValue
    }

}

fun main(args: Array<String>) {
    val pd = PropertyDelegation()
    //读取属性,实际上是调用属性的委托对象的 getter 方法
    println(pd.name)
    //写入属性,实际上是调用属性的委托对象的 setter 方法
    pd.name = "kotlin"
    println(pd.name)
}

上面代码将PropertyDelegation的 name 属性委托给 MyDelegation 对象。接 下来程序定义了 MyDelegation 类,由于该类的对象要作为读写属性的委托对象,因此该类必须实现符合要求的 getter、 setter方法,如 MyDelegation中代码所示。
当程序读取 PropertyDelegation对象的 name 属性时,实际上就是执行 MyDelegation对象所提供的 getValue()方法;当程序对 PropertyDelegation对象的 name 属性赋值时,实际上就是执行 MyDelegation对象所提供的setValue()方法。

实际上,上面的MyDelegation类已经实现了ReadWriteProperty接口的规范,因此程序完全可让该类实现 ReadWriteProperty < PropertyDelegation, String>接口,再为getValue()和setValue()方法添加override修饰符。

前面己经讲过, Kotlin不会为委托属性生成幕后字段、 getter和 setter方法,这是不言而喻的,因此委托属性的所有处理细节都由委托对象负责 。Kotlin会委托属性生成辅助属性,该辅助属性引用了属性的委托对象。当程序调用委托属性的 getter、 setter方法时,实际上就是执行委托对象的 getValue()、 setValue()方法。例如,我们定义了如下委托属性 :

class C{
    var prop: Type by MyDelegation()
}

上面代码定义了名为prop的委托属性,该属性的委托对象是MyDelegate。上面代码被编译器处理之后将生成如下代码 :

//这段代码是由编译器生成的
class C {
    //生成隐藏属性 ,隐藏属 性引用代理对象
    private val prop$delegate = MyDelegation()
    var prop: Type
    // getter 方法,实际上就是调用代理对象的 getValue ()方法
        get() = prop$delegate.getValue(this, this::prop)
    //setter 方法,实际上就是调用代理对象的 setValue ()方法
    set(value:Type) {
        prop$delegate.setValue(this, this::prop,value)
    }

}

在上面编译器生成的 getter、 setter 方法实现中,其实就是调用代理对象的 getValue()、 setValue()方法,传入的第一个参数 this代表正在调用 getter或 setter方法,第二个参数 this::prop 代表正在操作的目标属性 。

延迟属性

Kotlin 提供了 一个lazy()函数,该函数接受一个Lambda表达式作为参数,并返回一个 Lazy<T>对象 。
Lazy<T>对象包含了一个符合只读属性委托要求的 getValue()方法,因此该 Lazy<T>对象可作为只读属性的委托对象 。Lazy<T>对象只能作为只读属性的委托对象 。

val lazyProp: String by lazy {
   println("第一次访问时执行代码块")
   "kotlin"
}

fun main(args: Array<String>) {
   //两次访问 lazyProp 属性
   println(lazyProp)
   println(lazyProp)
}

上面代码将 lazyProp 属性的委托对象指定为 lazy()函数的返回值: Lazy<T> 对象。这样当程序访问 lazyProp 属性时,实际上就是调用 Lazy<T>对象的 getValue()方法。
Lazy<T>的 getValue()方法的处理逻辑是:第一次调用该方法时,程序会计算 Lambda 表达式,并得到其返回值,以后程序再次调用该方法时,不再计算 Lambda表达式,而是直接使用第一次计算得到的返回值。
lazy()函数有如下两个版本。

  • fun <T> lazy(initializer: () -> T): Lazy<T>
  • fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T>

从上面代码可以看出 , 前面代码使用的是第一个版本,该版本的lazy()函数执行Lambda表达式时会提供自动添加保证线程安全的同步锁,因此开销略大。
上面第一个lazy()函数相当于第二个lazy()函数的第一个参数指定为LazyThreadSafetyMode.SYNCHRONIZED

如果程序不需要 lazy()函数保证线程安全,希望提供更好的性能,则可将 lazy()函数的第一个参数指定为如下两种模式。

  • LazyThreadSafetyMode.PUBLICATION : 在这种模式下,lazy()函数不会使用排他同步锁,多个线程可同时执行。
  • LazyThreadSafetyMode.NONE: 在这种模式下,lazy()函数不会有任何线程安全相关的操作和开销 。

例如,将上面的 lazyProp 改为如下形式来取消线程安全保证,从而获得最佳性能

val lazyProp: String by lazy (LazyThreadSafetyMode.NONE){
    println("第一次访问时执行代码块")
    "kotlin"
}

属性监听

Kotlin的kotlin.properties包下提供了一个Delegates对象声明 (单例对象),该对象中包含如下两个常用方法 。

  • fun<T> observable(initialValue: T, onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit): ReadWriteProperty<Any?, T>: 该方法返回 一个ReadWriteProperty对象,因此该方法的返回值可作为读写属性的委托对象。该方法的第一个参数用于为接受委托的属性指定初始值,第二个参数可接受 Lambda 表达式, 当接受委托的属性被设置值时,该 Lambda表达式就会被触发 。
  • fun <T> vetoable(initialValue: T, onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean): ReadWritePrope<Any?, T>: 该方法与前一个方法基本相同, 区别只是负责执行监听的 Lambda 表达式需要返回值,当该返回值为 true 时,接受委托的属性的新值设置成功; 否则设置失败。

例如如下程序。

import kotlin.properties.Delegates

var observableProp: String by Delegates.observable("默认值") {
    //Lambda表达式的第一个参数代表被监听的属性
    //第二个参数代表修改之前的值
    //第三个参数代表被设置的新值
    prop, old, new ->
    println("${prop}的${old}被改为${new}")
}

fun main(args: Array<String>) {
    //访问 observableProp 属性 , 不会触发监听器的 Lambda 表达式
    println(observableProp)
    //设置属性值,触发监听器的 Lambda 表达式
    observableProp ="kotlin"
    println(observableProp)
}

从上面的运行结果可以看出,无论我们为属性设置的新值是什么,总可以设置成功。如果需要对新值进行判断,然后再确定是否设置成功,则可使用 Delegates 对象的 vetoable()方法 。 例如如下代码:

import kotlin.properties.Delegates

var observableProp: Int by Delegates.vetoable(20) {
    //Lambda表达式的第一个参数代表被监听的属性
    //第二个参数代表修改之前的值
    //第三个参数代表被设置的新值
    prop, old, new ->
    println("${prop}的${old}被改为${new}")
    new > old
}

fun main(args: Array<String>) {
    //访问 observableProp 属性 , 不会触发监听器的 Lambda 表达式
    println(observableProp)
    //新值小于旧值,监听的 Lambda 表达式返回 false,新值设置失败
    observableProp = 15
    println(observableProp) //输出 20
    //新值大于旧值,监听的 Lambda 表达式返回 true,新值设置成功
    observableProp =25
    println(25)
}

上面 Lambda表达式内最后的表达式就是它的返回值,如果 new>old,则返回 true; 否则返回 false。由此可见,此处我们要求新设置的属性值必须比原属性值大才能设置成功。

现代编程语言几乎都提供了属性监听的功能,由于 Java 并没有真正意义上的 属性( Java 的属性等于field、 getter、 setter方法的组合),因此 Java对属性的监听需要通过 setter 方法来实现 。而 Kotlin 的属性监听则通过属性委托机制来实现。

使用Map存储属性值

Kotlin 的 Map 提供了如下方法:

operator fun <V, V1 : V> Map<in String, V>.getValue(thisRef: Any?, property:KProperty<*>): V1

该方法符合只读属性的委托对象的要求,这意味着 Map 对象可作为只读对象的委托 。

MutableMap 则 提供如下两个方法:

  • operator fun <V, V1 : V> Map<in String, V>.getValue(thisRef: Any?, property:KProperty<*>): V1
  • operator fun <V> MutableMap<in String, in V>.setValue(thisRef: Any?, property:
    KProperty<*>, value: V)

上面两个方法符合读写属性的委托对象的要求,这意味着 MutableMap对象可作为读写对象的委托。

因此程序可将类只读属性委托给 Map对象管理,在这种情况下,对象本身并不负责存储对象状态,而是将对象状态保存在 Map 集合中。这么做的好处是,当程序需要与外部接口(如JSON)通信时,程序并不需要将该对象直接暴露出来,只要将该对象属性所委托的Map暴露出来即可,而 Map 则完整地保存了整个对象状态。
例如,如下程序使用 Map 存储对象的只读属性。

class Item(val map: Map<String, Any?>) {
    val barCode: String by map
    val name: String by map
    val price: Double by map
}

fun main(args: Array<String>) {
    val item = Item(mapOf(
            "barCode" to "133355 ",
            "name" to "kotlin",
            "price" to 68.9
    ))

    println(item.barCode)
    println(item.name)
    println(item.price)
    println("--------------")
    //将对象持有的 map 暴露出来,其他程序可通过标准 Map 读取数据
    val map = item.map
    println(map["barCode"])
    println(map["name"])
    println(map["price"])
}

从上面代码可以看出,程序可以将 Item对象的所有只读属性都委托给一个Map对象,其实道理很简单 :Item对象的每个属性名就相当于 Map 的 key,属性值就相当于 key对应的value。指定Map 对象作为 Item对象的委托之后,接下来程序只要把 Item 的 Map 暴露出来,程序即可通过该 Map来获取 Item对象的状态。

如果对象包含的属性是读写属性,则需要使用 MutableMap作为委托对象 。例如如下程序:

class Mutableltem(var map :MutableMap<String,Any?>){
    var barCode: String by map
    var name: String by map
    var price: Double by map
}

fun main(args: Array<String>) {
    val item = Mutableltem(mutableMapOf())

    //设置 item 对象的属性,其实会委托给 MutableMap 处理
    item.barCode="111"
    item.name= "kotlin"
    item.price =20.1

    //将对象持有的 map 暴露出来,其他程序可通过标准 Map 读取数据
    val map = item.map
    println(map["barCode"])
    println(map["name"])
    println(map["price"])
}

上面程序将 Mutableltem 的读写属性委托给 MutableMap 对象,这样当程序设置 Mutableltem对象的属性值时,实际上是交给 MutableMap 负责处理的,相当于为 MutableMap 设置了key-value对。 因此程序后面可通过 MutableMap获取到为 Item设置的属性值。
反过来,如果程序为被委托的 MutableMap 对象设置了 key-value 对,实际上就是修改了Item对象的状态。

局部属性委托

从 Kotlin 1.1 开始, Kotlin 支持为局部变量指定委托对象。这种指定了委托对象的局部变量被称为“局部委托属性”,其实还是局部变量,只是对该变量的读取、赋值操作将会交给委托对象去实现。
与前面介绍的属性委托相似 ,局部变量的委托对象同样要遵守一定的要求。对于只读属性而言,同样需要实现 operator修饰的 getValue(thisRef: Nothing?, property: !<Property<*>)方法, 注意该方法的第一个参数是 Nothing?类型, Nothing 是 Kotlin 提供的 一个特殊的类,专门用于代表永不存在的对象。
由于局部变量不属于任何对象, 因此它的委托对象的 getValue()方法的第一个参数自然也就不存在了,因此 Kotlin使用 Nothing?来声明 getValue()方法的第一个参数的类型 。
对于读写属性而言,则需要实现 getValue()和 setValue()两个方法,其中setValue()方法的第一个参数的类型也应该声明为 Nohting?,道理是一样的。
下面是一个局部属性委托的示例:

import kotlin.reflect.KProperty

class MyDelegation {
    private var _backValue = "默认值"

    operator fun getValue(thisRef: Nothing?,
                          property: KProperty<*>): String {
        println("${thisRef}的${property.name}属性执行getter方法")
        return _backValue
    }

    operator fun setValue(thisRef: Nothing?,
                          property: KProperty<*>, newValue: String) {
        println("${thisRef}的${property.name}属性执行 setter 方法,传入参数值为:${newValue}")
        _backValue = newValue
    }
}

fun main(args: Array<String>) {
    var name:String by MyDelegation()
    //访问局部变量 , 实际上是调用 MyDelegation 对象的 getValue()方法
    println(name)
    //对局部变量赋值,实际上是调用 MyDelegation 对象的 setValue()方法
    name = "kotlin"
    println(name)
}

从上面代码可以看出,局部变量的委托对象的 getValue()和setValue()方法的第一个参数类型变成了 Nothing?,其他变化不大。

与普通属性类似的是,程序当然也可以利用 Kotlin所提供的标准委托,比如可以利用 lazy() 函数对局部变量延迟初始化 。程序如下:

fun main(args: Array<String>) {
    val name :String by lazy{
        println("”计算 name局部变量")
        "kotlin"
    }

    //第一 次访问 name 属性时,会执行 Lambda 表达式
    println(name)
    //以后再次访问 name 属性时,则直接使用第一次计算的值
    println(name)
}

委托工厂

除提供 getValue()、 setValue()方法的对象可作为属性的委托对象之外,从 Kotlin1.1 开始, 一种类似于“委托工厂”的对象也可作为委托对象。委托工厂需要提供如下方法。

  • operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): 该方法的两个参数与委托的 getValue()方法的两个参数的意义相同 。

如果上面方法返回 ReadOnlyPrope即对象,那么该对象可作为只读属性的委托对象 ; 如果返回 ReadWriteProperty对象,则该对象就可作为读写属性的委托对象。

使用 provideDelegate()方法来生成委托的好处是,Kotlin 会保证对象的属性被初始化时调用该方法来生成委托,这样我们即可在该方法中加入任何自定义代码,完成任何自定义逻辑。
下面示例示范了“委托工厂”的用法:

import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

class MyTarget {
    //该属性的委托对象是 provideDelegate()方法返回的 MyDelegation对象
    var name: String by PropertyChecker()

}

class PropertyChecker {
    operator fun provideDelegate(thisRef: MyTarget,
                                 prop: KProperty<*>): ReadWriteProperty<MyTarget, String> {
        //插入口定义代码,可执行任意业务操作
        checkProperty(thisRef, prop.name)
        //返回实际的委托对象
        return MyDelegation()
    }

    private fun checkProperty(thisRef: MyTarget, name: String) {
        println("”---检查属性----")
    }
}

class MyDelegation : ReadWriteProperty<MyTarget, String> {

    private var _backValue = "默认值"

    override fun getValue(thisRef: MyTarget, property: KProperty<*>): String {
        println("${thisRef}的${property.name}属性执行getter方法")
        return _backValue
    }

    override fun setValue(thisRef: MyTarget, property: KProperty<*>, value: String) {
        println("${thisRef}的${property.name}属性执行 setter 方法,传入参数值为:${value}")
        _backValue = value
    }

}

fun main(args: Array<String>) {
    //创建对象(初始化属性),调用委托工厂的 provideDelegate()方法
    var  pd =MyTarget()
    //读取属性 ,实际上是调用属性的委托对象的 getter 方法
    println(pd.name)
    //写入属性 ,实际上是调用属性的委托对象的 setter 方法
    pd.name ="java"
    println(pd.name)
}

上面代码指定属性的委托对象为PropertyChecker,该对象是一个委托工厂,它提供了provideDelegate()方法来生成委托,该方法在生成委托之前先调用 checkProperty()方法执行检查,具体检查什么,由开发者自己决定;然后该方法才返回真正的委托对象: MyDelegation。 从上面描述可以看出,使用委托工厂的好处在于:程序可以在属性初始化时自动回调委托工厂的 provideDelegate()方法,而该方法则可以执行自己的业务操作。
属性的委托依然由 MyDelegation 对象负责,只是在属性初始化时插入了业务检查方法: checkProperty()。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容