比较多个JavaBean属性的最佳方法是什么?

16

我需要比较两个对象(同一个类的实例)中的数十个字段,并在有差异时进行一些记录和更新。元代码可能看起来像这样:

if (a.getfield1 != b.getfield1)
  log(a.getfield1 is different than b.getfield1)
  b.field1 = a.field1

if (a.getfield2!= b.getfield2)
  log(a.getfield2 is different than b.getfield2)
  b.field2 = a.field2

...

if (a.getfieldn!= b.getfieldn)
  log(a.getfieldn is different than b.getfieldn)
  b.fieldn = a.fieldn
代码中的所有比较都非常简洁,我希望能够更加紧凑一些。如果我能够有一个方法来接受setter和getter的方法调用作为参数,并对所有字段调用它,那就太好了。但很遗憾,这在Java中是不可能的。
我想到了三种选择,每种选择都有自己的缺点。
1. 使用反射API查找getter和setter
丑陋,如果字段名称更改,可能会导致运行时错误。
2. 将字段更改为public并直接操作它们,而不使用getter和setter
同样丑陋,并将类的实现暴露给外部世界。
3. 让包含类(实体)进行比较,更新已更改的字段并返回日志消息
实体不应参与业务逻辑。
所有字段都是字符串类型,如果需要,我可以修改拥有字段的类的代码。
编辑:该类中有一些字段不得进行比较。

你是在比较两个不同类的实例还是同一类的两个实例? - rudolfson
它们是同一类的实例。我编辑了这个问题。谢谢您指出这一点。 - tputkonen
9个回答

19

使用注解

如果您标记需要比较的字段(不管它们是否是私有的,您仍然不会失去封装性),然后获取这些字段并进行比较。可以按照以下方式进行:

在需要比较的类中:

@ComparableField 
private String field1;

@ComparableField
private String field2;

private String field_nocomparable;

在外部类中:

public <T> void compare(T t, T t2) throws IllegalArgumentException,
                                          IllegalAccessException {
    Field[] fields = t.getClass().getDeclaredFields();
    if (fields != null) {
        for (Field field : fields) {
            if (field.isAnnotationPresent(ComparableField.class)) {
                field.setAccessible(true);
                if ( (field.get(t)).equals(field.get(t2)) )
                    System.out.println("equals");
                field.setAccessible(false);
            }
        }
    }
}

代码未经过测试,但如果有帮助,请告诉我。

1
虽然使用反射仍然有点丑陋,但如果你想使用反射,这可能是最好的选择。 - sleske
2
以上代码不完整,它没有访问属于超类的字段 - 为此,您需要一些递归。 - mP.
3
使用此注解的人需要记得为你的注解接口定义 @Retention(RetentionPolicy.RUNTIME)。 - tputkonen
1
好答案 +1!但是有一个小提示:(fields != null) 检查是不必要的,因为 getDeclaredFields 从不返回 null。 - Tim Büthe
泛型在这里没有任何效果。它等同于 public void compare(Object t, Object t2) - user102008

4

JavaBeans API 旨在帮助内省。它自 Java 1.2 版本以来一直存在,自 1.4 版本以来一直非常可用。

演示代码,用于比较两个 bean 中的属性列表:

  public static void compareBeans(PrintStream log,
      Object bean1, Object bean2, String... propertyNames)
      throws IntrospectionException,
      IllegalAccessException, InvocationTargetException {
    Set<String> names = new HashSet<String>(Arrays
        .asList(propertyNames));
    BeanInfo beanInfo = Introspector.getBeanInfo(bean1
        .getClass());
    for (PropertyDescriptor prop : beanInfo
        .getPropertyDescriptors()) {
      if (names.remove(prop.getName())) {
        Method getter = prop.getReadMethod();
        Object value1 = getter.invoke(bean1);
        Object value2 = getter.invoke(bean2);
        if (value1 == value2
            || (value1 != null && value1.equals(value2))) {
          continue;
        }
        log.format("%s: %s is different than %s%n", prop
            .getName(), "" + value1, "" + value2);
        Method setter = prop.getWriteMethod();
        setter.invoke(bean2, value2);
      }
    }
    if (names.size() > 0) {
      throw new IllegalArgumentException("" + names);
    }
  }

示例调用:

compareBeans(System.out, bean1, bean2, "foo", "bar");

如果您选择使用注释,考虑放弃反射并使用编译时注释处理器或其他代码生成器生成比较代码。


1
这个解决方案与选项1有什么不同?它更好,因为使用了Beans API而不是直接使用Relections API。但是你仍然需要为比较命名所有字段,当你重命名其中一些字段时,仍然存在问题。或者我有什么遗漏吗? - rudolfson
1
是的,那个问题仍然存在,但我没有看到解决它的方法(没有注释)。然而,相对于BeanInfo,反射更加“低级”。 - tputkonen
如果(如所述)此比较/更新的逻辑必须保留在bean之外,则注释字段不是理想的设计决策(尽管将所有逻辑放在一个地方很好)。我概述的方法也不是理想的 - 由于其本质,反射无法在编译时捕获问题(尽管缺少字段将在运行时被捕获)。生成的直接调用(基于注释或属性列表)比两者都更好,并避免编写样板文件,但只有OP知道它是否值得努力。 - McDowell
事实上,我改变了主意并决定采用注解方式,因为它具有类型安全性,并且以某种方式自然地指定要在实体中比较的字段。感谢所有回答我的人,我学到了很多! - tputkonen
到目前为止,这是一个简单而优雅的解决方案,但是有人可以解释一下方法内这两行代码的作用吗?方法 setter = prop.getWriteMethod();setter.invoke(bean2, value2); - Neo182

2

我会选择选项1,但我会使用 getClass().getDeclaredFields() 来访问字段,而不是使用名称。

public void compareAndUpdate(MyClass other) throws IllegalAccessException {
    for (Field field : getClass().getDeclaredFields()) {
        if (field.getType() == String.class) {
            Object thisValue = field.get(this);
            Object otherValue = field.get(other);
            // if necessary check for null
            if (!thisValue.equals(otherValue)) {
                log(field.getName() + ": " + thisValue + " <> " + otherValue);
                field.set(other, thisValue);
            }
        }
    }
}

这里有一些限制(如果我没记错的话):

  • 比较方法必须在同一个类中实现(我认为无论如何都应该是这样的),而不是在外部类中实现。
  • 只使用这个类中的字段,而不使用超类中的字段。
  • 需要处理IllegalAccessException异常(我在上面的示例中只是将其抛出)。

这将比较所有字段,但有一些字段我不想比较。 - tputkonen
它比较所有的字符串字段,对吧。从您的说明中没有看到这一点。 但是您可以通过使用String的自己子类来“标记”要比较的字段,只需实现必要的构造函数即可。这可以是您类的内部私有类。字段的getter和setter仍应使用String。 - rudolfson
或者更好的做法(对于这么多注释表示抱歉 :-)),是在那些字段上使用注解。 - rudolfson
正如我上面提到的 - 这只比较当前类中的字段,而不访问超类中的字段。 - mP.

1

这可能也不是太好,但在我看来,它比你提出的两个方案都要好得多。

提供一个单一的getter/setter对,接受一个数字索引字段,然后让getter/setter将索引字段解引用到相关的成员变量上,如何?

i.e.:

public class MyClass {
    public void setMember(int index, String value) {
        switch (index) {
           ...
        }
    }

    public String getMember(int index) {
        ...
    }

    static public String getMemberName(int index) {
        ...
    }
}

然后在你的外部类中:

public void compareAndUpdate(MyClass a, MyClass b) {
    for (int i = 0; i < a.getMemberCount(); ++i) {
        String sa = a.getMember();
        String sb = b.getMember();
        if (!sa.equals(sb)) {
            Log.v("compare", a.getMemberName(i));
            b.setMember(i, sa);
        }
    }
}

这样至少可以让你将所有重要的逻辑保留在被检查的类中。


这样是不行的,因为有一些字段我不想进行比较。很抱歉在问题中没有指出这一点,我会进行编辑。但还是感谢您提供的思路。 - tputkonen
可以翻译为:没问题 - 只是不要在我给出的示例代码中公开这些字段。 - Alnitak

1

自从

所有字段都是字符串类型,如果需要,我可以修改拥有这些字段的类的代码。

你可以尝试使用这个类:

public class BigEntity {

    private final Map<String, String> data;

    public LongEntity() {
        data = new HashMap<String, String>();
    }

    public String getFIELD1() {
        return data.get(FIELD1);
    }

    public String getFIELD2() {
        return data.get(FIELD2);
    }

    /* blah blah */
    public void cloneAndLogDiffs(BigEntity other) {
        for (String field : fields) {
            String a = this.get(field);
            String b = other.get(field);

            if (!a.equals(b)) {
                System.out.println("diff " + field);
                other.set(field, this.get(field));
            }
        }
    }

    private String get(String field) {
        String value = data.get(field);

        if (value == null) {
            value = "";
        }

        return value;
    }

    private void set(String field, String value) {
        data.put(field, value);
    }

    @Override
    public String toString() {
        return data.toString();
    }

神奇代码:

    private static final String FIELD1 = "field1";
    private static final String FIELD2 = "field2";
    private static final String FIELD3 = "field3";
    private static final String FIELD4 = "field4";
    private static final String FIELDN = "fieldN";
    private static final List<String> fields;

    static {
        fields = new LinkedList<String>();

        for (Field field : LongEntity.class.getDeclaredFields()) {
            if (field.getType() != String.class) {
                continue;
            }

            if (!Modifier.isStatic(field.getModifiers())) {
                continue;
            }

            fields.add(field.getName().toLowerCase());
        }
    }

这个类有几个优点:

  • 在类加载时只需反射一次
  • 非常简单地添加新字段,只需添加新的静态字段(在这里更好的解决方案是使用注解:如果你关心使用反射也适用于Java 1.4)
  • 你可以将这个类重构为一个抽象类,所有派生类都可以获得数据和cloneAndLogDiffs()
  • 外部接口是类型安全的(你还可以很容易地强制不可变性)
  • 没有setAccessible调用:这个方法有时会有问题

1

虽然选项1可能不太好看,但它可以完成工作。选项2更加丑陋,并且会使您的代码面临无法想象的漏洞。即使您最终排除了选项1,我也希望您保留现有的代码,而不选择选项2。

话虽如此,如果您不想将其作为静态列表传递给方法,您可以使用反射获取类的字段名称列表。假设您想比较所有字段,则可以在循环中动态创建比较。

如果不是这种情况,而您要比较的字符串只是某些字段,则可以进一步检查字段并仅隔离那些属于String类型的字段,然后进行比较。

希望这可以帮助到您,

Yuval =8-)


