Kotlin入门系列:第三章 类、对象和接口

1 定义类继承结构

1.1 kotlin中的接口

kotlin接口与java8中的相似:它们可以包含抽象方法的定义以及非抽象方法的实现(与java8中的默认方法类似),但它们不能包含任何状态。

// interface关键字提供一个接口,接口后面是没有接括号的,对象有
interface Clickable {
	fun click()
	
	// 在java8中,接口默认方法实现需要使用关键字default声明
	// 在kotlin中只需要像正常的函数声明实现接口默认方法即可
	fun showOff() = println("I'm clickable!")
}

// 实现接口,kotlin用":"代替java的extends和implements
// 和java一样,可以实现多个接口,只能继承一个类
class Button: Clickable {
	override fun click() = println("I was clicked")
}

使用:
Button().click()

如果类的两个实现接口都提供了相同的默认方法,需要你在类中具体实现对应的方法或者使用父类的默认实现:

interface Clickable {
	fun click()
	fun showOff() = prlintln("I'm clickable!")
}

interface Focusable {
	fun setFocus(b: Boolean) = println("I ${if (b) "got" else "lost"} focus")
	fun showOff() = println("I'm focusable!")
}

class Button:Clickable, Focusable {
	override fun click() = println("I was clicked")
	// Clickable和Focusable接口都提供了相同的默认方法,需要强制提供自己的实现
	override fun showOff() {
		// 不能用super.showOff(),因为两个接口都有相同的默认方法
		// 要指定使用哪个父类的默认方法
		super<Clickable>.showOff()
		super<Focusable>.showOff()
	}
}

kotlin是基于java6的,为什么可以支持像java8的默认方法呢?

interface Flyer {
	val speed: Int // 定义抽象属性
	fun kind()
	fun fly() {
		println("I can fly")
	}
}

反编译成java代码:

public interface Flyer {
	int getSpeed();
	void kind();
	void fly();
	public static final class DefaultImpls {
		public static void fly(Flyer $this) {
			String var1 = "I can fly";
			System.out.println(var1);
		}
	}
}

可以发现,kotlin支持默认方法是因为提供了一个静态内部类。

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

java的类和方法默认是 open 可继承重写的,而kotlin中默认都是 final 不可继承重写的。如果想允许一个类创建子类,需要使用 open 修饰符来标示,此外还需要给每一个需要被重写的属性或方法添加 open 修饰符。

class Bird {
	val weight: Double = 500.0
	val color: String = "blue"
	val age: Int = 1
	fun fly() {}
}

把上面的类反编译成java:

// 可以看到类默认是final不可继承的
public final class Bird {
	private final double weight = 500.0D;
	@NotNull
	private final String color = "blue";
	private final int age = 1;

	public final double getWeight() { return this.weight; }
	@NotNull
	public final String getColor() { return this.color; }
	public final int getAge() { return this.age; }
	public final void fly() {}
}

如果我们要让类可继承,需要手动指定 open

// 类声明为open,可以被子类继承
open class RichButton: Clickable, Focusable {
	// 函数不可重写
	fun disable() {}
	// 函数可重写
	open fun animate() {}
	// 重写了一个open的函数且该函数也是open的
	override fun click() {}
	// 声明重写的类不能再被子类重写,使用final修饰符
	final override fun setFocus(b: Boolean) {}
}

在kotlin中抽象和java一样,使用 abstract 修饰符,抽象方法都是 open 的。

abstract class Animated {
	// 抽象方法,默认是open公开的
	abstract fun animate()
	// 抽象类中的非抽象函数并不是默认open的,如果需要被子类重写需要标注为open
	open fun stopAnimating() {}
	// 默认final不可重写的
	fun animateTwice() {}
}

在kotlin中的接口不能使用 finalopen 或者是 abstract 修饰,接口中的成员始终是 open 的,不能将其声明为 final。如果它没有函数体它就是 abstract 的,但是 abstract 关键字不是必须的。

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

1.3 可见性修饰符:默认为public

在kotlin中的可见性修饰符与java一样,也是 privateprotectedpublic

