Kotlin学习(六):对象和委托
对象
对象表达式
在 Java 中,有 个匿名类的概念,也就是在创建类时,无须指定类的名字。匿名类用于方法的参数类型。基本理念是方法参数需要接收一个类或接口的实例,而这个实例只是在该方法中临时用一下,并没有必要单独定义一个类,或单独创建一个对象变量。 因此,就在传入方法参数值的同时创建了类的实例。下面的代码演示了 Java 中匿名类的使用。
public class MyClass {
public String name;
public MyClass(String name) {
this.name = name;
}
public void verify() {
System.out.println("verify");
}
}
public class Test {
public static void process(MyClass myClass) {
myClass.verify();
}
public static void main(String[] args) {
process(new MyClass("Mike") {
@Override
public void verify() {
System.out.println(name + " verify ");
}
});
}
}
在 Kotlin 中,也有类似的功能,但不是匿名类,而是对象。要想创建一个对象,需要是用 object 关键字,该对象要继承的类需要与 object 之间用 ( : ) 分隔,如 **MyClass ( “Mike” ) ** 。
open class MyClass(name: String) {
open var name = name
open fun verify() {
println("verify")
}
}
fun process(obj: MyClass) {
obj.verify()
if (obj is MyInterface) {
obj.closeData()
}
}
fun main(args: Array<String>) {
process(object : MyClass("Mike") {
override fun verify() {
println("$name verify")
}
})
}
对象和类一样,只能有一个父类,但可以实现多个接口。在下面的代码中,对象不仅继承了 MyClass 类,还实现了 MyInterface 接口,该接口的成员函数有一个默认实现。
open class MyClass(name: String) {
open var name = name
open fun verify() {
println("verify")
}
}
fun process(obj: MyClass) {
obj.verify()
if (obj is MyInterface) {
obj.closeData()
}
}
interface MyInterface {
fun closeData() {
println("closeData")
}
}
fun main(args: Array<String>) {
process(object : MyClass("Bill"), MyInterface {
override fun verify() {
println("$name verify ")
}
})
}
如果只想建立一个对象,不想从任何类继承,也不实现任何接口,可以按下面的方式建立对象。
fun foo() {
var adHo = object {
var x: Int = 0
var y: Int = 0
}
println("x = ${adHo.x} , y = ${adHo.y}")
}
声明匿名对象
匿名对象只能用在本地 ( 函数 ) 或 private 声明中。如果将匿名对象用于 public 函数的返回值,或 public 属性的类型,那么 Kotlin 编译器会将这些函数或属性的返回类型重新定义为匿名对象的父类型,如果匿名对象没有实现任何接口,也没有从任何类继承,那么父类型就是 Any 。因此,添加在匿名对象中的任何成员都将无法访问。
class MyClass(name: String) {
private fun foo() = object {
var x: String = "x"
}
fun publicFoo() = object {
var x: String = "x"
}
fun bar() {
var x1 = foo().x
}
}
访问封闭作用域内的变量
在 Java 中,匿名对象访问封闭作用域内的变量,需要用 final 声明该变量,这也就意味着在匿名对象中无法修改封闭作用域内变量的值。在 Java 8 中,如果只是使用封闭作用域内的变量,该变量并需要使用 final ,但一旦修改变量的值,就必须使用 final 来声明变量。其实,在 Java 8 中,规则并没有改变,只是在使用变量时,封闭作用域内的变量是一个隐式的 final 变量,知道该变量被修改的那一刻,才要求使用 final 声明变量。
public class Test {
public static void process(MyClass myClass) {
myClass.verify();
}
public static void main(String[] args) {
int n = 20;
process(new MyClass("Mike") {
@Override
public void verify() {
n = 30;
if (n == 20) {
System.out.println("success");
} else {
System.out.println("failed");
}
}
});
}
}
在这段代码中, process 函数参数类型是 MyClass 。调用 process 方法时使用匿名对象,井在匿名对象的实现函数 test 中访问了 main 函数作用域中的 变量。在这种情况下, test 函数中只能读取 的值(无论 是否使用 final 声明)。
如果这段代码用 Kotlin 实现,在匿名对象中就可以任意访问变量 n 了,包括修改 n 的值。
open class MyClass {
open fun test() {
}
}
fun process(obj: MyClass) {
obj.test()
}
fun main(args: Array<String>) {
var n = 20
process(object : MyClass() {
override fun test() {
if (n == 20) {
println("success")
n = 30
} else {
println("failed")
}
}
})
}
伴生对象
在 Kotlin 中并没有静态类成员的概念,但并不等于不能实现类似于静态类成员的功能。伴生对象**( Companion Objects ) **就是 Kotlin 用来解决这个问题的语法糖。
如果在 Kotlin 类中定义对象,那么就称这个对象为该类的伴生对象。伴生对象要使用companion 关键字声明。
class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
}
该伴生对象的成员可通过只使用类名作为限定符来调用:
val instance = MyClass.create()
可以省略伴生对象的名称,在这种情况下将使用名称 Companion :
class MyClass {
companion object { }
}
val x = MyClass.Companion
其自身所用的类的名称(不是另一个名称的限定符)可用作对该类的伴生对象 (无论是否具名)的引用:
class MyClass1 {
companion object Named { }
}
val x = MyClass1
class MyClass2 {
companion object { }
}
val y = MyClass2
请注意,即使伴生对象的成员看起来像其他语言的静态成员,在运行时他们仍然是真实对象的实例成员,而且,例如还可以实现接口:
interface Factory<T> {
fun create(): T
}
class MyClass {
companion object : Factory<MyClass> {
override fun create(): MyClass = MyClass()
}
}
val f: Factory<MyClass> = MyClass
当然,在 JVM 平台,如果使用 @JvmStatic 注解,你可以将伴生对象的成员生成为真正的静态方法和字段。
对象表达式和对象声明之间的语义差异
对象表达式和对象声明之间有一个重要的语义差别:
- 对象表达式是在使用他们的地方立即执行(及初始化)的;
- 对象声明是在第一次被访问到时延迟初始化的;
- 伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配。
委托
委托 ( Delegate ) 其实是一种非常好的代码重用方式,有点类似 AOP ( 面向方面编程 ) ,也就是将在多个地方出现的代码放到同一个地方,以便被多个类和属性重用。
类的委托
委托模式己被实践证明为类继承模式之外的另一种很好的替代方案,Kotlin 直接支持委托模式, 因此用户不必再为了实现委托模式而手动编写那些 “无聊” 的重复代码。例如, Derived 类可以继承 Base 接口,并将 Base 接口所有的 public 方法委托给一个指定的对象。
interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}
class Derived(b: Base) : Base by b
fun main() {
val b = BaseImpl(10)
Derived(b).print()
}
从这段代码可以看出,Derived 类使用 by 关键字将 Base 类的 print 函数 托给了一个对象。该对象需要通过 Derived 类的主构造器传入,而且该对象的类必须实现 Base 接口 。在本例中,该对象是 Baselmpl 类的实例。如果 Derived 类不进行委托,就需要再实现一遍 print 函数。
属性委托
在实际应用中,有很多类属性的 getter 和setter 函数的代码相同或类似,当然,从技术上来说,在每一个类中编写相同的代码是可行的,但这样就会造成代码的大量冗余,而且维护困难。为了解决这个问题,Kotlin 允许属性委托,也就是将属性的 getter 和 setter 函数的代码放到一个委托类中,如果在类中声明属性,只需要指定属性委托类。这样,相同的代码,就可以在同一个地方,大大减少代码的冗余,也让代码更容易维护。下面先看一个不使用属性委托的例子。
class MyClass1 {
var name: String = ""
get() :String {
println("MyClass1.get 已经被调用")
return field
}
set(value) {
println("MyClass1.set 已经被调用")
field = value
}
}
class MyClass2 {
var name: String = ""
get() :String {
println("MyClass2.get 已经被调用")
return field
}
set(value) {
println("MyClass2.set 已经被调用")
field = value
}
}
fun main(args: Array<String>) {
var c1 = MyClass1()
var c2 = MyClass2()
c1.name = "Bill"
c2.name = "Mike"
println(c1.name)
println(c2.name)
}
在这段代码中,有两个类 MyClass1 和 MyClass2 , 这两个类都有一个 name 属性,该属性的 getter 和setter 函数的代码非常相似,因此产生了代码元余。 下面就用委托类解决这个问题。
所谓委托类,就是一个包含 getValue 和 setValue 函数的类。 这两个函数用 operator 声明。在使用委托类时, 需要用 by 关键字, 创建委托类实例的代码放在 by 后面,代码如下:
class Delegate {
var name: String = ""
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
val className = thisRef.toString().substringBefore("@")
println("${className}.get 已经被调用")
return name
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
val className = thisRef.toString().substringBefore("@")
println("${className}.set 已经被调用")
name = value
}
}
class MyClass1 {
var name :String by Delegate()
}
class MyClass2 {
var name: String by Delegate()
}
fun main(args: Array<String>) {
var c1 = MyClass1()
var c2 = MyClass2()
c1.name = "Bill"
c2.name = "Mike"
println(c1.name)
println(c2.name)
}
现在执行这段代码,与前面未使用委托类时输出的内容完全一样。现在即使有更多的拥有 name 属性的类,只要 name 属性的 getter 和 setter 函数的实现类似,都可以将 name 属性委托 Delegate 类。
委托类的初始化函数
如果委托类有主构造器,也可以向主构造器传入一个初始化函数。这时,可以定义一个委托函数的返回值是委托类,并在委托时指定初始化函数。下面的代码是基于上一小节代码的改进。在这段代码中 ,为委托类加了一个主构造器,并传入了一个初始化函数。初始化函数返回 String 类型的值,该值会在委托类中的 getValue 和 setValue 函数中调用.
class Delegate<T>(initializer: () -> T) {
var name: String = ""
var className = initializer()
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
println("${className}.get 已经被调用")
return name
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("${className}.set 已经被调用")
name = value
}
}
fun <T> delegate(initializer: () -> T): Delegate<T> = Delegate(initializer)
class MyClass1 {
var name: String by delegate {
println("MyClass1.name初始化函数调用")
"<MyClass1>"
}
}
class MyClass2 {
var name: String by delegate {
println("MyClass2.name初始化函数调用")
"<MyClass2>"
}
}
fun main(args: Array<String>) {
var c1 = MyClass1()
var c2 = MyClass2()
c1.name = "Bill"
c2.name = "Mike"
println(c1.name)
println(c2.name)
}
属性委托要求
这里我们总结了委托对象的要求。
对于一个只读属性(即 val 声明的),委托必须提供一个操作符函数 getValue() ,该函数具有以下参数:
thisRef —— 必须与 属性所有者 类型(对于扩展属性——指被扩展的类型)相同或者是其超类型。property —— 必须是类型 KProperty<*> 或其超类型。
getValue() 必须返回与属性相同的类型(或其子类型)。
class Resource
class Owner {
val valResource: Resource by ResourceDelegate()
}
class ResourceDelegate {
operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
return Resource()
}
}
对于一个可变属性(即 var 声明的),委托必须额外提供一个操作符函数 setValue() , 该函数具有以下参数:
thisRef —— 必须与 属性所有者 类型(对于扩展属性——指被扩展的类型)相同或者是其超类型。property —— 必须是类型 KProperty<*> 或其超类型。value — 必须与属性类型相同(或者是其超类型)。
class Resource
class Owner {
var varResource: Resource by ResourceDelegate()
}
class ResourceDelegate(private var resource: Resource = Resource()) {
operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
return resource
}
operator fun setValue(thisRef: Owner, property: KProperty<*>, value: Any?) {
if (value is Resource) {
resource = value
}
}
}
getValue() 或/与 setValue() 函数可以通过委托类的成员函数提供或者由扩展函数提供。 当你需要委托属性到原本未提供的这些函数的对象时后者会更便利。 两函数都需要用 operator 关键字来进行标记。
委托类可以实现 ReadOnlyProperty 和 ReadWriteProperty 接口中的一个,前者只包含 getValue 函数,后者包含了 getValue 和 setValue 函数。这些接口被声明在 Kotlin 标准库中的kotlin.properties 包内 。这两个接口 定义代码如下:
public fun interface ReadOnlyProperty<in T, out V> {
public operator fun getValue(thisRef: T, property: KProperty<*>): V
}
public interface ReadWriteProperty<in T, V> : ReadOnlyProperty<T, V> {
public override operator fun getValue(thisRef: T, property: KProperty<*>): V
public operator fun setValue(thisRef: T, property: KProperty<*>, value: V)
}
标准委托
Kotlin 标准库中提供了一些委托函数,可以实现几种很有用的委托。
惰性装载
lazy 是一个函数,接受一个 Lambda 表达式作为参数(初始化函数,与前面实现的 delegate 委托函数类似,返回一个 Lazy<T> 类型的实例,这个实例可以作为一个委托,实现惰性加载属性 ( lazy property ) :第一次调用 get() 时,将会执行从 lazy 函数传入的 Lambda 表达式,然后会记住这次执行的结果,以后所有对 get() 的调用都只会简单地返回以前记住的结果。
val lazyValue: String by lazy {
println("computed")
"HELLO"
}
fun main(args: Array<String>) {
var testDelegate = TestDelegate()
println(testDelegate.lazyValue)
println(testDelegate.lazyValue)
}
默认情况下,对于 lazy 属性的求值是同步锁的(synchronized):该值只在一个线程中计算,并且所有线程会看到相同的值。如果初始化委托的同步锁不是必需的,这样多个线程可以同时执行,那么将 LazyThreadSafetyMode.PUBLICATION 作为参数传递给 lazy() 函数。 而如果你确定初始化将总是发生在与属性使用位于相同的线程, 那么可以使用 LazyThreadSafetyMode.NONE 模式:它不会有任何线程安全的保证以及相关的开销。
可观察属性 Observable
所谓可观察属性就是当属性变化时可以拦截其变化。实现观察属性值变化的委托函数是Delegates.observable 。该函数接受两个参数,第1个是初始化值,第2个是属性值变化事件的响应器 ( handler) 。每次我们向属性赋值时,响应器都会被调用(在属性赋值处理完成之后)。响应器函数有3个参数,被赋值的属性 (prop) 、赋值前的旧属性值 (old) ,以及赋值后的新属性值 (new) 。
var name: String by Delegates.observable("Jack") {
prop, old, new ->
println("old = [${old}], new = [${new}]")
}
fun main(args: Array<String>) {
var testDelegate = TestDelegate()
println(testDelegate.lazyValue)
println(testDelegate.lazyValue)
testDelegate.name = "john"
testDelegate.name = "Bill"
}
阻止属性的赋值操作
如果你希望能够拦截属性的赋值操作,并且还能够“否决”赋值操作,那么不要使用 observable 函数,而应该使用 vetoable 函数。传递给 vetoable 函数的事件响应器会返回一个布尔类型的值 如果返回 true 表示 许给属性赋值,如果返回 false ,就会否决属性的赋值(仍然保留原来的值)。
var age: Int by Delegates.vetoable(20) {
prop, old, new ->
println(" old = [${old}], new = [${new}]")
var result = true
if (new == 30) {
result = false
println("年龄不能为$new")
}
result
}
fun main(args: Array<String>) {
var testDelegate = TestDelegate()
testDelegate.age=35
println("age = ${testDelegate.age}")
testDelegate.age=30
println("age = ${testDelegate.age}")
}
Map 委托
有一种常见的使用场景是将 Map 中的 key-value 映射到对象的属性中,这通常在解析 JSON 数据时用到。下面的代码中有 User 类,该类有 name 和 age 两个属性。通过主构造器传入 一个 Map 对象,并将 Map 中的 key 的值映射到同名的属性中 。所使用的方法就是每一个需要映射的属性使用 by 指定 Map 作为自己的委托。
class User(var map: Map<String, Any>) {
val name: String by map
val age: Int by map
}
fun main(args: Array<String>) {
var map = mapOf(
"name" to "Mike",
"age" to 25
)
var user = User(map)
println(" name = ${user.name} , age = ${user.age}")
}
我们可以看到,在 User 类中使用了 val 声明 name 和 age 属性,这就意味着这两个属性值是不可修改的,即使将 val 改成 var 也不行,因为 Map 只有 getValue 方法,而没有 setValue 方法, 所以 Map 只能通过 User 对象读取 name 和 age 属性值,而不能通过 User 对象设置 name 和 age 属性值。
如果要让 User 类的 name 和 age 属性映射到可读写的委托,需要将这两个属性委托给 MutableMap 。
class MutableUser(var map: MutableMap<String, Any>) {
var name: String by map
var age: Int by map
}
fun main(args: Array<String>) {
var map = mutableMapOf<String, Any>(
"name" to "Jack",
"age" to 30
)
var user = MutableUser(map)
println("初始化值: name = ${user.name} , age = ${user.age}")
user.name = "Bill"
println("修改后: map = $map ")
map["age"] = 18
println("修改后值: name = ${user.name} , age = ${user.age}")
}
局部委托属性
你可以将局部变量声明为委托属性。 例如,你可以使一个局部变量惰性初始化:
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)
if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}
memoizedFoo 变量只会在第一次访问时计算。 如果 someCondition 失败,那么该变量根本不会计算。
小结
对象和委托是 Kotlin 中两个比较大的语法糖。对象相当于 Java 中的匿名对象,伴生对象也可以代替 Java 中的静态类成员使用。而委托的主要作用就是实现代码重用 ,有点类似 AOP (面向方面编程)。
|