写点什么

与 Brian Goetz 聊 Java 的数据类

  • 2018-02-26
  • 本文字数:5123 字

    阅读完需:约 17 分钟

看新闻很累?看技术新闻更累?试试下载 InfoQ 手机客户端,每天上下班路上听新闻,有趣还有料!

作为 Oracle 的 Java 语言架构师,Brian Goetz 一直致力于 Java 编程语言在生产力和性能上的日臻完美。最近, Goetz 撰文绍了数据类(data classes)这一可能整合到 Java 语言中的实验性理念。他的研究工作很好地证明了,数据类完全可以与一些即将推出的 Java 特性自然结合,例如值类型(value types)模式匹配(pattern matching)等。但是要使数据类概念为成为 Java 语言的组成部分,还有大量的工作要做。Goetz 基于时常提及的“数据就是数据”这一前提,探讨了数据类上存在的问题及一些权衡考虑。

动机

在编写一个 Java 类时,无论它是多么简单或多么复杂,我们通常都需要在其中加入大量的“八股代码”(boilerplate code)。这使得 Java 落下了一个“过于繁琐”的名声。对此,Goetz 在文中解释道:

即便是对于一个十分简单的数据载体(data carrier)类,如果我们要负责任地编写它的代码,也必须在类中加入大量低价值的、重复性的代码,其中包括构造函数、访问器、equals()hashCode()toString()等。有时候开发人员会试图走点捷径,例如忽略一些重要的方法,但这将会导致一些出人意料的行为,或是降低了代码的可调试性。也可能会因为开发人员并不想再定义一个类,就在服务中硬塞入了一个并不是完全合适的替代类,仅是考虑到该替代类具有“正确的形状”。

IDE 会帮助开发人员填充大部分的代码,但编写代码只是问题的一小部分。要帮助代码阅读者从几十行样板代码中提炼出“我是 x、y 和 z 的一个普通数据载体”这样的设计意图,IDE 爱莫能助。重复代码是错误隐匿的好地方。如果有可能的话,最好应该彻底消除这些错误隐匿点。

类似于 Scala(case),Kotlin(data)和 C#(record)中定义的类声明在设计上就是紧凑的,这可能同样适用于 Java,Java 类会成为开销最小的普通数据载体。尽管“普通数据载体”(plain data carrier)一词并没有一个正式的定义,但大多数 Java 开发人员应该对此了然于胸。Java 社区的确对语言中提供数据类机制持欢迎态度,但是具体到每个人,不同人对普通数据载体的理解可能会是千差万别的。在文中,Goetz 使用了“盲人摸象”的典故,给出了如下解释:

Algebraic Annie 会说,“数据类只是一种代数产品类型”。和 Scala 的case类一样,它与模式匹配一并出现,并且最好以不可变形式提供。Annie 喜欢选择密封口的甜点(译者注:一些 Java 经典编程书籍中常以“Dessert”类作为例子代码,诸如《Effective Java》)。

Boilerplate Billy 会说,“数据类只是一种具有更好语法的普通类”。他会因为其在可变性、扩展性或封装性上的限制而大发雷霆。Billy 的兄弟 JavaBean Jerry 会说,“数据类一定是用于 JavaBeans 类的,所以我当然可以使用get()set()之类的方法”。注意,他的妹妹 POJO Patty 正沉溺于企业 POJO 中。她提醒我们,她希望数据类可以通过 Hibernate 等框架代理(Proxyable)。

Tuple Tommy 会说,“数据类只是一种标称元组(nominal tuple)”。他甚至可能不希望数据类具有超出核心Object方法以外的方法。他希望数据类只是一种最简单的聚合,他甚至可能期望数据类不具有名称,这样,两个具有相同“形状”的数据类可以自由地相互转换。

Values Victor 说,“数据类实际上只是一种更为透明的值类型。”

所有这些人因对“数据类”的共同喜好而联手,但是每个人对数据类具有自己的看法。可能并不存在一个能让所有人都满意的解决方案。

理解问题

在 Goetz 看来,数据类的概念并非仅限于减少样板代码,而是“表征了更深层次的问题”,将封装的成本均摊在所有的 Java 类中。作为面向对象的基本原则,抽象和封装使得 Java 开发人员可以跨越下列各种边界编写健壮和安全的代码:

  • 维护边界;
  • 安全和可信的边界;
  • 完整性边界;
  • 版本化边界。