kotlin与java可见性修饰符的不同点:

  • 如果没有声明可见性修饰符,默认是 public

  • java中的默认可见性是包私有,在kotlin中作为替代方案,提供了一个新的修饰符 internal,表示“只在模块内部可见”。一个模块就是一组一起编译的kotlin文件,这有可能是一个IntelliJ IDEA模块、一个Eclipse项目、一个Maven或Gradle项目或者一组使用调用Ant任务进行编译的文件。

// internal简单说就是为了让类或属性,我希望它在其他模块不可见,比如
// base module有一个bean类Lession,我不希望app module这个模块能够访问到我base module的这个Lession类
// 内部类也同理
internal class Lession {}
  • kotlin允许在顶层声明中使用 private 可见性,包括类、函数和属性

  • protected 修饰符在kotlin中不能从同一个包中访问一个 protected 成员,protected 成员只在类和它的子类中可见(因为kotlin中已经没有了包的概念,都是文件和类)

  • 类的扩展函数不能访问它的 privateprotected 成员

修饰符含义与java比较
publickotlin中默认修饰符,全局可见与java中public效果相同
protected受保护修饰符,类及子类可见含义一致,但作用域除了类及子类外,包内也可见
private私有修饰符,类内修饰只有本类可见,类外修饰文件内可见私有修饰符,只有类内可见
internal模块内可见

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

在kotlin中,嵌套类是不能访问外部类的实例。

interface State: Serializable

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

在java中:

public class Button implements View {
	@Override
	public State getCurrentState() {
		return new ButtonState();
	}
	
	@Override
	public void restoreState(State state) { ... }

	// 内部类ButtonState
	// 这会导致异常java.io.NotSerializableException
	// ButtonState即使实现了State这个序列化接口,但也不能被序列化成功,因为默认会持有外部类Button的引用
	// 因为Button是不可序列化的,并且它的引用破坏了ButtonState的序列化
	// 解决这个问题,只需要将ButtonState声明为static,删除默认获取外部类引用
	public class ButtonState implements State { ... }
}

在kotlin中:

class Button: View {
	override fun getCurrentState() = ButtonState()

	override fun restoreState(state: State) { ... }
	
	// 在kotlin中,这个类与java中使用static声明的静态类类似
	// 如果嵌套类需要持有外部类的引用变为内部类,需要使用修饰符inner声明
	// 嵌套类是不持有外部类的引用,内部类是持有外部类的引用的
	class ButtonState: State { ... }
}

在kotlin中内部类要引用外部类实例也与java不同:

class Outer {
	val test: String = "test"

	inner class Inner {
		// 获取外部类实例需要使用this@外部类名称获取
		fun getOuterReference(): Outer = this@Outer

		fun getTest(): String = this@Outer.test
	}
}

// 如果是创建kotlin内部类(也就是java的嵌套类),可以直接创建
Outer.Inner()

// 如果是创建kotlin嵌套类(inner class),需要先创建外部类才能创建内部类
val outer = Outer()
outer.Inner()

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

在 kotlin 中一般会使用密封类相当于 java 的枚举类使用,密封类和其子类只能写在同一个文件中:

// 通常会将密封类的子类写在里面,作为枚举使用
sealed class SuperCommand {
	object UP : SuperCommand()
	object DOWN : SuperCommand()
	object LEFT : SuperCommand()
	object RIGHT : SuperCommand()
	class PACE(var pace: Int) : SuperCommand()
}

fun exec(view: View, superCommand: SuperCommand) = when(superCommand) {
	SuperCommand.UP -> {}
	SuperCommand.DOWN -> {}
	SuperCommand.LEFT -> {}
	SuperCommand.RIGHT -> {}
	is SuperCommand.PACE -> {}
}
interface Expr

class Num(val value: Int): Expr
class Sum(val left: Expr, val right: Expr): Expr

fun eval(e: Expr): Int = 
	when(e) {
		is Num -> e.value
		is Sum -> eval(e.right) + eval(e.left)
		// 总是不得不添加一个默认分支很不方便
		// 而且如果Expr添加了一个新子类,这里又忘了添加分支判断,很容易出现潜在的bug
		else -> throw IllegalArgumentException("Unknown expression")
	}

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

