MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Kotlin数据类使用指南

2021-06-256.5k 阅读

Kotlin 数据类基础

在 Kotlin 中,数据类是一种特殊的类,它主要用于存储数据。Kotlin 数据类的设计目的是简化常规数据承载类的编写。这些类通常只包含一些属性来保存数据,以及相应的 getters、setters(对于可变属性)、equals()、hashCode() 和 toString() 方法。

数据类的定义

定义一个数据类非常简单,只需要在类声明前加上 data 关键字。例如,我们定义一个表示用户的简单数据类:

data class User(val name: String, val age: Int)

在这个例子中,User 类有两个属性:name(不可变,因为使用了 val)和 age(同样不可变)。Kotlin 编译器会自动为这个数据类生成以下方法:

  1. 主构造函数中所有属性的 getters:对于 val 类型的属性,只有 getters;对于 var 类型的属性,会同时生成 getters 和 setters。例如,对于 User 类的 name 属性,可以通过 user.name 来获取值。
  2. equals() 方法:基于所有属性进行值比较。例如:
val user1 = User("Alice", 30)
val user2 = User("Alice", 30)
println(user1 == user2) // 输出 true
  1. hashCode() 方法:与 equals() 方法一致,基于所有属性生成哈希码。
  2. toString() 方法:以一种可读的格式返回对象的字符串表示。例如,User("Alice", 30)toString() 结果可能是 User(name=Alice, age=30)
  3. copy() 方法:用于创建对象的副本,并且可以选择性地修改部分属性值。稍后我们会详细介绍。

数据类的限制

  1. 主构造函数至少要有一个参数:数据类的主构造函数必须至少包含一个参数,否则会编译错误。例如,下面的定义是不允许的:
// 编译错误:Data class must have at least one primary constructor parameter
data class EmptyDataClass
  1. 必须是 openfinal(默认)或 sealed:数据类不能是 abstract,它默认是 final,如果希望被继承,可以声明为 open 或者 sealed。例如:
open data class OpenUser(val name: String, val age: Int)
  1. 数据类不能继承其他类(但可以实现接口):数据类不能有超类,但是可以实现一个或多个接口。例如:
interface UserInterface {
    fun printUserInfo()
}

data class ImplementingUser(val name: String, val age: Int) : UserInterface {
    override fun printUserInfo() {
        println("Name: $name, Age: $age")
    }
}

数据类的属性

声明属性

数据类的属性声明与普通类类似,但由于数据类的特殊性,一些规则有所不同。如前面提到的,属性可以使用 val 声明为只读,或者使用 var 声明为可变。

data class Product(var name: String, val price: Double)

在这个 Product 数据类中,name 是可变的,price 是只读的。这意味着我们可以在创建对象后修改 name 的值,但不能修改 price 的值。

val product = Product("Widget", 10.0)
product.name = "New Widget"
// product.price = 15.0 // 编译错误,price 是只读的

自定义访问器

虽然 Kotlin 编译器会为数据类属性自动生成默认的访问器,但在某些情况下,我们可能需要自定义访问器。例如,我们可能希望在获取属性值时进行一些计算,或者在设置属性值时进行一些验证。

data class Temperature(var celsius: Double) {
    val fahrenheit: Double
        get() = celsius * 1.8 + 32
    set(value) {
        celsius = (value - 32) / 1.8
    }
}

在这个 Temperature 数据类中,我们定义了一个只读属性 fahrenheit,它的值是根据 celsius 计算得到的。同时,我们为 fahrenheitsetter 进行了自定义,使得在设置 fahrenheit 值时,会相应地更新 celsius 的值。

数据类的构造函数

主构造函数

数据类的主构造函数用于初始化属性。所有在主构造函数中声明的属性,都会参与到编译器自动生成的方法(如 equals()hashCode() 等)中。

data class Point(val x: Int, val y: Int)

这里,Point 类的主构造函数有两个参数 xy,它们被声明为属性。这些属性会被用于 equals() 方法的比较、hashCode() 的生成等。

辅助构造函数

数据类也可以有辅助构造函数,但辅助构造函数必须直接或间接调用主构造函数。例如,我们可以为 Point 类添加一个辅助构造函数,使得可以通过极坐标来创建点:

data class Point(val x: Int, val y: Int) {
    constructor(radius: Double, angle: Double) : this((radius * Math.cos(angle)).toInt(), (radius * Math.sin(angle)).toInt())
}

在这个例子中,辅助构造函数 constructor(radius: Double, angle: Double) 接收极坐标的半径 radius 和角度 angle,并通过计算转换为直角坐标,然后调用主构造函数来创建 Point 对象。

数据类的 copy() 方法

copy() 方法的作用

copy() 方法是数据类的一个非常有用的特性,它允许我们创建一个对象的副本,并可以选择性地修改部分属性的值。这在函数式编程中非常常见,因为它可以避免直接修改对象的状态,而是创建一个新的对象。

data class Book(val title: String, val author: String, val year: Int)

val originalBook = Book("1984", "George Orwell", 1949)
val newBook = originalBook.copy(year = 2023)
println(newBook) // 输出 Book(title=1984, author=George Orwell, year=2023)

