| almacenamiento de datos | Almacenamiento de Datos | ||||||
| Anterior | Siguiente |
|
Ahora se mostrarán clases que permiten almacenar
colecciones de objetos, bien de una forma u otra, para facilitar ciertas
operaciones que son habituales en la programación.
Mucho de lo que se podría decir de los arrays se ha comentado antes, aquí sólo interesa el array
como almacén de objetos. Hay dos características que diferencian a los arrays
de cualquier otro tipo de colección: eficiencia y tipo. El array
es la forma más eficiente que Java proporciona para almacenar y acceder a una
secuencia de objetos. El array es una simple secuencia lineal, que hace que el
acceso a los elementos sea muy rápido, pero el precio que hay que pagar por
esta velocidad es que cuando se crea un array su tamaño es fijado y no se
puede cambiar a lo largo de la vida del objeto. Se puede sugerir la creación
de un array de tamaño determinado y luego, ya en tiempo de ejecución, crear
otro más grande, mover todos los objetos al nuevo y borrar el antiguo. Esto es
lo que hace la clase Vector, que se verá posteriormente, pero debido a
la carga que supone esta flexibilidad, un Vector es menos eficiente que
un array, en cuestiones de velocidad.
Los otros tipos de colecciones disponibles en Java:
Vector, Stack y Hashtable; pueden contener cualquier tipo
de objeto, sin necesidad de que sea de un tipo definido. Esto es así porque
tratan a sus elementos como si fuesen Object, la clase raíz de todas
las clases Java. Esto es perfecto desde el punto de vista de que se construye
solamente una colección, y cualquier tipo de objeto puede ser almacenado en
ella. Pero aquí es donde los arrays vuelven a ser más eficientes que las
colecciones genéricas, porque cuando se crea un array hay que indicar el tipo
de objetos que va a contener. Esto significa que ya en tiempo de compilación
se realizan comprobaciones para que no se almacene en el array ningún objeto
de tipo diferente al que está destinado a contener, ni que se intente extraer
un objeto diferente. Desde luego, Java controlará de que no se envíe un
mensaje inadecuado a un objeto, ya sea en tiempo de compilación como en tiempo
de ejecución.
Las colecciones de clases manejan solamente los
identificadores, handles, de los objetos. Un array, sin embargo, puede
crearse para contener tipos básicos directamente, o también identificadores de
objetos. Es posible utilizar las clases correspondientes a los tipos básicos,
como son Integer, Double, etc., para colocar tipos básicos
dentro de una colección. El colocar una cosa u otra es cuestión de eficiencia,
porque es mucho más rápida la creación y acceso en un array de tipos básicos
que en uno de objetos del tipo básico.
Desde luego, si se está utilizando un tipo básico y se
necesita la flexibilidad que ofrece una colección de expandirse cuando sea
preciso, el array no sirve y habrá que recurrir a la colección de objetos del
tipo básico. Quizás se podría pensar que un Vector especializado en
cada uno de los tipos básicos podría ser casi igual de eficiente que un array,
pero por desgracia, Java no proporciona mas que un tipo de genérico de
Vector, en el que se puede meter de todo.
Cuando se necesitan características más sofisticadas
para almacenar objetos, que las que proporciona un simple array, Java pone a
disposición del programador las clases colección: Vector,
BitSet, Stack y Hashtable, que mejora en la plataforma
Java 2 con las nuevas Colecciones: Map, List y Set.
El siguiente diagram muestra la jerarquía de clases
que integran las colecciones en la plataforma Java 2.
|
|
|
|
En la figura destaca la separación entre
Collection y Map. Además, a la hora de diseñar aplicaciones que
utilicen colecciones Java, es importante que el programador recuerde los
siguientes puntos:
Entre otras características, las clases
Collection se redimensionan automáticamente, por lo que se puede
colocar en ellas cualquier número de objetos, sin necesidad de tener que ir
controlando continuamente en el programa la longitud de la
colección.
La gran desventaja del uso de las colecciones
en Java es que se pierde la información de tipo cuando se coloca un objeto en
una colección. Esto ocurre porque cuando se escribió la colección, el
programador de esa colección no tenía idea del tipo de datos específicos
que se iban a colocar en ella, y teniendo en mente el hacer una herramienta lo
más general posible, se hizo que manejase directamente objetos de tipo
Object, que es el objeto raíz de todas las clases en Java. La solución
es perfecta, excepto por dos razones:
La verdad es que no todo es tan negro, Java no permite
que se haga uso inadecuado de los objetos que se colocan en una colección. Si
se introduce un coche en una colección de animales mamíferos, al
intentar extraer el coche se obtendrá una excepción. Y del mismo modo, si se
intenta colocar un moldeo al coche que se está sacando de la colección para
convertirlo en animal mamífero, también se obtendrá una excepción en tiempo de
ejecución. El ejemplo java411.java
ilustra estas circunstancias.
import java.util.*;
class Coche {
private int numCoche;
Coche( int i ) {
numCoche = i;
}
void print() {
System.out.println( "Coche #"+numCoche );
}
}
class Barco {
private int numBarco;
Barco( int i ) {
numBarco = i;
}
void print() {
System.out.println( "Barco #"+numBarco );
}
}
public class java411 {
public static void main( String args[] ) {
Vector coches = new Vector();
for( int i=0; i < 7; i++ )
coches.addElement( new Coche( i ) );
// No hay ningun problema en añadir un barco a los coches
coches.addElement( new Barco( 7 ) );
for( int i=0; i < coches.size(); i++ )
(( Coche )coches.elementAt( i ) ).print();
// El barco solamente es detectado en tiempo de ejecución
}
}
Como se puede observar, el uso de un Vector es
muy sencillo: se crea uno, se colocan elementos en él con el método
addElement() y se recuperan con el método elementAt().
Vector tiene el método size() que permite conocer cuántos
elementos contiene, para evitar el acceso a elementos fuera de los límites del
Vector y obtener una excepción.
Las clases Coche y Barco son distintas,
no tienen nada en común excepto que ambas son Object. Si no se indica
explícitamente de la clase que se está heredando, automáticamente se hereda de
Object. La clase Vector maneja elementos de tipo Object,
así que no solamente es posible colocar en ella objetos Coche
utilizando el método addElement(), sino que también se pueden colocar
elementos de tipo Barco sin que haya ningún problema ni en tiempo de
compilación, ni a la hora de ejecutar el programa. Cuando se recupere un objeto
que se supone es un Coche utilizando el método elementAt() de la
clase Vector, hay que colocar un moldeo para convertir el objeto
Object en el Coche que se espera, luego hay que colocar toda la
expresión entre paréntesis para forzar la evaluación del moldeo antes de
llamar al método println() de la clase Coche, sino habrá un error
de sintaxis. Posteriormente, ya en tiempo de ejecución, cuando se intente
moldear un objeto Barco a un Coche, se generará una excepción,
tal como se puede comprobar en las siguientes líneas, que reproducen la salida
de la ejecución del ejemplo:
% java java411
Coche #0
Coche #1
Coche #2
Coche #3
Coche #4
Coche #5
Coche #6
Exception in thread "main" java.lang.ClassCastException
at java411.main(java411.java:34)
Lo cierto es que esto es un fastidio, porque puede ser
la fuente de errores que son muy difíciles de encontrar. Si en una parte, o en
varias partes, del programa se insertan elementos en la colección, y se
descubre en otra parte diferente del programa que se genera una excepción es
porque hay algún elemento erróneo en la colección, así que hay que buscar el
sitio donde se ha insertado el elemento de la discordia, lo cual puede llevar
a intensas sesiones de depuración. Así que, para enredar al principio, es
mejor empezar con clases estandarizadas en vez de aventurarse en otras más
complicadas, a pesar de que estén menos optimizadas.
En cualquier clase de colección, debe haber una forma
de meter cosas y otra de sacarlas; después de todo, la principal finalidad de
una colección es almacenar cosas. En un Vector, el método
addElement() es la manera en que se colocan objetos dentro de la
colección y llamando al método elementAt() es cómo se sacan.
Vector es muy flexible, se puede seleccionar cualquier cosa en
cualquier momento y seleccionar múltiples elementos utilizando diferentes
índices.
Si se quiere empezar a pensar desde un nivel más alto,
se presenta un inconveniente: la necesidad de saber el tipo exacto de la
colección para utilizarla. Esto no parece que sea malo en principio, pero si
se empieza implementando un Vector a la hora de desarrollar el
programa, y posteriormente se decide cambiarlo a List, por eficiencia,
entonces sí es problemático.
El concepto de enumerador, o iterador,
que es su nombre más común en C++ y OOP, puede utilizarse para alcanzar el
nivel de abstracción que se necesita en este caso. Es un objeto cuya misión
consiste en moverse a través de una secuencia de objetos y seleccionar
aquellos objetos adecuados sin que el programador cliente tenga que conocer la
estructura de la secuencia . Además, un iterador es normalmente un objeto
ligero, lightweight, es decir, que consumen muy pocos recursos, por lo
que hay ocasiones en que presentan ciertas restricciones; por ejemplo, algunos
iteradores solamente se puede mover en una dirección.
La Enumeration en Java es un ejemplo de un
iterador con esas características, y las cosas que se pueden hacer
son:
Y esto es todo. No obstante, a pesar de su
simplicidad, alberga bastante poder. Para ver cómo funciona, el ejemplo java412.java, es la modificación de
anterior, en que se utilizaba el método elementAt() para seleccionar
cada uno de los elementos. Ahora se utiliza una enumeración para el mismo
propósito.
/**
* Sencilla Colección con una Enumeration
*/
import java.util.*;
class Coche {
private int numCoche;
Coche( int i ) {
numCoche = i;
}
void print() {
System.out.println( "Coche #"+numCoche );
}
}
class Barco {
private int numBarco;
Barco( int i ) {
numBarco = i;
}
void print() {
System.out.println( "Barco #"+numBarco );
}
}
public class java412 {
public static void main( String args[] ) {
Vector coches = new Vector();
for( int i=0; i < 7; i++ )
coches.addElement( new Coche( i ) );
// No hay ningun problema en añadir un barco a los coches
coches.addElement( new Barco( 7 ) );
Enumeration e = coches.elements();
while( e.hasMoreElements() )
(( Coche )e.nextElement()).print();
// El barco solamente es detectado en tiempo de ejecución
}
}
El único código interesante del ejemplo es el cambio de las líneas del
ejemplo original:
for( int i=0; i < coches.size(); i++ )
(( Coche )coches.elementAt( i ) ).print();
por estas otras en que se utiliza la enumeración para
recorrer la secuencia de objetos:
while( e.hasMoreElements() )
(( Coche )e.nextElement()).print();
Con la Enumeration no hay que preocuparse del
número de elementos que contenga la colección, ya que del control sobre ellos
se encargan los métodos hasMoreElements() y
nextElement().
Un iterador es un mecanismo que permite recorrer los
elementos de una estructura de datos. La interfaz Iterator es parte del
entorno de las colecciones en Java, que es extendida por la interfaz
ListIterator, que incorpora métodos para desplazarse hacia delante y
hacia atrás y realizar modificaciones en la lista durante las iteraciones.
El uso de iteradores se hace necesario a la hora de
mainpular estructuras de datos complejas en donde no sea obvia la forma de
desplazarse por ellas; por ejemplo, estructuras de tipo árbol. Además, los
iteradores proporcionan una interfaz uniforme para acceder a tipos de datos
diferentes. El ejemplo java433.java es
una aplicación muy simple que presenta los datos contenidos en cualquier tipo
de colección o mapa.
import java.util.*;
public class java433 {
static void muestraElementos( Object obj ) {
// En el caso de una colección de tipo Map, hay que recuperar las
// claves y los valores correspondientes a los elementos, para poder
// utilizar el iterador sobre la lista de elementos que se obtenga
if( obj instanceof Map )
obj = (( Map )obj).entrySet();
// Mostramos los elementos de la colección, siempre que se trate de
// una colección
if( obj instanceof Collection ) {
Collection coleccion = (Collection)obj;
Iterator iterador = coleccion.iterator();
while( iterador.hasNext() )
System.out.println( iterador.next() );
}
// En caso de que no estemos ante una colección, indicamos el fallo
else {
System.out.println( "No es una colección válida" );
}
}
public static void main( String args[] ) {
// List
List lista = new ArrayList();
lista.add( "línea 1" );
lista.add( "línea 2" );
muestraElementos( lista );
// Set
Set conjunto = new TreeSet();
conjunto.add( "línea 3" );
conjunto.add( "línea 4" );
muestraElementos( conjunto );
// Map
Map mapa = new HashMap();
mapa.put( "línea 5:clave","línea 5:valor" );
mapa.put( "línea 6:clave","línea 6:valor" );
muestraElementos( mapa );
}
}
Colecciones de tipo List y Set extienden
la interfaz Collection, pero Map no, aunque es posible extraer
un conjunto de datos de un objeto Map y luego iterar sobre él.
Los métodos que porporciona la interfaz
Iterator son:
El ejemplo java434.java intenta modificar una lista
mientras se están realizando iteraciones sobre ella. El código del ejemplo es
el que se reproduce a continuación.
import java.util.*;
public class java434 {
public static void main( String args[] ) {
// Creamos la lista de elementos de prueba
List lista = new ArrayList();
lista.add( "línea 1" );
lista.add( "línea 2" );
lista.add( "línea 3" );
lista.add( "línea 4" );
lista.add( "línea 5" );
lista.add( "línea 6" );
lista.add( "línea 7" );
// Creamos el iterador que utilizaremos para recorrer la lista
Iterator iterador = lista.iterator();
for( int i=0; iterador.hasNext(); i++ ) {
String elemento = (String)iterador.next();
// Intentamos eliminar el tercer elemento de la lista
if( elemento.equals("línea 3") ) {
// Esta línea provoca una excepción al eliminar
// el elemento de tipo
// ConcurrentModificationException
lista.remove( i );
// Si se comenta la línea anterior y se descomenta
// la siguiente, se elimina la excepción
// iterador.remove();
}
// Imprimimos la lista original
System.out.println( elemento );
}
// Imprimimos la lista sin el elemento eliminado antes
iterador = lista.iterator();
while( iterador.hasNext() )
System.out.println( "--"+iterador.next() );
}
}
Si el lector ejecuta la aplicación, obtendrá una
excepción de tipo ConcurrentModificationException, provocada al
intentar eleiminar un elemento de la lista mientras se iteraba sobre ella.
Tanto las implementaciones de la lista como el iterador permiten detectar el
problema. Sin embargo, es posible eliminar de forma segura el elemento
utilizando el método remove() que proporciona la implementación del
iterador. Si el lector comenta la línea de código que invoca al método
remove() de la lista, y descomenta la línea que llama al método
remove() del iterador, ya no aparecerá la excepción cuando se elimine
el elemento de la lista.
También es posible utilizar un iterador para filtrar
la salida de otro iterador. El ejemplo java435.java permite que un iterador filtre
el contenido de la lista para obtener solamente aquellos elementos de esa
lista que implementan la interfaz Number, como pueden ser
Integer o Double.
En la plataforma Java 2 se proporcionan bibliotecas de
colecciones que satisfacen casi cualquier proyecto que se emprenda en donde
sea necesario el uso de un sistema propio de almacenamiento de datos. A
continuación se verán cada una de estas bibliotecas de colecciones por separado
para dar una idea del potencial que se ha incorporado a Java.
El Vector es muy simple y fácil de utilizar.
Aunque los métodos más habituales en su manipulación son addElement()
para insertar elementos en el Vector, elementAt() para
recuperarlos y elements() para obtener una Enumeration con el
número de elementos del Vector, lo cierto es que hay más métodos, y al
igual que sucede con todas las bibliotecas de Java, se remite al lector a que
consulte la documentación electrónica que proporciona Javasoft, para
conocer todos los métodos que componen esta clase.
Las colecciones estándar de Java contienen el método
toString(), que permite obtener una representación en forma de
String de sí mismas, incluyendo los objetos que contienen. Dentro de
Vector, por ejemplo, toString() va saltando a través de los
elementos del Vector y llama al método toString() para cada uno
de esos elementos. En caso, por poner un ejemplo, de querer imprimir la
dirección de la clase, parecería lógico referirse a ella simplemente como
this (los programadores C++ estarán muy inclinados a esta posibilidad),
tal como muestra el ejemplo java413.java
y que se reproduce en las siguientes líneas.
import java.util.*;
public class java413 {
public String toString() {
return( "Dirección del objeto: "+ this +"\n" );
}
public static void main( String args[] ) {
Vector v = new Vector();
for( int i=0; i < 10; i++ )
v.addElement( new java413() );
System.out.println( v );
}
}
El ejemplo no puede ser más sencillo, simplemente crea
un objeto de tipo java413 y lo imprime; sin embargo, a la hora de
ejecutar el programa lo que se obtiene es una secuencia infinita de
excepciones. Lo que está pasando es que cuando se le indica al
compilador:
"Direccion del objeto: "+ this
el compilador ve un String seguido del operador
+ y otra cosa que no es un String,
así que intenta convertir this en un String. La conversión la
realiza llamando al método toString() que genera una llamada recursiva,
llegando a llenarse la pila.
Si realmente se quiere imprimir la dirección del
objeto en este caso, la solución pasa por llamar al método toString()
de la clase Object. Así, si en vez de this se coloca
super.toString(), el ejemplo funcionará. En otros casos, este método
también funcionará siempre que se esté heredando directamente de Object
o, aunque no sea así, siempre que ninguna clase padre haya sobreescrito el
método toString().
Se llama así lo que en realidad es un Vector de
bits. Lo que ocurre es que está optimizado para uso de bits. Bueno, optimizado
en cuanto a tamaño, porque en lo que respecta al tiempo de acceso a los
elementos, es bastante más lento que el acceso a un array de elementos del
mismo tipo básico.
Además, el tamaño mínimo de un BitSet es de 64
bits. Es decir, que si se está almacenando cualquier otra cosa menor, por
ejemplo de 8 bits, se estará desperdiciando espacio.
En un Vector normal, la colección se expande
cuando se añaden más elementos. En el BitSet ocurre lo mismo pero
ordenadamente. El ejemplo java414.java,
muestra el uso de esta colección.
/**
* Este ejemplo demuestra la utilizacion de BitSets y un poco de su
* manipulación
*/
import java.util.*;
public class java414 {
public static void main(String args[]) {
Random aleat = new Random();
// Toma el bit menos significativo devuelto por nextInt()
byte bt = (byte)aleat.nextInt();
BitSet bbyte = new BitSet();
for( int i=7; i >= 0; i-- ) {
if( ( (1 << i) & bt ) != 0 )
bbyte.set( i );
else
bbyte.clear( i );
}
System.out.println( "Valor byte: "+bt );
printBitSet( bbyte );
short st = (short)aleat.nextInt();
BitSet bshort = new BitSet();
for( int i=15; i >= 0; i-- ) {
if( ( (1 << i) & st ) != 0 )
bshort.set( i );
else
bshort.clear( i );
}
System.out.println( "Valor short: "+st );
printBitSet( bshort );
int it = aleat.nextInt();
BitSet bint = new BitSet();
for( int i=31; i >= 0; i-- ) {
if( ( (1 << i) & it ) != 0 )
bint.set( i );
else
bint.clear( i );
}
System.out.println( "Valor int: "+it );
printBitSet(bint);
// Prueba BitSets mayores o iguales a 64 bits
BitSet b1 = new BitSet();
b1.set( 127 );
System.out.println( "Fija el bit 127: "+b1 );
BitSet b2 = new BitSet( 65 );
b2.set( 255 );
System.out.println( "Fija el bit 255: "+b2 );
BitSet b3 = new BitSet( 512 );
b3.set( 1023 );
System.out.println( "Fija el bit 1023: "+b3 );
}
static void printBitSet( BitSet b ) {
System.out.println( "Bits: "+b );
String bbits = new String();
for( int j=0; j < b.size(); j++ )
bbits += ( b.get( j ) ? "1" : "0" );
System.out.println( "Patrón de bits: "+bbits );
}
}
Se utiliza el generador de números aleatorios para
obtener un byte, un short y un int, que son
convertidos a su patrón de bits e incorporados al BitSet.
Un Stack es una Pila, o una
colección de tipo LIFO (last-in, first-out). Es decir, lo
último que se coloque en la pila será lo primero que se saque. Como en todas
las colecciones de Java, los elementos que se introducen y sacan de la pila
son Object, así que hay que tener cuidado con el moldeo a la hora de
sacar alguno de ellos.
Los diseñadores de Java, en vez de utilizar un
array como bloque para crear un Stack, han hecho que
Stack derive directamente de Vector, así que tiene todas las
características de un Vector más alguna otra propia ya del
Stack. El ejemplo siguiente, java415.java, es una demostración muy
simple del uso de una Pila que consisten en leer cada una de las
líneas de un array y colocarlas en un String.
import java.util.*;
public class java415 {
static String diasSemana[] = {
"Lunes", "Martes", "Miércoles", "Jueves",
"Viernes", "Sábado", "Domingo" };
public static void main( String args[] ) {
Stack pila = new Stack();
for( int i=0; i < diasSemana.length; i++ )
pila.push( diasSemana[i]+" " );
System.out.println( "pila = "+pila );
// Tratando la Pila como un Vector:
pila.addElement( "Esta es la última linea" );
// Se imprime el elemento 5 (sabiendo que la cuenta empieza en 0)
System.out.println( "Elemento 5 -> "+pila.elementAt( 5 ) );
System.out.println( "Elementos introducidos:" );
while( !pila.empty() )
System.out.println( pila.pop() );
}
}
Cada línea en el array diasSemana se inserta en el Stack con push() y
posteriormente se retira con pop(). Para ilustrar una afirmación
anterior, también se utilizan métodos propios de Vector sobre el
Stack. Esto es posible ya que en virtud de la herencia un Stack
es un Vector, así que todas las operaciones que se realicen sobre un
Vector también se podrán realizar sobre un Stack, como por
ejemplo, elementAt().
Un Vector permite selecciones desde una
colección de objetos utilizando un número, luego parece lógico pensar que hay
números asociados a los objetos. Bien, entonces ¿qué es lo que sucede cuando
se realizan selecciones utilizando otros criterios? Un Stack podría
servir de ejemplo: su criterio de selección es "lo último que se haya
colocado en el Stack". Si rizamos la idea de "selección
desde una secuencia", nos encontramos con un mapa, un
diccionario o un array asociativo. Conceptualmente, todo parece
ser un vector, pero en lugar de acceder a los objetos a través de un número,
en realidad se utiliza otro objeto. Esto nos lleva a utilizar
claves y al procesado de claves en el programa. Este concepto se
expresa en Java a través de la clase abstracta Dictionary. El interfaz
para esta clase es muy simple:
Todo es lo que corresponde a un Diccionario
(Dictionary), que no es excesivamente difícil de implementar.
La biblioteca estándar de Java solamente incorpora una
implementación de un Dictionary, la Hashtable. Hashtable
tiene la mismo interfaz básica que Dictionary, pero difiere en algo muy
importante: la eficiencia. Si en un Dictionary se realiza un
get() para obtener un valor, la búsqueda es bastante lenta a través del
vector de claves. Aquí es donde la Hashtable acelera el proceso, ya que
en vez de realizar la tediosa búsqueda línea a línea a través del vector de
claves, utiliza un valor especial llamado código hash. El código
hash es una forma de conseguir información sobre el objeto en cuestión y
convertirlo en un int relativamente único
para ese objeto. Todos los objetos tienen un código hash y
hashCode() es un método de la clase Object. Una
Hashtable coge el hashCode() del objeto y lo utiliza para cazar
rápidamente la clave. El resultado es una impresionante reducción del tiempo
de búsqueda. La forma en que funciona una tabla Hash se escapa del
Tutorial, hay muchos libros que lo explican en detalle, por ahora es
suficiente con saber que la tabla Hash es un Diccionario muy
rápido y que un Diccionario es una herramienta muy útil.
Para ver el funcionamiento de la tabla Hash
está el ejemplo java417.java, que
intenta comprobar la aleatoriedad del método Math.random(). Normalmente,
debería producir una distribución perfecta de números aleatorios, pero para
poder comprobarlo sería necesario generar una buena cantidad de números
aleatorios y comprobar los rangos en que caen. Una Hashtable es
perfecta para este propósito al asociar objetos con objetos, en este caso, los
valores producidos por el método Math.random() con el número de veces
en que aparecen esos valores.
/**
* Esta es una demostración de la utilización de la clase HashTable
*/
import java.util.*;
class Contador {
int i = 1;
public String toString() {
return( Integer.toString( i ) );
}
}
class java417 {
public static void main( String args[] ) {
Hashtable ht = new Hashtable();
for( int i=0; i < 10000; i++ ) {
// Genera un número cuasi-aleatorio entre 0 y 20
Integer r = new Integer( (int)( Math.random()*20 ) );
if( ht.containsKey( r ) )
(( Contador )ht.get( r ) ).i++;
else
ht.put( r, new Contador() );
}
System.out.println( ht );
}
}
En el método main(), cada vez que
se genera un número aleatorio, se convierte en objeto Integer para que
pueda ser manejado por la tabla Hash, ya que no se pueden utilizar
tipos básicos con una colección, porque solamente manejan objetos. El método
containsKey() comprueba si la clave se encuentra ya en la colección. En
caso afirmativo, el método get() obtiene el valor asociado a la clave,
que es un objeto de tipo Contador. El valor i dentro del contador se incrementa para indicar que el número
aleatorio ha aparecido una vez más.
Si la clave no se encuentra en la colección, el método
put() colocará el nuevo par clave-valor en la tabla Hash. Como
Contador inicializa automáticamente su variable i a 1 en el momento de crearla, ya se indica que es la primera
vez que aparece ese número aleatorio concreto.
Para presentar los valores de la tabla Hash,
simplemente se imprimen. El método toString() de Hashtable
navega a través de los pares clave-valor y llama a método toString() de
cada uno de ellos. El método toString() de Integer está
predefinido, por lo que no hay ningún problema en llamar a toString()
para Contador. Un ejemplo de ejecución del programa sería la salida que
se muestra a continuación:
% java java417
{19=526, 18=533, 17=460, 16=513, 15=521, 14=495, 13=512, 12=483,
11=488, 10=487, 9=514, 8=523, 7=497, 6=487, 5=489, 3=509, 2=503,
1=475, 0=505}
Al lector le puede parecer superfluo el uso de la
clase Contador, que parece que no hace nada que no haga ya la clase
Integer. ¿Por qué no utilizar int
o Integer? Pues bien, int no puede
utilizarse porque como ya se ha indicado antes, las colecciones solamente
manejan objetos, por ello están las clases que envuelven a esos tipos básicos
y los convierten en objetos. Sin embargo, la única cosa que pueden hacer estas
clases es inicializar los objetos a un valor determinado y leer ese valor. Es
decir, no hay modo alguno de cambiar el valor de un objeto correspondiente a
un tipo básico, una vez que se ha creado. Esto hace que la clase
Integer sea inútil para resolver el problema que plantea el ejemplo,
así que la creación de la clase Contador es imprescindible.
En el ejemplo se utiliza la clase Integer, que
forma parte de la biblioteca estándar de Java como clave para la tabla
Hash, y funciona perfectamente porque tiene todo lo necesario para
funcionar como clave. Pero un error muy común se presenta a la hora de crear
clases propias para que funcionen como claves. Por ejemplo, supóngase que se
quiere implementar un sistema de predicción del tiempo en base a objetos de
tipo Oso y tipo Prediccion, para detectar cuando entra la
primavera. Tal como se muestra en el ejemplo java418.java, la cosa parece muy sencilla,
se crean las dos clases y se utiliza Oso como clave y Prediccion
como valor.
import java.util.*;
// Utilizamos un oso para saber si está hibernando en su temporada de
// invierno o si ya tiene que despertarse porque le llega la primavera
class Oso {
int número;
Oso( int n ) {
número = n;
}
}
// En función de la oscuridad, o claridad del día, pues intenta
// saber si ya ha la primavera ha asomado a nuestras puertas
class Prediccion {
boolean oscuridad = Math.random() > 0.5;
public String toString() {
if ( oscuridad )
return( "¡Seis semanas mas de Invierno!" );
else
return( "¡Entrando en la Primavera!" );
}
}
public class java418 {
public static void main( String args[] ) {
Hashtable ht = new Hashtable();
for ( int i=0; i < 10; i++ )
ht.put( new Oso( i ),new Prediccion() );
System.out.println( "ht = "+ht+"\n" );
System.out.println( "Comprobando la predicción para el oso #3:" );
Oso oso = new Oso( 3 );
if ( ht.containsKey( oso ) )
System.out.println( (Prediccion)ht.get( oso ) );
}
}
Cada Oso tendrá un número de identificación,
por lo que sería factible buscar una Prediccion en la tabla Hash
de la forma: "Dime la Prediccion asociada con el Oso
número 3". La clase Prediccion contiene un booleano que es
inicializado utilizando Math.random(), y una llamada al método
toString() convierte el resultado en algo legible. En el método
main(), se rellena una Hashtable con los Osos y sus
Predicciones asociadas. Cuando la tabla Hash está completa, se
imprime. Y ya se hace la consulta anterior sobre la tabla para buscar la
Prediccion que corresponde al Oso número 3.
Esto parece simple y suficiente, pero no funciona. El
problema es que Oso deriva directamente de la clase raíz Object,
que es lo que ocurre cuando no se especifica una clase base, que en última
instancia se hereda de Object. Luego es el método hashCode() de
Object el que se utiliza para generar el código hash para cada
objeto que, por defecto, utiliza la dirección de ese objeto. Así, la primera
instancia de Oso(3) no va a producir un código hash igual que
producirá una segunda instancia de Oso(3), con lo cual no se puede
utilizar para obtener buenos resultados de la tabla.
Se puede seguir pensando con filosofía ahorrativa y
decir que todo lo que se necesita es sobreescribir el método hashCode()
de la forma adecuada y ya está. Pero, esto tampoco va a funcionar hasta que se
haga una cosa más: sobreescribir el método equals(), que también es
parte de Object. Este es el método que utiliza la tabla Hash
para determinar si la clave que se busca es igual a alguna de las claves que
hay en la tabla. De nuevo, el método Object.equals() solamente compara
direcciones de objetos, por lo que un Oso(3) probablemente no sea igual
a otro Oso(3).
Por lo tanto, a la hora de escribir clases propias que
vayan a funcionar como clave en una Hastable, hay que sobreescribir los
métodos hashCode() y equals(). El ejemplo java419.java ya se incorporan estas
circunstancias.
import java.util.*;
// Si se crea una clase que utilice una clave en una Tabla Hash, es
// imprescindible sobreescribir los metodos hashCode() y equals()
// Utilizamos un oso para saber si está hibernando en su temporada de
// invierno o si ya tiene que despertarse porque le llega la primavera
class Oso2 {
int número;
Oso2( int n ) {
número = n;
}
public int hashCode() {
return( número );
}
public boolean equals( Object obj ) {
if ( (obj != null) && (obj instanceof Oso2) )
return( número == ((Oso2)obj).número );
else
return( false );
}
}
// En función de la oscuridad, o claridad del día, pues intenta
// saber si ya ha la primavera ha asomado a nuestras puertas
class Prediccion {
boolean oscuridad = Math.random() > 0.5;
public String toString() {
if ( oscuridad )
return( "¡Seis semanas mas de Invierno!" );
else
return( "¡Entrando en la Primavera!" );
}
}
public class java419 {
public static void main(String args[]) {
Hashtable ht = new Hashtable();
for ( int i=0; i < 10; i++ )
ht.put( new Oso2( i ),new Prediccion() );
System.out.println( "ht = "+ht+"\n" );
System.out.println( "Comprobando la prediccion para el oso #3:");
Oso2 oso = new Oso2( 3 );
if ( ht.containsKey( oso ) )
System.out.println( (Prediccion)ht.get( oso ) );
}
}
El método hashCode() devuelve el número que
corresponde a un Oso como un identificador, siendo el programador el
responsable de que no haya dos números iguales. El método hashCode() no
es necesario que devuelva un identificador, sino que eso es necesario porque
equals() debe ser capaz de determinar estrictamente cuando dos objetos
son equivalentes.
El método equals() realiza dos comprobaciones
adicionales, una para comprobar si el objeto es null, y, en caso de que no lo sea, comprobar que sea una
instancia de Oso, para poder realizar las comparaciones, que se basan
en los números asignados a cada objeto Oso. Cuando se ejecuta este
nuevo programa, sí se produce la salida correcta. Hay muchas clases de la
biblioteca de Java que sobreescriben los métodos hashCode() y
equals() basándose en el tipo de objetos que son capaces de
crear.
Las tablas Hash son utilizadas también por
muchas clases de la biblioteca estándar de Java, por ejemplo, para obtener las
propiedades del sistema se usa la clase Properties que hereda
directamente de Hashtable. Y además, contiene una segunda
Hashtable en donde guarda las propiedades del sistema que se usan
por defecto.
La implementación Properties es un objeto de
tipo Hashtable especializado en el uso de cadenas de texto. De este
modo, al momento de recuperar valores de la tabla Hashtable es
necesario moldear el tipo de texto que se recupera, mientras que con
Properties siempre se recupera una cadena de texto. También soporta la
carga y almacenamiento de propiedades desde canales de entrada y salida. El
conjunto de propiedades más utilizado es el que corresponde a la lista de
propiedades del sistema, que se puede recuperar mediante la llamada al
método System.getProperties().
|
| Home | Anterior | Siguiente | Indice | Correo |
No hay comentarios:
Publicar un comentario