Pytanie Jaki jest najlepszy sposób porównania kilku właściwości javabean?


Muszę porównać dziesiątki pól w dwóch obiektach (instancje tej samej klasy) i wykonać pewne rejestrowanie i aktualizowanie w przypadku różnic. Kod Meta może wyglądać mniej więcej tak:

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

Kod ze wszystkimi porównaniami jest bardzo zwięzły i chciałbym go jakoś uprościć. Byłoby miło, gdybym mógł mieć metodę, która wzięłaby jako wywołanie metody parametrów do ustawiacza i pobierającego i wywoływała to dla wszystkich pól, ale niestety nie jest to możliwe w przypadku java.

Wymyśliłem trzy opcje, z których każda ma swoje wady.

1. Użyj interfejsu API do refleksji, aby dowiedzieć się, kto pobiera i ustawia
Brzydki i może powodować błędy czasu pracy w przypadku zmiany nazw pól

2. Zmień pola na publiczne i manipuluj nimi bezpośrednio, bez używania modułów pobierających i ustawiających
Również brzydki i wystawiłby implementację klasy na świat zewnętrzny

3. Wykonaj porównanie klasy zawierającej (obiekt), zaktualizuj zmienione pola i zwróć komunikat dziennika
Podmiot nie powinien brać udziału w logice biznesowej

Wszystkie pola są typu String i mogę w razie potrzeby zmodyfikować kod klasy będącej właścicielem pól.

EDYCJA: W klasie jest kilka pól, których nie wolno porównywać.


14
2018-05-15 07:47


pochodzenie


Czy porównujesz wystąpienia dwóch różnych klas lub dwóch wystąpień tej samej klasy? - rudolfson
Są to instancje tej samej klasy. Zmontowałem to pytanie. Dzięki za wskazanie tego. - tputkonen


Odpowiedzi:


Posługiwać się Adnotacje.

Jeśli zaznaczysz pola, które chcesz porównać (nie ważne, czy są one prywatne, nadal nie tracisz enkapsulacji, a następnie uzyskaj te pola i porównaj je. Może to wyglądać następująco:

W klasie, które należy porównać:

@ComparableField 
private String field1;

@ComparableField
private String field2;

private String field_nocomparable;

A w klasie zewnętrznej:

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

Kod nie jest testowany, ale daj mi znać, jeśli pomoże.


16
2018-05-15 08:48



Wciąż nieco brzydka, ponieważ używa refleksji, ale jeśli chcesz użyć refleksji, prawdopodobnie najlepsza opcja. - sleske
Powyższy kod jest niekompletny, nie odwiedza pól należących do klas Super - do tego potrzebna jest rekursja. - mP.
Uwaga dla osoby używającej tego: pamiętaj, aby zdefiniować @Retention (RetentionPolicy.RUNTIME) dla interfejsu adnotacji. - tputkonen
Nice Odpowiedź +1! Ale jedna mała uwaga: Kontrola (pola! = Zero) jest niepotrzebna, ponieważ getDeclaredFields nigdy nie zwraca wartości null. - Tim Büthe
leki generyczne nie mają tutaj żadnego wpływu. jest to odpowiednik public void compare(Object t, Object t2) - user102008


The JavaBeans API ma na celu pomóc w introspekcji. Pojawił się w takiej czy innej formie od wersji Java 1.2 i był dość użyteczny od wersji 1.4.

Kod demonstracyjny, który porównuje listę właściwości w dwóch komponentach 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);
    }
  }

Przykładowa inwokacja:

compareBeans(System.out, bean1, bean2, "foo", "bar");

Jeśli przejdziesz do trasy adnotacji, rozważ odbijanie refleksów i generowanie kodu porównania za pomocą kompilacji procesor adnotacji lub jakiś inny generator kodu.


4
2018-05-15 09:13



Dzięki, wydaje się to idealne! - tputkonen
Gdzie to rozwiązanie różni się od opcji 1? Jest ładniejszy ze względu na użycie API Beans zamiast bezpośredniego korzystania z API Relections. Ale wciąż musisz wymienić wszystkie swoje pola do porównania, nadal pozostawiając problem, gdy zmienisz niektóre z nich. A może coś przeoczyłem? - rudolfson
Tak, ta kwestia pozostaje, ale nie widzę sposobu na obejście jej (bez adnotacji). Jednak odbicie jest znacznie bardziej "niskie" w porównaniu do BeanInfo. - tputkonen
Jeśli (jak podano) logika dla tego porównania / aktualizacji musi być usunięta z komponentu bean, wówczas adnotacja pól nie jest idealną decyzją projektową (chociaż umieszczenie całej logiki w jednym miejscu jest miłe). Podejście, które przedstawiłem, również nie jest idealne - ze swojej natury refleksja nie może wychwycić problemów podczas kompilacji (chociaż brakujące pola zostaną przechwycone w czasie wykonywania). Wygenerowane połączenia bezpośrednie (oparte na adnotacjach lub na listach właściwości) byłyby lepsze niż jedno i unikają pisania tablicy kotłów, ale tylko OP wie, czy warto to robić. - McDowell
W rzeczywistości zmieniłem zdanie i zdecydowałem się przejść w sposób adnotacyjny, ze względu na bezpieczeństwo typu iw pewnym sensie naturalne jest określenie pól do porównania w jednostce. Dzięki za wszystkich, którzy odpowiedzieli, wiele się nauczyłem! - tputkonen


