内容提要
本文介绍如何用JDK 28预览特性JEP 401的值类实现股票代码Ticker:以单个int字段存储,采用37字符字母表、每字符6位编码最多5个字符,零作填充;构造函数负责打包,toString负责解码。由于编码保持字符顺序,可直接比较int实现Comparable,并实现CharSequence。值类可被标量化和扁平数组存储,基准测试显示遍历约快1.4–1.5倍,但依赖JVM优化,需实测。
延伸解读
编码顺序与比较的巧妙对应
文章将字符按“.”、数字、大写字母的顺序映射为1–37,并让首字符占据最高位。这样打包后的int比较结果与字符串的字典序完全一致,且前缀较短者因零填充而自然排前。因此compareTo只需比较int,无需解码。这种设计依赖于字母表顺序与String.compareTo一致,若扩展字符集需保持此顺序。
性能收益有条件,需实测
基准测试显示,在特定JDK构建和大型具体数组遍历场景下,值类数组比普通对象数组快约1.4–1.5倍,原因是JVM可能将int字段扁平化存储并标量化。但该优化依赖C2编译和类型可见性,通过CharSequence等接口调用可能阻碍优化,且toString()、subSequence()仍会分配字符串。文章强调结果仅反映遍历,不涵盖构造、解码等操作,实际应用应使用JMH测量自身工作负载。
生产使用需考虑交易所规则与扩展限制
示例仅支持1–5个字符,但纳斯达克等交易所允许更长代码,生产类型应遵循具体交易所或数据源格式。此外,JDK 28中值对象字段数据达64位或更多时通常无法在可空字段或数组中扁平化,当前30位编码可扁平化,但若扩展字符集或长度需重新设计。文章还探讨了用两个未用位存储长度、按字符频率编码等方向,但均需权衡编解码与比较开销。
Q&A
Valhalla 值类 Ticker 如何用一个 int 存储股票代码?
Ticker 使用 37 个字符的字母表(.、0-9、A-Z),每个字符用 6 位编码,最多 5 个字符共 30 位,剩余 2 位不用。构造函数将字符按顺序打包进 int,不足 5 个字符时右侧用零填充;toString 从高位到低位每 6 位解码,遇到零停止。
Ticker 值类为什么可以直接比较 int 来实现 Comparable?
因为编码保持了字符顺序:字母表中 '.' 在数字前,数字在大写字母前,且第一个字符位于最高位。当一个是另一个的前缀时,零填充使较短的排在前面。最高两位始终为零,所以有符号 int 比较结果与字符串比较一致,无需解码。
Ticker 实现 CharSequence 有什么好处?
实现 CharSequence 后,调用者可以直接读取长度和单个字符,无需先转成 String。length() 通过扫描非零槽计数,charAt() 直接提取指定槽的编码,两者都不构建字符串;subSequence() 则通过 toString() 解码后截取。Ticker 还可直接传给接受 CharSequence 的 API,如 StringBuilder.append。
使用 Ticker 值类能带来多少性能提升?
基准测试显示,遍历包含 800 万个元素的 ValueTicker[] 数组比等价的 IdentityTicker[] 数组快约 1.4–1.5 倍(中位加速 1.47×,范围 1.44×–1.52×)。这得益于值类可能被标量化以及扁平数组存储,但优化依赖 JVM 和代码上下文,需实际测量。
Ticker 值类有哪些使用限制?
Ticker 仅支持 1 到 5 个字符,字符集限于大写 A-Z、数字 0-9 和点。它不验证代码是否在交易所上市,且 5 字符限制是示例性的,生产环境需遵循交易所或数据源的格式。此外,标量化和扁平化是 JVM 优化,不保证发生;调用 toString() 或 subSequence() 仍可能分配字符串。
如何编译和运行使用 Ticker 值类的代码?
需要使用包含值对象预览特性的 JDK 28 构建。编译时向 javac 传递 --enable-preview --release 28,运行时向 java 传递 --enable-preview。预览特性必须在编译和运行两个阶段都启用。