El uso de colecciones de Java con eficacia mediante la implementación de equals (), hashCode (), y compareTo ()

Gracias a asistir a los usuarios locales de Java Grupo «Oracle ® Certificación Certified Java Programmer ™ Boot Camp» de manera intermitente durante varios años, han entrado en el siglo 21 y comenzado a utilizar las colecciones de Java de la forma en que estaban destinados. Especialmente en lo que respecta a la clasificación y la eliminación de registros duplicados, estas clases son una eficiencia de dios a enviar en términos de tiempo de codificación y tiempo de ejecución y podría decirse que, en términos de facilidad de lectura también. Pero el uso libre de errores de estas clases requiere el cumplimiento estricto del contrato de iguales () , hashCode () y, a menudo compareTo () .

A pesar de que los IDEs populares generan automáticamente talones de algunos de estos métodos, usted debe entender cómo funcionan, en particular, cómo los tres métodos funcionan juntos , porque no veo muchos de IDE escribir métodos significativos compareTo () todavía. Durante gran parte de lo que sigue, estoy endeudada a Joshua Bloch y su libro, Effective Java. Comprarlo, leerlo, vivirlo.

# 1. El comportamiento de los iguales () , hashCode () , y compareTo () debe ser consistente.

# 2. Debe basar estos métodos en campos cuyos valores no cambian mientras se encuentren en una colección. Si almacena un objeto en una colección (por ejemplo, como una llave en una HashMap) y es hashcode cambios, por lo general, no podrá recuperarla de esa tabla hash más! Gracias a «Programación en Scala» Sección 30.2 p. 657. Véase también mi post más adelante mutabilidad de objetos. Puede usar colecciones de manera efectiva con los objetos mutables, siempre y cuando esos objetos utilizan claves suplentes. En estos ejemplos guardo mi clave sustituta en un id campo class="keyword"> privado con pública getId () y setId () Métodos tantos marcos populares esperan.

Kiss

  / / Tenga esto método privado, ya que proporciona acceso a los campos privados!  
@ Transient
privada Objeto [] campos () {
/ / Mientras tu clase no tiene ningún arrays internos,
/ / simplemente lista sus campos aquí.

nueva Object [] { elemento , startDate , endDate };
}

Esto debería funcionar para cualquier clase que no tiene campos de matriz. Esto podría (y muchas veces debe) ser aprovechado para compareTo () y toString () también.

equals ()

a.equals (b) debe devolver true cuando a y b representan el mismo objeto. No es que a y b son a la vez los cerdos, o incluso que son dos cerdos llamado Wilbur, pero que ambos representan el cerdo llamado Wilbur en la historia, La telaraña de Charlotte. Bloch (artículo 8), dice que el igual a () debe ser reflexiva, simétrica y transitiva algunas otras cosas, así que no voy a cubrir aquí. Para cualquier valor no nulo:

  • x.equals (x) debe ser verdad
  • Si x.equals (y. ) a continuación, y.Equals (x) tiene que ser verdad.
  • Si x.equals (y) y y. es igual a (z) a continuación, x.equals (z) también debe ser verdad.

  @ Override  
públicos boolean iguales ( otros ) {
/ / funcionamiento baratos primero ...
si ( esta == otros ) { volver true ; }

si ( ( otros == nula ) | |
! ( otros instanceof MyClass ) | |
( esta . hashCode () ! = otros . hashCode ()) ) {
false ;
}
/ / Detalles ...
definitiva MyClass que = ( MyClass ) otros ;

/ / Si se trata de un objeto de base de datos y los dos tienen la misma clave sustituta (id),
/ / son la misma.
si ( ( id ! = 0 ) && ( que . getId () ! = > 0 ) ) {
retorno
( id == que . getId ( ));
}

/ / Si no es un objeto de base de datos, comparar los campos "significativas" aquí.
Arrays . iguales ( campos () que . campos ());
}

de esperar que es evidente que ambos objetos tanto sea válido antes de compararlas. Pero, ¿te das cuenta de que a pesar de que accedí a la esta campos directamente, me usedpublic Accessor getXxx () métodos para acceder a la que campos? Esta técnica protege contra objetos sustitutos. Marcos de persistencia o de comunicación hacen que los objetos sustitutos temporales (a menudo utilizando la reflexión) como una manera de evitar ir a buscar gráficos de objetos enteros hasta que se demuestre cada objeto a ser necesario. Dependiendo de la estructura que está trabajando, un sustituto puede ser reemplazado por un objeto real por primera vez un campo distinto de Identificación se accede, pero es más seguro y menos error propenso a usar siempre getXxx () métodos de acceder directamente a los campos privados. Por lo menos con Hibernate, esta es siempre un objeto real (no sustituto).

