Capítulo 7 Índice



Descargar 499,08 Kb.
Página1/5
Fecha de conversión05.08.2017
Tamaño499,08 Kb.
  1   2   3   4   5

Capítulo 7

Índice

1. Objetivo de Certificación 6.2 - Sobrescribiendo hashCode() y equals()
1.1. Método toString()
1.2. Método equal()
1.3. Significado de no sobrescribir equals()
1.4. Implementando un método equals()
1.5. Contrato equals()
1.6. Sobrescribiendo hashCode
1.7. Entendiendo los HashCodes
1.8. Implementando hashCode()
1.9. El contrato hashCode()
2. Objetivo de Certificación 6.1 - Colecciones
2.1. Que hacer con una colección
2.2. Interfaces y clases del framework Collection
3. Objetivo de Certificación 6.5 - Usando el framework Collections
3.1. ArrayList Básicos
3.2. Autoboxing con Collections
3.3. Clasificando (sorting) Collections y Arrays
3.4. Sorting Collections
3.5. Interfaz Comparable
3.6. Sorting con Comparator
3.7. Sorting con la clase Arrays
3.8. Buscando en Arrays y Collections
3.9. Convirtiendo Arrays a Listas (y viceversa)
3.10. Usando Lists
3.11. Usando Sets
3.12. Usando Maps
3.13. Usando la clase PriorityQueue
3.14. Descripción Método para Arrays y Colecciones
3.15. Descripción Método para List, Set, Map, y Queue
4. Objetivo de Certificación 6.3 y 6.4 - Tipos genéricos
4.1. Manera legal de hacer colecciones
4.2. Genéricos y el legado de código
4.3. Mezclando colecciones genéricas y no genéricas
4.4. Polimorfismo y Genéricos
4.5. Métodos genéricos
4.6. Declaraciones genérico
4.7. Creando tu propia clase genérica
4.8. Creación de métodos genéricos


1. Objetivo de Certificación 6.2 - Sobrescribiendo hashCode() y equals()

6.2 Distinguir entre redefiniciones correctas e incorrectas de los métodos hashCode y equals, y explicar la diferencia entre "==" y el método equal.

Para el examen no necesitamos sabernos todos los métodos de un objeto, pero necesitaremos saber sobre los métodos que veremos a continuación en la siguiente tabla.



Método

Descripción

boolean equals (Object obj)

Decide si dos objetos son equivalentes de manera significativa.

void finalize()

Llamado por garbage collector cuando se da cuenta de que el objeto no puede ser referenciado.

int hashcode()

Devuelve un valor hashcode entero para un objeto, así que el objeto puede ser usado en clases Collection que use hashing, incluyendo Hashtable, HashMap y HashSet.

final void notify()

Se despierta un hilo que está a la espera de este objeto de bloqueo.

final void notifyAll ()

Se despiertan todos los hilos que están a la espera de este objeto de bloqueo.

final void wait()

Hace que el hilo actual tenga que esperar hasta que otro hilo llame a notify() o notifyAll().

String toString()

Devuelve una representación en texto de un objeto

En el Capítulo 9 se estudia wait(), notify() y notifyAll(). El método finalize() lo hemos visto en el Capítulo 3. Así que en esta sección nos centraremos en los métodos toString(), hashCode() y equals().


1.1. Método toString()

Sobrescribir toString() cuando quieres simplemente ser capaz de leer algo significativo acerca de los objetos de tu clase. Un código llama al método toString() de tu objeto cuando quiere leer detalles útiles sobre tu objeto. Cuando tu pasas la referencia a un objeto al método System.out.println, por ejemplo, el método toString del objeto es llamado. Veamos un ejemplo:

public class HardToRead {

public static void main (String [] args) {

HardToRead h = new HardToRead();

System.out.println(h);

}

}


Ejecutando la clase HardToRead obtenemos:



% Java HardToRead
HardToRead@a47e0

La salida anterior es la que obtienes cuando no sobrescribes el método toString() de la clase Object. Como vemos la salida es el nombre de la clase seguida del símbolo @ y seguido de la representación hexadecimal sin signo del hashCode del objeto.


Viendo esta salida deberías sobrescribir el método toString() en tus clases, por ejemplo:

