2009-05-15 12 views
14

Necesito comparar docenas de campos en dos objetos (instancias de la misma clase), y hacer un poco de registro y actualización en caso de que haya diferencias. Meta código podría ser algo como esto:¿Cuál es la mejor manera de comparar varias propiedades 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 

El código con todas las comparaciones es muy concisa, y me gustaría hacer de alguna manera más compacta. Sería bueno si pudiera tener un método que tomaría como método de parámetros las llamadas a setter y getter, y llamar a esto para todos los campos, pero desafortunadamente esto no es posible con java.

He encontrado tres opciones, cada una de las cuales tiene sus propios inconvenientes.

1. Uso de la API de reflexión para averiguar captadores y definidores
feo y podría causar errores de tiempo de ejecución en nombres de los casos de campos cambian

2. Cambiar los campos al público y manipularlos directamente sin utilizar captadores y definidores
feo, así y expondría implementación de la clase de mundo externo

3. Tener la clase que contiene (entidad) hacer la comparación, actualizar cambiado campos y ret urna mensaje de registro
Entidad no deben tomar parte en la lógica de negocio

Todos los campos son de tipo String, y puedo modificar el código de la clase propietaria de los campos, si es necesario.

EDITAR: Hay algunos campos de la clase que no se pueden comparar.

+0

¿Está comparando los casos de dos clases diferentes o dos instancias de la misma clase? – rudolfson

+0

Son instancias de la misma clase. Edité la pregunta. Gracias por señalar esto. – tputkonen

Respuesta

16

Use Annotations.

Si marca los campos que usted necesita para comparar (no importa si son privados, todavía no pierde la encapsulación, y luego conseguir esos campos y compararlos Podría ser de la siguiente manera:.

en la clase que necesita ser comparada:

@ComparableField 
private String field1; 

@ComparableField 
private String field2; 

private String field_nocomparable; 

Y en la clase externa:.

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

el código no se prueban, pero quiero saber si ayuda a

+0

Todavía es algo feo, ya que utiliza la reflexión, pero si desea utilizar la reflexión, probablemente la mejor opción. – sleske

+1

El código anterior está incompleto. No visita los campos pertenecientes a las clases Super, para eso necesitará alguna recursión. –

+3

Aviso para alguien que usa esto: recuerde definir @Retention (RetentionPolicy.RUNTIME) para su interfaz de anotación. – tputkonen

1

Esto probablemente tampoco sea demasiado agradable, pero es mucho menos malo (en mi humilde opinión) que cualquiera de las dos alternativas que ha propuesto.

¿Qué le parece proporcionar un solo par getter/setter que tome un campo de índice numérico y luego tener la desreferencia de getter/setter del campo de índice a la variable miembro relevante?

es decir .:

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

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

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

Y luego en su clase externa:

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

Esto al menos le permite mantener toda la lógica importante en la clase que está siendo examinada.

+0

Esto no funcionaría porque hay algunos campos que no quiero comparar. Perdón por no señalar esto en la pregunta, lo editaré. Gracias por la idea sin embargo. – tputkonen

+0

está bien, simplemente no exponga esos campos en el código de ejemplo que di. – Alnitak

1

Si bien la opción 1 puede ser fea, hará el trabajo. La opción 2 es aún más fea y abre tu código a vulnerabilidades que no puedes imaginar. Incluso si finalmente descarta la opción 1, le ruego que conserve su código actual y no opte por la opción 2.

Habiendo dicho esto, puede usar el reflejo para obtener una lista de los nombres de campo de la clase, si No quiero pasar esto como una lista estática al método. Suponiendo que desea comparar todos los campos, puede crear dinámicamente las comparaciones en un bucle.

Si este no es el caso, y las cadenas que compara son solo algunos de los campos, puede examinar más los campos y aislar solo aquellos que son del tipo String, y luego proceder a comparar.

Espero que esto ayude,

Yuval = 8-)

2

Yo iría por la opción 1, pero me gustaría utilizar getClass().getDeclaredFields() acceder a los campos en lugar de utilizar los nombres.

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

Hay algunas restricciones aquí (si no me equivoco):

  • El método de comparación tiene que ser implementado en la misma clase (en mi opinión, debería - independientemente de su implementación) no en uno externo.
  • Solo se usan los campos de esta clase, no los de una superclase.
  • Manejo necesario de IllegalAccessException necesario (simplemente lo arrojo en el ejemplo anterior).
+0

Esto compararía todos los campos, pero hay algunos campos que no quiero comparar. – tputkonen

+0

Compara todos los campos de cadena, derecha. No viste esto por tu explicación. Sin embargo, podría 'marcar' los campos que desee comparar utilizando una subclase propia de String, solo implementar los constructores necesarios. Esta podría ser una clase privada interna de tu clase. Getters y setters para los campos todavía deberían usar String. – rudolfson

+0

O mejor aún (lo siento por tantos comentarios :-)), use una Anotación en esos campos. – rudolfson

0

También propongo una solución similar a la de Alnitak.

Si los campos necesitan ser iterados al comparar, ¿por qué no prescindir de los campos separados y poner los datos en una matriz, un HashMap o algo similar que sea apropiado?

Luego puede acceder a ellos mediante programación, compararlos, etc. Si se deben tratar diferentes campos de & de diferentes maneras, puede crear clases de ayuda apropiadas para los valores, que implementan una interfaz.