0
我也提出与Alnitak类似的解决方案。
如果需要在比较时迭代字段,为什么不放弃单独的字段,将数据放入数组、HashMap或其他适当的数据结构中。
然后您可以以编程方式访问它们、比较它们等。如果不同的字段需要以不同的方式处理和比较,您可以为值创建适当的帮助程序类,这些类实现一个接口。
然后您只需执行
valueMap.get("myobject").compareAndChange(valueMap.get("myotherobject")

或者类似这样的东西...


0
一个广泛的想法:
创建一个新类,其对象接受以下参数:要比较的第一个类,要比较的第二个类以及对象的getter和setter方法名称列表,其中仅包括感兴趣的方法。
您可以使用反射查询对象的类,并从中获取可用方法。假设参数列表中的每个getter方法都包含在类的可用方法中,则应该能够调用该方法以获取比较值。
大致草图如下(如果不是非常完美,请见谅...这不是我的母语):
public class MyComparator
{
    //NOTE: Class a is the one that will get the value if different
    //NOTE: getters and setters arrays must correspond exactly in this example
    public static void CompareMyStuff(Object a, Object b, String[] getters, String[] setters)
    {
        Class a_class = a.getClass();
        Class b_class = b.getClass();

        //the GetNamesFrom... static methods are defined elsewhere in this class
        String[] a_method_names = GetNamesFromMethods(a_class.getMethods());
        String[] b_method_names = GetNamesFromMethods(b_class.getMethods());
        String[] a_field_names = GetNamesFromFields(a_class.getFields());

        //for relative brevity...
        Class[] empty_class_arr = new Class[] {};
        Object[] empty_obj_arr = new Object[] {};

        for (int i = 0; i < getters.length; i++)
        {
            String getter_name = getter[i];
            String setter_name = setter[i];

            //NOTE: the ArrayContainsString static method defined elsewhere...
            //ensure all matches up well...
            if (ArrayContainsString(a_method_names, getter_name) &&
                ArrayContainsString(b_method_names, getter_name) &&
                ArrayContainsString(a_field_names, setter_name)
            {
                //get the values from the getter methods
                String val_a = a_class.getMethod(getter_name, empty_class_arr).invoke(a, empty_obj_arr);
                String val_b = b_class.getMethod(getter_name, empty_class_arr).invoke(b, empty_obj_arr);
                if (val_a != val_b)
                {
                    //LOG HERE
                    //set the value
                    a_class.getField(setter_name).set(a, val_b);
                }
            } 
            else
            {
                //do something here - bad names for getters and/or setters
            }
        }
    }
} 

0

你说你现在已经为所有这些字段设置了getter和setter?好的,那么将底层数据从一堆独立的字段改成一个数组。将所有的getter和setter更改为访问该数组。我会为索引创建常量标记,而不是使用数字进行长期维护。还要创建一个并行数组,指示应处理哪些字段。然后创建一个通用的getter/setter对,使用一个索引,以及一个用于比较标志的getter。像这样:

public class SomeClass
{
  final static int NUM_VALUES=3;
  final static int FOO=0, BAR=1, PLUGH=2;
  String[] values=new String[NUM_VALUES];
  static boolean[] wantCompared={true, false, true};

  public String getFoo()
  {
    return values[FOO];
  }
  public void setFoo(String foo)
  {
    values[FOO]=foo;
  }
  ... etc ...
  public int getValueCount()
  {
    return NUM_VALUES;
  }
  public String getValue(int x)
  {
    return values[x];
  }
  public void setValue(int x, String value)
  {
    values[x]=value;
  }
  public boolean getWantCompared(int x)
  {
    return wantCompared[x];
  }
}
public class CompareClass
{
  public void compare(SomeClass sc1, SomeClass sc2)
  {
    int z=sc1.getValueCount();
    for (int x=0;x<z;++x)
    {
      if (!sc1.getWantCompared[x])
        continue;
      String sc1Value=sc1.getValue(x);
      String sc2Value=sc2.getValue(x);
      if (!sc1Value.equals(sc2Value)
      {
        writeLog(x, sc1Value, sc2Value);
        sc2.setValue(x, sc1Value);
      }
    }
  }
}

我只是凭感觉写下这段代码,并未进行测试,所以代码中可能存在错误,但我认为概念应该是可行的。

由于您已经有了getter和setter,任何使用此类的其他代码应该继续正常工作。如果没有其他代码使用此类,则可以丢弃现有的getter和setter,并仅使用数组执行所有操作。


我们的Java Bean也是一个JPA实体,因此这种方法在我们的情况下不起作用。 - tputkonen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接