public class BobTest {

public static void main (String[] args) {

Bob f = new Bob("GoBobGo", 19);

 

System.out.println(f);

}

}



class Bob {

int shoeSize;



String nickName;

Bob(String nickName, int shoeSize) {

this.shoeSize = shoeSize;

this.nickName = nickName;

}

public String toString() {



return ("I am a Bob, but you can call me " + nickName +

". My shoe size is " + shoeSize);

}

}


Esto es un poco mas legible:
% Java BobTest
I am a Bob, but you can call me GoBobGo. My shoe size is 19


1.2. Método equal()

Aprendimos algo sobre el método equal() en capítulos anteriores, cuando vimos las clases de envoltura. Discutimos cómo comparar dos referencias a objeto utilizando el operador == evaluando a verdadero sólo cuando ambas referencias se refieren al mismo objeto. Vimos que la clase String y las clases de envoltura tienen sobrescrito los métodos equal() (heredado de la clase Object), de modo que podíamos comparar dos objetos diferentes (del mismo tipo) para ver si su contenido es equivalente de manera significativa.


Cuando realmente necesitas saber si dos referencias son idénticas, usa "==". Pero si no queremos comparar las referencias si no los objetos en si, usa el método equal(). Para cada clase que escribas, debes decidir si tiene sentido comparar dos instancias iguales. Para algunas clases, podrías decidir que dos objetos nunca pueden ser iguales. Por ejemplo, imagina una clase Car que tiene variables de instancia como modelo, año, configuración (realmente no quieres que dos Car con iguales atributos sean tratados como el mismo). Si dos referencias hacen referencia del mismo Car, entonces sabrás que ambas hablan del mismo coche, y no de dos coches con los mismos atributos.Así que en el caso de Car es posible que no puedas necesitar, o querer sobrescribir el método equals(). De acuerdo, sabrás que no es el fin de la historia.

1.3. Significado de no sobrescribir equals()

Si no sobrescribes los métodos equals() de una clase, no seras capaz de usar estos objetos como clave en un hashtable y probablemente no obtendrás conjuntos exactos, de tal manera que no hay concepto duplicados.


El método equals() en la clase Object usa solo el operador "==" para las comparaciones, así que a menos que sobrescribas equals(), dos objetos serán considerados iguales solo si las dos referencias se refieren al mismo objeto.
Veamos que significa que no se pueda usar un objeto como clave de un hashtable. Imagínate que tienes un Car, un Car especifico que quieres meter en un HashMap (tipo de hashtable que veremos mas adelante en este capítulo), de tal forma que puedas buscar un coche en particular y recibir el objeto Person que representa al propietario. . Así que añades la instancia Car como clave del HashMap (junto con un objeto Person que representa el valor). Pero ¿que sucede cuando quieres hacer una búsqueda?, querrás decirle a la colección HashMap, "aquí esta el coche, ahora dame el objeto Person que corresponde a dicho coche". Pero ahora estás en un problemas a menos que todavía tengas una referencia al objeto exacto que ha utilizado como clave cuando se añadió a la colección. En otras palabras, no puedes crear un objeto Car idéntico y usarlo para la búsqueda.
La conclusión es: si quieres que los objetos de tu clase sean usados como claves para un hashtable (o como elemento en cualquier estructura de datos que use equivalencias para buscar y/o recuperar un objeto, entonces debes sobrescribir equals() de tal forma que dos instancias diferentes puedan ser consideradas la misma. Así que para el objeto Car deberíamos sobrescribir el método equals() para que se compare el valor único VIN (Número de identificación del vehículo) como la comparación base. De esta forma, puedes usar una instancia cuando añadas el Car a la colección y recrear una instancia idéntica cuando quieras hacer una búsqueda del objeto como clave de un hashtable. Sobrescribiendo el método equals() para Car también te permite la posibilidad de que más de un objeto representen a un único coche, que podría no ser seguro en tu diseño. Afortunadamente, el String y las clases de envoltura trabajan bien como claves en hashtable, ya que tienen el método equals() sobrescrito. Así que en lugar de utilizar la actual instancia de Car como la clave en el par Car/Propietario, podrías simplemente usar un String que represente el identificador único para el Car. De esta forma, nunca tendrás más de una instancia representando un coche especifico, pero podrás usar aún el coche, o al menos uno de los atributos del coche, como la clave de búsqueda.

1.4. Implementando un método equals()

Vamos a decir que sobrescribes el método equals en tu clase, debería ser como este:

public class EqualsTest {

public static void main (String [] args) {

Moof one = new Moof(8);

Moof two = new Moof(8);

if (one.equals(two)) {

System.out.println("one and two are equal");

}

}



}

class Moof {

private int moofValue;

Moof(int val) {

moofValue = val;

}

public int getMoofValue() {



return moofValue;

}

public boolean equals(Object o) {



if ((o instanceof Moof) && (((Moof)o).getMoofValue()

== this.moofValue)) {

return true;

} else {


return false;

}

}



}

Veamos este código en detalle. En el método main() de EqualTest, crearemos dos instancias Moof, pasándole al constructor el mismo valor 8. Viendo al constructor de la clase Moof, lo que hace es asignar el valor a la variable de instancia moofValue. Ahora imagínate que decides que dos objetos Moof son iguales si tienen el mismo moofValue. Así que sobrescribes el método equals() y comparas los moofValues. Es así de sencillo. Pero vamos a desglosar que sucede en el método equals().

1. public boolean equals(Object o) {

2. if ((o instanceof Moof) && (((Moof)o).getMoofValue()== this.moofValue)) {

3. return true;

4. } else {

5. return false;

6. }


7. }

Lo primero de todo, debes observar todas las normas de sobrescritura y en la linea 1 de hecho estamos declarando una valida redefinición del método equals() que heredamos de Object.


En la linea 2 es donde esta toda la acción. Lógicamente, debemos hacer dos cosas en orden para hacer una valida comparación de igualdad.
Primero, asegurarnos que el objeto que estamos poniendo a prueba es del tipo correcto!. Se presenta polimorficamente como tipo Object, así que necesitamos hacer una prueba con instanceof. Ademas, todavía tienes que hacer el instanceof para estar seguro que podrías hacer el casting al argumento para el tipo correcto de tal forma que puedas acceder a sus método o variables en orden para hacer la comparación. Recordar, si el objeto no pasa el test instanceof, entonces obtendrás una excepción en tiempo de ejecución ClassCastException. Por ejemplo:

public boolean equals(Object o) {

if (((Moof) o).getMoofValue() == this.moofValue){

// the preceding line compiles, but it's BAD!

return true;

} else {


return false;

}

}


El casting (Moof)o fallara si "o" no hace referencia a algo que pasa el test IS-A Moof.


Segundo, comparar los atributos que nos preocupan (en este caso moofValue). Sólo el desarrollador puede decidir lo que hace que dos instancias sean iguales.
Hacemos el casting para indicarle al compilador que el método que queremos usar no es de Object si no de la clase Moof en particular. Si no hiciésemos el casting no podríamos compilar porque el compilador vería al objeto referenciado como un Object y Object no tiene el método getMoofValue(). Pero como dijimos anteriormente, solo con el casting también falla en tiempo de ejecución si el objeto referenciado por "o" no es casteable a Moof. Así que no olvides usar el la primera prueba instanceof. Aquí apreciamos el operador &&, si la prueba instanceof falla, nunca haremos el casting, de tal forma que nos aseguraremos que no haya fallo en tiempo de ejecución:

if ((o instanceof Moof) && (((Moof)o).getMoofValue()

== this.moofValue)) {

return true;

} else {

return false;

}

Si nos fijamos en la especificación de la API de Java de la clase Object, encontraras que el método equals() es llamado un contrato especificado. Un contrato en Java es un conjunto de reglas que deberían ser seguidas si quieres proporcionar una implementación correcta como los demás esperan que sea. O ponerlo de otra forma, sin seguir el contrato, tu código todavía puede compilar y ejecutar, pero tu código puede romperse en tiempo de ejecución de alguna forma inesperada.



Exam Watch

1.5. Contrato equals()

Siguiendo estrictamente la documentación de Java, el contrato de equals() dice:



  • Es reflexivo. Para cualquier referencia por ejemplo x, x.equals(x) debería devolver true.

  • Es simétrico. Para cualquier par de referencias, por ejemplo x e y, si x.equals(y) es true si y solo si y.equals(x) devuelve true.

  • Es transtivo. Para cualquier conjunto de referencias, por ejemplo 'x', 'y' y 'z', si x.equals(y) devuelve true y para y.equals(z) devuelve true, entonces z.equals(x) debe devolver true.

  • Es consistente. Para cualquier pareja de referencias, por ejemplo x e y, hacer x.equals(y) consistentemente devuelve true o consistentemente devuelve false, ninguna información usada en comparaciones equals modifican al objeto.

  • Para cualquier referencia no nula, por ejemplo x, x.equals(null) debe devolver false.

No hemos visto el método hashCode(), pero equals() y hashCode están unidos por un contrato que especifica que si dos objetos son considerados iguales usando equals(), entonces deben obtener igual resultado con hashCode. Por lo tanto para estar absolutamente seguros, debes tener una regla, si sobrescribes equals(), sobrescribe hashCode también. Así que veamos hashCode.




1.6. Sobrescribiendo hashCode

HashCode es usado normalmente para incrementar el rendimiento de las grandes colecciones de datos. El valor hashCode de un objeto es usado por algunas clases de colecciones. Aunque puedes pensar de esto que es un tipo de numero identificador (ID) de un objeto, no es necesariamente único. Las colecciones como HashMap y HashSet usan el valor hashCode de un objeto para determinar como debe ser almacenado en la colección y también sirve para localizar al objeto en la colección. Para el examen no necesitas conocer en profundidad detalles de como las clases Collection que usan "hashing" son implementadas, pero necesitas saber que colecciones lo usan (pero, todos tienen "hash" en el nombre). Debes también ser capaz de reconocer una adecuada o correcta implementación de hashCode(). Esto no significa ni legal ni eficiente. Es perfectamente legal tener un terrible método hashCode ineficiente en tu clase, siempre que no viole el contrato que se especifica en la documentación de la clase Object. Así que para el examen, si se te pide que selecciones un uso apropiado o correcto de hashCode, no pienses en legal o eficiente.




1.7. Entendiendo los HashCodes

Para entender que es apropiado y correcto, tenemos que ver como usan algunas de las colecciones los hashCodes.


Imagínate un conjunto de cubos alineados en el suelo. Tienen un trozo de papel con el nombre propio y coges el nombre y calculas un código entero para cada letra, por ejemplo A es 1, B es 2 y así sucesivamente añadiendo los valores numéricos a todas las letras. Un nombre propio siempre resulta el mismo código, veamos la siguiente figura.

No introducimos nada al azar, simplemente tenemos un algoritmo que siempre se ejecuta de la misma manera dada una entrada especifica, la salida siempre sera idéntica para cualquier par de entradas idénticas. Ahora veremos la manera de usar este código para determinar cual es el cubo al que le tengo que colocar el papel (imagínate que cada cubo representa un código numérico diferente). Ahora imagina que alguien viene, te muestra un nombre y dice, "Por favor, recupere el pedazo de papel que coincida con este nombre." Así que tu buscas el nombre que te muestran y ejecutas el mismo algoritmo de generación de hashCode. El HashCode te dirá en que cubo deberías encontrar el nombre.


Es posible que hayas encontrado algún fallo en nuestro sistema. Dos nombres diferentes podrían obtener el mismo resultado al aplicar el algoritmo. Por ejemplo, los nombre Amy y May tienen las mismas letras, así que el hashCode sera idéntico para ambos. Esto es aceptable, pero significa que cuando alguien te pregunte por la pieza de papel de Amy, todavía tienes que buscar a través de los cubos de lectura cada nombre hasta que nos encontramos con Amy en lugar de May. El hashCode solo te dice a que cubo debes ir, pero no como localizar el nombre una vez estés en el cubo.

Exam Watch

Por lo tanto, para la eficiencia, su objetivo es tener los papeles distribuidos uniformemente como sea posible en todos los cubos. Idealmente, puede ser que sólo tenga un nombre por cubo de manera que cuando alguien pida un documento se puede calcular simplemente el hashCode y sólo agarrar un papel del cubo correcto (sin tener que ir buscando a través de diferentes papeles en el cubo hasta que usted localice el exacto que está buscando). Los menos eficientes (pero aún funcionales) generadores de hashCode devolverían el mismo hashCode independientemente del nombre, a fin de que todos los papeles aterrizen en el mismo cubo, mientras que los otros estan vacíos. The bucket-clerk would have to keep going to that one bucket and flipping painfully through each one of the names in the bucket until the right one was found. And if that's how it works, they might as well not use the hashcodes at all but just go to the one big bucket and start from one end and look through each paper until they find the one they want.

Este ejemplo de distribución a través de los cubos es similar a la manera en que se utilizan los hashcodes en las colecciones. Al poner un objeto en una colección que utiliza hashcodes, la colección utiliza el hashCode del objeto para decidir en qué cubo/ranura el objeto debe estar. Luego, cuando quieras buscar ese objeto (o, para un hashtable, recuperar el valor asociado con ese objeto), tienes que darle a la colección una referencia a un objeto que la colección compare con los objetos que posea. Mientras que el objeto (almacenado en la colección, al igual que un papel en el cubo) que estas intentando buscar tenga el mismo hashCode que el objeto que estas utilizando para la búsqueda , entonces el objeto se encontrara. Pero… y este es un Big One, imaginar qué pasaría si, volviendo a nuestro ejemplo de nombres, le enseño el cubo de trabajo un nombre y el código calculado sobre la mitad de las letras en el nombre en lugar de todos ellos. Ellos nunca encontrarían el nombre en el cubo porque no se buscaría en el cubo correcto!

Ahora se puede ver por qué si dos objetos se consideran iguales, su hashcodes también debe ser igual? De lo contrario, nunca seras capaz de encontrar el objeto desde el método hashCode por defecto de la clase Object casi siempre viene con un número único para cada objeto, incluso si el método equals()es sobrescrito de tal manera que dos o más objetos se consideran iguales. No importa la igualdad de los objetos si sus hashcodes no reflejan esa igualdad. Por lo tanto, una vez más: si dos objetos son iguales, sus hashcodes debe ser iguales también.


1.8. Implementando hashCode()

¿Que comprobación hace realmente un algoritmo de hashCode? La gente tiene sus tesis sobre los algoritmos hash, desde el punto de vista de la informática, esto va más allá del alcance del examen. La parte que nos interesa aquí es la cuestión de si sigues el contrato. Y para seguir el contrato, piensa lo que haces en el método equals(). Tu comparas atributos. Debido a que la comparación casi siempre implica a los valores de las variables (Recuerda cuando vimos a dos objetos Moof y los considerabas iguales si sus variables int moofValues eran las mismas?). Tu implementación de hashCode () debe utilizar la misma variables. He aquí un ejemplo:

class HasHash {

public int x;

HasHash(int xVal) { x = xval; }

 

public boolean equals(Object o) {



HasHash h = (HasHash) o; // Don't try at home without

// instanceof test

if (h.x == this.x) {

return true;

} else {

return false;

}

 

}



public int hashCode() { return (x * 17) ; }

}


Este método equals() dice que dos objetos son iguales si tienen el mismo valor x, así que los objetos con el mismo valor x tendrán que devolver idéntico hashcodes.

Exam Watch

Normalmente, veras que los métodos hashCode() hacen alguna combinación de XOR-ing con una variable instancia de la clase, junto con alguna multiplicación de un número primo. En cualquier caso, mientras que la meta es obtener una distribución amplia y aleatoria de los objetos, el contrato requiere solo que dos objetos iguales tengan idéntico hashCode. El examen no pretende hacer métodos hashCode() eficientes, pero si debe reconocer si funcionara o no funcionara (es decir, que el objeto sea encontrado en una colección hash).


Ahora que sabemos que dos objetos iguales deben tener idéntico hashCode, ¿es cierto lo contrario?. ¿Dos objetos con idéntico hashcode tienen que ser considerados iguales?. Piénsalo, podrías tener multitud de objetos en el mismo cubo porque sus hashcodes son idénticos, pero a menos que ellos pasen la prueba equals(), no van a ser encontrados en la colección tan fácilmente. Esto seria el resultado de una implementación ineficiente. Es legal y correcto. pero lentoooo.


1.9. El contrato hashCode()

Ahora viendo la fabulosa documentación de la API de Java para la clase Object, podemos presentar el contrato de hashCode():



  • Siempre que es invocado con el mismo objeto más de una vez durante la ejecución de una aplicación Java, el hashCode () debe devolver constantemente el mismo número entero, siempre y cuando la información utilizada para las comparaciones equals() no modifiquen el objeto. Este entero no es necesario que mantenga la coherencia de la ejecución en una aplicación a otra ejecución de la misma aplicación.




  • Si dos objetos son iguales de acuerdo con el método equals(java.lang.Object), entonces se pide que el método hashCode() en cada uno de los dos objetos debe devolver el mismo resultado entero.




  • No es necesario que si dos objetos son desiguales en función del método equals(Java.lang.Object), entonces se pide que hashCode () en cada uno de los dos objetos debe devolver como resultado dos enteros distintos. Sin embargo, el programador debe ser consciente de que la producción de distintos resultados enteros para los objetos puede mejorar el rendimiento de tablas hash.

Y lo que esto significa para ti es ...



Condición

Requerido

No requerido (pero permitido)

x.equals(y) == true

x.hashCode() == y.hashCode()




x.hashCode() == y.hashCode()




x.equals(y) == true

x.equals(y) == false




No requisitos hashCode()

x.hashCode() =! y.hashCode()

x.equals(y) == false



Por lo tanto, echemos un vistazo a ¿qué otra cosa podría causar un método hashCode () falle. ¿Qué ocurre si se incluyera una variable transient en tu método hashCode ()? Si bien eso es legal (el compilador no se queja), en algunas circunstancias, un objeto que puso en una colección no se encontrara. Como sabemos, la serialización guarda un objeto para que pueda ser reanimado después de la desserailización de vuelta a la plena objectness. Pero peligro, recordar que las variables transient no se guardan cuando un objeto es serializado. Un mal escenario puede tener un aspecto como este:

class SaveMe implements Serializable{

transient int x;

int y;

SaveMe(int xVal, int yVal) {



x = xVal;

y = yval;

}

public int hashCode() {



return (x ^ y); // Legal, but not correct to

// use a transient variable

}

public boolean equals(Object o) {



SaveMe test = (SaveMe)o;

if (test.y == y && test.x == x) { // Legal, not correct

return true;

} else {


return false;

}

}



}


Esto es lo que podría suceder utilizando código como el ejemplo anterior:

1. Obtener algún estado de un objeto (asignar valores a sus variables de instancia).
2. Colocar el objeto en un HashMap, usando el objeto como un elemento clave.
3. Guardar el objeto a un archivo utilizando serialización sin alterar cualquiera de sus estado.
4. Recuperar el objeto del fichero a través de la desserialización.
5. Utilizar el objeto desserializado (traído de vuelta a la vida en el heap) para obtener el objeto del HashMap.

Vaya. El objeto de la colección y el mismo objeto, supuestamente cuando vuelven a la vida ya no son idénticos. El objeto de la variable transient volverá con un valor por defecto y no con el valor que tenia la variable en el momento en que se salvó (o puesto en el HashMap). Por lo tanto, utilizando el código anterior SaveMe, si el valor de x es 9, cuando el caso se pone en el HashMap, desde entonces x se utiliza para el cálculo de la hashCode, cuando el valor de x cambie, el hashcode también cambia. Y cuando ese mismo ejemplo de SaveMe es traído de vuelta de desserialización, x == 0, independientemente del valor de x en el momento en que el objeto era serializado. Por lo tanto, el nuevo cálculo de hashCode dará un hashCode diferente, y el equals () fallara también ya que el método equals() usa x para determinar la igualdad de un objeto.

En pocas palabras: las variables transient pueden realmente ser un lío con las implementaciones de equals() y hashCode(). Mantener las variables no transient o, si es que debe marcarse transient, no utilizarla entonces para determinar hashcodes o la igualdad.

  1   2   3   4   5


La base de datos está protegida por derechos de autor ©absta.info 2016
enviar mensaje

    Página principal