Su método equals () debe ya sea comparar campos "importantes" (los que identifican de forma única el objeto) o sustituto llaves - no tanto! El peligro de proporcionar un campo de campo por igual a comparación de un objeto de base de datos es que va a trabajar en algunos casos con objetos no válidos, pero no siempre funciona, por lo tanto yo prefiero tener fracasar rápido, que me dejes rascarse cabeza cuando un bug intermitente surge en la producción. Para los objetos de base de datos que siempre uso claves suplentes porque al hacerlo reconoce que todo lo relacionado con un objeto puede cambiar con el tiempo, sin embargo, sigue siendo esencialmente el mismo objeto (el artista antes conocido como Prince). Para objetos que no son de base de datos, debe comparar los campos que identifican de forma única un objeto válido.

hashCode () Artículo

Object.hashCode () ):

  • x.hashCode () debe ser siempre igual y.hashCode () cuando x.equals (y) . Si rompes esta nada va a funcionar.
  • Está bien que x.hashCode () para y.hashCode igual () cuando x. es igual a (y) es falsa, pero es bueno para minimizar esto.

  @ Override  
públicos int hashCode () {
si ( id == 0 ) {
Arrays . hashCode ( campos ());
}
/ / return (posiblemente truncada) sustituta clave
( int ) id ;
}

El punto de hashCode es ser una prueba muy barato "poder-iguales". Nada es más rápido que el truncamiento de una clave sustituta. Si el objeto no tiene una clave sustituta, entonces la comparación campo por campo en esta solución enfatiza la corrección sobre la velocidad, aunque en realidad debería ser bastante rápido. Si no es lo suficientemente rápido, por alguna razón, hay algunos trucos que puede utilizar para acelerar las cosas.

A veces usted puede caber varios campos en un único int. Por ejemplo, en una clase de la fecha, es posible utilizar los siguientes elementos que da cada día una representación única de enteros:

  volver  (año * 10 000) + (mes * 100) + días; 

En la base 10 de los dígitos se codifican como AAAAMMDD que es a la vez leíble humano y ordena lo mismo que aa Fecha, un int y un String. En binario que es 00000000YYYYYYYYYYY000MMMM0DDDDD. Si la velocidad supera a la legibilidad, puede utilizar las operaciones de cambio de lugar de multiplicaciones y seguir manteniendo los bits significativos (31 días <2 ^ 5, 12 meses <2 ^ 4), por lo que los bits resultantes serían 000000000000YYYYYYYYYYYMMMMDDDDD (que no tiene sentido convertida en base 10 y útil sólo como un código hash)

  volver  (año <<9) | (mes <<5) | día; 

Para una clase con importantes campos booleanos que puede representar a todos sin pérdidas en el código hash:

  int  ret = (ishappy 1: 0);  
si (isHungry) {
ret "comentario"> / / | = 2; / / 2 ^ 1 = 2 = B0010
}
si (isSleepy) {
ret | = 4; / / 2 ^ 2 = 4 = B0100
}
si (isAntsy) {
ret | = 8; / / 2 ^ 3 = 8 = B1000
}
ret;

Declarando su clase como una clase de "caso" en la Scala se encarga de todos los elementos anteriores para usted. Previene la herencia, pero para las clases simples, se ahorra un montón de pensamiento y escribir! Para las clases no de casos, debe hacer más trabajo en Scala que en Java con el fin de apoyar significativo es igual a las comparaciones con la herencia. Usted tiene que poner en práctica un método canEqual (), así que apoye la idea de que una clase padre puede pensar que fue "lo suficientemente cerca" a una clase de niños, pero el niño podría pensar que eran diferentes (ya que define campos adicionales relevantes a los iguales método ()), por lo que el niño debe implementar canEqual () y el padre debe comprobarlo con el fin de bloquear el padre de pensar que son iguales. Nunca he sido mordido por esto en Java, pero yo no lo veo de inmediato lo impide.

compareTo ()

  • Si this.equals (que) == true , entonces this.compareTo ( que) debe devolver 0.
  • Si this.equals (que) == false , entonces this.compareTo (que) debe NO devuelva 0.

esta se menos-que que a continuación, que se quedarían en el mismo orden. Pero cuando el código "no sabe cómo ordenar" como "menos-que" (o "mayor que"), se termina rompiendo algoritmos como Montón-Ordenar y búsqueda binaria que ignoran la mitad de los elementos ordenados restantes en cada comparación . Un valor de retorno no modificable para "no-sé" crea bolsas que se ordenan de forma local, pero fuera de orden mundial.

Hace poco encontramos con este problema cuando hemos añadido una característica a nuestro clon software, alguien clonó un montón de objetos, y luego los elimina. Los objetos fueron ordenados y duplicados removidos llamando TreeSet.addAll (), a continuación, los borrados queridos fueron retirados antes de mostrarlos. Desde equals () devuelve falso (los clones tenían diferentes identificadores únicos) pero cada otro campo significativo fue igual, compareTo () devolvió "no-sé", como "menos que" y en silencio se rompió la clasificación.

