我正在尝试为clone()
大型项目中的各种操作编写单元测试,并且我想知道某个地方是否存在一个现有的类,该类能够接受两个相同类型的对象,进行深度比较,并说是否是否相同?
我正在尝试为clone()
大型项目中的各种操作编写单元测试,并且我想知道某个地方是否存在一个现有的类,该类能够接受两个相同类型的对象,进行深度比较,并说是否是否相同?
Answers:
Unitils具有以下功能:
通过反射进行平等声明,并具有不同的选项,例如忽略Java默认值/空值和忽略集合的顺序
unitils
存在缺陷,因为它即使在变量没有明显影响的情况下也可以比较变量。比较变量的另一个(不希望的)结果是不支持纯闭包(没有闭包状态)。另外,它要求比较的对象具有相同的运行时类型。我袖手旁观,并创建了自己的深度比较工具版本,以解决这些问题。
我喜欢这个问题!主要是因为几乎从未回答过或回答得很差。就像没有人知道。维尔京领土:)
首先,甚至不要考虑使用equals
。如equals
javadoc中所定义,的契约是等价关系(自反,对称和可传递),而不是等价关系。为此,它也必须是反对称的。唯一的实现equals
是(或曾经是)真正的平等关系是中的java.lang.Object
。即使您确实使用equals
过比较图表中的所有内容,但违反合同的风险仍然很高。正如Josh Bloch在Effective Java中指出的那样,等于的约定很容易打破:
“根本没有办法扩展可实例化的类并在保留平等合同的同时增加一个方面”
除了布尔方法到底有什么用呢?实际上封装原始副本和克隆副本之间的所有差异会很好,您不觉得吗?另外,我在这里假设您不想为图表中的每个对象编写/维护比较代码而感到烦恼,而是在寻找随源随着时间变化而变化的东西。
太好了,您真正想要的是某种状态比较工具。该工具的实现方式实际上取决于域模型的性质和性能限制。根据我的经验,没有通用的魔术子弹。而且在大量迭代中它会很慢。但是对于测试克隆操作的完整性,它将做得很好。最好的两个选择是序列化和反射。
您将遇到的一些问题:
XStream非常快,并且与XMLUnit结合使用仅需几行代码即可完成这项工作。XMLUnit很不错,因为它可以报告所有差异,或者仅在找到的第一个差异处停止。它的输出包括到不同节点的xpath,这很好。默认情况下,它不允许无序收集,但是可以将其配置为允许无序收集。注入特殊的差异处理程序(称为DifferenceListener
),可以指定处理差异的方式,包括忽略顺序。但是,一旦您想做除最简单的自定义之外的任何事情,就变得很难编写,并且细节往往被束缚到特定的域对象上。
我个人的喜好是使用反射来循环遍历所有声明的字段,并深入研究每个字段,并在进行过程中跟踪差异。警告词:除非您喜欢堆栈溢出异常,否则请不要使用递归。使用堆栈将内容保持在范围内(使用LinkedList
或者其他的东西)。我通常会忽略瞬态和静态字段,并且会跳过已经比较过的对象对,因此,如果有人决定编写自引用代码,我就不会陷入无限循环(但是无论如何我都会比较原始包装器,因为通常会重复使用相同的对象ref)。您可以预先配置内容,以忽略集合的排序并忽略特殊类型或字段,但是我想通过注释在字段本身上定义状态比较策略。恕我直言,这正是注释的含义,它使有关类的元数据在运行时可用。就像是:
@StatePolicy(unordered=true, ignore=false, exactTypesOnly=true)
private List<StringyThing> _mylist;
我认为这实际上是一个非常棘手的问题,但完全可以解决!一旦有了适合自己的东西,它就非常非常方便:)
所以,祝你好运。如果您提出的只是纯粹的天才,请不要忘记分享!
请参阅java-util中的DeepEquals和DeepHashCode():https : //github.com/jdereg/java-util
此类完全符合原始作者的要求。
public
适用于所有类型的内容进行比较,而不是仅适用于集合/映射。
您可以使用EqualsBuilder.reflectionEquals()简单地覆盖类的equals()方法。作为解释在这里:
public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj);
}
只需比较Hibernate Envers修订的两个实体实例即可。我开始写自己的不同文章,但随后找到了以下框架。
https://github.com/SQiShER/java-object-diff
您可以比较两个相同类型的对象,它将显示更改,添加和删除。如果没有变化,则对象相等(理论上)。为在检查期间应忽略的吸气剂提供了注释。框架具有比相等性检查更广泛的应用程序,即我正在使用它来生成更改日志。
它的性能还可以,在比较JPA实体时,请确保首先将它们与实体管理器分离。
我在XStream中使用:
/**
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object o) {
XStream xstream = new XStream();
String oxml = xstream.toXML(o);
String myxml = xstream.toXML(this);
return myxml.equals(oxml);
}
/**
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
XStream xstream = new XStream();
String myxml = xstream.toXML(this);
return myxml.hashCode();
}
在AssertJ中,您可以执行以下操作:
Assertions.assertThat(expectedObject).isEqualToComparingFieldByFieldRecursively(actualObject);
可能并非在所有情况下都可以使用,但是在您认为更多的情况下都可以使用。
文档说明如下:
根据属性/字段比较(包括继承的对象),通过递归属性/字段来确定被测对象(实际)等于给定对象。如果实际的equals实现不适合您,这将很有用。递归属性/字段比较不适用于具有自定义equals实现的字段,即,将使用覆盖的equals方法代替逐字段比较的字段。
递归比较处理周期。默认情况下,浮点数的精度为1.0E-6,而浮点数的精度为1.0E-15。
您可以为每个(嵌套的)字段或类型指定自定义比较器,分别使用ComparatorForFields(Comparator,String ...)和usingComparatorForType(Comparator,Class)。
要比较的对象可以是不同的类型,但必须具有相同的属性/字段。例如,如果实际对象具有一个名称String字段,则期望另一个对象也具有一个。如果对象具有相同名称的字段和属性,则将在该字段上使用属性值。
isEqualToComparingFieldByFieldRecursively
现在已弃用。请assertThat(expectedObject).usingRecursiveComparison().isEqualTo(actualObject);
改用:)
http://www.unitils.org/tutorial-reflectionassert.html
public class User {
private long id;
private String first;
private String last;
public User(long id, String first, String last) {
this.id = id;
this.first = first;
this.last = last;
}
}
User user1 = new User(1, "John", "Doe");
User user2 = new User(1, "John", "Doe");
assertReflectionEquals(user1, user2);
Hamcrest具有Matcher samePropertyValuesAs。但是它依赖于JavaBeans Convention(使用getter和setter)。如果要比较的对象的属性没有getter和setter,则此方法将无效。
import static org.hamcrest.beans.SamePropertyValuesAs.samePropertyValuesAs;
import static org.junit.Assert.assertThat;
import org.junit.Test;
public class UserTest {
@Test
public void asfd() {
User user1 = new User(1, "John", "Doe");
User user2 = new User(1, "John", "Doe");
assertThat(user1, samePropertyValuesAs(user2)); // all good
user2 = new User(1, "John", "Do");
assertThat(user1, samePropertyValuesAs(user2)); // will fail
}
}
用户bean-带有getter和setter
public class User {
private long id;
private String first;
private String last;
public User(long id, String first, String last) {
this.id = id;
this.first = first;
this.last = last;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getFirst() {
return first;
}
public void setFirst(String first) {
this.first = first;
}
public String getLast() {
return last;
}
public void setLast(String last) {
this.last = last;
}
}
isFoo
为Boolean
属性使用read方法的POJO,否则这将非常有用。自2016年以来已经有一个PR进行修复。 github.com/hamcrest/JavaHamcrest/pull/136
如果您的对象实现可序列化,则可以使用以下方法:
public static boolean deepCompare(Object o1, Object o2) {
try {
ByteArrayOutputStream baos1 = new ByteArrayOutputStream();
ObjectOutputStream oos1 = new ObjectOutputStream(baos1);
oos1.writeObject(o1);
oos1.close();
ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
ObjectOutputStream oos2 = new ObjectOutputStream(baos2);
oos2.writeObject(o2);
oos2.close();
return Arrays.equals(baos1.toByteArray(), baos2.toByteArray());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
您的链接列表示例并不难处理。当代码遍历两个对象图时,它将访问的对象放置在Set或Map中。在遍历另一个对象引用之前,将测试此集合以查看对象是否已经遍历。如果是这样,则无需进一步。
我同意上面的人所说的使用LinkedList(就像一个Stack,但是上面没有同步方法,所以它更快)。理想的解决方案是使用堆栈遍历对象图,同时使用反射来获取每个字段。一次编写完之后,所有的equals()和hashCode()方法都应调用此“外部” equals()和“外部” hashCode()。您再也不需要客户的equals()方法。
我写了一些遍历完整对象图的代码,列在Google Code中。请参阅json-io(http://code.google.com/p/json-io/)。它将Java对象图序列化为JSON并从中反序列化。它处理所有Java对象,无论是否具有公共构造函数,可序列化或不可序列化等。相同的遍历代码将成为外部“ equals()”和外部“ hashcode()”实现的基础。顺便说一句,JsonReader / JsonWriter(json-io)通常比内置的ObjectInputStream / ObjectOutputStream更快。
此JsonReader / JsonWriter可以用于比较,但对哈希码无济于事。如果您想要通用的hashcode()和equals(),则需要它自己的代码。我也许可以通过通用图访问者实现这一目标。我们拭目以待。
其他注意事项-静态字段-很容易-可以跳过它们,因为所有equals()实例的静态字段值都相同,因为所有实例之间都共享静态字段。
至于瞬态字段-这将是一个可选选项。有时您可能希望瞬态不计算其他时间。“有时您感觉像是个坚果,有时却不然。”
返回到json-io项目(对于我的其他项目),您将找到外部equals()/ hashcode()项目。我还没有名字,但这很明显。
Apache提供了一些东西,将两个对象都转换为字符串并比较字符串,但是您必须重写toString()
obj1.toString().equals(obj2.toString())
覆盖toString()
如果所有字段都是原始类型:
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
@Override
public String toString() {return
ReflectionToStringBuilder.toString(this);}
如果您有非原始字段和/或集合和/或地图:
// Within class
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
@Override
public String toString() {return
ReflectionToStringBuilder.toString(this,new
MultipleRecursiveToStringStyle());}
// New class extended from Apache ToStringStyle
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.util.*;
public class MultipleRecursiveToStringStyle extends ToStringStyle {
private static final int INFINITE_DEPTH = -1;
private int maxDepth;
private int depth;
public MultipleRecursiveToStringStyle() {
this(INFINITE_DEPTH);
}
public MultipleRecursiveToStringStyle(int maxDepth) {
setUseShortClassName(true);
setUseIdentityHashCode(false);
this.maxDepth = maxDepth;
}
@Override
protected void appendDetail(StringBuffer buffer, String fieldName, Object value) {
if (value.getClass().getName().startsWith("java.lang.")
|| (maxDepth != INFINITE_DEPTH && depth >= maxDepth)) {
buffer.append(value);
} else {
depth++;
buffer.append(ReflectionToStringBuilder.toString(value, this));
depth--;
}
}
@Override
protected void appendDetail(StringBuffer buffer, String fieldName,
Collection<?> coll) {
for(Object value: coll){
if (value.getClass().getName().startsWith("java.lang.")
|| (maxDepth != INFINITE_DEPTH && depth >= maxDepth)) {
buffer.append(value);
} else {
depth++;
buffer.append(ReflectionToStringBuilder.toString(value, this));
depth--;
}
}
}
@Override
protected void appendDetail(StringBuffer buffer, String fieldName, Map<?, ?> map) {
for(Map.Entry<?,?> kvEntry: map.entrySet()){
Object value = kvEntry.getKey();
if (value.getClass().getName().startsWith("java.lang.")
|| (maxDepth != INFINITE_DEPTH && depth >= maxDepth)) {
buffer.append(value);
} else {
depth++;
buffer.append(ReflectionToStringBuilder.toString(value, this));
depth--;
}
value = kvEntry.getValue();
if (value.getClass().getName().startsWith("java.lang.")
|| (maxDepth != INFINITE_DEPTH && depth >= maxDepth)) {
buffer.append(value);
} else {
depth++;
buffer.append(ReflectionToStringBuilder.toString(value, this));
depth--;
}
}
}}
停止进行如此深入的比较可能是一个问题。以下应该怎么办?(如果实现这样的比较器,这将是一个很好的单元测试。)
LinkedListNode a = new LinkedListNode();
a.next = a;
LinkedListNode b = new LinkedListNode();
b.next = b;
System.out.println(DeepCompare(a, b));
这是另一个:
LinkedListNode c = new LinkedListNode();
LinkedListNode d = new LinkedListNode();
c.next = d;
d.next = c;
System.out.println(DeepCompare(c, d));
Using an answer instead of a comment to get a longer limit and better formatting.
如果这是评论,那么为什么要使用答案部分?这就是为什么我标记它。不是因为?
。答案已经被其他人标记,他们没有留下任何评论。我刚在审查队列中得到这个。可能对我不利,我应该更加小心。
我认为受Ray Hulha解决方案启发的最简单的解决方案是序列化对象,然后对原始结果进行深入比较。
序列化可以是字节,json,xml或简单的toString等。ToString似乎更便宜。龙目岛为我们生成免费的易于定制的ToSTring。请参见下面的示例。
@ToString @Getter @Setter
class foo{
boolean foo1;
String foo2;
public boolean deepCompare(Object other) { //for cohesiveness
return other != null && this.toString().equals(other.toString());
}
}
从Java 7开始,Objects.deepEquals(Object, Object)
。