考虑到一些类中固有的复杂性,例如 SocketInputStream ,因此上述边界是必不可少的。但是,如果有一个如下声明的Point类,它定义了两个整数组件的一个普通数据载体:

复制代码
record Point(int x,int y) { ... }

类似于Point的类也需要关注上述边界吗?Goetz 对此解释道:

在各个类间,虽然边界(例如,构造函数的参数是如何映射到状态的?如何从状态中导出相等合约?)的建立和维护成本是固定的,但其所带来的好处并非一成不变的,成本有时会偏离收益。这就是 Java 开发人员所说的“过多仪式”的问题所在。问题并非在于这些“仪式”是否有存在的价值,而是在于即便这些仪式并没有提供显著的好处,开发人员也必须要调用它们。

在 Java 给出的封装模型中,类的表示与构造、状态访问和相等方法是完全分离的。很多类并需要给出构造、状态访问和相等判断等方法。类与边界间的关系越是简单,将越可能从简单的模型中受益。在一个简单模型中,我们可以将类定义为类中状态的简单包裹,并从类中获取状态、构造、相等和状态访问间的关系。

此外应指出,表示与 API 解耦的成本,超出了声明样板成员的开销。封装在本质上就是破坏信息。

数据类的要求

下面我们使用上面声明的Point类,考虑将Point类的“去语法糖”(desugared)声明作为普通数据载体。

复制代码
final class Point extends java.lang.DataClass {
public final int x;
public final int y;
public Point(int x,int y) {
this.x = x;
this.y = y;
}
// Point(int x,int y) 的解构模式。
// 基于状态实现的 equals()、hashCode() 和 toString()。
// 公开的读取访问器 x() 和 y()。
}

在对普通数据载体设计的进一步研究后,Goetz 定义了一组要求(或约束),“用于安全并机械地生成构造函数、模式提取器(Extractor)、访问器(Accessor)、equals()hashCode()、和toString()等的样板” 。他写道:

我们称一个类C是状态向量S的透明载体(transparent carrier),如果C满足:

  • 存在将状态向量实例映射为C实例的函数ctor : S -> C,(构造函数可能会拒绝一些无效的状态向量,例如分母为零的有理数)。
  • 存在将C实例在ctor域中映射为状态向量S的全函数(total function)dtor : C -> S
  • 对于C的任意一个实例sctor(dtor(c))根据Cequals()合约等于c
  • 对于两个状态向量s1s2,如果一个向量的各个组件与另一个向量的相应组件相等(根据组件的equals()合约),那么或者cstor(s1)cstor(s2)都是未定义的,或者两者根据Cequals()合约相等。
  • 对于等价的实例cd,调用同一操作将生成相等的结果,即c.m()d.m()相等。并且在操作后,cd应该依然是等价的。

这些不变条件试图达成这样的需求,即载体是透明的,并且在类的表示、类的构造和解构之间存在着一种简单并可预测的关系。API 就是类的表示。

数据类和模式匹配

Goetz 指出,普通数据载体的优势在于,“可将数据类实例在聚合形式和迸发状态之间来回自由转换”。这非常适用于和模式匹配一起工作。正如模式匹配一文所展示的,Goetz 在文中介绍了利用 switch结构的解构及其可改进之处。鉴于此,我们可以编写如下代码。

复制代码
interface Shape { ... }
record Point (int x,int y) { ... }
record Rect(Point p1,Point p2) implements Shape { ... }
record Circle(Point center,int radius) implements Shape { ... }
...
switch(shape) {
case Rect(Point(var x1,var y1),Point(var x2,var y2)) : ...
case Circle(Point(var x,var y),int radius): ...
}

Shape的任一具体实例,都可很容易地在switch语句中解构。这对于序列化、JSON 和 XML 的编排和解排(marshal/unmarshal)以及数据库映射等外部化(externalization)同样十分有用。

改善设计空间

Goetz 指出,在普通数据载体的要求中存在一些折衷。他解释说:

最简单的(也是最严格的)数据类模型就是将数据类作为一个final类,其中每个状态组件具有public final字段、public构造函数、签名匹配状态描述的解构模式,以及对核心Object方法的基于状态的实现,甚至不允许具有其它的成员(或隐式成员的显式实现)。这实质上是对标称元组的一种最严格的解释。