// 将基类标记为密封
// sealed修饰符隐含这个类是一个open类
// 在这种情况下,Expr类有一个只能在类内部调用的private构造方法,而且也不能声明一个sealed接口
// 因为如果这样做,kotlin编译器不能保证任何人都不能在java代码中实现这个接口
sealed class Expr {
	// 将所有可能的Expr的子类作为嵌套类列出
	class Num(val value: Int): Expr()
	class Sum(val left: Expr, val right: Expr): Expr()
}

fun eval(e: Expr): Int = 
	// when表达式处理所有的sealed类
	// 而且当你在Expr添加新的子类时,在编译时when表达式会报错提示需要在哪里添加修改
	when(e) {
		is Expr.Num -> e.value
		is Expr.Sum -> eval(e.left) + eval(e.right)
		// 不需要再提供else分支判断
	}

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

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

在kotlin中创建一个类:

// var表示相应的属性会用构造方法的参数来初始化,比如nickname会自动生成getter和setter,如果val就只生成访问该属性的getter
// 这是最常用的创建有参构造类的方式,其实它是通过几个步骤后才简化到当前的简洁
class User(var nickname: String?) {
	constructor() : this(null)
}

// 最原始的User对象
class User {
	var nickname: String? = null

	// 一个无参构造函数
	constructor() {}

	// 一个有参构造函数
	constructor(nickname: String?) {
		this.nickname = nickname
	}
}

// 进行第一步简化
class User constructor(nickname: String?) {
	var nickname: String? = null

	// 因为声明了主构造函数,所以其他构造函数必须要初始化到主构造函数
	constructor() : this(null) 

	// 这里是将本来的一个有参构造函数作为了这个类的主构造函数
	// 怎样确定主构造函数?把最常用的那个作为主构造函数就行了
	// 一个类可以有多个init代码块,多个init代码块按顺序从上往下执行
	init {
		this.nickname = nickname 
	}
}

// 进行第二步简化
class User constructor(nickname: String?) {
	var nickname: String? = nickname // 把属性赋值移到这边

	constructor() : this(null) 
}

// 进行第三步简化
class User constructor(var nickname: String?) { // 把属性移动到主构造函数中
	constructor() : this(null)
}

// 最后一步简化
class User(var nickname: String?) { // 构造方法参数没有注解或可见性修饰符,constructor也可以去掉b g
	constructor() : this(null)
}

// 提供一个默认值
class User(val nickname: String, val issubscribed: Boolean = true)

// 类有一个父类,主构造方法同样需要初始化父类
open class User(val nickname: String) {...}
class TwitterUser(nickname: String): User(nickname) {...}

// 类没有任何构造方法,将会生成一个不带任何参数的默认构造方法
open class Button

// 类继承了父类且没有提供任何构造方法,必须显式调用父类的构造方法,即使父类构造方法没有任何参数
// 如果Button是接口,则不需要加上括号
class RadioButton: Button()

// 类不被其他代码实例化创建,构造方法标记为private
class Secretive private constructor() {}

// 创建对象
val alice = User("Alick") // 创建对象不需要new关键字
val carol = User("Carol", isSubscribed = false) // 显式地为某些构造方法参数表明名称

被括号围起来的语句块 (val nickname: String) 就叫主构造方法,它主要有两个目的:表明构造方法的参数,以及定义使用这些参数初始化的属性。

constructor 关键字用来开始一个主构造方法或从构造方法的声明。

init 关键字用来引入一个初始化语句块。这种语句块包含了在类被创建时执行的代码,并会与主构造方法一起使用。因为 constructor 主构造方法不能包含初始化代码,所以需要 init 来初始化代码,一个类可以有多个初始化语句块。

注意:如果所有的构造方法参数都有默认值,编译器会生成一个额外的不带参数的构造方法来使用所有的默认值。这可以让kotlin使用库时变得更简单,因为可以通过无参构造方法来实例化类。

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

在kotlin中,类重载多个构造方法没有在java中那么常见,更多的是kotlin在创建对象时可以指定构造方法参数的默认值来实现。

