Kotlin能够提供比 "普通 "Java更多的东西,这总是让我感到惊奇,数据类也不例外。在这篇文章中,我们将探讨Kotlin的数据类是如何从老式的POJO中取出所有的模板的,内置的equals 、hashcode 、copy 方法的力量,并学习使用生成的componentN 帮助器轻松地进行重构。最后,我们将检查一下继承与数据类混合时的一个小问题。
开始吧!
什么是Kotlin?
作为一个简单的复习,Kotlin是一种现代的、静态类型的语言,可以在JVM上编译使用。它经常被用于任何你想用Java的地方,包括Android应用程序和后端服务器(使用JavaSpring或Kotlin自己的Ktor)。
Kotlin的数据类与Java的老习惯相比如何?
如果你以前在Java中设置了一个POJO,你可能已经处理了一些模板式的代码:getters和setters,一个用于调试的漂亮的toString ,一些用于equals ,如果你想有可比性的话,hashCode ,重写,重复,对吗?
嗯,Kotlin不喜欢所有这些仪式。他们创建了一个特殊类型的class 来处理。
- 一个基于你的构造器参数生成的
equals函数(而不是不太有用的内存引用) - 基于这些构造函数参数的一个漂亮的、人类可读的
toString()值 - 一个
copy函数,可以随意克隆实例,而不需要自己在构造函数之间铺设管道。 - 通过使用圆括号实现结构化的能力
()
与过去的POJO标准相比,这些都是一些相当大的胜利。Kotlin不仅可以为你处理所有的getters和setters(因为构造函数参数默认是公开的),而且它还免费为你提供了可比较性
让我们来学习一下,我们如何能把这样一个巨大的类声明。
class UniversityStudentBreakfast {
private int numEggs;
public UniversityStudentBreakfast(int numEggs) {
this.numEggs = numEggs;
}
public int getNumEggs() {
return numEggs;
}
public void setNumEggs(int numEggs) {
this.numEggs = numEggs;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UniversityStudentBreakfast breakfast = (UniversityStudentBreakfast) o;
return numEggs == breakfast.numEggs;
}
@Override
public String toString() {
return "UniversityStudentBreakfast(" +
"numEggs='" + numEggs + '\'' +
')';
}
// don't get me started on copy-ability...
}
...并把它变成一个漂亮的单行代码
data class UniversityStudentBreakfast(
val numEggs: Int,
)
使用内置的equals 特质
让我们从比标准类更大的增值开始:基于我们的构造器参数的内置平等函数。
简而言之,Kotlin会生成一个整洁的equals 函数(外加一个免费的hashCode 函数),该函数会评估你的构造函数参数以比较你的类的实例。
data class UniversityStudentBreakfast(
val numEggs: Int,
)
val student1Diet = UniversityStudentBreakfast(numEggs=2)
val student2Diet = UniversityStudentBreakfast(numEggs=2)
student1Diet == student2Diet // true
student1Diet.hashCode() == student2Diet.hashCode() // also true
注意:在内部,当比较时,这将调用所有构造函数参数的
equals 函数。遗憾的是,这意味着当你的数据类包含列表或对其他类的引用时,内存引用问题又会出现。
使用toString 方法
是的,数据类给了你一个很好的toString 帮助,使调试更简单。我们没有为我们上面的UniversityStudentBreakfast 类得到一个随机的内存引用,而是得到了一个很好的构造器键到值的映射。
println(student1Diet)
// -> UniversityStudentBreakfast(numEggs=2)
使用copy 特质
Kotlin的copy 特质解决了传统类的一个常见缺陷:我们想利用一个现有的类,并建立一个新的类,只是略有不同。传统上,有两种方法可以解决这个问题。第一种是手工将所有的东西从一个构造函数输送到另一个构造函数。
val couponApplied = ShoppingCart(coupon="coupon", eggs=original.eggs, bread=original.bread, jam=original.jam...)
......但这是非常令人厌恶的,尤其是当我们有嵌套引用需要担心重复的时候。选项二是简单地承认失败,并使用以下方法打开所有的变异 [apply {...}](https://kotlinlang.org/docs/scope-functions.html#apply):
val couponApplied = original.apply { coupon = "coupon" }
...但如果你的团队正在使用函数式编程技术,你可能不喜欢这种方法。如果我们能有一个类似于apply 的语法就好了,它不会改变原始值...
好消息是什么?如果你使用的是一个数据类,copy ,你就可以做到这一点!
data class ShoppingCart(
val coupon: String, // just a regular "val" will work
val eggs: Int,
val bread: Int,
...
)
val original = checkoutLane.ringUpCustomer()
val couponApplied = original.copy(coupon="coupon")
你还会注意到,copy 只是一个普通的函数调用,没有lambda的选项。这就是Kotlin编译器的魅力--它根据构造函数的参数 ,为你生成所有的参数。
揭开Kotlin中componentN 的神秘面纱
对于数据类,每个属性都可以作为一个组件使用扩展函数来访问,如component1、component2等,其中的数字对应于参数在构造函数中的位置。你也许可以用一个例子来说明这个问题。
data class MyFridge(
val doesPastaLookSketchy: Boolean,
val numEggsLeft: Int,
val chiliOfTheWeek: String,
)
val fridge = MyFridge(
doesPastaLookSketchy=true,
numEggsLeft=0,
chiliOfTheWeek="Black bean"
)
fridge.component1() // true
fridge.component2() // 0
fridge.component3() // "Black bean"
你可能在想,"好吧,但我为什么要通过调用component57() 来获取一个值呢?"这个问题很合理!你可能不会像这样直接调用这些帮助器。然而,这些对于Kotlin的内部结构是非常有用的,可以实现解构。
用Kotlin数据类进行解构
假设我们有一对地图上的坐标。我们可以使用Pair 类将这种类型表示为整数的Pair 。
val coordinates = Pair<Int, Int>(255, 255)
那么我们如何从这里抓出x和y的值呢?好吧,我们可以使用我们之前看到的那些组件函数。
val x = coordinates.component1()
val y = coordinates.component2()
或者,我们可以在我们的变量声明中使用parens () ,来解除结构。
val (x, y) = coordinates
很好!现在我们可以让Kotlin为我们调用那些丑陋的组件函数。
我们可以对我们自己的数据类使用同样的原则。例如,如果我们想让我们的坐标有第三个z维度,我们可以做一个漂亮的Coordinates 类,像这样。
data class Coordinates(
val x: Int,
val y: Int,
val z: Int,
)
然后按照我们认为合适的方式进行解构 。
val (x, y, z) = Coordinates(255, 255, 255)
****注意:当参数顺序没有被暗示时,这可能会变得很棘手。是的,很明显,在我们的
Coordinates 的例子中,x 在y 之前(它在z 之前)。但是,如果工程师心不在焉地将值z 移到构造函数的顶部,他们可能会破坏整个代码库的结构化语句
继承的一个重要问题
当你开始习惯于使用数据类时,你可能会开始使用它们作为各种场合的类型安全对象。
但是,不要这么快!当你开始面向对象时,问题就开始出现了。为了扩展我们前面的Fridge 例子,假设你想要一个有额外字段的特殊数据类来代表你自己的厨房混乱。
data class Fridge(
val doesPastaLookSketchy: Boolean,
val numEggsLeft: Int,
)
data class YourFridge(
val servingsOfChickenNoodleLeft: Int,
) : Fridge()
换句话说,你想从第一个data class ,并保持平等和复制特性的完整。但是如果你在操场上尝试这样做,你会得到一个讨厌的异常。
No value passed for parameter 'doesPastaLookSketchy'
No value passed for parameter 'numEggsLeft'
嗯,看来我们需要重复我们的Fridge 构造函数,以允许我们所有的值通过。让我们这么做吧。
data class Fridge(
open val doesPastaLookSketchy: Boolean,
open val numEggsLeft: Int,
)
data class YourFridge(
override val doesPastaLookSketchy: Boolean,
override val numEggsLeft: Int,
val servingsOfChickenNoodleLeft: Int,
) : Fridge(doesPastaLookSketchy, numEggsLeft)
...这就给我们留下了一个非常不同的异常
Function 'component1' generated for the data class conflicts with member of supertype 'Fridge'
Function 'component2' generated for the data class conflicts with member of supertype 'Fridge'
This type is final, so it cannot be inherited from
现在看来,在这些构造函数参数上使用override 是有问题的。这归结于Kotlin编译器的一个限制:为了让componentN() 帮助器指向正确的值,数据类需要保持 "最终"。
所以,一旦你设置了这些参数,它们就不能被推翻(甚至不能被扩展)。
幸运的是,只要父类不是数据类,你就可以取消我们的继承。一个抽象类可能会给我们带来好处。
abstract class Fridge(
open val doesPastaLookSketchy: Boolean,
open val numEggsLeft: Int,
)
data class YourFridge(
override val doesPastaLookSketchy: Boolean,
override val numEggsLeft: Int,
val servingsOfChickenNoodleLeft: Int,
) : Fridge(doesPastaLookSketchy, numEggsLeft)
是的,我们仍然需要使用override 来复制我们想要的参数,但它确实为我们的数据类之间的共享参数提供了一些类型安全,同时保持了平等、复制和散列特性的工作秩序。
总结
正如你所知道的,数据类提供了一些很好的好处,而开发者的开销几乎为零。这就是为什么我建议在你使用常规class 的任何地方使用data ,以获得额外的可比较性好处。所以,去重写一些POJO吧!
The postUsing Kotlin data classes to eliminate Java POJO boilerplatesappeared first onLogRocket Blog.