Wybrałbym opcję 1, ale użyłbym getClass().getDeclaredFields() aby uzyskać dostęp do pól zamiast używać nazw.

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

Tutaj są pewne ograniczenia (jeśli mam rację):

  • Metoda porównywania musi być zaimplementowana w tej samej klasie (moim zdaniem powinna - niezależnie od jej implementacji) nie w zewnętrznej.
  • Tylko pola z tej klasy są używane, a nie te z nadklasy.
  • Obsługa koniecznego wyjątku IllegalAccessException (po prostu wyrzucam go w powyższym przykładzie).

2
2018-05-15 08:22



To porównałoby wszystkie pola, ale jest kilka pól, których nie chcę porównywać. - tputkonen
Porównuje wszystkie pola String, po prawej. Nie widziałem tego z twojego wyjaśnienia. Możesz jednak "zaznaczyć" pola, które chcesz porównać, używając własnej podklasy String, po prostu implementiny niezbędnych konstruktorów. To może być wewnętrzna prywatna klasa twojej klasy. Osoby pobierające i ustawiające dla pól powinny nadal używać String. - rudolfson
Albo jeszcze lepiej (przepraszam za tak wiele komentarzy :-)), użyj adnotacji na tych polach. - rudolfson
Jak wspomniałem powyżej - to tylko porównuje Pola w bieżącej klasie, nie robi pól odwiedzin w super klasie. - mP.
@mP Wspomniałem już o tym na mojej liście ograniczeń. - rudolfson


Prawdopodobnie nie jest to zbyt piękne, ale jest o wiele mniej złe (IMHO) niż jedna z dwóch proponowanych przez ciebie alternatyw.

Co powiesz na dostarczenie pojedynczej pary getter / setter, która pobiera pole indeksowe liczbowe, a następnie ma getter / setter dereferencji pole indeks do odpowiedniej zmiennej członek?

to znaczy.:

public class MyClass {
    public void setMember(int index, String value) {
        switch (index) {
           ...
        }
    }

    public String getMember(int index) {
        ...
    }

    static public String getMemberName(int index) {
        ...
    }
}

A następnie w twojej zewnętrznej klasie:

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

To przynajmniej pozwala zachować całą istotną logikę w klasie, która jest badana.


1
2018-05-15 07:57



Nie zadziałałoby to, ponieważ istnieje kilka pól, których nie chcę porównywać. Przepraszam, że nie wskazałem tego w pytaniu, będę je edytować. Dzięki za pomysł. - tputkonen
to dobrze - po prostu nie wystawiaj tych pól na przykładowy kod, który dałem. - Alnitak


Podczas gdy opcja 1 może być brzydka, dostanie zadanie. Opcja 2 jest jeszcze brzydsza i otwiera twój kod na luki, których nie możesz sobie wyobrazić. Nawet jeśli ostatecznie wykluczysz opcję 1, modlę się, abyś zachował swój istniejący kod i nie wybieraj opcji 2.

Powiedziawszy to, możesz użyć odbicia, aby uzyskać listę nazw pól klasy, jeśli nie chcesz przekazać tej listy jako statycznej listy. Zakładając, że chcesz porównać wszystkie pola, możesz dynamicznie tworzyć porównania w pętli.

Jeśli tak nie jest, a łańcuchy, które porównujesz, są tylko niektórymi polami, możesz dalej badać pola i izolować tylko te, które są typu String, a następnie przystąp do porównania.

Mam nadzieję że to pomoże,

Yuval = 8-)


1
2018-05-15 07:58





od

Wszystkie pola są typu String i mogę w razie potrzeby zmodyfikować kod klasy będącej właścicielem pól.

możesz spróbować tej klasy:

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

magiczny kod:

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

ta klasa ma kilka zalet:

  • odzwierciedla się raz, podczas ładowania klasy
  • to jest po prostu dodawanie nowych pól, wystarczy dodać nowe pole statyczne (lepsze rozwiązanie tutaj korzysta z adnotacji: w przypadku, gdy dbasz o to, że używasz refleksji, również java 1.4)
  • możesz zmienić tę klasę w klasie abstrakcyjnej, wszystkie klasy pochodne po prostu uzyskają obie
    dane i cloneAndLogDiffs ()
  • zewnętrzny interfejs jest bezpieczny (można też łatwo narzucić niezmienność)
  • Nie setAccessible wzywa: ta metoda czasami jest problematyczna