但有时候kotlin中也有特殊需要来重载多个构造方法。

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

// super调用父类的构造方法
class MyButton: View {
	constructor(ctx: Context): super(ctx) {...}
	constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) {...}
}

// this调用自己的另一个构造方法
class MyButton: View {
	constructor(ctx: Context): this(ctx, MY_STYLE) {...}
	constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) {...}
}

2.3 实现在接口中声明的属性

// 实现该接口需要提供一个取得nickname值的方式
interface User {
	val nickname: String
}

class PrivateUser(override val nickname: String) : User {...}
class SubscribingUser(val email: String) : User {
	override val nickname: String get() = email.substringBefore('@') // 自定义getter
}
// 假设getFacebookName()在其他地方定义存在的
// 这个函数开销巨大:它需要与Facebook建立连接来获取想要的数据
class FacebookUser(val accountId: Int) : User {
	override val nickname = getFacebookName(accountId) // nickname属性初始化
}

接口还可以包含getter和setter属性,只要它们没有引用一个支持字段(支持字段需要在接口中存储状态,而这是不允许的)。

interface User {
	val email: String // 因为email在nickname中引用,所以必须被子类重写实现提供数值
	val nickname: String get() = email.substringBefore('@') // 结果值在每次访问时通过计算得到
}

2.4 通过getter或setter访问支持字段

// 自定义setter
// 在kotlin中,访问属性的getter和setter不再需要添加该属性的getter和setter方法,而是属性默认具备
// setter函数体中使用了特殊标识符field来访问支持字段的值
class User(val name: String) {
	var address: String = "unspecified"
		set(value) {
			println("""Address was changed for $name: "$field" -> "$value".""".trimIndent())
			// kotlin中提供了一个field来访问和保存属性的值,注意:不能和java中那样直接使用address = address是死循环
			field = value 
		}
		get() = field
}

>>>val user = User("Alice")
>>>>user.address = "Elsenheimerstrasse 47, 80687 Muenchen" // 调用setter
Address was changed for Alice: "unspecified" -> "Elsenheimerstrasse 47, 80687 Muenchen".

注意:可以只重定义可变属性的一个访问器。(即上例中,只能重定义可变属性address的setter访问器)

2.5 修改访问器的可见性

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

// 修改属性couter可见性为只能在类内部修改,通过addWord()函数暴露给外部去修改
class LengthCounter {
	var counter: Int = 0
		private set // 修改属性counter的setter属性是private,getter默认可访问

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

>>>val lengthCounter = LengthCounter()
>>>lengthCounter.addWord("Hi")
>>>println(lengthCounter.counter)

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

在java中,类都提供了equal、hashCode、toString,虽然IDE会帮你自动生成,但类也有太多的样板代码。

3.1 通过对象方法

3.1.1 字符串表示:toString()

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

3.1.2 对象相等性:equals()

在java中,基本数据类型使用 == 对比的是值;对象之间对比的是引用,equals 对比的才是对象的值。
在kotlin中,== 是比较两个对象的默认方式,在对比对象时会自动调用 equals,如果重写了 equals ,能够很安全地使用 == 来比较实例;引用之间的比较,通过 ===

>>>val client1 = Client("Alice", 1)
>>>val client2 = Client("Alice", 1)
>>>pritln(client1 == client2) // 在kotlin中,==检查对象是否相等,而不是比较引用。这里会编译成调用equals
false

class Client(val name: String, val postalCode: Int) {
	// 重写equals,重写后client1 == client2返回true,一般重写equals还要重写hashCode
	override fun equals(other: Any?): Boolean {
		if (other == null || other !is Client) return false

		return name == other.name && postalCode == other.postalCode
	}
	
	override fun toString() = "Client(name = $name, postalCode = $postalCode)"
}

3.1.3 Hash容器:hashCode()

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

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

在kotlin中,不再需要重写equals、toString、hashCode(除非是有特别的实现方式需要去重写),只需要添加一个关键字 data 修饰符自动生成。

data class Bird(var weight: Double, var age: Int, var color: String)

上面的数据类反编译成java:

public final class Bird {
	private double weight;
	private int age;
	@NotNull
	private String color;

