2009-05-15 8 views
14

J'ai besoin de comparer des dizaines de champs dans deux objets (instances de la même classe), et de faire de la journalisation et mise à jour en cas de différences. Code Meta pourrait ressembler à ceci:Quel est le meilleur moyen de comparer plusieurs propriétés javabéennes?

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 

Le code avec toutes les comparaisons est très laconique, et je voudrais faire en quelque sorte plus compact. Ce serait bien si je pouvais avoir une méthode qui prendrait comme paramètre la méthode calls to setter et getter, et l'appelle pour tous les champs, mais malheureusement ce n'est pas possible avec java.

J'ai proposé trois options, chacune ayant ses propres inconvénients.

1. Utilisez l'API de réflexion pour trouver des accesseurs
laid et pourrait provoquer des erreurs d'exécution dans les noms de cas des champs changent

2. Changer les champs pour le public et les manipuler directement sans utiliser getters et setters
laid aussi bien et exposerait la mise en œuvre de la classe à

monde extérieur

3. Demandez la classe contenant (entité) faire la comparaison, la mise à jour changé les champs et ret message journal urne
entité ne doit pas prendre part à la logique métier

Tous les champs sont de type chaîne, et je peux modifier le code de la classe propriétaire des champs si nécessaire.

EDIT: Certains champs de la classe ne doivent pas être comparés.

+0

Vous comparez les instances de deux classes différentes ou deux instances de la même classe? – rudolfson

+0

Ce sont des instances de la même classe. J'ai édité la question. Merci de l'avoir signalé. – tputkonen

Répondre

16

Utilisez Annotations.

Si vous marquez les champs que vous avez besoin de comparer (peu importe si elles sont privées, vous ne perdez pas encore l'encapsulation, puis obtenir les champs et les comparer Il pourrait être le suivant:.

dans la classe qui doivent être comparés:

@ComparableField 
private String field1; 

@ComparableField 
private String field2; 

private String field_nocomparable; 

Et dans la classe externe.

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

le code n'a pas été testé, mais laissez-moi savoir si aide

+0

Encore un peu moche car il utilise la réflexion, mais si vous voulez utiliser la réflexion, c'est probablement la meilleure option. – sleske

+1

Le code ci-dessus est incomplet, il ne visite pas les champs appartenant à Super classes - pour cela, vous aurez besoin d'une certaine récursivité. –

+3

Avis pour quelqu'un utilisant ceci: n'oubliez pas de définir @Retention (RetentionPolicy.RUNTIME) pour votre interface d'annotation. – tputkonen

1

Ce n'est probablement pas trop beau non plus, mais c'est beaucoup moins mauvais (IMHO) que l'une des deux alternatives que vous avez proposées.

Que diriez-vous de fournir une seule paire de getter/setter qui prend un champ d'index numérique et ensuite getter/setter déréférencer le champ d'index à la variable membre pertinente?

i.e. .:

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

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

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

Et puis dans votre classe externe:

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

Ce au moins vous permet de garder toute la logique importante dans la classe qui est en cours d'examen.

+0

Cela ne fonctionnerait pas car il y a des champs que je ne veux pas comparer. Désolé de ne pas le signaler dans la question, je vais le modifier. Merci pour l'idée cependant. – tputkonen

+0

c'est bien - ne pas exposer ces champs dans l'exemple de code que j'ai donné. – Alnitak

1

Alors que l'option 1 peut être laide, elle fera le travail. L'option 2 est encore plus laide, et ouvre votre code à des vulnérabilités que vous ne pouvez pas imaginer. Même si vous finissez par exclure l'option 1, je vous prie de conserver votre code existant et de ne pas opter pour l'option 2.

Cela dit, vous pouvez utiliser la réflexion pour obtenir une liste des noms de champs de la classe, si vous ne le faites pas. ne veux pas passer cela comme une liste statique à la méthode. En supposant que vous voulez comparer tous les champs, vous pouvez ensuite créer dynamiquement les comparaisons, dans une boucle.

Si ce n'est pas le cas, et que les chaînes que vous comparez ne sont que quelques champs, vous pouvez examiner les champs plus en détail et isoler uniquement ceux qui sont de type String, puis procéder à la comparaison.

Hope this helps,

Yuval = 8)

2

Je pencherais pour l'option 1, mais je voudrais utiliser getClass().getDeclaredFields() pour accéder aux champs au lieu d'utiliser les noms.

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

Il y a quelques restrictions ici (si je ne me trompe pas):

  • La comparaison méthode doit être mis en œuvre dans la même classe (à mon avis, il devrait - quelle que soit sa mise en œuvre) non dans un externe.
  • Seuls les champs de cette classe sont utilisés, pas ceux d'une superclasse.
  • Traitement de IllegalAccessException nécessaire (je le lance dans l'exemple ci-dessus).
+0

Ceci comparerait tous les champs mais il y a quelques champs que je ne veux pas comparer. – tputkonen

+0

Il compare tous les champs String, à droite. Je n'ai pas vu cela à partir de votre explication. Vous pouvez cependant marquer les champs que vous voulez comparer en utilisant une propre sous-classe de String, mais implémentez simplement les constructeurs nécessaires. Cela pourrait être une classe privée interne de votre classe. Les getters et setters pour les champs devraient toujours utiliser String. – rudolfson

+0

Ou mieux encore (désolé pour tant de commentaires :-)), utilisez une annotation sur ces champs. – rudolfson

0

Je proposerais également une solution similaire à celle d'Alnitak.

Si les champs doivent être itérés lors de la comparaison, pourquoi ne pas se passer des champs séparés, et mettre les données dans un tableau, un HashMap ou quelque chose de similaire qui est approprié.

