Kotlin基础知识五——数据类和类委托

2020/7/28 23:03:49

本文主要是介绍Kotlin基础知识五——数据类和类委托,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一、通用对象方法

(1)toString()

class Client(val name: String, val postalCode: Int) {
    override fun toString() = "Client(name=$name, postalCode=$postalCode)"
}
复制代码

(2)equals()

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

//重写equals
class Client(val name: String, val postalCode: Int) {
    override fun equlas(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)"
}
复制代码
  • Java和Kotlin中关于“==”的区别

在Java中,可以使用“==”运算符来比较基本数据类型和引用数据类型。基本数据类型比较值,引用数据类型比较引用。

“==”是比较两个对象的默认方式:本质上说它就是通过调用equals来比较这两个值的。 因此,equals在类中被重写后,“==”能很安全地比较实例。

(3)hashCode()

hashCode方法通常和equals一起被重写。

  • 重写示例
class Client(val name: String, val postalCode: Int) {
    ......
    override fun hashCode(): Int = name.hashCode() * 31 + postalCode
}
复制代码

二、数据类,自动生成通用方法的实现

(1)数据类定义

data class Client(val name: String, val postalCode: Int)
复制代码

一个重写了所有标准Java方法的类:

  • equals用来比较实例
  • hashCode用来作为例如HashCode这种基于哈希容器的键
  • toString用来为类生成按声明顺序排列的所有字段的字符串表达形式

(2)数据类和不可变性:copy()方法

虽然数据类的属性并没有要求是val——同样可以使用var——但还是强烈推荐只使用只读属性,让数据类的实例不可变。

不可变对象:一旦一个对象呗创建出来,它会一直保持初始状态,也不用担心在你的代码工作时其他线程修改了对象的值。

为了让不可变对象的数据类变得更容易,Kotlin编译器为它们多生成了一个方法:一个允许copy类的实例方法,并在copy的同时修改某些属性的值。

创建副本通常是修改实例的好选择:副本有着单独的生命周期而且不会影响代码中引用原始实例的位置。

示例:

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

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

想其他类添加一些行为,即使它并没有被设计为可拓展的。 一个常用的实现方式以装饰器模式闻名。

  • 示例一
class DelegatingCollection<T>: Collection<T> {
    private val innerList = arrayListOf<T>()
    
    override val size: Int get() = innnerList.size
    override fun isEmpty(): Boolean = innerList.isEmpty()
    override fun contains(element: T): Boolean = innerList.contains(element)
    override fun iterator(): Iterator<T> = innerList.iterator()
    override fun containsAll(elements: Collection<T>): Boolean =
            innerList.containsAll(elements)
}
复制代码

Kotlin的使用:Kotlin将委托作为一个语言级别的功能做了头等支持。

无论什么时候是是实现一个接口,你都可以使用by关键字将接口的实现委托到另外一个对象。

  • 示例二
class DelegatingCOllection<T> (
        innerList: Collection<T> = ArrayList<T>()
): Collection<T> by innerList {}
复制代码

类中所有的方法实现都消失了。编译器会生成它们,并且实现与DelegatingCollection的例子是相似的。

若需要修改某些方法的行为时,可以重写它们,这样你的方法就会被调用而不是使用生成的方法。

  • 示例三
class CountingSet<T> (
        val innerSet: MutableCollection<T> = HashSet<T>()
): MutableCollection<T> by innerSet {
    var objectsAdded = 0
    
    override fun add(element: T): Boolean {
        objectsAdded ++
        return innerSet.add(element)
    }
    
    override fun addAll(c: Collection<T>): Boolean {
        objectsAdded += c.size
        return innerSet.addAll(c)
    }
}

