【问题标题】:What is the best way to compare several javabean properties?比较几个 javabean 属性的最佳方法是什么?
【发布时间】:2009-05-15 07:47:18
【问题描述】:

我需要比较两个对象(同一类的实例)中的数十个字段,并进行一些记录和更新以防有差异。元代码可能如下所示:

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

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

编辑:类中有一些字段不能比较。

【问题讨论】:

  • 您是在比较两个不同类的实例还是同一类的两个实例?
  • 它们是同一类的实例。我编辑了这个问题。感谢您指出这一点。

标签: java reflection


【解决方案1】:

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

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

【讨论】:

  • 使用反射还是有点难看,但如果你想使用反射,可能是最好的选择。
  • 上面的代码不完整,它没有访问属于超类的字段——因为你需要一些递归。
  • 使用这个的人注意:记得为你的注释接口定义@Retention(RetentionPolicy.RUNTIME)。
  • 不错的答案 +1!但有一点需要注意: (fields != null) 检查是不必要的,因为 getDeclaredFields 永远不会返回 null。
  • 泛型在这里没有作用。相当于public void compare(Object t, Object t2)
【解决方案2】:

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 或其他代码生成器生成比较代码。

【讨论】:

  • 此解决方案与选项 1 有何不同?由于使用 Beans API 而不是直接使用 Reelections API,它会更好。但是您仍然必须为所有字段命名以进行比较,当您重命名其中一些字段时仍然会出现问题。还是我忽略了什么?
  • 是的,这个问题仍然存在,但我看不到解决它的方法(没有注释)。然而,与 BeanInfo 相比,反射更加“低级”。
  • 如果(如前所述)此比较/更新的逻辑必须保留在 bean 之外,那么注释字段不是一个理想的设计决策(尽管将所有逻辑放在一个地方很好) .我概述的方法也不理想 - 就其性质而言,反射无法在编译时捕获问题(尽管缺失的字段将在运行时被捕获)。生成的直接调用(基于注释或基于属性列表)会比任何一个都好,并且避免编写样板,但只有 OP 知道它是否值得努力。
  • 事实上,我改变了主意并决定采用 Annotations 方式,因为类型安全,并且在某种程度上指定要在实体中比较的字段是很自然的。感谢所有回答的人,我学到了很多东西!
  • 到目前为止,这是一个简单而优雅的解决方案,但是有人可以解释一下这两行kode在方法中的作用吗? \n 方法设置器 = prop.getWriteMethod(); \n setter.invoke(bean2, value2);
【解决方案3】:

我会选择选项 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);
            }
        }
    }
}

这里有一些限制(如果我是对的):

  • compare 方法必须在同一个类中实现(我认为它应该——不管它的实现如何)而不是在外部类中。
  • 仅使用此类中的字段,而不使用超类中的字段。
  • 需要处理 IllegalAccessException(我只是在上面的示例中抛出它)。

【讨论】:

  • 这将比较所有字段,但有些字段我不想比较。
  • 它比较所有字符串字段,对。从你的解释中没有看到这一点。但是,您可以通过使用自己的 String 子类来“标记”要比较的字段,只需实现必要的构造函数。这可能是您班级的内部私人班级。字段的 getter 和 setter 仍应使用 String。
  • 或者更好(抱歉这么多 cmets :-)),在这些字段上使用注释。
  • 正如我上面提到的 - 这只会比较当前类中的字段,它不会访问超类中的字段。
【解决方案4】:

这也可能不太好,但它远没有你提出的两个替代方案那么邪恶(恕我直言)。

提供一个获取数字索引字段的单个 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);
        }
    }
}

这至少允许您将所有重要的逻辑保留在正在检查的类中。

【讨论】:

  • 这行不通,因为有些字段我不想比较。很抱歉没有在问题中指出这一点,我将对其进行编辑。不过谢谢你的想法。
  • 没关系 - 只是不要在我提供的示例代码中公开这些字段。
【解决方案5】:

虽然选项 1 可能很难看,但它可以完成工作。选项 2 更难看,它会将您的代码暴露给您无法想象的漏洞。即使您最终排除了选项 1,我也祈祷您保留现有代码,不要选择选项 2。

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

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

希望这会有所帮助,

尤瓦尔=8-)

【讨论】:

    【解决方案6】:

    因为

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

    你可以试试这个课程:

    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调用:这种方法有时会出现问题

    【讨论】:

      【解决方案7】:

      一个广泛的想法:

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

      【讨论】:

        【解决方案8】:

        您说您目前对所有这些领域都有 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 实体,所以这种方法在我们的例子中不起作用。
        【解决方案9】:

        我也会提出与 Alnitak 类似的解决方案。

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

        然后您可以以编程方式访问它们,比较它们等。如果需要以不同方式处理和比较不同的字段,您可以为这些值创建适当的帮助类,实现一个接口。

        那你就可以了

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

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-11-28
          • 2015-08-21
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多