Каков наилучший способ сравнить несколько свойств javabean?

Мне нужно сравнить десятки полей в двух объектах (экземпляры одного и того же класса) и сделать некоторые записи и обновления в случае наличия различий. Мета-код может выглядеть примерно так:

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 отражения, чтобы узнать, как получить геттеры и сеттеры.
Ужасно и может вызвать ошибки времени выполнения в именах полей полей

2. Измените поля на публичные и манипулируйте ими напрямую, не используя геттеры и сеттеры.
Уродливые, а также продемонстрировали бы реализацию класса во внешнем мире

3. Содержит содержащий класс (сущность) сравнение, обновить измененные поля и вернуть сообщение журнала
Сущность не должна участвовать в бизнес-логике

Все поля являются строковыми типами, и я могу изменить код класса, владеющего полями, если это необходимо.

EDIT: В классе есть несколько полей, которые нельзя сравнивать.

Ответы

Ответ 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);
            }
        }
    }
}

Код не проверен, но дайте мне знать, если это поможет.

Ответ 2

API JavaBeans предназначен для помощи в интроспекции. Он был в той или иной форме с версии Java версии 1.2 и был довольно полезен с версии 1.4.

Демо-код, который сравнивает список свойств в двух beans:

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

Если вы идете по маршруту аннотаций, подумайте о демпинговом отражении и генерируете код сравнения с компилятором обработчик аннотации или какой-либо другой генератор кода.

Ответ 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);
            }
        }
    }
}

Здесь есть некоторые ограничения (если я прав):

  • Метод сравнения должен быть реализован в том же классе (по-моему, он должен - независимо от его реализации) не во внешнем.
  • Используются только поля этого класса, а не одно из суперкласса.
  • Требуется обращение с IllegalAccessException (я просто брошу его в примере выше).

Ответ 4

Это, вероятно, тоже не очень хорошо, но это намного меньше зла (IMHO), чем любая из двух предложенных вами альтернатив.

Как насчет предоставления одной пары 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, а затем перейти к сравнению.

Надеюсь, что это поможет,

Yuval = 8 -)

Ответ 6

так

Все поля являются строковыми типами, и я могу изменить код класса, владеющего полями, если это необходимо.

вы можете попробовать этот класс:

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()
  • внешний интерфейс является типичным (вы также можете легко навязывать неизменность)
  • no setAccessible вызывает: иногда этот метод проблематичен

Ответ 7

Я также предложил бы аналогичное решение тому, что сделал Алнитак.

Если при сопоставлении полей необходимо повторять, почему бы не обойтись без отдельных полей, и поместить данные в массив, HashMap или что-то подобное, что подходит.

Затем вы можете обращаться к ним программно, сравнивать их и т.д. Если разные поля нужно обрабатывать и сравнивать по-разному, вы можете создать классы вспомогательных помощников для значений, которые реализуют интерфейс.

Тогда вы могли бы просто сделать

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

или что-то в этом роде...

Ответ 8

Широкая мысль:

Создайте новый класс, чей объект принимает следующие параметры: первый класс для сравнения, второй класс для сравнения и списки имен методов 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
            }
        }
    }
} 

Ответ 9

Вы говорите, что у вас есть геттеры и сеттеры для всех этих полей? Итак, измените базовые данные из группы отдельных полей в массив. Измените все получатели и сеттеры для доступа к массиву. Я бы создал постоянные теги для индексов, а не использовал числа для долгосрочной ремонтопригодности. Также создайте параллельный массив флагов, указывающий, какие поля должны обрабатываться. Затем создайте общую пару getter/setter, в которой используется индекс, а также геттер для флага сравнения. Что-то вроде этого:

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

Я только что написал это с головы, я его не тестировал, поэтому их могут быть ошибки в коде, но я думаю, что концепция должна работать.

Поскольку у вас уже есть получатели и сеттеры, любой другой код, использующий этот класс, должен продолжать работать без изменений. Если нет другого кода, использующего этот класс, отбросьте существующие геттеры и сеттеры и просто сделайте все с помощью массива.

Ответ 10

Я программирую структуру под названием jComparison (https://github.com/mmirwaldt/jcomparison), которая обнаруживает различия (и сходства) между двумя объектами java, строками, картами и коллекции. Есть много демоверсий, которые показывают вам, как их использовать. Однако это альфа-версия 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}