在这个例子中,我们通过 originalBook.copy(year = 2023) 创建了一个新的 Book 对象 newBook,它与 originalBook 除了 year 属性不同外,其他属性都相同。

实现自定义 copy() 方法

虽然 Kotlin 编译器会为我们自动生成 copy() 方法,但在某些复杂情况下,我们可能需要自定义 copy() 方法。例如,当数据类包含一些特殊的属性,在复制时需要特殊处理。

data class ComplexNumber(val real: Double, val imaginary: Double) {
    val magnitude: Double
        get() = Math.sqrt(real * real + imaginary * imaginary)

    fun copy(real: Double = this.real, imaginary: Double = this.imaginary): ComplexNumber {
        return ComplexNumber(real, imaginary)
    }
}

在这个 ComplexNumber 数据类中,我们自定义了 copy() 方法,它接收 realimaginary 参数,默认值为当前对象的属性值。这样,在复制对象时,可以根据需要修改实部和虚部的值。

数据类与解构声明

解构声明基础

解构声明是 Kotlin 提供的一种方便的语法,它允许我们将对象的属性分解到多个变量中。数据类天然支持解构声明,因为编译器会为数据类生成相应的组件函数。

data class Pair<T, U>(val first: T, val second: U)

val pair = Pair(1, "Hello")
val (a, b) = pair
println("a = $a, b = $b") // 输出 a = 1, b = Hello

在这个例子中,我们将 Pair 对象 pairfirst 属性解构到变量 a 中,second 属性解构到变量 b 中。

自定义解构声明

对于数据类,编译器会根据主构造函数的参数自动生成组件函数(component1()component2() 等)。但如果我们需要自定义解构行为,可以手动定义这些组件函数。

data class Rectangle(val width: Int, val height: Int) {
    operator fun component3(): Int {
        return width * height
    }
}

val rectangle = Rectangle(5, 10)
val (w, h, area) = rectangle
println("Width: $w, Height: $h, Area: $area") // 输出 Width: 5, Height: 10, Area: 50

在这个 Rectangle 数据类中,我们手动定义了 component3() 函数,使得在解构 Rectangle 对象时,可以同时获取宽度、高度和面积。

数据类的继承与多态

继承数据类

如前面提到的,数据类可以声明为 open,以便被其他类继承。继承数据类的子类可以继承父类的属性和自动生成的方法。

open data class Shape(val color: String)

data class Circle(val radius: Double, color: String) : Shape(color)

在这个例子中,Circle 类继承自 open 数据类 ShapeCircle 类除了有自己的 radius 属性外,还继承了 Shape 类的 color 属性。同时,Circle 类也继承了 Shape 类由编译器自动生成的方法,如 equals()hashCode() 等。

多态性

由于数据类可以实现接口,因此可以利用多态性。例如,我们定义一个形状绘制接口,并让数据类实现它:

interface ShapeDrawer {
    fun draw()
}

open data class Shape(val color: String) : ShapeDrawer

data class Circle(val radius: Double, color: String) : Shape(color) {
    override fun draw() {
        println("Drawing a circle with color $color and radius $radius")
    }
}

data class Rectangle(val width: Int, val height: Int, color: String) : Shape(color) {
    override fun draw() {
        println("Drawing a rectangle with color $color, width $width and height $height")
    }
}

fun drawShapes(shapes: List<ShapeDrawer>) {
    shapes.forEach { it.draw() }
}

val shapes = listOf(Circle(5.0, "Red"), Rectangle(10, 20, "Blue"))
drawShapes(shapes)

在这个例子中,CircleRectangle 类都继承自 Shape 类并实现了 ShapeDrawer 接口。drawShapes() 函数接收一个 ShapeDrawer 列表,并调用每个形状的 draw() 方法,实现了多态行为。

数据类在集合中的使用

作为集合元素

数据类非常适合作为集合的元素。由于数据类有自动生成的 equals()hashCode() 方法,在集合中进行查找、比较等操作会更加方便。

data class Fruit(val name: String, val price: Double)

val fruitList = listOf(Fruit("Apple", 1.5), Fruit("Banana", 0.5))
val apple = Fruit("Apple", 1.5)
println(fruitList.contains(apple)) // 输出 true

在这个例子中,我们创建了一个包含 Fruit 数据类对象的列表 fruitList。由于 Fruit 类有自动生成的 equals() 方法,所以可以方便地使用 contains() 方法来检查列表中是否包含特定的水果。

集合操作与数据类

我们可以对包含数据类对象的集合进行各种操作。例如,使用 map() 函数来转换集合中的元素:

data class Employee(val name: String, val salary: Double)

val employees = listOf(Employee("Alice", 5000.0), Employee("Bob", 6000.0))
val newEmployees = employees.map { it.copy(salary = it.salary * 1.1) }
newEmployees.forEach { println("Name: ${it.name}, New Salary: ${it.salary}") }

在这个例子中,我们使用 map() 函数对 employees 列表中的每个 Employee 对象进行操作,通过 copy() 方法创建新的 Employee 对象,并将工资提高 10%。

数据类与 JSON 序列化/反序列化

