在Java 8中,某些类在Javadoc中有一个小注释,说明它们是基于值的类 。 其中包括简短说明的链接,以及有关不使用它们的限制。 这很容易被忽略,如果这样做,则可能会在将来的Java版本中以微妙的方式破坏代码。 为了避免这种情况,我想在自己的文章中介绍基于价值的类,尽管我已经在其他文章中提到了最重要的部分。
总览
在详细说明这些限制之前,本文将首先探讨为什么存在基于值的类以及为什么限制了它们的使用(如果您不耐烦,请跳至此处 )。 它将以关于FindBugs的注释结束,不久便可以为您提供帮助。
背景
让我们快速了解为什么引入了基于值的类以及JDK中存在的类。
他们为什么存在?
Java的未来版本很可能包含值类型。 我将在未来几周内写他们( 所以 留 调整 ),并会在一些细节呈现出来。 尽管它们肯定有好处,但本博文未涉及这些好处,这可能会使限制显得毫无意义。 相信我,他们不是! 或者不要相信我自己去看 。
现在,让我们看看我已经写了一些关于值类型的内容:
该想法的最大简化是,用户可以定义一种不同于类和接口的新型类型。 它们的主要特征是它们将不会通过引用(如类)来处理,而是通过值(如基元)来处理。 或者,正如Brian Goetz在他的介绍性文章《价值观的状态》中所说的那样:
像类一样的代码,像int一样工作!
重要的是要添加值类型将是不变的-就像今天的原始类型一样。
在Java 8中,值类型之前是基于值的类 。 未来它们的精确关系尚不清楚,但可能与装箱和拆箱原语(例如
Integer
和int
)相似。
设计Optional时,现有类型与将来值类型之间的关系变得很明显。 在指定和记录基于价值的类的局限性时也是如此。
存在哪些基于价值的类?
这些都是我在JDK中找到的所有标记为基于值的类:
- java.util: 可选 , OptionalDouble , OptionalLong , OptionalInt
- java.time: 持续时间 , 即时 , LOCALDATE的 , LocalDateTime , 本地时间 , MONTHDAY , OffsetDateTime , OffsetTime , 期间 , 年 , YearMonth , ZonedDateTime , 了zoneid , ZoneOffset
- java.time.chrono: HijrahDate , JapaneseDate , MinguaDate , ThaiBuddhistDate
我无法保证此列表是完整的,因为我没有找到列出所有列表的官方来源。
另外,还有一些非JDK类应该被认为是基于值的,但不要这样说。 一个例子是Guava的Optional 。 还可以安全地假设大多数代码库将包含旨在基于值的类。
有趣的是,现有的拳击类(如Integer
, Double
等)未标记为基于值。 这样做似乎很可取-毕竟它们都是此类的原型-但这样做会破坏向后兼容性,因为它将使与新限制相抵触的所有用途追溯无效。
Optional
是新的,而免责声明在第一天就到了。另一方面,Integer
可能受到了无可救药的污染,而且我确信,如果Integer
不再是可锁定的,它将破坏重要代码的空子(尽管我们可能会这样认为)练习。)Brian Goetz – 2015年1月6日(格式化我的)
不过,它们非常相似,因此我们称它们为“价值至上”。
特点
在这一点上,尚不清楚如何实现值类型,它们的确切属性是什么以及它们如何与基于值的类交互。 因此,对后者施加的限制不是基于现有要求,而是源自某些所需的值类型特征。 这些限制是否足以在将来与值类型建立关系还不清楚。
话虽如此,让我们继续上面的引用:
在Java 8中,值类型之前是基于值的类 。 未来它们的精确关系尚不清楚,但可能与装箱和拆箱原语(例如
Integer
和int
)相似。 此外,编译器可能会自由地在两者之间进行静默切换以提高性能。 恰恰是,来回切换(即删除并稍后重新创建引用)也禁止将基于身份的机制应用于基于值的类。
这样实现的JVM不再需要跟踪基于值的实例的身份,这可以带来实质性的性能改进和其他好处。
身分识别
身份一词在这种情况下很重要,因此让我们仔细看看。 考虑一个可变对象,该对象会不断更改其状态(例如正在修改的列表)。 即使对象总是“看起来”不同,我们仍然会说它是同一对象。 因此,我们区分对象的状态和身份。 在Java中,状态相等由equals
(如果适当实现)和身份相等通过比较引用来确定。 换句话说,对象的身份由其引用定义。
现在假设JVM将如上所述处理值类型和基于值的类。 在那种情况下,两者都不会具有有意义的身份。 值类型将没有一个开始,就像int
一样。 相应的基于值的类仅仅是值类型的盒子,JVM可以随意销毁和随意创建它们。 因此,尽管当然有对单个盒子的引用,但完全不能保证它们将如何存在。
这意味着,即使程序员可以查看代码并遵循在各处传递的基于值的类的实例,JVM的行为也可能有所不同。 它可能会删除引用(从而破坏对象的标识)并将其作为值类型传递。 如果是身份敏感操作,则可能会重新创建一个新引用。
关于身份,最好考虑基于值的类,例如整数:谈论“ 3”的不同实例( int
)是没有意义的,谈论“ 11:42 pm”的不同实例也没有意义( LocalTime
)。
州
如果基于值的类的实例没有标识,则只能通过比较它们的状态(通过实现equals
来确定)来确定其equals
。 这具有重要的含义,即状态相同的两个实例必须完全可互换,这意味着用另一个实例替换一个这样的实例必须不会产生任何明显的影响。
这间接确定了应将哪些内容视为基于值的实例状态的一部分。 所有类型为基本类型或其他基于值的类的字段都可以成为其一部分,因为它们也可以完全互换(所有“ 3”和“ 11:42 pm”的行为都相同)。 普通班比较棘手。 由于操作可能取决于它们的身份,因此如果基于vale的实例都引用相同但不相同的实例,则通常无法将其交换。
例如,考虑锁定String
,然后将其包装在Optional
。 在其他地方,将使用相同的字符序列创建另一个String
并将其包装。 然后,这两个Optionals
不可互换,因为即使它们都包装了相等的字符序列,这些String
实例也不相同,一个实例用作锁,而另一个实例则充当锁。
严格解释这意味着基于值的类必须考虑引用本身,而不是将引用字段的状态包括在其自身的状态中。 在上面的示例中,仅当Optionals
实际指向同一字符串时,才应将其视为相等。
但是,这可能过于严格,因为必须对给定的以及其他有问题的示例进行某种程度的解释。 强制基于值的类忽略诸如String
和Integer
类的“值-ish”类的状态非常违反直觉。
值类型框
被计划为值类型的框会增加一些其他要求。 如果不深入探讨值类型,这些将很难解释,因此我现在不再这样做。
局限性
首先,需要注意的是,在Java 8中,所有限制都是纯人工的。 JVM并不了解这类类的第一件事,并且您可以忽略所有规则而不会出错。 但这在引入值类型时可能会发生巨大变化。
正如我们在上面看到的,基于值的类的实例没有保证的身份,在定义相等性方面的宽松程度较低,并且应该符合值类型框的预期要求。 这有两个含义:
- 该类必须相应地构建。
- 该类的实例不得用于基于身份的操作。
这是Javadoc中所述限制的基础,因此可以将其分为对类的声明和其实例的使用的限制。
申报地点
直接来自文档(编号和格式编号):
基于值的类的实例:
- 是最终的且不可变的(尽管可能包含对可变对象的引用);
- 具有
equals
,hashCode
和toString
实现,这些实现仅根据实例的状态而不是根据其标识或任何其他对象或变量的状态计算;- 不使用身份敏感的操作,例如实例之间的引用相等(
==
),实例的身份哈希码或实例的固有锁上的同步;- 仅基于
equals()
而不是基于引用相等(==
)被视为相等;- 没有可访问的构造函数,而是通过工厂方法实例化的,该方法对提交的实例的身份不作任何承诺;
- 在相等时可以自由替换,这意味着在任何计算或方法调用中互换
equals()
任意两个实例x
和y
都不会在行为上产生任何可见的变化。
通过上面讨论的内容,大多数这些规则都是显而易见的。
规则1的动机是基于价值的类,即价值类型的框。 出于技术和设计原因,这些必须是最终的且不可更改,并将这些要求转移到其包装盒中。
规则2 模糊地解决了有关如何定义基于值的类的状态的问题。 规则的精确效果取决于对“实例状态”和“任何其他变量”的解释。 读取它的一种方法是在状态中包括“值-ish”类,并将典型的引用类型视为其他变量。
第3到第6个数字考虑丢失的身份。
有趣的是, Optional
打破了规则2,因为它在包装的值上调用了equals
。 同样, java.time
和java.time.chrono
所有基于值的类都通过可序列化(这是一个基于身份的操作,请参见下文) java.time.chrono
打破规则3。
使用网站
再次从文档中:
如果程序尝试直接通过引用相等性或通过呼吁同步,身份哈希,序列化或任何其他身份敏感机制间接地将两个引用区分为基于值的类的相等值,则可能会产生不可预测的结果。
考虑到丢失的身份,直接区分参考是不言而喻的。 但是,没有任何解释说明为什么列出的示例违反了该规则,所以让我们仔细看看。 我列出了所有可以解决的违规事项,并给出了简短的解释和具体案例( vbi代表基于值的类的实例 ):
参考比较:这显然根据实例的身份来区分实例。
vbi的序列化:希望使值类型可序列化,并且有意义的定义似乎很简单。 但是,今天,序列化对对象身份做出了承诺,这与基于身份的无价值类的概念相冲突。 在当前的实现中,序列化在遍历对象图时也使用对象标识。 因此,目前,必须将其视为基于身份的操作,应避免使用。
情况:
- 可序列化类中的非临时字段
- 通过ObjectOutputStream.writeObject直接序列化
锁定vbi:使用对象标头访问实例的监视器–基于值的类的标头可以自由删除和重新创建,并且基本/值类型没有标头。
情况:
- 在同步块中使用
- 调用Object.wait,Object.notify或Object.notifyAll
身份哈希码:要求该哈希码在实例的生存期内保持不变。 由于基于价值的类的实例可以自由删除并重新创建,因此对于开发人员来说有意义的意义不能得到保证。
情况:
- System.identityHashCode的参数
- 键入IdentityHashMap
突出强调其他违规或改进说明的评论,深表感谢!
查找错误
当然,了解所有这一切是很好的,但这并不意味着阻止您超越规则的工具并不会真正有帮助。 作为FindBugs的重度用户,我决定要求项目实施此功能,并创建了功能请求 。 该票证涵盖了使用场所的限制,并将帮助您在JDK以及您自己的基于值的类(带有注释的类)中维护它们。
出于对FindBugs的好奇并希望做出贡献,我决定着手尝试自己实施它。 因此,如果您要问为什么花这么长时间准备好该功能,现在您知道了:这是我的错。 但是谈话很便宜,所以为什么不加入我的行列呢? 我在GitHub上放置了一个FindBugs克隆 ,您可以看到此pull请求中的进度。
一旦完成,我计划也要实现声明站点的规则,因此可以确保在值类型最终出现时,正确编写了基于值的类并准备就绪。
反射
我们已经看到,基于值的类是值类型的先驱。 随着Java的变化,这些实例将没有有意义的身份,并且定义它们的状态的可能性也将受到限制,这将对其声明和使用产生限制。 这些限制已详细讨论。
翻译自: https://www.javacodegeeks.com/2015/02/value-based-classes.html