	public final double getWeight() { return this.weight; }
	public final void setWeight(double var1) { this.weight = var1; }
	public final int getAge() { return this.age; }
	public final void setAge(int var1) { this.age = var1; }
	@NotNull
	public final String getColor() { return this.color; }
	public final void setColor(@NotNull String var1) {
		Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
		this.color = var1;
	}
	
	public Bird(double weight, int age, @NotNull String color) {
		Intrinsics.checkParameterIsNotNull(color, "color");
		super();
		this.weight = weight;
		this.age = age;
		this.color = color;
	}
	
	public final double component1() { return this.weight; }
	public final int component2() { return this.age; }
	@NotNull
	public final String component3() { return this.color; }
	
	@NotNull
	public final Bird copy(double weight, int age, @NotNull String color) {
		Intrinsics.checkParameterIsNotNull(color, "color");
		return new Bird(weight, age, color);
	}

	public static Bird copy$default(Bird var0, double var1, int var3, String var4, int var5, Object var6) {
		if ((var5 & 1) != 0) {
			var1 = var0.weight; 
		} 
		if ((var5 & 2) != 0) {
			var3 = var0.age;
		}
		if ((var5 & 4) != 0) {
			var4 = var0.color;	
		}
		return var0.copy(var1, var3, var4);
	}
	
	public String toString() { ... }
	public int hashCode() { ... }
	public boolean equals(Object var1) { ... }
}

数据类使用也有限制条件:

  • 必须拥有一个构造方法,该方法至少包含一个参数,一个没有数据的数据类是没有任何用处的

  • 与普通类不同,数据类构造方法的参数强制使用 varval 声明

  • data class 之前不能用 abstractopensealedinner 修饰

  • 如果有一个类尝试继承 data class 声明的类会报错,因为数据类是 final

  • kotlin 1.1版本前数据类只允许实现接口,之后的版本可以实现接口和继承类

3.2.1 数据类和不可变性:copy()

为了让使用不可变对象的数据类变得更容易,kotlin编译器为它们多生成了一个方法:copy()。类的copy能创建一个副本,让原始对象不受影响(其实就是类似与java中,该类实现了Cloneable接口,重写了clone(),让类可以生成副本

还是以数据类Bird说明:

public final class Bird {
	...
	
	@NotNull
	public final Bird copy(double weight, int age, @NotNull String color) {
		Intrinsics.checkParameterIsNotNull(color, "color");
		return new Bird(weight, age, color);
	}

	// var0代表被copy的对象
	public static Bird copy$default(Bird var0, double var1, int var3, String var4, int var5, Object var6) {
		if ((var5 & 1) != 0) {
			// copy时若未指定具体属性的值,则使用被copy对象的属性值
			var1 = var0.weight;  
		} 
		if ((var5 & 2) != 0) {
			var3 = var0.age;
		}
		if ((var5 & 4) != 0) {
			var4 = var0.color;	
		}
		return var0.copy(var1, var3, var4);
	}
}	

val b1 = Bird(20.0, 1, "blue")
val b2 = b1
b2.age = 2
println(b1.age) // 输出为2,因为b2的引用指向b1

val b1 = Bird(20.0, 1, "blue")
val b2 = b1.copy()
b2.age = 2

println(b1.age) // 输出为1,因为b2是copy()创建的新对象

需要注意的是,copy() 是浅拷贝的方式。所以在使用 copy() 的时候要注意使用场景,因为数据类的属性可以被修饰为 var,这便不能保证不会出现引用修改问题。

3.2.2 by 委托

3.2.2.1 类委托

委托是一项技巧,由三个角色构成:约束、委托对象和被委托对象。有2个对象参与同一个请求的处理,接受请求的对象将请求委托给另一个对象来处理。

在这里插入图片描述

  • 约束:约束是接口或抽象类,它定义了通用的业务类型,也就是需要被代理的业务

  • 被委托对象:具体的业务逻辑执行者

  • 委托对象:将约束类定义的业务委托给被委托对象

// 约束
interface IGamePlayer {
	fun rank()

	fun upgrade()
}

// 被委托对象,具体业务逻辑执行者,实现了约束接口
class RealGamePlayer(private val name: String) : IGamePlayer {
	override fun rank() {
		println("$name rank")
	}

	override fun upgrade() {
		println("$name upgrade")
	}
}

// 委托对象,通过by关键字将具体的业务逻辑处理委托给player,实现了约束接口
class DelegateGamePlayer(private val player: IGamePlayer) : IGamePlayer by player

// 通过by关键字类委托相当于:
class DelegateGamePlayer(private val player: IGamePlayer) : IGamePlayer {
	override fun rank() {
		player.rank()
	}
	override fun upgrade() {
		player.upgrade()
	}
}

// by关键字委托了player对象但还是自己重写了:
class DelegateGamePlayer(private val player: IGamePlayer) : IGamePlayer {
	override fun rank() {
		println("override rank")
	}
	
	override fun upgrade() {
		println("override upgrade")
	}
}

val realGamePlayer = RealGamePlayer("real game player")
val delegateGamePlayer = DelegateGamePlayer(realGamePlayer)
delegateGamePlayer.rank()
delegateGamePlayer.upgrade()

输出:
real game player rank
real game player upgrade

// by委托但还是重写了,说明不使用委托的对象,会输出重写的函数结果
override rank
override upgrade

在kotlin中,委托用关键字 by 修饰,by 后面就是你委托的对象,可以是一个表达式,也可以是一个具体委托对象。

委托对象和被委托对象都实现了约束。

3.2.2.2 属性委托

在kotlin中,有一些常见的属性类型,虽然我们可以在每次需要的时候手动实现它们,但是很麻烦,各种样板代码存在。为了解决这些问题,kotlin标准为我们提供了属性委托。

class Test {
	var prop: String by Delegate()
}

属性委托主要委托的是这个属性的 getter/setter 方法。属性的 getter/setter 会委托给被委托对象的 getValue()/setValue() 方法,因此被委托对象需要提供 getValue()/setValue() 方法。如果属性是 val 只需要提供 getValue(),如果是 vargetValue()/setValue() 都需要提供。

class Delegate {
	operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
		return "$thisRef, thank you for delegating '${property.name}' to me!"
	}

	operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
		println("$value has beean assigned to '${property.name}' in $thisRef.")
	}
}

