比较几个javabean属性的最佳方法是什么?

时间:2009-05-15 07:47:18

标签: java reflection

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

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。让包含类(实体)进行比较,更新更改的字段并返回日志消息
实体不应参与业务逻辑

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

编辑:课堂上有一些字段不得比较。

10 个答案:

答案 0 :(得分:16)

使用Annotations

如果您标记需要比较的字段(无论它们是否为私有字段,您仍然不会丢失封装,然后获取这些字段并进行比较。它可能如下所示:

在需要比较的班级中:

@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 :(得分: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");

如果你去注释路线,考虑转储反射并用编译时annotation processor或其他代码生成器生成比较代码。

答案 2 :(得分: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(我只是在上面的示例中抛出它)。

答案 3 :(得分:1)

这可能也不太好,但它比你提出的两种替代方案中的任何一种都更不邪恶(恕我直言)。

如何提供一个获取数字索引字段的getter / setter对,然后让getter / setter将索引字段取消引用到相关的成员变量?

即:

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);
        }
    }
}

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

答案 4 :(得分:1)

虽然选项1可能很难看,但它可以完成工作。选项2甚至更加丑陋,并打开您无法想象的漏洞代码。即使您最终排除了选项1,我也建议您保留现有代码而不是选项2。

说完这些之后,如果您不想将此作为静态列表传递给方法,则可以使用反射来获取类的字段名称列表。假设您要比较所有字段,则可以循环动态创建比较。

如果不是这种情况,并且您比较的字符串只是某些字段,则可以进一步检查字段并仅隔离String类型的字段,然后继续进行比较。 / p>

希望这有帮助,

Yuval = 8 - )

答案 5 :(得分: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());
        }
    }

这个班有几个好处:

  • 在班级加载时反映一次
  • 只是添加新字段,只需添加新的静态字段(这里是一个更好的解决方案) 正在使用Annotations:在你关心使用反射的情况下也可以使用java 1.4)
  • 你可以在一个抽象类中重构这个类,所有派生类都得到两个 data和cloneAndLogDiffs()
  • 外部接口是类型安全的(您也可以轻松强加不变性)
  • 没有setAccessible来电:这种方法有时会出现问题

答案 6 :(得分:0)

我也会提出一个类似于Alnitak的解决方案。

如果在比较时需要迭代字段,为什么不省去单独的字段,并将数据放入数组,HashMap或类似的适当的东西。

然后您可以通过编程方式访问它们,比较它们等。如果需要处理不同的字段,通过不同的方式进行比较,您可以为值创建适当的辅助类,从而实现接口。

然后你可以做

valueMap.get("myobject").compareAndChange(valueMap.get("myotherobject")

或类似的东西......

答案 7 :(得分:0)

一个广泛的想法:

创建一个新类,其对象采用以下参数:要比较的第一个类,要比较的第二个类,以及getter和amp的列表。对象的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
            }
        }
    }
} 

答案 8 :(得分:0)

你说你现在有所有这些领域的吸气剂和制定者吗?好的,然后将基础数据从一堆单个字段更改为数组。更改所有getter和setter以访问该数组。我会为索引创建常量标记,而不是使用数字来实现长期可维护性。还要创建一个并行的标志数组,指示应处理哪些字段。然后创建一个使用索引的通用getter / setter对,以及compare标志的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,只使用该数组执行所有操作。

答案 9 :(得分:-1)

我正在编写一个名为jComparison(https://github.com/mmirwaldt/jcomparison)的框架,它找到两个java对象,字符串,映射和集合之间的差异(和相似之处)。 有许多演示向您展示如何使用它。 然而,这是一个alpha版本atm,我还没有决定我想选择哪个许可证。

注意:我是该框架的作者。

查看https://github.com/mmirwaldt/jcomparison/blob/master/core-demos/src/main/java/net/mirwaldt/jcomparison/core/object/ComparePersonsDemo.java

下的演示

它显示了一个虚构类Person和Address的示例。 演示的输出是:

Similarities:

private final net.mirwaldt.jcomparison.core.object.Person$Sex net.mirwaldt.jcomparison.core.object.Person.sex:
MALE


Differences:

private final double net.mirwaldt.jcomparison.core.object.Person.moneyInPocket:
ImmutableDoublePair{leftDouble=35.12, rightDouble=148.96}

private final int net.mirwaldt.jcomparison.core.object.Person.age:
ImmutableIntPair{leftInt=32, rightInt=45}


Comparisons:

name:
personA :   'Mich[a]el'
personB :   'Mich[]el'

Features:
Feature of personA only :   '{WRISTWATCH_BRAND=CASIO}'
Feature of personB only :   '{TATTOO_TEXT=Mum}'
personA and personB have different features :   '{SKIN_COLOR=ImmutablePair [leftValue= white, rightValue= black]}'
personA and personB have similar features:  '{HAIR_COLOR=brown}'

Leisure activities:
Leisure activities of personA only :    '[Piano]'
Leisure activities of personB only :    '[Tennis, Jogging]'
personA and personB have similar leisureActivities:     '[Swimming]'


Address:
Similarities:

private final int net.mirwaldt.jcomparison.core.object.Address.zipCode:
81245

private final net.mirwaldt.jcomparison.core.object.Address$Country net.mirwaldt.jcomparison.core.object.Address.country:
GERMANY

private final java.lang.String net.mirwaldt.jcomparison.core.object.Address.streetName:
August-Exter-Str.

private final java.lang.String net.mirwaldt.jcomparison.core.object.Address.city:
Munich


Differences:

private final int net.mirwaldt.jcomparison.core.object.Address.houseNumber:
ImmutableIntPair{leftInt=10, rightInt=12}