Каков наилучший способ сравнить несколько свойств Javabean? - PullRequest
14 голосов
/ 15 мая 2009

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

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. Сделайте, чтобы содержащий класс (объект) сделал сравнение, обновил измененные поля и возвратил сообщение журнала
Сущность не должна принимать участие в бизнес-логике

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

РЕДАКТИРОВАТЬ: В классе есть несколько полей, которые нельзя сравнивать.

Ответы [ 10 ]

16 голосов
/ 15 мая 2009

Использование Аннотации .

Если вы отметите поля, которые нужно сравнить (независимо от того, являются ли они частными, вы все равно не потеряете инкапсуляцию, а затем получите эти поля и сравните их. Это может быть следующим:

В классе, который нужно сравнить:

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

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

4 голосов
/ 15 мая 2009

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

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

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

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

2 голосов
/ 15 мая 2009

Я бы выбрал вариант 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 (я просто выбрасываю его в приведенном выше примере).
1 голос
/ 15 мая 2009

так

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

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

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)
  • Вы могли бы реорганизовать этот класс в абстрактный класс, все производные классы просто получили бы оба
    data и cloneAndLogDiffs ()
  • внешний интерфейс безопасен (вы также можете легко установить неизменность)
  • нет setAccessible вызовы: этот метод иногда проблематичен
1 голос
/ 15 мая 2009

Хотя вариант 1 может быть уродливым, он выполнит свою работу. Вариант 2 еще страшнее и открывает ваш код для уязвимостей, которые вы не можете себе представить. Даже если вы в конечном итоге исключите вариант 1, я прошу вас сохранить существующий код и не переходить к варианту 2.

Сказав это, вы можете использовать отражение, чтобы получить список имен полей класса, если вы не хотите передавать это как статический список методу. Предполагая, что вы хотите сравнить все поля, вы можете динамически создавать сравнения в цикле.

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

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

Ювал = 8 -)

1 голос
/ 15 мая 2009

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

Как насчет предоставления одной пары получатель / установщик, которая принимает числовое поле индекса, а затем получатель / установщик разыменовывает поле индекса с соответствующей переменной-членом?

т.е:.

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

Это, по крайней мере, позволяет вам сохранить всю важную логику в изучаемом классе.

0 голосов
/ 10 декабря 2017

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

Примечание: я являюсь автором этой структуры.

Посмотрите демоверсию под 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}
0 голосов
/ 15 мая 2009

Вы говорите, что в настоящее время у вас есть геттеры и сеттеры для всех этих полей? Хорошо, тогда измените базовые данные из группы отдельных полей в массив. Измените все методы получения и установки для доступа к массиву. Я бы создал постоянные теги для индексов, а не использовал числа для долгосрочной ремонтопригодности. Также создайте параллельный массив флагов, указывающих, какие поля должны быть обработаны. Затем создайте общую пару 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);
      }
    }
  }
}

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

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

0 голосов
/ 15 мая 2009

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

Создайте новый класс, объект которого принимает следующие параметры: первый класс для сравнения, второй класс для сравнения и списки имен методов получения и установки для объектов, в которые включены только интересующие методы.

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

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

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 голосов
/ 15 мая 2009

Я бы также предложил решение, подобное Alnitak.

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...