Kotlin专题「十三」:数据类(Data Classes)

前言:过程会苦,但结果怡人。

一、概述

1.1 数据类

  我们经常创建主要用于保存接口返回的Json数据的类。在这样的类中,只包含了一些需要的数据,以及处理这些数据编写的方法。在 Kotiln 中,这被称为一个数据类,并使用 data 修饰。

	//使用 data 关键字修饰
    data class User(val name: String, val age: Int)

数据类构造函数中必须至少有一个参数,并且必须是使用 var 或者 val修饰。如果没有结构体时,大括号 {} 可以省略。

	//调用
    var user = User("Kotlin", 23)
    println("User:name == ${user.name} | age == ${user.age}")

打印数据如下:

User:name == Kotlin | age == 23

1.2 注意事项

(1)编译器会自动地从主构造函数中根据所声明的属性派生下列函数:

  • equals()/hashCode()
  • toString() 格式如:User(name="jimi", age=20)
  • 自动生成 component1()component2()componentN() 函数并对应属性的声明顺序;
  • copy() 函数(下面会讲解到)。

(2)数据类为了确保生成代码的一致性和有意义的行为,必须满足以下条件:

  • 主构造函数至少有一个参数;
  • 所有主构造函数的参数需要标记为 var 或者 val
  • 数据类不能声明为 abstractopensealedinner
  • Kotlin1.1之前,数据类只能实现接口,不能继承其他类。

(3)此外,在成员继承方面,成员生成遵循以下规则:

  • 如果数据类中有equals()hashCode()toString() 的明确定义,或者在超类中有 final 实现,那么就不会生成这些函数,而使用现有的实现;
  • 如果一个超类型是 open 且返回兼容类型的 componentN() 函数,则为数据类生成相应的函数并覆盖超类型的函数。如果超类型的函数由于不兼容的签名或 final 不能被覆盖,则会报错;
  • 从已经具有相匹配签名的 copy() 函数的类型派生数据类,在 Kotlin 1.2中是不赞成的,在 Kotlin 1.3中是禁止的;
  • 不允许为 componentN()copy() 函数提供显式实现。

从 Kotlin 1.1开始,数据类可以拓展其他类。(例如,参考密封类)

二、使用

2.1 无参构造函数

注意:因为数据类的构造函数是必须至少有一个参数的,如果需要有一个无参数的构造函数,则必须为所有属性指定默认值(构造函数),那么就可以使用无参构造函数了。

	//数据类
	data class User(val name: String = "som", val age: Int = 0)

	//如果主构造函数所有参数都有默认值,编译器会生成一个附加的无参的构造函数,这个构造函数会直接使用默认值
	var user = User()
	println("User:name == ${user.name} | age == ${user.age}")

可以看到,打印的参数是主构造函数中的默认值。打印数据如下:

User:name == som | age == 0

相比 Java 的数据类中大量的 get()set() 方法,Kotlin 的 data class 真的很简洁。

2.2 类主体中声明属性

注意,编译器仅为自动生成的函数使用主构造函数中定义属性。你也可以在类主体中声明属性:

    data class Person(val name: String = "Android") {
        var age: Int = 0
    }

只有属性 name 将在 toString()equals()hashCode()copy() 实现中调用,并且只有一个组件函数component1() 。虽然两个Person对象可以有不同的年龄,但他们会被平等对待。

 	//调用
    var person = Person()
    var person2 = Person()
    person.age = 10
    person2.age = 20
    println("Person:name == ${person.name} | age == ${person.age}")
    println("Person2:name == ${person2.name} | age == ${person2.age}")

打印数据如下:

Person:name == Android | age == 10
Person2:name == Android | age == 20

2.3 解构声明

Kotlin 中定义一个数据类,则会根据参数的个数默认生成 componentN() 函数,一个参数就生成生成 component1(),两个参数就生成生成 component1()component2(),依次类推。它们就是用于解构声明的,为数据类生成的组件函数允许它们在解构声明中使用:

    val jack = User(name = "Jack", age = 99)
    val (name, age) = jack //解构声明将jack 对象分解为name和age两个新变量,可以独立使用
    println("析构声明:name == $name | age == $age")

打印数据如下:

解构声明:name == Jack | age == 99

解构声明时把一个对象分解成许多变量,变量可以独立使用。

2.4 标准的数据类

标准库提供了类 Pair 和类 Triple ,不过在大多数情况下,命名数据类是更好的设计选择,因为它们通过为属性提供更多有意义的名称,是代码更具可读性。

两个标准类都实现了 toList()toString() 方法,类 Pair 只能传递两个参数,类 Triple 只能传递三个参数:

    val pair = Pair(1, 2)
    val triple = Triple("一", "二", "三")

	//执行
	println("标准库:Pair == $pair | Triple == $triple")//调用了内部的toString()方法
    println("Pair.toList  == ${pair.toList()}") //toList()转换成集合
    println("Triple.toList  == ${triple.toList()}")//toList()转换成集合