A continuación, sólo se podía hacer

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

o algo por el estilo ...

0

Un amplio pensamiento:

Crear una nueva clase que tiene por objeto los siguientes parámetros: la primera clase para comparar, la segunda clase para comparar, y una lista de getter & nombres de métodos setter para los objetos, donde solo se incluyen los métodos de interés.

Puede consultar con reflexión la clase del objeto, y de ahí sus métodos disponibles. Suponiendo que cada método getter en la lista de parámetros se incluye en los métodos disponibles para la clase, debería poder llamar al método para obtener el valor de comparación.

Aproximadamente esbozado algo así como (disculpas si no es super-perfecto ...no es mi idioma principal):

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

El JavaBeans API está destinado a ayudar con la introspección. Ha existido de una forma u otra desde Java versión 1.2 y ha sido bastante utilizable desde la versión 1.4.

código de demostración que compara una lista de propiedades en dos granos: invocación

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

muestra:

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

Si vas a la ruta anotaciones, considere vertido reflexión y generar el código de la comparación con una tiempo de compilación annotation processor o algún otro generador de código.

+0

¡Gracias, esto parece perfecto! – tputkonen

+1

¿En qué se diferencia esta solución de la opción 1? Es más agradable debido al uso de la API Beans en lugar de utilizar directamente la API Relections. Pero aún debe nombrar todos sus campos para comparar, y aún así dejar el problema cuando cambie el nombre de algunos de ellos. ¿O pasé por alto algo? – rudolfson

+1

Sí, ese problema persiste, pero no veo una forma de evitarlo (sin anotaciones). Sin embargo, la reflexión es mucho más "de bajo nivel" en comparación con BeanInfo. – tputkonen

1

desde

Todos los campos son de tipo String, y puedo modificar el código de la clase propietaria de los campos, si es necesario.

podría intentar esta clase:

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

código mágico:

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

esta clase tiene varias ventajas:

  • refleja una vez, en la carga de clases
  • es muy sim ply agregando nuevos campos, simplemente agregue un nuevo campo estático (una mejor solución aquí está usando Anotaciones: en el caso que le interese usar la reflexión también java 1.4)
  • podría refactorizar esta clase en una clase abstracta, todas las clases derivadas solo obtienen tanto
    datos y cloneAndLogDiffs()
  • la interfaz externa es typesafe (también se podría imponer fácilmente inmutabilidad)
  • no setAccessible llamadas: este método es problemático veces
0

usted dice que actualmente tiene captadores y definidores para todos estos ¿campos? De acuerdo, luego cambie los datos subyacentes de un grupo de campos individuales a una matriz. Cambie todos los getters y setters para acceder a la matriz. Crearía etiquetas constantes para los índices en lugar de usar números para la mantenibilidad a largo plazo. Cree también una matriz paralela de indicadores que indiquen qué campos se deben procesar. Luego, cree un par getter/setter genérico que use un índice, así como un getter para el indicador de comparación. Algo como esto:

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

acaba de escribir este de la parte superior de mi cabeza, yo no lo he probado, por lo que su puede haber errores en el código, pero creo que el concepto debería funcionar.

Como ya tiene getters y setters, cualquier otro código que use esta clase debería continuar sin modificar. Si no hay otro código que use esta clase, deseche los getters y setters existentes y simplemente haga todo con la matriz.

+0

Nuestro Java Bean también es una entidad JPA, por lo que este enfoque no funcionaría en nuestro caso. – tputkonen

-1

Estoy programando un marco llamado jComparison (https://github.com/mmirwaldt/jcomparison) que encuentra diferencias (y similitudes) entre dos objetos java, cadenas, mapas y colecciones. Hay muchas demostraciones que le muestran cómo usarlo. Sin embargo, es una versión alpha atm y no estoy seguro de qué licencia quiero elegir.

Nota: soy el autor de ese marco.

Mira la demostración bajo https://github.com/mmirwaldt/jcomparison/blob/master/core-demos/src/main/java/net/mirwaldt/jcomparison/core/object/ComparePersonsDemo.java

Se muestra un ejemplo con la Persona clases ficticio y Dirección. La salida de la demo es:

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

Esta publicación no parece un intento de responder esta pregunta. Se espera que cada publicación aquí sea un intento explícito de * responder * a esta pregunta; si tiene una crítica o necesita una aclaración de la pregunta u otra respuesta, puede [publicar un comentario] (// stackoverflow.com/help/privileges/comment) (como este) directamente debajo de ella. Elimine esta respuesta y cree un comentario o una nueva pregunta. Ver: [Hacer preguntas, obtener respuestas, sin distracciones] (// stackoverflow.com/tour) – Machavity

+0

Solo el enlace a su propia biblioteca o tutorial no es una buena respuesta. Vincularlo, explicar por qué soluciona el problema, proporcionar un código sobre cómo hacerlo y negar que lo escribió (lo que hizo) lo convierte en una mejor respuesta. Ver: [** ¿Qué significa "buena" autopromoción? **] (// meta.stackexchange.com/q/182212) y [Cómo no ser un spammer] (// stackoverflow.com/help/promotion). – Makyen

+0

Mejoraré mi respuesta – mmirwaldt

Cuestiones relacionadas