lunes, 3 de septiembre de 2012

Cómo implementar correctamente hashCode() e equals() en Java

Este post no intenta ser una discusión filosófica sobre la correcta forma de implementar equals y hashCode sino que sólo intenta mostrar la forma práctica de hacerlo con ejemplos.

Vamos primero con los ejemplos para aquellos que llegan acá buscando la solución sin leer mucho. Al final está la teoría.

Conozco dos muy buenas bibliotecas con utilidades para implementar equals y hashCode, así que doy un ejemplo con cada una de ellas.

Ejemplo usando jakarta commons-lang

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

public class MyClass {
 private String field1;
 private int field2;
 private double field3;

 @Override
 public boolean equals(Object obj) {
  if (this == obj) {
   return true;
  }
  if (!(obj instanceof MyClass)) {
   return false;
  }
  MyClass other = (MyClass) obj;

  // Now we compare field by field
  return new EqualsBuilder()
      .append(this.field1, other.field1)
      .append(this.field2, other.field2)
      .append(this.field3, other.field3)
      .isEquals();
 }

 @Override
 public int hashCode() {
  return new HashCodeBuilder()
      .append(field1)
      .append(field2)
      .append(field3)
      .toHashCode();
 }
}

class MySubClass extends MyClass {
 private String field4;

 @Override
 public boolean equals(Object obj) {
  if (this == obj) {
   return true;
  }
  if (!(obj instanceof MySubClass)) {
   return false;
  }
  MySubClass other = (MySubClass) obj;

  // Now we compare field by field
  return new EqualsBuilder()
      .appendSuper(super.equals(obj)) // Note the call to super
      .append(this.field4, other.field4)
      .isEquals();
 }

 @Override
 public int hashCode() {
  return new HashCodeBuilder()
      .appendSuper(super.hashCode()) // Note the call to super
      .append(field4)
      .toHashCode();
 }
}

Ejemplo usando Guava

import com.google.common.base.Objects;

public class MyClass {
 private String field1;
 private int field2;
 private double field3;

 @Override
 public boolean equals(Object obj) {
  if (this == obj) {
   return true;
  }
  if (!(obj instanceof MyClass)) {
   return false;
  }
  MyClass other = (MyClass) obj;

  // Now we compare field by field
  return Objects.equal(this.field1, other.field1)
      && Objects.equal(this.field2, other.field2)
      && Objects.equal(this.field3, other.field3);
 }

 @Override
 public int hashCode() {
  return Objects.hashCode(field1, field2, field3);
 }
}

class MySubClass extends MyClass {
 private String field4;

 @Override
 public boolean equals(Object obj) {
  if (this == obj) {
   return true;
  }
  if (!(obj instanceof MySubClass)) {
   return false;
  }
  MySubClass other = (MySubClass) obj;

  // Now we compare field by field
  return super.equals(obj) // Note the call to super
      && Objects.equal(this.field4, other.field4);
 }

 @Override
 public int hashCode() {
  return Objects.hashCode(super.hashCode(), field4); // Note the call to super
 }
}

La teoría

La regla es que si se implementa equals(), también se debe implementar hashCode() porque siempre se debe cumplir lo siguiente:
  • Si dos objetos son iguales, deben tener el mismo hashCode.
  • Si dos objetos tienen diferente hashCode entonces deben ser distintos.

En el caso del equals, también es necesario que se cumpla lo siguiente:
Dados los objetos A, B y C:
  • Si A.equals(B) entonces B.equals(A) y viceversa
  • A.equals(A) debe ser true
  • A.equals(null) debe ser false
  • Si A.equals(B) y B.equals(C), entonces debe cumplirse que A.equals(C)
La única regla que no se cumple del todo con los ejemplos es la primera, en el caso del equals de las subclases.

 @Test
 public void testEqualsObject() {
  MyClass a = new MyClass();
  MyClass b = new MySubClass();
  assertTrue(a.equals(b)); // Returns true and perhaps it shouldn't
  assertFalse(b.equals(a)); // Returns false
 }
Una alternativa es implementar el comienzo del equals así:
  @Override
  public boolean equals(Object obj) {
    if (obj == null) {
      return false;
    }
    if (obj == this) {
      return true;
    }
    if (obj.getClass() != getClass()) {
      return false;
    }

    MyClass other = (MyClass) obj;

    // Now we compare field by field
    //...
  }

