Каков наилучший способ сравнить несколько свойств 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}