- Java到Kotlin:代码重构指南
- (英)邓肯·麦格雷戈 (英)纳特·普莱斯
- 1544字
- 2025-01-03 16:16:37
3.2 数据类的局限性
数据类的一个缺点是不提供封装。我们看到了编译器如何为数据类生成equals、hashCode和toString方法,但没有提到它还生成了一个copy方法,该方法为当前的值对象创建一个新的副本,并为新副本中的一个或多个属性设置不同于原来的值。
例如,以下代码创建了一个电子邮件地址的副本,其localPart为postmaster,并且具有和原对象相同的域:
![](https://epubservercos.yuewen.com/F01371/31542990504176506/epubprivate/OEBPS/Images/52_01.jpg?sign=1739598648-yAG9XNWWoLCQ8INSmqnpvsLzxphg1rBu-0-095fe2711ab1fd5bb0acda6e2b825978)
对于很多类型来说,这非常方便。但是,当一个类对其内部表示进行抽象或在其属性之间维护不变性时,copy方法允许调用端代码直接访问值的内部状态,这可能会破坏其不变性。
让我们看一下Travelator应用中的一个抽象数据类型——Money类:
![](https://epubservercos.yuewen.com/F01371/31542990504176506/epubprivate/OEBPS/Images/52_02.jpg?sign=1739598648-DtMyEioQ9iwnaPFpqSy0D1oex8Su0DSc-0-1946e977976568b1d0b3eeddb5b3cf03)
![](https://epubservercos.yuewen.com/F01371/31542990504176506/epubprivate/OEBPS/Images/53_01.jpg?sign=1739598648-CY0rWSA2XcXI4nqsKNBgMo4fu0B7Ch1x-0-f0dfc912b1abb6f079374ca538f0d181)
❶ 构造函数是私有的。其他类通过调用静态的Money.of方法获取Money值,该方法确保金额的精度与货币的最小单位一致。大多数货币都可以换算为100个最小单位(两位小数),但有些货币的最小单位数少一些,有些多一些。例如,日元没有最小单位,约旦第纳尔由1000菲尔组成。
of方法遵循现代Java的编码约定,它从源头上区分了具有身份标识的对象(由new运算符构造)和值(从静态方法获得,不可变)。Java的时间API(例如,LocalDate.of(2020,8,17))和集合API中最新添加的方法(例如,List. of(1,2,3)创建一个不可变列表)遵循此约定。
该类为String或int金额提供了一些方便的of方法重载。
❷ Money值基于JavaBean的约定方式公开了其金额和货币属性,尽管它实际上并不是JavaBean。
❸ equals和hashCode方法实现了值语义。
❹ toString方法返回其属性的表示,可以展示给用户,而不仅仅用于调试。
❺ Money提供了货币价值计算的操作。例如,可以将货币值相加。add方法通过直接调用构造函数(而不是使用Money.of)来构造新的Money值,因为BigDecimal. add的结果已经有了正确的精度,所以我们可以避免在Money.of中设置精度的开销。
BigDecimal.setScale方法令人困惑。尽管其方法名类似于JavaBean的属性设置器,但实际上它并不改变BigDecimal对象。与EmailAddress和Money类一样,BigDecimal是一个不可变的值类型,因此setScale会返回具有指定精度的新BigDecimal值。
Sun在Java 1.1的标准库中加入了BigDecimal类。此版本还包括第一版的JavaBeans API。围绕Beans API的大肆宣传使JavaBeans编码约定被广泛采用,即便对于像BigDecimal这种并非JavaBean的类也是如此(参见第1章)。当时并没有针对值类型的Java约定。
如今,我们避免使用set前缀为不改变调用者状态的方法命名,而是当方法返回一个对调用者的转换时,使用方法名来强调其意图。一个常见的约定是对影响单个属性的转换使用with前缀,这将使Money类中的代码变为:
![](https://epubservercos.yuewen.com/F01371/31542990504176506/epubprivate/OEBPS/Images/54_02.jpg?sign=1739598648-KivnlBFqIjQiW3zP6SECXkFwwPmefht1-0-8e5ff44c232f601a785f1c5af7b1a109)
在Kotlin中,可以编写扩展函数来修复此类历史问题。如果我们正在编写大量运用BigDecimal进行计算的代码,那么这样做可以提高代码的清晰度,可能是值得的:
![](https://epubservercos.yuewen.com/F01371/31542990504176506/epubprivate/OEBPS/Images/54_03.jpg?sign=1739598648-IdpTsZhrn9HtaYVrvjNkkXukpIZeP0qS-0-b132607c69ca9f7d8001310e1d6967b2)
将Money类转换为Kotlin会生成以下代码:
![](https://epubservercos.yuewen.com/F01371/31542990504176506/epubprivate/OEBPS/Images/54_04.jpg?sign=1739598648-H1v4jHO3gvc61040rx8S2xefKAxEj98x-0-27ea42b411e8c81359df0d600eedb114)
![](https://epubservercos.yuewen.com/F01371/31542990504176506/epubprivate/OEBPS/Images/55_01.jpg?sign=1739598648-rZ3mJAxj3IvKLJQJaLKPG9IDzD8dxaPQ-0-cc59ee059939fc05fc3a179da24d1150)
Kotlin类中仍然有一个主构造函数,但该构造函数现在被标记为私有的。其语法有点笨拙:我们重新格式化了翻译器生成的代码,使其更易于扫描。与EmailAddress.parse一样,静态的of工厂函数现在是带有@JvmStatic注解的伴生对象上的方法。总的来说,代码并没有比原来的Java简洁多少。
我们可以通过将它变成数据类来进一步减少代码量吗?
当我们将其Money类改为数据类时,IntelliJ会高亮显示主构造函数的private关键字并发出警告:
![](https://epubservercos.yuewen.com/F01371/31542990504176506/epubprivate/OEBPS/Images/55_02.jpg?sign=1739598648-zOp18EyxWISxCyRn5gi1pB4Htv4L9P5Q-0-2da6a7160968d6cb7036a480a8170bb9)
这是怎么回事?
Money类的实现中隐藏着一个细节,其属性之间保持着一种不变性,确保金额字段的精度等于货币字段中最小单位货币的默认位数。通过私有构造函数防止Money类之外的代码创建违反不变性的值。Money.of(BigDecimal,Currency)方法确保其不变性对于新的Money值是正确的。add方法可以保持该不变性,因为将两个具有相同精度的BigDecimal值相加会产生一个也具有相同精度的BigDecimal,所以它可以直接调用构造函数。这样,构造函数只需要为字段赋值,在知道它永远不会调用违反其不变性的参数时是安全的。
但是,数据类的copy方法始终是公开的,这样会允许调用端代码创建违反不变性的Money值。与EmailAddress不同,像Money这样的抽象数据类型不能实现为Kotlin的数据类。
如果值类型必须在其属性之间保持不变性,则不要将其定义为数据类。
我们可以使用将在后面章节中遇到的Kotlin功能使类更加简洁和方便。因此,我们暂时放下Money类,第12章将再次讨论它,对它进行全面的改进。