La desventaja es que las subclasses deberían implementar el equals teniendo en cuenta los campos de la superclase sin poder llamar a super.equals().

Como sea, es algo a tener en cuenta y cada uno deberá balancear las ventajas y desventajas de cada alternativa.

Apéndice: Si querés incluir estas bibliotecas en maven

<dependency>
    <groupid>org.apache.commons</groupid>
    <artifactid>commons-lang3</artifactid>
    <version>3.1</version>
</dependency>
<dependency>
    <groupid>com.google.guava</groupid>
    <artifactid>guava</artifactid>
    <version>13.0</version>
</dependency>




domingo, 27 de abril de 2008

Escribí tu currículum vitae en XML

La problemática

A lo mejor a alguno de ustedes le pasa como a mi, que odio escribir un CV en Word/Writer o cualquier otro procesador de textos que ande por ahí.

Así que la primera vez que tuve que escribir un CV, lo hice en HTML puro y estático. Esto mezclaba contenido con presentación, pero era bien simple y quedaba tal cual yo quería. El problema con los procesadores de texto es que uno nunca termina de saber exactamente qué es lo que pone internamente para darle el formato y a veces es difícil lograr el formato deseado.

XML al rescate

Cuando trabajaba en Zauber me mostraron el xmlresume, que permitía escribir el CV en un archivo xml y luego procesarlo para presentarlo en formato HTML o PDF según se requiriera.

Como xmlresume parece no tener más soporte desde hace tiempo, decidí cambiarme a otro y es así como actualmente utilizo hr-xsl.

Cual de los dos utilizar es realmente una cuestión de gustos, porque hr-xsl tampoco parece estar muy activo hoy en día, pero bueno, ya me amoldé a este último y les quiero compartir la forma en que yo lo utilizo.

Cómo uso hr-xsl