这一出发点是简单且稳定的,几乎每个人都会从中发现一些值得反对之处。那么,我们是否可以在放宽这些约束条件的同时,继续秉持那些我们在语义上想要得到的优点?下面给出一些严格出发点的可能扩展方向,以及各个方向间的相互作用。

这些方向涵盖了大范围的设计元素及相关问题:

  • 接口及一些额外方法。
    • 存在违反“只有状态”规则的风险。
  • 重写隐式成员。
    • 存在违反简单数据载体要求的风险。
  • 额外的构造器。
    • 确保对象状态和状态描述是等价的。
  • 额外的字段。
    • 存在违反“状态、整体状态以及只有状态”规则的风险。
  • 扩展。
    • 与数据类和正常类间扩展相关的问题。
  • 可变性。
    • 对允许数据类可变的合理性存在质疑。
  • 字段的封装和访问器。
    • 确保封装的字段必须是可读取的。
  • 数组和保护性拷贝(defensive copy)。
    • 保护性拷贝违反了解构的不变性要求,应重建数组以确保一个同等的实例。
  • 线程安全。
    • 对数据类的可变性是如何实现线程安全的质疑。

总结

Java 在 2017 年发展势头强劲,今年有望推出多个备受关注的新特性。然而正如 Goetz 向 InfoQ 介绍的,数据类仍然被认为是一种“半成品”的理念。还需要更多的工作,才能完全理解应如何将这一理念转变为现实。

文末,Gozte 总结道:

对于在 Java 中设计一个用于“简单数据聚合”的特性,其中的关键问题在于确定我们愿意在何种程度上放弃自由度。如果试图对类的所有自由度建模,那么我们只是将复杂性转移了。为了获得一些收益,我们必须要接受一些限制。我们认为一个可接受的明智约束是,不允许使用封装将表示从 API 中解耦出来,也不允许使用封装去调解对状态的读取访问。反过来说,如果一个类可接受这些约束,将在句法和语义上提供显著的好处。

Oracle 技术团队的主要成员 Vicente Romero 最近发布了数据类开发的“首次公开推送”(initial public push)。它给出在 Amber 项目的代码库的基准分支上。

Goetz 向 InfoQ 介绍了他在数据类上的研究工作:

InfoQ:您在发表该文后,社区都有哪些反馈?

Brian Goetz:反馈在预期中,即对此理念有一些非常积极的评论,还有各种关于如何“改进”的建议,其中大多数相互并不一致。也就是说,人们喜欢这个理念,但正如预期那样,许多人希望我们能将设计中心向一个方向或是另一个方向做一些偏斜,以适应他们的个人偏好。数据类作为一个非常主观的特性,这正是我们所预期的。

InfoQ:您是否想到有朝一日数据类机制会整合到 Java 编程语言中?如果是这样,要解决您在文中谈及所有问题,还需要做哪些努力?

Goetz:这将需要一些“烘焙时间”。在一个语言的设计中,无论你的第一个想法考虑得多么仔细,都难免会出错。第二个想法同样如此。许多语言功能需要数次乃至更多次的迭代,才能最终找到一个正确的着陆点。所以我们需要去做试验、设计原型、收集反馈、迭代并再迭代,直到我们认为自己已经抵达了正确之处。

InfoQ:在数据类上,推广非 Java 语言实现紧凑类(例如,Scala 的 case 类)的 Rebase(版本衍合)是否会成为一个目标?

Goetz:每种语言都有自己的表层语法。但是,数据类关联了其它一些语言的特性(例如,模式匹配),并且我们希望(与 Lambda 一样)其它一些语言会针对这些特性提供运行时支持,并从中获得可互操作的好处。

InfoQ:据您所知,在实现更紧凑的类声明上,Scala,Kotlin 和 C#的架构师是否面临着类似的挑战?

Goetz:的确如此。但是 Kotlin 和 Scala 在项目一开始就比 C#走得更近,因此需要处理的限制也更少。每种语言在设计空间上会略有差异。

InfoQ:对于数据类,您希望我们的读者能了解的最重要信息是什么?