1
2018-05-15 09:41





Szeroka myśl:

Utwórz nową klasę, której obiekt przyjmuje następujące parametry: pierwszą klasę do porównania, drugą klasę do porównania oraz listę nazw metod pobierających i ustawiających dla obiektów, w których uwzględnione są tylko interesujące metody.

Możesz zapytać za pomocą refleksji o klasę obiektu, a także o jego dostępne metody. Zakładając, że każda metoda gettera na liście parametrów jest zawarta w dostępnych metodach dla klasy, powinieneś być w stanie wywołać metodę, aby uzyskać wartość do porównania.

Z grubsza naszkicowałem coś takiego (przepraszam, jeśli nie jest super-doskonały ... nie mój główny język):

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
2018-05-15 09:12





Mówisz, że obecnie masz zdobywców i seterów dla wszystkich tych pól? Okay, a następnie zmień podstawowe dane z grupy pojedynczych pól na tablicę. Zmień wszystkie moduły pobierające i ustawiające, aby uzyskać dostęp do tablicy. Stworzyłem stałe tagi dla indeksów, zamiast używać liczb do długoterminowej konserwacji. Utwórz także równoległą tablicę flag wskazującą, które pola powinny zostać przetworzone. Następnie utwórz ogólną parę getter / setter, która używa indeksu, a także getter dla flagi porównania. Coś takiego:

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

Właśnie napisałem to z mojej głowy, nie testowałem tego, więc mogą to być błędy w kodzie, ale myślę, że koncepcja powinna zadziałać.

Ponieważ już masz moduły pobierające i ustawiające, każdy inny kod korzystający z tej klasy powinien nadal działać niezmieniony. Jeśli nie ma innego kodu używającego tej klasy, wyrzuć istniejące pliki pobierające i ustawiające i po prostu wykonaj wszystko za pomocą tablicy.


0
2018-05-15 17:43



Nasz komponent Java bean jest również jednostką JPA, więc takie podejście nie zadziałałoby w naszym przypadku. - tputkonen


Proponowałbym również rozwiązanie podobne do rozwiązania Alnitaka.

Jeśli pola muszą być powtórzone podczas porównywania, to dlaczego nie zrezygnować z oddzielnych pól i umieścić dane w tablicy, HashMap lub coś podobnego, co jest właściwe.

Następnie można uzyskać do nich dostęp programowo, porównać je itp. Jeśli różne pola muszą być traktowane i porównywane na różne sposoby, można utworzyć odpowiednie klasy pomocnicze dla wartości, które implementują interfejs.

Wtedy możesz po prostu zrobić

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

lub coś w tym stylu ...


0
2018-05-15 08:55





Programuję framework o nazwie jComparison (https://github.com/mmirwaldt/jcomparison), która znajduje różnice (i podobieństwa) między dwoma obiektami java, łańcuchami, mapami i kolekcjami. Istnieje wiele pokazów, które pokazują, jak z niego korzystać. Jednak jest to bankomat w wersji alpha i nie jestem niezdecydowany, którą licencję chcę wybrać.

Uwaga: jestem autorem tej struktury.

Spójrz na demo w ramach https://github.com/mmirwaldt/jcomparison/blob/master/core-demos/src/main/java/net/mirwaldt/jcomparison/core/object/ComparePersonsDemo.java

Pokazuje przykład z fikcyjnymi klasami Osoba i adres. Dane wyjściowe wersji demonstracyjnej:

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}

-1
2017-12-10 00:30



Ten post nie wygląda na próbę odpowiedzi na to pytanie. Każdy post tutaj powinien być wyraźną próbą odpowiedź to pytanie; jeśli masz krytykę lub potrzebujesz wyjaśnienia pytania lub innej odpowiedzi, możesz to zrobić zostaw komentarz (jak ten) bezpośrednio pod nim. Usuń tę odpowiedź i utwórz komentarz lub nowe pytanie. Widzieć: Zadawaj pytania, otrzymuj odpowiedzi, nie rozpraszaj - Machavity
Samo połączenie z własną biblioteką lub samouczkiem nie jest dobrą odpowiedzią. Powiązanie z nim, wyjaśnienie, dlaczego rozwiązuje problem, dostarczenie kodu, jak to zrobić i zrzeczenie się, że je napisałeś (co zrobiłeś), daje lepszą odpowiedź. Widzieć: Co oznacza "dobra" autopromocja? i Jak nie być spamerem. - Makyen
Poprawię moją odpowiedź - mmirwaldt
Edytowałem swoją odpowiedź. Myślę, że to pytanie implikuje bardziej ogólne rozwiązanie problemu znajdowania różnic między dwoma obiektami java. - mmirwaldt