Tengo todo el programa para generar el CV en un repositorio de Subversion propio con la siguiente jerarquía de carpetas (y algunos comentarios):
.
|-- bin // aca está el soft de hr-xsl
| |-- HRXML
| | `-- HRXML200704
| | `-- HR-XML-2_5
| `-- hr-xsl
| `-- hr-xsl-0.11
|-- releases
| |-- 1.0.0-pre-xml // esto era en HTML
| | |-- 2005-03-31
| | |-- 2005-12-21
| | |-- 2007-03-20
| | `-- 2007-04-03
| |-- 2.0.0-xmlresume-zauber // esto era xmlresume
| | `-- 2007-05-07
| `-- 3.0.0-hr-xsl // este es el actual, hr-xsl
| |-- 2007-09-04
| |-- 2008-01-14
| `-- 2008-04-16
`-- trunk
|-- .project // proyecto de eclipse (lo edito con eclipse)
|-- generator-en // scripts de ant para construir el CV en ingles
| |-- build.xml
| `-- parameters.xsl
|-- generator-es // scripts de ant para construir el CV en español
| |-- build.xml
| `-- parameters.xsl
`-- src
|-- currVitae.htm // html con frames que encuadra la presentacion
|-- epereda-en.html // autogenerado html en inglés
|-- epereda-en.pdf // autogenerado pdf en inglés
|-- epereda-en.txt // autogenerado texto en inglés
|-- epereda-en.xml // codigo fuente del cv en inglés
|-- epereda-es.html // autogenerado html en español
|-- epereda-es.pdf // autogenerado pdf en español
|-- epereda-es.txt // autogenerado texto en español
|-- epereda-es.xml // codigo fuente del cv en español
|-- style.css // hoja de estilos para el html
`-- top.html // frame superior con links a las diferentes versiones


La idea es que edito lo que está en la carpeta trunk/src usando Eclipse (obviamente se puede editar con lo que quieran, pero yo estoy acostumbrado a usar Eclipse). Luego corro los scripts de ant localizados en trunk/generator-en y trunk/generator-es y cuando estoy contento con el resultado de los archivos generados hago una copia a la carpeta de releases/3.0.0-hr-xs con la fecha actual.

Requerimentos de hr-xsl

Los requerimentos de hr-xsl están detallados en la documentación del sitio oficial, pero se resumen en lo siguiente:

  • Java 5.0 o superior

  • Ant 1.6 o superior

  • Lynx: sólo si se quiere generar archivo .txt



Decargar fuentes

Pueden descargar mi código fuente desde acá. Es posible que sea lento porque la conexión de internet de ese server no es muy buena.

La idea de brindarles el código fuente es que a lo mejor les es útil basarse en él para hacer su propio CV.

Resultado final

El producto terminado puede verse acá: epere4.com.ar/cv

domingo, 20 de abril de 2008

¿Cómo funciona synchronized en Java?

Mucha gente que usa Java no tiene claro cómo funciona realmente la palabra clave synchronized. Voy a intentar explicarlo acá porque es realmente muy fácil si se tienen en cuenta algunas cosas.

En primer lugar, usar synchronized en un método de instancia es lo mismo que poner un bloque de synchronized(this){} que contenga todo el código del método. Es decir,
public synchronized void metodo() {
   // codigo del metodo aca
}
es lo mismo que
public void metodo() {
   synchronized(this) {
      // codigo del metodo aca
   }
}
Si el método es de clase entonces es lo mismo pero el bloque de synchronized se aplica a la clase. Ejemplo, si el método está en la clase MiClase entonces esto
public static synchronized void metodo() {
   // codigo del metodo aca
}
es lo mismo que
public static void metodo() {
   synchronized(MiClase.class) {
      // codigo del metodo aca
   }
}


Habiendo dicho esto, el problema se reduce a entender cómo funciona el synchronized de un bloque porque, como vimos, el synchronized en métodos de instancia o de clase son simplemente casos especiales de esto.

El bloque synchronized lleva entre paréntesis la referencia a un objeto. Cada vez que un thread intenta acceder a un bloque sincronizado le pregunta a ese objeto si no hay algún otro thread que ya tenga el lock para ese objecto. En otras palabras, le pregunta si no hay otro thread ejecutando algun bloque sincronizado con ese objeto (y recalco que es ese objeto porque en eso radica la clave para entender el funcionamiento)
Si el lock está tomado por otro thread, entonces el thread actual es suspendido y puesto en espera hasta que el lock se libere. Si el lock está libre, entonces el thread actual toma el lock del objeto y entra a ejecutar el bloque. Al tomar el lock, cuando venga el proximo thread a intentar ejecutar un bloque sincronizado con ese objeto, será puesto en espera.
¿Cuándo se libera el lock? Se libera cuando el thread que lo tiene tomado sale del bloque por cualquier razón: termina la ejecución del bloque normalmente, ejecuta un return o lanza una excepción.

Es importante notar una vez más que el lock es sobre un objeto en particular. Si hay dos bloques synchronized que hacen referencia a distintos objetos (por más que ambos utilicen el mismo nombre de variable), la ejecución de estos bloques no será mutuamente excluyente.

Conclusiones:

De todo esto se puede concluir que un método synchronized de instancia puede ser ejecutado al mismo tiempo que uno de clase.
Además, dos threads pueden ejecutar el mismo método de instancia al mismo tiempo aunque este sea synchronized si la invocación se hace a dos objetos diferentes.
Por último, todos los métodos synchronized de instancia son mutuamente excluyentes entre sí. Es decir que, dado un objeto compartido por más de un thread, sólo uno de ellos puede acceder en un determinado momento a uno de esos métodos de instancia. Los otros threads deberán esperar aunque hayan querido acceder a otro de los métodos (porque todos están synchronizados con el mismo this).

Puede encontrarse mucho más info del tema en este tutorial de sun (en inglés).

martes, 4 de diciembre de 2007

Eclipse Quick Access (aka Ctrl + 3)

Ya que les estuve comentando algo de Eclipse Europa en el post anterior, les paso otro tip. Este me lo pasó mi amigo Claudio.

Se trata de la madre de todos los atajos. Si te gusta usar atajos de teclado, entonces probá Ctrl + 3 en Eclipse Europa.

Con eso se abre la ventana del Quick Access. Permite acceder a cualquier Vista, Perspectiva, Comando, Editor, etc con sólo tipear su nombre (o parte de él, porque, por supuesto, al igual que el buscador de clases Ctrl+Shif+T, soporta caracteres comodines, escribir sólo las iniciales de lo que se busca, etc, etc).

Básicamente, se puede acceder a casi todo con el Ctrl+3!!!

miércoles, 26 de septiembre de 2007

Save Actions en Eclipse Europa

A esta altura, esta característica de Eclipse Europa no es nada nueva, pero vale la pena comentarla.

A partir de la versión Europa de Eclipse existe la posibilidad de definir ciertas acciones a ejecutar automáticamente cada vez que se grabe un archivo.

Yo en general sólo uso las que están marcadas en la figura, que te autoformatean el código (como hacer Ctrl+Shif+F, digamos) y te organizan los import (análogo a Ctrl+Shif+O).


Pero hay un montón de cosas que se pueden configurar como Additional Actions. En la figura de abajo se ven algunas, pero esta es la lista simplificada (para más detalles mirá directamente en Eclipse que está todo muy claro):
  • Que automáticamente ponga o no bloques en los if/else/for/while.
  • Automáticamente convertir los for en foreachs (cuando se pueda, claro).
  • Rodear las condiciones (o no) con paréntesis.
  • Hacer que automáticamente se agregue la palabrita final a los métodos, variables privadas y/o variables locales.
  • Usar o no el this para acceder a métodos y variables de instancia.
  • Forzar que automáticamente se use el nombre de la clase cuando se utilicen métodos o campos estáticos.
  • Remover automáticamente miembros privados que no sean usados (ojo con este porque te hace desaparecer las cosas si las dejaste de usar temporalmente).
  • Remover casteos innecesarios
  • Agregar automáticamente las anotaciones @Deprecated y @Override.
  • Remover espacios en blanco al final de las líneas.
  • Ordenar los métodos (de acuerdo a cómo se configure en Members Sort Order)


Eso es todo, ojalá les sirva!

viernes, 6 de julio de 2007

¿Por qué la gente hace e.printStackTrace() en vez de relanzar como RuntimeException?

¿No les pasó de ver código similar a este?
try {
    InputStream input = new FileInputStream("myFile.txt");
} catch (FileNotFoundException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}
Seguro que sí. Desde verlo en ejemplos en libros de texto (porque "total, es un ejemplo, se entiende que no es lo correcto") hasta en códigos de programas reales. ¡Pero si hasta eclipse autogenera ese try-catch!

Es una porquería. No sirve para nada y oculta bugs. La razón más común por la que se lo hace creo que es porque no se quiere manejar la excepción. Esto puede ser porque ese error nunca ocurre, porque no quiero que mi método tenga que tener un throws, o por desconocimiento, no se.

La forma correcta es esta:
try {
    InputStream input = new FileInputStream("myFile.txt");
} catch (FileNotFoundException e) {
    e.printStackTrace(); // esto es opcional, obviamente
    throw new RuntimeException(e);
}
De esta manera, al relanzar la excepción como RuntimeException nuestro método no tiene que declarar throws FileNotFoundException (para este ejemplo, claro está) y si el error ocurre lo vamos a ver enseguida porque nuestra aplicación va a fallar con un hermoso stack trace.

Podemos hacer que eclipse nos ayude con esto, modificando el code template. Vamos a Windows->Preferences->Java->Code Style->Code Templates y buscamos el que se llama Catch Block Body.

El valor por default es:
// ${todo} Auto-generated catch block
${exception_var}.printStackTrace();
Yo sugiero cambiarlo a:
${exception_var}.printStackTrace();
throw new RuntimeException(${exception_var});

Ah, de paso les comento, con respecto a lo que comenté hace un tiempo, que ya salió el Eclipse Europa.

Disfruten!

martes, 19 de junio de 2007

El blog para escribir y del.icio.us para poner links

Este post va dedicado a los bloggeros que aparecen en codear, pero es extensible a todos los bloggers en general.

No está bueno leer post que sólo tiran un link sin ninguna explicación. Para eso está del.icio.us. Ahí uno puede tener feed de la persona que quiera (los míos, por ejemplo, están acá) y ver qué links le gusta guardar.

No se cómo es la estrategia de lectura de feeds de la gente, pero en mi caso particular tengo muchos y no leo en profundidad todos. Los leo por encima y si alguno me interesa, profundizo. Si un post sólo tiene un link ni lo leo porque acceder al link ya retrasa todo (y sin ninguna certeza de que el tema vaya a interesarme).