Goetz:数据类关注的是数据,而非语法上的简洁性。数据类为在对象模型中建模纯数据提供了一种自然的方式。并非所有的类都是纯数据载体,即便这些类想要使用数据类提供的简洁性优点。

InfoQ:您的数据类研究近期将会有何进展?

Goetz:我正在将数据类所需的特性分解为一些更细粒度的特性,以适用于所有的类。例如,即便对于一个明显并非仅是数据载体的类,其中的构造函数也会充斥着一些易于出错的重复代码。我们可以通过在构造函数的参数和表示间建立更高层次上的对应关系,来取代构造函数。这将使数据类更简单,成为一种只是用于其它语言特性的语法糖。这样,无需将该特性硬塞入到数据类中,更多的类就可以从中受益。

相关资源

查看英文原文: Brian Goetz Speaks to InfoQ on Data Classes for Java

2018-02-26 18:002353
用户头像

发布了 391 篇内容, 共 134.3 次阅读, 收获喜欢 256 次。

关注

评论

发布
暂无评论
发现更多内容

Kotlin学习手记——注解,flutter下拉加载

android 程序员 移动开发

Linux编程之权限系统与工具使用(二),【大牛系列教学】

android 程序员 移动开发

LeakCanary源码分析以及ContentProvider的优化方案,android移动开发

android 程序员 移动开发

MaterialDesign系列文章(十一)Google2018年大会新出的控件汇总集合

android 程序员 移动开发

MVVM系列之三:ViewModel,最新Android开发进阶

android 程序员 移动开发

offer求比较+部分大厂Android面经+真题解析(1),覆盖所有面试知识点

android 程序员 移动开发

offer求比较+部分大厂Android面经+真题解析,给2021的Android一些建议

android 程序员 移动开发

超牛无损图片放大神器PhotoZoom,你值得拥有!

淋雨

PhotoZoom

Kotlin协程它不香吗?,kotlin开发游戏

android 程序员 移动开发

Kotlin系列三:空指针检查,万字长文总结Android多进程

android 程序员 移动开发

Native开发工具之交叉编译移植(五),android开发基础教程视频

android 程序员 移动开发

OkHttp踩坑记:为何 response,androidui设计

android 程序员 移动开发

OOM问题原理解析(二),android开发教程pdf

android 程序员 移动开发

livedatabus详解,阿里是如何用他来做淘宝架构的?,android开发视频百度云

android 程序员 移动开发

万人逐鹿、十强争霸!华为云GaussDB数据库两大重量级赛事圆满落幕

华为云数据库小助手

GaussDB 大赛 华为云数据库

LeakCanary源码学习二:LeakCanary,sw实战营文件下载

android 程序员 移动开发

ObjectBox 集成指南,建议细读

android 程序员 移动开发

Kotlin学习手记--泛型、泛型约束、泛型型变,天呐

android 程序员 移动开发

Kotlin的自定义View,实现带弧形的进度条,androidjetpack详解

android 程序员 移动开发

lambda表达式(3)-shawn,挑战大厂重燃激情

android 程序员 移动开发

Meterial Design常见控件的使用(一),安卓面试题2018中高级

android 程序员 移动开发

Meterial Design常见控件的使用(八),【面试总结】

android 程序员 移动开发

Native开发工具之应用开发编辑器&打包发布(一),kotlin构造器

android 程序员 移动开发

Kotlin学习手记——注解(1),2021年字节跳动74道高级程序员面试

android 程序员 移动开发

Kotlin(五)深入理解Kotlin类与接口,androidndk开发视频

android 程序员 移动开发

Meterial Design常见控件的使用(五),移动端h5开发框架

android 程序员 移动开发

OkHttp 的 IO 操作和进度监听,android应用开发实训总结

android 程序员 移动开发

Okio—— 更加高效易用的IO库,一线互联网架构师Android框架体系架构

android 程序员 移动开发

MVPArms官方快速组件化方案开源,来自5K star的信赖,安卓性能优化和内存优化

android 程序员 移动开发

《个人信息保护法》正式实施,企业如何保证数据安全合规?

腾讯安全云鼎实验室

数据安全

Go语言HTTPServer开发的六种实现

FunTester

HTTP Fasthttp Server Go 语言 FunTester

与Brian Goetz聊Java的数据类_Java_Michael Redlich_InfoQ精选文章