打印数据如下:

标准库:Pair == (1, 2) | Triple == (,,)
Pair.toList  == [1, 2]
Triple.toList  == [,,]

我们来看看标准库的内部实现:

@file:kotlin.jvm.JvmName("TuplesKt")
package kotlin
//表示两个值组成的泛型对
public data class Pair<out A, out B>(
        public val first: A,
        public val second: B ) : Serializable {
	//toString()方法返回Pair 的两个参数值
    public override fun toString(): String = "($first, $second)"
}

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
//转换为数组
public fun <T> Pair<T, T>.toList(): List<T> = listOf(first, second)

public data class Triple<out A, out B, out C>(
        public val first: A,
        public val second: B,
        public val third: C     ) : Serializable {
	//toString()方法返回Triple的参数值
    public override fun toString(): String = "($first, $second, $third)"
}
//转换为数组
public fun <T> Triple<T, T, T>.toList(): List<T> = listOf(first, second, third)

其实我们实际项目中极少用到标准库类,因为自定义的数据类会使语义更清晰,更接近复杂的业务,了解即可。

三、自动派生的函数

从上面可以知道,数据类中编译器会自动地从主构造函数中根据所声明的属性派生下列函数:

  • equals()/hashCode()
  • toString() 格式如:User(name="jimi", age=20)
  • 自动生成 component1()component2()componentN() 函数并对应属性的声明顺序;
  • copy() 函数(下面会讲解到)。

3.1 toString()函数

数据类已经重写 toString() 方法,默认打印成员变量的键值对。而其他普通类的toString() 方法默认打印对象地址:

	data class User(val name: String, val age: Int)//数据类
	class Student(val name: String, val age: Int)//普通类

	//执行
    var dataUser = User("数据类", 10)
    var student = Student("普通类", 20)

    println("toString(): dataUser == $dataUser | student == $student")

打印数据如下:

toString(): dataUser == User(name=数据类, age=10) | student == com.suming.kotlindemo.blog.DataClassActivity$Student@30acc19

可以看到数据类的 toString() 方法是以User(name=数据类, age=10)格式打印的,而普通类打印的仍是对象地址。

3.2 equals()/hashCode()函数

Kotlin 和 Java 中一样 重写了equals()方法那么也要重写 hashCode() 方法。

    var user1 = User("数据类", 10)
    var user2 = User("数据类", 10)

    var student1 = Student("普通类", 20)
    var student2 = Student("普通类", 20)

    println("数据类:equals == ${user1.equals(user2)} | user1 == ${user1.hashCode()} | user2 == ${user2.hashCode()}")
    println("普通类:equals == ${student1.equals(student2)} | student1 == ${student1.hashCode()} | student2 == ${student2.hashCode()}")

打印数据如下:

数据类:equals == true | user1 == 799061869 | user2 == 799061869
普通类:equals == false | student1 == 169784542 | student2 == 199415487

数据类复写了 equals()/hashCode() 方法,会对对象中的 nameage 比较,如果两者相等,则返回true;而普通类则会默认调用 Anyequals() ,该方法默认是比较两个对象的地址,不同对象地址不同,打印false。

3.3 copy()函数

通常情况下,我们需要复制一个对象,改变它的一些属性,但保持其余的不变。这就是 copy() 函数作用,目的是便于数据复制。可以类比于 Java 中的 clone() 方法,使用上面的 User 类举个例子:

	//定义复制函数,将User以及参数赋值给copy()函数
	fun copy(name: String, age: Int) = User(name, age)

可以通过User实例调用赋值函数,也可以更改部分属性值,调用如下:

    var oldUser = user.copy(age = 500)
    println("copy():name == ${oldUser.name} | age == ${oldUser.age}")

打印数据如下:

copy():name == som | age == 500

3.4componentN() 函数

componentN() 函数是编译器会自动地从主构造函数中根据所声明的属性派生的函数,并对应属性的声明顺序。N表示主构造函数中参数个数,比如 component1()表示第一个参数、component2()表示第二个参数,依次类推。注意,只能是主构造函数中的属性。

    var user = User("Kotlin", 23)
	println("componentN:component1 == ${user2.component1()} | component2 == ${user2.component2()}")

打印数据如下:

componentN:component1 == Kotlin | component2 == 23

componentN() 函数与对应构造函数属性的顺序,如上面的构造函数只有 nameage 两个属性,那么就只有 component1()component2() ,如果你要打印 component3() 那么编译器会报错,表示找不到这个方法。

至此,本文结束!


源码地址:https://github.com/FollowExcellence/KotlinDemo-master

请尊重原创者版权,转载请标明出处:https://blog.csdn.net/m0_37796683/article/details/108078923 谢谢!

你可能感兴趣的:(Kotlin)