概述

在现代应用开发中,数据类经常用于与 JSON 数据进行交互。Kotlin 有许多库可以实现 JSON 序列化和反序列化,其中 Gson 和 Moshi 是比较常用的。数据类的特性使得它们非常适合与这些库配合使用。

使用 Gson 进行 JSON 序列化/反序列化

首先,添加 Gson 依赖到项目中。在 Gradle 中,可以这样添加:

implementation 'com.google.code.gson:gson:2.8.9'

然后,定义一个数据类并使用 Gson 进行序列化和反序列化:

import com.google.gson.Gson

data class Person(val name: String, val age: Int)

fun main() {
    val person = Person("John", 30)
    val gson = Gson()
    val json = gson.toJson(person)
    println("Serialized JSON: $json")

    val deserializedPerson = gson.fromJson(json, Person::class.java)
    println("Deserialized Person: ${deserializedPerson.name}, ${deserializedPerson.age}")
}

在这个例子中,我们使用 Gson 将 Person 数据类对象序列化为 JSON 字符串,然后又将 JSON 字符串反序列化为 Person 对象。

使用 Moshi 进行 JSON 序列化/反序列化

同样,先添加 Moshi 依赖到项目中。在 Gradle 中:

implementation 'com.squareup.moshi:moshi:1.14.0'

然后进行序列化和反序列化操作:

import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory

data class Car(val make: String, val model: String, val year: Int)

fun main() {
    val moshi = Moshi.Builder()
      .add(KotlinJsonAdapterFactory())
      .build()
    val car = Car("Toyota", "Corolla", 2023)
    val adapter: JsonAdapter<Car> = moshi.adapter(Car::class.java)
    val json = adapter.toJson(car)
    println("Serialized JSON: $json")

    val deserializedCar = adapter.fromJson(json)
    deserializedCar?.let {
        println("Deserialized Car: ${it.make}, ${it.model}, ${it.year}")
    }
}

在这个例子中,我们使用 Moshi 将 Car 数据类对象进行序列化和反序列化。Moshi 同样利用了 Kotlin 数据类的特性,使得 JSON 操作更加便捷。

数据类的性能考虑

自动生成方法的性能

虽然 Kotlin 编译器为数据类自动生成的方法(如 equals()hashCode() 等)非常方便,但在性能敏感的场景下,需要考虑其性能。例如,equals() 方法会比较所有属性,这可能在属性较多时导致性能下降。如果性能是关键因素,可以考虑手动实现这些方法,只比较关键属性。

data class LargeDataClass(
    val property1: String,
    val property2: String,
    // 更多属性...
    val propertyN: String
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false
        other as LargeDataClass
        // 只比较关键属性
        return property1 == other.property1
    }

    override fun hashCode(): Int {
        return property1.hashCode()
    }
}

在这个 LargeDataClass 中,我们手动实现了 equals()hashCode() 方法,只比较 property1 属性,以提高性能。

内存占用

数据类由于包含多个属性和自动生成的方法,可能会占用较多的内存。在内存受限的环境中,需要谨慎使用。例如,如果数据类中的某些属性可以通过计算得到,而不是存储在对象中,可以考虑将其定义为计算属性,以减少内存占用。

data class Rectangle(val width: Int, val height: Int) {
    val area: Int
        get() = width * height
}

在这个 Rectangle 数据类中,area 属性是一个计算属性,它不会额外占用内存来存储值,而是在需要时进行计算。

数据类的最佳实践

保持数据类简单

数据类应该专注于存储数据,尽量避免在数据类中添加复杂的业务逻辑。如果需要添加业务逻辑,可以考虑将其封装到一个单独的服务类或扩展函数中。

data class Order(val items: List<String>, val total: Double)

fun calculateDiscount(order: Order): Double {
    // 计算折扣的业务逻辑
    return if (order.total > 100) order.total * 0.1 else 0.0
}

在这个例子中,Order 数据类只负责存储订单的商品列表和总价,而计算折扣的业务逻辑放在了 calculateDiscount() 函数中。

使用数据类进行数据传输

数据类非常适合作为不同层之间的数据传输对象(DTO)。例如,在 Web 应用中,可以使用数据类来接收和发送 JSON 数据。这样可以保持代码的清晰和一致性。

data class UserDTO(val username: String, val email: String)

// 假设这是一个处理用户注册的函数
fun registerUser(userDTO: UserDTO) {
    // 处理用户注册逻辑
}

在这个例子中,UserDTO 数据类用于在不同层之间传输用户注册信息。

结合函数式编程风格

利用数据类的 copy() 方法和 Kotlin 的函数式编程特性,可以实现不可变数据的处理,使代码更易于理解和维护。

data class ShoppingCart(val items: List<String>, val total: Double)

fun addItemToCart(cart: ShoppingCart, item: String, price: Double): ShoppingCart {
    val newItems = cart.items + item
    val newTotal = cart.total + price
    return cart.copy(items = newItems, total = newTotal)
}

在这个例子中,addItemToCart() 函数通过 copy() 方法创建一个新的 ShoppingCart 对象,而不是直接修改原对象,遵循了函数式编程的原则。