println(Test().prop)
Test.prop = "Hello, delegate prop"

输出:
Test@5197848c, thank you for delegating 'prop' to me!
Hello, delegate prop has been assigned to 'prop' in Test@17f052a3
  • thisRef:必须于属性所有者类型(对于扩展属性——指被扩展的类型)相同或者是它的超类型。比如这里类型是 Any?,它是类型 String 的超类型

  • property:必须是类型 KProperty<*> 或其超类型

  • value:必须与属性同类型或者是它的子类型

但是如果每次属性委托都要自己去写一个类实现 getValue()/setValue() 实在太麻烦了,万一填错参数或填错参数类型怎么办?kotlin标准库已经为我们提供了两个接口来实现,原理是相同的:

// 属性为val
interface ReadOnlyProperty<in R, out T> {
	operator fun getValue(thisRef: R, property: KProperty<*>): T
}
// 属性为var
interface ReadWriteProperty<in R, T> {
	operator fun getValue(thisRef: R, property: KProperty<*>): T
	operator fun setValue(thisRef: R, property: KProperty<*>, value T)
}

class Delegate1: ReadOnlyProperty<Any, String> {
	override fun getValue(thisRef: Any, property: KProperty<*>): String {
		return "xxxx"
	}
}

class Delegate2: ReadWriteProperty<Any, String> {
	override fun getValue(thisRef: Any, property: KProperty<*>): Int {
		return "xxxx"
	}

	override fun setValue(thisRef: Any, property: KProperty<*>, value: String) {
		println("$value")
	}
}

class Test {
	val prop1: String by Delegate1()
	var prop2: String by Delegate2()
}
3.2.2.3 延迟属性(lazy properties)

延迟属性:其值只在首次访问时计算,只能用在声明为 val 的属性上。

// 第一次调用会执行lazy的lambda表达式并记录结果
// 后续调用只会返回记录结果
val lazyProp: String by lazy {
	println("lazy calculate property")
	"default lazy prop"
}

println(lazyProp)
println(lazyProp)
println(lazyProp)

输出:
lazy calculate property
default lazy prop
default lazy prop
default lazy prop

lazy 可以接收三种参数:

  • LazyThreadSafetyMode.SYNCHRONIZED:添加同步锁,使lazy延迟初始化线程安全,默认值

  • LazyThreadSafetyMode.PUBLICATION:初始化的lambda表达式可以在同一时间被多次调用,但是只有第一个返回的值作为初始化的值

  • LazyThreadSafetyMode.NONE:没有同步锁,多线程访问时,初始化的值是未知的,非线程安全。一般情况下不推荐使用这种方式,除非能保证初始化和属性始终在同一个线程

3.2.2.4 可观察属性(observable、vetoable)

可观察属性(observable properties):监听器会收到有关此属性变更的通知。

如果你要观察一个属性的变化过程,那么可以将属性委托给 Delegate.observable

public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
            ReadWriteProperty<Any?, T> =
        object : ObservableProperty<T>(initialValue) {
            override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
        }
  • initialValue:初始值

  • onChange:属性值被修改时的回调处理器,回调有三个参数 property 被赋值的属性、oldValue 旧值、newValue 新值

var obserableProp: String by Delegates.observable("defaultValue") { property, oldValue, newValue ->
	println("property: $property: $oldValue -> $newValue")
}

observableProp = "first change value"
observableProp = "second change value"

输出:
property:var observableProp: kotlin.String: defaultValue -> first change value
property:var observableProp: kotlin.String: first change value -> second change value

vetoableobservable 一样可以观察属性值变化,不同的是,vetoable 可以通过处理器函数决定属性值是否生效。

var vetoableProo: Int by Delegates.vetoable(0) { _, oldValue, newValue ->
	newValue > oldValue
}

println(vatoableProp=$vetoableProp")
vetoableProp = 10
println(vatoableProp=$vetoableProp")
vetoableProp = 5 
println(vatoableProp=$vetoableProp")
vetoableProp = 100
println(vatoableProp=$vetoableProp")

输出:
vetoableProp=0  
// 0 -> 10,10 > 0成立
vetoableProp=10
// 10 -> 5,5 > 10不成立
vetoableProp=10
// 10 -> 100, 100 > 10成立
vetoableProp=100
  • 把多个属性存储在一个map中,而不是每个存在单独的字段中

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

object 关键字遵循同样的核心理念:这个关键字定义一个类并同时创建一个实例。

4.1 对象声明(即kotlin的单例模式)

在java中,单例对象使用的一般是单例模式,在kotlin中可以通过对象声明的方式实现。

// object关键字进行对象声明,需要注意的是,该对象用object关键字对象声明后相当于一个单例,不能通过构造方法创建它
// 使用object关键字声明的类,内部所声明的方法和属性都相当于java中的static(是看起来,不是就是static,要注意)
object Payroll {
	val allEmplyees = arrayListOf<Person>()

	fun calculateSalary() {
		for (person in allEmplyees) {
			...
		}
	}
}

// 使用对象声明的属性和方法调用
Payroll.addEmplyees.add(Person(...))
Payroll.calculateSalary()
// 在java中访问
Payroll.INSTANCE.addEmpolyees.add(Person(...))
Payroll.INSTANCE.calculateSalary()

data class Person(val name: String) {
	object NameComparator : Comparator<Person> {
		override fun compare(p1: Person, p2: Person): Int = p1.name.compareTo(p2.name)
	}
}

>>>val persons = listOf(Person("Bob"), Person("Alice"))
>>>println(persons.sortedWith(Person.NameComparator))
[Person(name=Alice), Person(name=Bob)]

4.2 伴生对象(即kotlin的工厂模式)

4.2.1 伴生对象实现工厂模式

kotlin中的类不能拥有静态成员(即kotlin中没有java的 static 关键字静态字段),作为替代,kotlin依赖包级别函数(大多数情况下替代java的静态方法)和对象声明(大多数情况下替代java的静态方法和静态字段)。在大多数情况下,还是推荐使用顶层函数。但是顶层函数不能访问类的 private 成员。

因此如果你需要写一个可以在没有类实例的情况下调用但是需要访问类内部的函数,可以将其写成那个类中的对象声明的成员。

// 在java中,类A是这样的
public class A {
	public static void bar() {
		System.out.println("static method called")
	}
}

// 初步转换成kotlin的时候是这样的
class A {
	// 根据上面对object的说明,object中内部的所有方法都可以相当于静态的方式进行访问
	object Inner {
		fun bar() {
			println("kotlin object called")
		}
	}
}
// 此时访问方式:
>>A.Inner.bar()

// companion关键字声明,获得了直接通过容器类名称来访问这个对象的方法和属性的能力,调用就类似于java在访问静态方法
class A {
	// 这里加了companion,其实就是为了省略掉一个内部类名
	companion object {
		fun bar() {
			println("Companion object called")
		}
	}
}

>>>A.bar() // 直接通过类名访问

companion 关键字就是伴生对象的实现方式(工厂模式)。

class User {
	val nickname: String

	constructor(email: String) {
		nickname = email.substringBefore('@')
	}

	constructor(facebookAccountId: Int) {
		nickname = getFacebookName(facebookAccountId)
	}
}

// 上面的方式使用伴生对象方式实现工厂模式
// 主构造方法标记为private
class User private constructor(val nickname: String) {
	// 声明伴生对象
	// 伴生对象的名字可以省略,也可以声明出来处理一些特殊用途companion object NewUser
	// val subscribingUser = User.NewUser.newSubscribingUser()
	// val facebookUser = User.NewUser.newFacebookUser()
	// 省略伴生对象名字,默认的名字会分配为Companion
	// 在java代码中使用伴生对象:User.Companion.newSubscribingUser()
	companion object {
		fun newSubscribingUser(email: String) = User(email.substringBefore('@'))	
		fun newFacebookUser(accountId: Int) = User(getFacebookName(accountId))
	} 
}

>>>val subscribingUser = User.newSubscribingUser("bob@gmail.com")
>>>val facebookUser = User.newFacebookUser(4)
>>>println(subscribingUser.nickname)

4.2.2 伴生对象实现单例

伴生对象除了能实现工厂模式,还能实现单例模式:

class Single private constructor() {
	companion object {
		fun getInstance(): Single {
			return Holder.instance
		}
	}

	private object Holder {
		val instance = Single()
	}
}

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

4.3.1 在伴生对象中实现接口

interface JSONFactory<T> {
	fun fromJSON(jsonText: String): T
}

class Person(val name: String) {
	// 伴生对象实现接口
	companion object: JSONFactory<Person> {
		override fun fromJSON(jsonText: String): Person = ...
	}
}

fun loadFromJSON<T>(factory: JSONFactory<T>): T {...}

// 因为Person类中的伴生对象没有名称,所以可以直接使用Person名称传入,此时的Person名称相当于JSONFactory实例
loadFromJSON(Person)

4.3.2 伴生对象扩展

class Person(val firstName; String, val lastName: String) {
	// 声明一个空的伴生对象
	companion object {}
}

// 声明一个扩展函数
fun Person.Companion.fromJSON(json: String) : Person {...}

>>>val p = Person.fromJSON(json)

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

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

注意:匿名对象不是对象声明(不是单例模式),每次对象表达式被执行都会创建一个新的对象实例。

与java匿名内部类只能扩展一个类或实现一个接口不同,kotlin的匿名对象可以实现多个接口或者不实现接口。

window.addMouseListener(object : MouseAdapter() {
	override fun mouseClicked(e: MouseEvent) {...}
	override fun mouseEntered(e: MouseEvent) {...}
})

val listener = object : MouseAdapter() {
	override fun mouseClicked(e: MouseEvent) {...}
	override fun mouseEntered(e: MouseEvent) {...}
}

fun countClicks(window: Window) {
	var clickCount = 0

	window.addMouseListener(object : MouseAdapter() {
		override fun mouseClicked(e: MouseEvent) {
			clickCount++
		}
	})
}
```





  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值