// 运行代码:
>>> val cset = CountingSet<Int>()
>>> cset.addAll(listOf(1, 1, 2)
>>> println("${cset.objectsAdded} objects were added, ${cset.size} remain")
3 objects were added, 2 remain
复制代码

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

object关键字在多种情况下出现,但是它们都遵循同样的核心理念:这个关键字定义一个类并同时创建一个实例(即一个对象)。让我们来看看使用它的不同场景:

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

(1)对象声明:创建单例

Kotlin通常使用对象声明功能为这一切提供了最高级的语言支持。对象声明将类声明与这类的单一实例声明结合到一起。

  • 示例
object Payroll {
    val allEmployees = arrayListOf<Person>()
    
    fun calculateSalary() {
        for(Person in allEmployees) {
            ...
        }
    }
}
复制代码

对象声明通过object关键字引入。一个对象声明可以非常高效地以一句话来定义一个类和一个该类的变量

与类一样,一个对象声明也可以包含属性、方法、初始化语句块等的声明。唯一不允许的就是构造方法(包括主构造方法和从构造方法)。

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

Payroll.allEmptyees.add(Person(...))
Payroll.calculateSalary()
复制代码

对象声明同样可以继承自类和接口

object CaseInsensitiveFileComparator: Comparator<File> {
    override fun compare(file1: File, file2: File): Int {
        return file1.path.compareTo(file2.path, ignoreCase = true)
    }
}
复制代码

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

Kotlin中的类不能拥有静态成员;Java的static关键字并不是Kotlin语言的一部分。

作为替代,Kotlin依赖包级别函数(在大多数情况下能够替代Java的静态方法)和对象声明(在其他情况下替代Java的静态方法,同时还包括静态字段)。

顶层函数不能访问类的private成员。若需要在没有实例的情况下调用但是需要访问类内部的函数,可以将其写成那个类的对象声明的成员。这个函数的一个例子就是工厂方法

在类中定义的对象之一可以使用一个特殊的关键字来标记:companion

如果这样做,就获得了直接通过容器类名称来访问这个对象的方法和属性的能力,不再需要显式地指明对象的名称。

示例:

// 定义
class A {
    companion object {
        fun bar() {
            println("Companion object called")
        }
    }
}

// 测试
>>> A.bar()
Companion object called
复制代码

伴生对象,可以访问类中的所有private成员,包括private构造方法,它是实现工厂模式的理想选择。

  • 定义一个拥有多个从构造方法的类
class User {
    val nickname: String
    
    constructor(email: String) {
        nickname = email.substringBefore('@')
    }
    
    constructor(facebookAccountId: Int) {
        nickname = getFacebookName(facebookAccountId)
    }
}
复制代码

表明相同逻辑的另一种方法,就是使用工厂方法来创建类实例,而不是通过多个构造方法。

// 定义
class User private constructor(val nickname: String) {
    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)
bob
复制代码

工厂方法是非常有用的。它们可以根据它们的用途来名称,并且能够返回声明这个方法的类的子类。但是伴生对象成员子类中不能被重写

(3)作为普通对象使用的伴生对象

伴生对象是一个声明在类中的普通对象。它可以有名字,实现一个接口或者有扩展函数或属性。

  • 声明一个命名伴生对象
// 定义
class Person(val name: String) {
    companion object Loader {
        fun fromJSON(jsonText: String): Person = ...
    }
}

// 测试
// 可以通过两种方式来调用fromJSON
>>> person = Person.Loader.fromJSON("{name: 'Dmitry'}")
>>> person.name
Dmitry
>>> person2 = Person.fromJSON("{name: 'Brent'}")
Brent
复制代码

在大多数情况下,通过包含伴生对象的类的名字来引用伴生对象,所以不必关心它的名字。若省略伴生对象的名字,默认的名字将会分配为Companion。

  • 在伴生对象中实现接口

就像其他对象声明一样,伴生对象也可以实现接口。可以直接将包含它的类的名字当作实现了该接口的对象实例来使用。

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 {
    ...
}

// 将伴生对象实例传入函数中
loadFromJSON(Person)
复制代码
  • 为伴生对象定义一个扩展函数
class Person(val firstName: String, val lastName: String) {
    // 声明一个空的伴生对象
    companion object {
        
    }
}

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

// 调用
val p = Person.fromJSON(json)
复制代码

调用fromJSON就像他是一个伴生对象定义的方法一样,但实际上它是作为扩展函数在外部定义的。看起来像一个成员,但实际并不是。

为了能够为你的类定义扩展,必须在其中声明一个伴生对象,即使是一个空的

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

object关键字不仅仅能用来声明单例式的对象,还能用来声明匿名对象

匿名对象提点了Java中匿名内部类的用法。

  • 使用匿名对象来实现事件监听器
window.addMouseListener(
    // 声明一个继承MouseAdapter的匿名对象
    object: MouseAdapter() {
        // 重写MouseAdapter方法
        override fun mouseClicked(e: MouseEvent) {
            // ...
        }
        
        override fun mouseEntered(e: MouseEvent) {
            // ...
        }
    }
)
复制代码

除了去掉了对象的名字外,语法是与对象声明相同的。对象表达式声明了一个类并创建了该类的一个实例,但是并没有给这个类或是实例分配一个名字。

给对象分配一个名字,可以将其存储到一个变量中:

val listener = object: MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { ... }
    override fun mouseClicked(e: MouseEvent) { ... }
}
复制代码

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

与对象声明不同,匿名对象不是单例。每次对象表达式被执行都会创建一个新的对象实例。

与Java的匿名类一样,在对象表达式中的代码可以访问创建它的函数中的变量。但是与Java不同,访问并没有被限制在final变量,还可以在对象表达式中修改变量的值。

fun countClicks(window: Window) {
    // 声明局部变量
    var clickCount = 0
    
    window.addMouseListener(object: MouseAdaper(){
        override fun mouseClicked(e: MouseEvent) {
            //更新变量的值
            clickCount ++
        }
    })
}
复制代码


这篇关于Kotlin基础知识五——数据类和类委托的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程