Ensuite, vous pouvez y accéder par programme, les comparer, etc. Si différents champs doivent être traités de différentes manières, vous pouvez créer des classes auxiliaires appropriées pour les valeurs qui implémentent une interface.

alors vous pouvez faire

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

ou quelque chose le long de ces lignes ...

0

Une large pensée:

Créer une nouvelle classe dont l'objet prend les paramètres suivants: la première classe pour comparer, la deuxième classe à comparer, et une liste de getter & noms de méthodes de réglage pour les objets, où seules les méthodes d'intérêt sont inclus.

Vous pouvez interroger par réflexion la classe de l'objet et, par conséquent, ses méthodes disponibles. En supposant que chaque méthode getter de la liste des paramètres est incluse dans les méthodes disponibles pour la classe, vous devriez pouvoir appeler la méthode pour obtenir la valeur pour la comparaison.

À peu près esquissé quelque chose comme (excuse si ce n'est pas super-parfait ...pas ma langue maternelle):

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 
      } 
     } 
    } 
} 
4

Le JavaBeans API vise à aider à l'introspection. Il a été sous une forme ou une autre depuis la version 1.2 de Java et a été très utilisable depuis la version 1.4.

Code de démonstration qui compare une liste de propriétés à deux grains:

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

invocation Exemple:

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

Si vous allez la route des annotations, envisager le dumping réflexion et générer le code de comparaison avec un compilation annotation processor ou un autre générateur de code.

+0

Merci, cela semble parfait! – tputkonen

+1

Où cette solution est-elle différente de l'option 1? C'est plus agréable en raison de l'utilisation de l'API Beans au lieu d'utiliser directement l'API Relections. Mais vous devez toujours nommer tous vos champs à des fins de comparaison, tout en laissant le problème lorsque vous renommez certains d'entre eux. Ou ai-je oublié quelque chose? – rudolfson

+1

Oui, ce problème persiste, mais je ne vois pas comment le contourner (sans annotations). Cependant la réflexion est beaucoup plus "bas niveau" par rapport à BeanInfo. – tputkonen

1

depuis

Tous les champs sont de type chaîne, et je peux modifier le code de la classe propriétaire des champs si nécessaire.

vous pouvez essayer cette classe:

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

code magique:

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

cette classe a plusieurs avantages:

  • reflète une fois, au chargement des classes
  • c'est très sim ply ajouter de nouveaux champs, il suffit d'ajouter un nouveau champ statique (une meilleure solution ici utilise Annotations: dans le cas où vous vous souciez d'utiliser la réflexion fonctionne également java 1.4)
  • vous pouvez refactoriser cette classe dans une classe abstraite, toute la classe dérivée juste obtenir à la fois
    données et cloneAndLogDiffs()
  • l'interface externe est typesafe (vous pouvez aussi imposer facilement immuabilité)
  • pas setAccessible appels: cette méthode est problématique parfois
0

vous dites que vous avez actuellement accesseurs pour tous ces des champs? D'accord, modifiez les données sous-jacentes d'un groupe de champs individuels en un tableau. Changez tous les getters et setters pour accéder au tableau. Je créerais des balises constantes pour les index plutôt que d'utiliser des nombres pour la maintenabilité à long terme. Créez également un tableau parallèle d'indicateurs indiquant quels champs doivent être traités. Créez ensuite une paire getter/setter générique qui utilise un index, ainsi qu'un getter pour l'indicateur de comparaison. Quelque chose comme ceci:

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

Je viens d'écrire ce du haut de ma tête, je n'ai pas testé, donc peut-être leur bugs dans le code, mais je pense que le concept devrait fonctionner.

Comme vous avez déjà des getters et des setters, tout autre code utilisant cette classe devrait continuer à fonctionner tel quel. S'il n'y a pas d'autre code utilisant cette classe, jetez les getters et setters existants et faites tout simplement avec le tableau.

+0

Notre bean Java est également une entité JPA donc cette approche ne fonctionnerait pas dans notre cas. – tputkonen

-1

Je suis en train de programmer un framework appelé jComparison (https://github.com/mmirwaldt/jcomparison) qui trouve des différences (et des similarités) entre deux objets java, des chaînes, des cartes et des collections. De nombreuses démos vous montrent comment l'utiliser. Cependant, il s'agit d'une version alpha atm et je suis indécis quelle licence je veux choisir.

Note: Je suis l'auteur de ce cadre.

Regardez la démo sous https://github.com/mmirwaldt/jcomparison/blob/master/core-demos/src/main/java/net/mirwaldt/jcomparison/core/object/ComparePersonsDemo.java

Il montre un exemple avec les classes Personne et adresse fictive. La sortie de la démo est:

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

Ce message ne semble pas être une tentative pour répondre à cette question. Chaque message ici devrait être une tentative explicite de * répondre * à cette question; Si vous avez une critique ou avez besoin d'une clarification de la question ou d'une autre réponse, vous pouvez [poster un commentaire] (// stackoverflow.com/help/privileges/comment) (comme celui-ci) directement en dessous. Veuillez supprimer cette réponse et créer un commentaire ou une nouvelle question. Voir: [Poser des questions, obtenir des réponses, pas de distractions] (// stackoverflow.com/tour) – Machavity

+0

Un simple lien vers votre propre bibliothèque ou tutoriel n'est pas une bonne réponse. Lier à cela, expliquer pourquoi il résout le problème, fournir un code sur la façon de le faire et nier que vous l'avez écrit (ce que vous avez fait) apporte une meilleure réponse. Voir: [** Qu'est-ce qui signifie "Bon" auto-promotion? **] (// meta.stackexchange.com/q/182212) et [Comment ne pas être un spammeur] (// stackoverflow.com/help/promotion). – Makyen

+0

Je vais améliorer ma réponse – mmirwaldt

Questions connexes