Este fue un error muy difícil y miles de personas que utiliza activamente nuestro sistema en producción durante meses antes de que esto se informó. La solución consistió en comparar los identificadores únicos antes de abandonar. Creo que ahora que devolver un valor centinela, que también significa "menor que" o "mayor que" es inherentemente peligroso y equivocado. Volviendo "igual" está mal también (porque rompe la igual-contrato). Sin ninguna buena valor de retorno en esta situación, lo único que debe hacer es una excepción sin control (justificación para el uso de una excepción sin control está en mi Chequeado contra excepciones no comprobadas de correos).

El siguiente ejemplo debería ser bastante explica por sí mismo y ordena alfabéticamente por el campo de descripción.

   / ** /> ​​
menor que, igual a, o mayor que el objeto especificado (respectivamente).

@ param que el objeto va a comparar con ("que")

@ return -1 significa que están en orden (esto viene antes de eso), 0 significa que son iguales
(y Sets eliminará uno de ellos), 1 significa para intercambiarlas (que viene
antes de que esta )
* /

@ Override
public int compareTo (MyClass eso) {

/ / garantiza la coherencia con los iguales ()
si (<. span class = "palabra clave"> esta iguales (que)) {return class="keyword"> 0;}

/ / comparar padres por primera vez en una estructura jerárquica.
/ / Si esta clase puede ser su propio padre (por ejemplo, un
/ / tabla de auto-unión) esto es mucho más complicado. Voy a tener que
/ / hacer otro artículo sobre eso. Usted también puede tener que
/ / cheque por un padre nulo, pero este es el caso más simple:

int ret = esta getParent () compareTo (that.getParent ());..
si (ret = 0!) { retorno ret;}

si (Descripción == nula ) {
si (that.getDescription ()! = nula ) {
/ / ordena nulos primero
-1;}

} else if (that.getDescription () == nula ) {
/ / ordena nulos primero
1;
demás

{ / / Para cada prueba, comprobar y sólo devolverá un resultado distinto de cero
ret = description.compareToIgnoreCase (that.getDescription ());
si (ret = 0!) {return class="keyword"> ret;}
ret = description.compareTo (that.getDescription ());
si (ret = 0!) { volver ret;}}


/ / Si es posible haber clonado a juego con los objetos significativos
/ / campos pero aún así ser diferentes objetos (por ejemplo, antes de que se editan
/ / ser diferente) luego los Iguales () comprobar anteriormente podría haber regresado
/ / false y los campos significativos cheques todo podría haber vuelto
/ / "igual". El uso exclusivo de identificación de aquí se asegura de que no volvemos 0 y
/ / violar los iguales () de contratos, y también que nos ofrecen un
estable / / (que no cambia), la clasificación inequívoca de estos objetos.
/ / Vea mi "UPDATE" arriba ...

largo última thatId = that.getId ();
si ((id = 0) && (thatId = 0)!) {
largo diff = id - thatId ;
si (dif> 0) {
1;}
else if (dif <0) {
-1;}
más
IllegalStateException (
" Error iguales antes, ¿cómo pueden estos objetos ? ser "
" lo mismo ahora ");}

}

/ / Si no sabemos, entonces hay un error de código en algún lugar por encima.
/ / Romper ruidosamente por lo que se fija!

throw new IllegalStateException (
"ERROR DE CODIFICACIÓN: iguales fallado (prueba), pero los objetos parecen" +
". será el mismo" );
}

Todos los ejemplos anteriores están diseñados para trabajar con un framework de persistencia como Hibernate. Hibernate inicializar cualquier objeto de base de datos antes de llamar a cualquiera de sus métodos (que no sea getId ()). Así que su objeto puede confiar en sí mismo para ser inicializado dentro equals (), hashCode (), y compareTo (). En cuanto compareTo (), el objeto no debe confiar en que el otro objeto que se comparan a se inicializa. No hay campos de instancia deben ser llamados en el objeto "otros", sólo los descriptores de acceso get / set para que Hibernate tiene una oportunidad de inicializar el objeto si y sólo si eso se convierte en necesario. Al implementar un comparador (por oposición a Comparable), entonces ninguno de los objetos se puede confiar en que ser inicializado y get / set debe utilizarse en lugar de acceder directamente a los campos de instancia de ambos objetos. Si implementa su Comparador como una clase separada y sus campos son privados, el compilador hará cumplir esto para usted. Pero para Comparable y Comparadores implementado en el mismo archivo de clase como el objeto están comparando, el compilador no le avise, por lo que debe tener mucho cuidado

Nota:. I no han verificado esto, pero es lógico pensar que si cambia hashCode () es probable que tenga que actualizar el serialVersionUID tal como lo haría si ha cambiado cualquier campo persistente. Dado un conjunto sólo llama equals () de los objetos en el mismo cubo (los que tienen hashcodes similares), usted puede terminar con dos de los mismos objetos en el conjunto (una con el viejo hashCode y otra con el nuevo). No estoy seguro de si esto puede suceder en la práctica o no. Tal vez alguien va a publicar el código de prueba en los comentarios que lo demuestra de una manera u otra?

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *