内容
- 通用方法toString(),equals(),hashCode()
- 值对象类Data
- 类委托,简单的单例模式
- Object关键字介绍
一 通用方法
为了方便演示,我们首先定义一个Client类如下:
class Client(val name: String, val postalCode: Int) {
}复制代码
1.1通用方法toString
在java中如果没有重写toString,我们打印出来的直接是对象在内存中的地址值。同样的我们在kotlin中打印的也是地址的值,所以我们类似的可以在类里边重写toString方法:
class Client(val name: String, val postalCode: Int) {
//重写toString
override fun toString(): String {
return "name${name}postalCode${postalCode}"
}
}复制代码
1.2 通用方法equals
java中基本类型可以用==去比较值是否相等,而用在引用类型上比较的是内存地址。如果我们想要比较引用类型内的值是否相同,我们要重写equals,然后调用equals去比较值。
而在kotlin中==编译的时候会编译成调用equals,所以自定义一个类使用==是比较值还是内存地址,完全取决于你是否重写了equals方法。
就如我们上边定义的类,在没有重写equals方法的时候,比较的值对象返回的是false,如果想要比较值,我们必须重写equals方法:
class Client(val name: String, val postalCode: Int) {
//重写toString
override fun toString(): String {
return "name${name}postalCode${postalCode}"
}
//重写后==比较的是内容是否相同,不重写,比较的是内存地址
override fun equals(other: Any?): Boolean {
if (other == null || other !is Client) {
return false
}
return (name==other.name)&&(postalCode==other.postalCode)
}
}复制代码
这时候调用就是返回的true,比较的是值,而不是内存对象。当然如果你创建两次Client,他们的内存地址还是不一样的,只是值一样。
这里我们可以快速的理解基础一文章中说的为什么通过不同的方式去创建集合,用==去判断相等时,有时候相等,有时候不等,原因就在于创建方式是否重写了equals方法。
1.3 通用方法hashCode
如果两个对象相等,它们必须有着相同的hash。
先去找一个问题,还是上边类去比较:
val client1 = Client("张三", 45000)
val client2 = Client("张三", 45000)
val hashSetOf = hashSetOf<Client>(client1)
val contains = hashSetOf.contains(client2)
Log.e("rrrrrr",""+contains)复制代码
返回的是false,原因就在于Contains方法里边会先比较他们的hashcode的值,如果相等再去比较,很明显,你没有重写,所以值不想等,返回false。解决方案就是重写hashCode方法。
class Client(val name: String, val postalCode: Int) {
//重写toString
override fun toString(): String {
return "name${name}postalCode${postalCode}"
}
//重写后==比较的是内容是否相同,不重写,比较的是内存地址
override fun equals(other: Any?): Boolean {
if (other == null || other !is Client) {
return false
}
return (name == other.name) && (postalCode == other.postalCode)
}
//重写hashCode方法,让集合是否包含也返回true
override fun hashCode(): Int = name.hashCode() + postalCode
}复制代码
到此三个常用的方法已经讲完毕。
我们知道在java的IDE中我们可以通过模版去生成这三个方法,虽然不用手写,但是放到类里边还是看着不舒服。在Kotlin中你不必再去生成这些方法了。如果为你的类添加data修饰符,必要的方法将会隐式自动生成好。
二 值对象类data
2.1值对象的写法和隐藏的toString ,equals和hashCode范围
data class Client(val name: String, val postalCode: Int) 复制代码
简单一个修饰符,就隐式的重写了所有标准Java方法。
注意:生成的toString ,equals和hashCode仅仅只包括主构造方法中的属性,没有在主构造中声明的不会涵盖进去。
2.2数据类和不可变性:copy()方法
虽然数据类的属性并没有要求是val一一同样可以使用var一一但还是强烈推荐只使用只读性,让数据类的实例不可变。
原因:
1.异步安全
2.hashMap容器的优化,更加不容易出错
不可变的声明又会导致出现一个新的问题,如果需求要求改变值,那么就送你一首凉凉?事实上Kotlin为了解决这个问题又给你提供一个新的方法copy方法。目的就是想要改变值的时候调用,使用方法:
val client1 = Client("张三", 45000)
//这样写是错误的
//client1.name = "李四"
//需要这样去修改值,内部其实是创建一个新的client对象
client1.copy(name = "李四")复制代码
原理实现大概如下:
data class Client(val name: String, val postalCode: Int) {
//伪代码
fun copy2(name :String = this.name,postalCode:Int = this.postalCode)= Client(name,postalCode)
}复制代码
至此data类算是陈述完毕,可能有人会想到,实际开发中,数据那么多,难道我一个一个写?有没有像java那样自动生成的插件:我个人用的是JsonToKotlinClass自动生成的data类。
三类委托
java中的装饰者模式或者叫代理模式:模式的本质就是创建一个新类,实现与原始类一样的接口并将原来的类的实例作为一个字段保存。与原始类拥有同样行为的方法不用被修改,只需要调用原始方法,并添加你需要添加的逻辑。
按照这个方式我们用java的思想kotlin的代码去实现一下:
//定义登录接口
interface ILogin {
fun login(name: String, pwd: String)
}
复制代码
class Login : ILogin {
override fun login(name: String, pwd: String) {
Log.e("rrrrr", "拿着用户名和密码去请求网络")
}
}复制代码
结果发现没有做非空判断,我们可以使用代理模式,类似这样的方式去扩展:
class ExtendLogin(val obj: Login) : ILogin {
override fun login(name: String, pwd: String) {
if (name.isEmpty() || pwd.isEmpty()) {
Log.e("rrrr", "告诉用户没有输入帐号或密码")
} else {
obj.login(name, pwd)
}
}
}复制代码
上边是纯java思想的代码,那么Kotlin中的委托模式给我们提供了什么更简洁的方式呢?
这里要介绍一个关键字by,用于类的委托。我们可以把扩展代码写成这样:
class ExtendLogin(val obj: Login) : ILogin by Login(){
}复制代码
这就完全让另外一个类去实现这个方法,但是扩展的内容却没有地方去写了,那么我们继续重写接口定义的方法
class ExtendLogin(val obj: Login) : ILogin by Login(){
override fun login(name: String, pwd: String) {
if (name.isEmpty() || pwd.isEmpty()) {
Log.e("rrrr", "告诉用户没有输入帐号或密码")
} else {
obj.login(name, pwd)
}
}
}复制代码
这样就又可以从新写,但是却发现问题:
- by不by这个子类对象好像没有什么卵用。
- 且传递过来的对象和by的对象不是一个对象。
解释问题1:在这个例子中确实没有什么卵用。如果一个接口定义了N个方法,且你只需要去修改一个方法里边的逻辑,你使用by,会大大减少重复的代码。有人可能会说直接继承重写单独的一个方法buiu行了?这么费劲干什么,其实不然,继承和包装是完全不同的原理。
解释问题2:这里也是是涉及到单例模式,和上一节的私有构造,然后单例几乎是同一个问题。上一篇我们用私有构造方法+companion object关键字+自定义属性,非常辛苦的完成一个单例模式,这里给大家介绍一个更加简单的单例模式:
把Login类的class关键字替换成object关键字,这个时候Login类就变成了单例模式!这个时候单例的对象就是类的名字。这里我可把所有Login()换成Login。
四 Object关键字
通过上边的单例模式我们已经简单了解了关于object关键字的威力,他还有很多其他作用,但是它们都遵循同样的核心理念:这个关键字定义一个类并同时创建一个实例(换句话说就是一个对象)。使用场景有:
- 定义单例的一种方式
- 对象表达式用来替代Java的匿名内部类。
- 伴生对象可以持有工厂方法和其他与这个类相关,但在调用时并不依赖类实例的方法。它们的成员可以通过类名来访问。
4.1单例模式已经说过,这里不在陈述。
4.2伴生对象
4.2.1介绍
还记得我们上一篇复杂的实现单例模式的代码吗?那就是伴生对象。伴生对象写在类的内部,可以访问类中的所有private成员,包括private构造方法。伴生对象成员在子类中不能被重写。
4.2.2伴生对象实现接口和扩展方法和属性
它可以有名字,实现一个接口或者有扩展函数或属性。
有名字的伴生对象的写法
class Person {
companion object Loader {
}
}复制代码
实现接口
在实际开发的时候,我们会遇到很多String转换成对象的情况(当然我们在使用Retrofit+Rxjava之后请求之后的这种转化已经不需要),假如我们定义一个接口去统一这种功能:
interface JSONFactory<T> {
fun formJson(Json: String): T
}复制代码
对应伴生对象去实现如下:
class Person {
companion object Loader :JSONFactory<Person>{
override fun formJson(Json: String): Person {
return Gson().fromJson(Json,Person::class.java)
}
}
}复制代码
调用传入String返回person对象
val formJson = Person.Loader.formJson("")复制代码
扩展方法和属性:
扩展的方法和第二篇讲解的类的扩展方法和属性一样,上边代码可以修改成这样:
class Person {
companion object {
}
}
fun Person.Companion.formJson(Json: String) :Person{
return Gson().fromJson(Json,Person::class.java)
}复制代码
调用方式一样,Person类的结构更加明朗。
4.3匿名内部类
一个按钮的点击事件,可以有两种方法去实现
tv.setOnClickListener { }
tv.setOnClickListener(object :View.OnClickListener{
override fun onClick(v: View?) {
}
})复制代码
小结
data数据类的隐式实现的四个方法的基本逻辑,==和java的equals方法一样
代理模式的写法
object去实现单例
伴生对象的写法和扩展
匿名类的写法