Más sobre colecciones predefinidas de Java

Anuncio
Más sobre arreglos primitivos en Java
El arreglo es el único dato estructurado definido en forma nativa en el lenguaje.
Más allá de que mucha gente llame a estos arreglos “de bajo nivel”, entre ellos Bjarne Stroustrup,
tienen algunas ventajas nada desdeñables sobre las colecciones predefinidas de Java, entre ellas:
 Son más eficientes en términos de velocidad de ejecución.
 Permiten definir el tipo de datos de sus elementos, lo que a su vez provoca que el chequeo del
tipo de datos se haga en tiempo de compilación.
Su principales desventajas son:
 Su tamaño es fijo una vez definido (lo cual se hace en tiempo de ejecución).
 No tiene una biblioteca de operaciones predefinidas tan rica como las colecciones.
Por otro lado, tiene un aspecto que es al vez una ventaja y una desventaja: si se exceden los
límites del arreglo la JVM lanza una excepción de tipo RuntimeException. Decimos que es una ventaja
porque nos da una seguridad que nada tiene que ver con el “bajo nivel”, y lo pone al nivel de un ArrayList
(el arreglo definido como colección estándar en Java). Sin embargo, su principal desventaja es que esto
lleva tiempo y no se puede deshabilitar el chequeo, lo que pone a los arreglos primitivos de Java en
desventaja con respecto a los de C++, que no chequean dichos límites.
Otra cosa que hace del arreglo primitivo una colección más, es que se comporta como un objeto
cualquiera. Por ejemplo, el arreglo sigue el modelo de referencias de Java, y debe crearse en forma
dinámica. Por lo tanto, si declaramos:
int[ ] v1;
no hemos definido todavía el arreglo. Por lo tanto, las siguientes definiciones:
int[ ] v2 = new int[5];
v1 = v2;
introducen dos arreglos de 5 elementos, numerados del 0 al 4. Tanto v1 como v2 se refieren al
mismo arreglo, pues son referencias. Como los elementos de los arreglos son enteros, dichos elementos se
han creado junto con el arreglo. Si, en cambio, fueran objetos, se trataría de referencias, por lo que habría
que crear luego cada elemento antes de usarlo. Así se muestra en este otro ejemplo:
Elipse[ ] vectorElipses = new Elipse[5];
for (int i = 0; i < 5; i++)
vectorElipses[i] = new Elipse();
como cualquier otro objeto, los arreglos son liberados por el recolector de basura cuando se han
dejado de usar.
Las únicas operaciones definidas para arreglos primitivos son el operador [ ] y la función length,
que devuelve la longitud del arreglo, y que se usa como se muestra en el ejemplo:
int x = v1.length();
// en x queda 5
Java permite que una función devuelva un arreglo, como vemos en el ejemplo siguiente:
static Elipse[ ] obtenerElipses() {...}
(título del capítulo) - Pág. 2
Java provee también dos interfaces que, si bien no están limitadas a las colecciones, tienen un uso
importante en conjunto con éstas: Comparable y Comparator.
La interfaz Comparable expone un solo método, denominado compareTo. Tiene un parámetro de
tipo Object, y devuelve negativo, 0 o positivo, dependiendo de si el objeto sobre el que se está trabajando
es menor que el parámetro, igual o mayor, respectivamente..
Comparator es una interfaz que expone dos métodos: compare y equals. En principio tiene una
funcionalidad similar a la de Comparable. La función lógica equals tiene dos parámetros de tipo Object, e
informa si ambos valores son iguales o no. El método compare hace lo mismo que compareTo, pero con
dos objetos parámetros, por lo que puede usarse para pasar un Comparator como parámetro en los casos
en que no contemos con Comparable implementada. Como equals está bien definido en Object, en general
no es necesario redefinirlo. Veremos un ejemplo del uso de Comparator enseguida.
Existe una clase utilitaria llamada Arrays, que contiene métodos estáticos para trabajar con arreglos de todos los tipos primitivos. En este sentido, cumple un cometido parecido al de Collections para las
colecciones predefinidas. Las funciones definidas en Arrays son:
Función
equals(v1,v2)
Significado
Devuelve true si el arreglo v1 es igual a v2, es decir, si todos los elementos tienen igual contenido
(no igual referencia).
fill(v,x)
Llena el arreglo v de elementos con el valor x.
sort(v)
Ordena el arreglo v en forma ascendente. Para ello, la clase base del arreglo debe implementar la
interfaz Comparable.
binarySearch(v,x) Devuelve el índice del arreglo v donde se encuentre el valor x, o un número negativo que indique
el lugar donde debería encontrarse x. El arreglo debe estar previamente ordenado y la clase base
del arreglo debe implementar la interfaz Comparable.
sort(v,c)
Es otra versión del método sort, que sirve cuando el arreglo no implementa Comparable. Para eso
se le pasa el parámetro c, que debe ser instancia de una clase que implemente Comparator.
binaryEs otra versión del método binarySearch, que sirve cuando el arreglo no implementa Comparable.
Search(v,x,c)
Para eso se le pasa el parámetro c, que debe ser instancia de una clase que implemente
Comparator.
Lo que se dijo del uso de Comparator puede no haber quedado claro, así que nos detendremos en
ello. Si el tipo base del arreglo implementa la interfaz Comparable, podremos ordenar sin problema al
arreglo, ya que el método sort utilizará la implementación de la función compareTo que expone dicha
interfaz. Como con los arreglos primitivos se hace un chequeo de tipos en tiempo de compilación, si el tipo
de los elementos del arreglo no implementa Comparable, obtendremos un error de compilación.
Sin embargo, existe la posibilidad de pasarle un objeto de una clase que implemente la interfaz
Comparator. Supongamos que tenemos una clase Fraccion que no implementa Comparable, como la del
ejercicio resuelto en un capítulo precedente. En ese caso, si queremos ordenar un arreglo de fracciones
deberíamos implementar Comparator en una clase auxiliar, como sigue:
public class ComparadorFracciones implements Comparator {
public int compare(Object o1, Object o2) {
Fraccion r1 = (Fraccion)o1;
Fraccion r2 = (Fraccion)o2;
if (r1.numerador * r2.denominador > r1.denominador * r2.numerador)
return 1;
else if (r1.numerador * r2.denominador < r1.denominador * r2.numerador)
return -1;
else return 0;
}
-2-
ORIENTACIÓN A OBJETOS CON JAVA Y UML - pág. 3
}
Luego, para ordenar el arreglo, haremos así:
Fraccion[ ] v = new Fraccion[10];
// se trabaja y se llena el arreglo
Arrays.sort(v, new ComparadorFracciones());
Nótese que implementar Comparator sirve también si la implementación de compareTo no es la
que se necesita para resolver el ordenamiento.
Una ayuda adicional para trabajar con arreglos primitivos es la función arraycopy, definida como
método de clase en System. Esta función, al ser invocada como:
System.arraycopy(v1,inicio1,v2,inicio2,cantidad);
copia desde la posición inicio1 de v1 cantidad elementos a v2 empezando en su posición inicio2.
El método arraycopy es muy superior en desempeño que una copia realizada con un ciclo for. Si nos
salimos de los límites de alguno de los arreglos se eleva una excepción. Los arreglos deben tener el mismo
tipo base. Si dicho tipo base es una clase, se copian sólo referencias.
Más sobre colecciones predefinidas de Java
Los métodos de la interfaz Collection, y por lo tanto disponibles para todas las colecciones, son:
Método
boolean add(Object)
boolean addAll(Collection)
Resultado
Agrega un elemento a la colección y devuelve false si no puede hacerlo.
Agrega todos los elementos del argumento y devuelve false si no puede
hacerlo.
void clear()
Elimina todos los elementos de la colección.
boolean contains(Object)
Determina si el elemento está en la colección.
boolean containsAll(Collection) Determina si todos los elementos del argumento están en la colección.
Iterator iterator()
Devuelve un iterador para la colección.
boolean remove(Object)
Elimina el elemento de la colección y devuelve false si no puede hacerlo.
boolean removeAll(Collection)
Elimina de la colección todos los elementos que estén en el argumento.
boolean retainAll(Collection)
Deja en la colección el conjunto intersección con la colección argumento.
int size()
Devuelve el número de elementos de la colección.
Object[ ] toArray()
Devuelve un arreglo con todos los elementos de la colección.
Object[ ] toArray(Object[ ] a)
Devuelve un arreglo con todos los elementos de la colección, cuyo tipo
sea del mismo que el de los elementos del arreglo a.
Nótese que no hay un método get para obtener un elemento de cualquier colección. Esto hace que
si queremos recorrer una colección genérica debamos utilizar iteradores (este concepto lo veremos en el
ítem siguiente).
A continuación se agregan otros métodos disponibles para algunos tipos de colecciones:
Método
Object get (int i)
Object first()
Object last()
SortedSet subSet (Object desde, Object hasta)
Colección
List (LinkedList, ArrayList)
TreeSet
TreeSet
TreeSet
-3-
Devuelve
El i-ésimo elemento de la colección.
El primer elemento del conjunto.
El último elemento del conjunto.
Un
conjunto
parcial
entre
dos
elementos.
(título del capítulo) - Pág. 4
SortedSet headSet (Object hasta)
TreeSet
SortedSet tailSet (Object desde)
TreeSet
void put (Object k, Object v)
Map (TreeMap, HashMap)
boolean containsKey (Object k)
Map (TreeMap, HashMap)
boolean containsValue (Object v)
Map (TreeMap, HashMap)
Object get (Object k)
Map (TreeMap, HashMap)
Object firstKey()
TreeMap
Object lastKey()
TreeMap
SortedMap subMap (Object desde, Object TreeMap
hasta)
SortedMap headMap (Object hasta)
TreeMap
SortedMap tailMap (Object desde)
TreeMap
Un conjunto parcial hasta un elemento.
Un conjunto parcial desde un elemento.
Agrega un par con clave k y valor v.
Si se encuentra la clave k.
Si se encuentra el valor v.
El valor correspondiente a la clave k.
La primera clave del mapa.
La última clave del mapa.
Un mapa parcial entre dos claves.
Un mapa parcial hasta una clave.
Un mapa parcial desde una clave.
Adicionalmente, existe una clase utilitaria denominada Collections, que contiene una serie de
métodos estáticos para trabajar con cualquier tipo de colecciones. A continuación se muestran algunos
métodos de Collections:
Método
sort(Collection, Comparator)
binarySearch
(Collection
Object x, Comparator comp)
Significado
Ordena la colección en forma ascendente usando el Comparator.
c, Devuelve la posición de la colección donde se encuentre el valor x, o un número
negativo que indique el lugar donde debería encontrarse x, usando comp. La
colección debe estar previamente ordenada.
max(Collection) y min(Collection) Devuelven los elementos máximo y mínimo de la colección usando el método de
comparación natural.
max(Collection, Comparator) y Devuelven los elementos máximo y mínimo de la colección usando el
min(Collection, Comparator)
Comparator.
reverse()
Invierte todos los elementos de la colección sobre sí misma.
copy(List destino, List origen)
Copia los elementos de la lista origen a la destino.
fill(List l, Object o)
Llena la lista con el valor de o.
UnmodifiableCollecDevuelve una colección inmutable copiada de la argumento.
tion(Collection)
Implementación de colecciones con tipos en lenguajes con modelo de datos dinámico
En los lenguajes que no manejan genericidad de tipos, los elementos de las colecciones son de
alguna clase o de cualquiera de sus descendientes. Típicamente, como ocurre en Java, las colecciones
tienen elementos Object, lo que no da ningún control sobre los elementos que se puedan guardar en la
misma ni permite almacenar elementos de tipos primitivos1.
Esto significa, en primer lugar, que en una misma colección podemos guardar una elipse como
primer elemento, un número fraccionario como segundo, y así sucesivamente. Pero también significa que al
intentar extraer elementos podemos encontrarnos con cualquier cosa, debiendo chequear el tipo antes de
aplicarle cualquier operación o bien recurrir a excepciones2. Finalmente, como los elementos de las
colecciones podrán ser de cualquier tipo, no tenemos forma de aplicar a los elementos de las mismas
operaciones restringidas a ciertos tipos de datos sin hacer una transformación.
A continuación se muestra cómo restringir una lista de tipo ArrayList en Java, para que acepte
solamente elementos de tipo MiClase3:
public class MiLista {
private ArrayList arreglo = new ArrayList();
1
Como int, double, boolean, etc..
Veremos excepciones más adelante. Sin embargo, esta solución es aún menos elegante.
3 La idea la obtuve de Eckel [3].
2
-4-
ORIENTACIÓN A OBJETOS CON JAVA Y UML - pág. 5
public void add (MiClase e) {
arreglo.add(e);
}
public MiClase get (int i) {
return (MiClase)arreglo.get(i);
}
// todos los métodos adicionales necesarios
}
El problema se resolvió utilizando composición, ya que se puso una lista como atributo de MiLista.
La gran tentación hubiera sido utilizar herencia, haciendo que MiLista fuera una clase derivada de ArrayList.
Hasta conceptualmente hubiera sido correcto, pues una MiLista es una ArrayList. Sin embargo, resolverlo
de esa manera habría provocado que los métodos add y get de MiLista sobrecargaran a los de ArrayList y
se podrían seguir agregando elementos de cualquier tipo dentro de la lista.
Nótese que en esta solución:
 El chequeo de tipo se hace en tiempo de compilación.
 No se pierde el polimorfismo de la colección porque puedo poner instancias de descendientes
de MiClase en MiLista.
 El cliente de la clase no necesita hacer transformaciones de tipos explícitas.
De todas maneras, y a pesar de la solución mostrada, las clases parametrizadas predefinidas,
como en C++ y Eiffel, son más deseables y más elegantes. Las veremos poco más adelante.
Otro desafío distinto, pero en línea con el mismo problema, es la creación de iteradores que chequeen tipos. Eckel propone una solución, basada en los patrones de diseño Decorador y Proxy. La idea es
imponer la condición de que el iterador sólo pueda recorrer a través de elementos de una clase o sus
descendientes, y lance una excepción si se encuentra con un elemento cuyo tipo no es lo el que se espera.
La clase del iterador se determina al crearlo. A continuación se muestra una propuesta:
public class iteradorTipado implements iterator {
private iterator it;
private Class tipo;
public iteradorTipado (iterator i, Class t) {
it = i;
tipo = t;
}
public boolean hasNext () {
return it.hasNext();
}
public void remove () {
it.remove();
}
public Object next() {
Object o = it.next();
if (!tipo.isInstance(o))
// se lanza una excepción: las veremos en un capítulo posterior
throw new ClassCastException("iterador tipado como "+ tipo +
" encontró el tipo: "+o.getClass());
}
}
Iteradores definidos por el programador
Los iteradores que Java tiene definidos sirven sólo para las colecciones predefinidas. Para definir
iteradores para estructuras de datos no definidas como colecciones deberá crearse una nueva clase que
-5-
(título del capítulo) - Pág. 6
implemente Collection (para la estructura de datos) y otra que implemente Iterator (para el iterador). Por
ejemplo:
// archivo Matriz2D.java
package MatrizDosDimensiones;
public class Matriz2D implements Collection {
int cantFilas;
int cantCol;
Object [ ] [ ] m2;
public Matriz2D(int filas, int columnas) {
m2 = new Object[filas][columnas];
cantFilas = filas;
cantCol = columnas;
}
public Iterator iterator() {
return new IteradorMatriz2D(this);
}
// otros métodos ...
}
// archivo Matriz2D.java
package MatrizDosDimensiones;
public class IteradorMatriz2D implements Iterator {
private int filaActual;
private int colActual;
private Matriz2D m2;
public IteradorMatriz2D (Matriz2D m) {
filaActual = 1;
colActual = 1;
m2 = m;
}
public Object next() {
if (colActual < cantCol)
colActual++;
else {
filaActual++;
colActual = 1;
}
}
public boolean hasNext() {
return !((filaActual == cantFilas) && (colActual == cantCol));
}
}
El diagrama de clases sería:
-6-
ORIENTACIÓN A OBJETOS CON JAVA Y UML - pág. 7
«uses»
«interface»
Iterator
+next() : Object
+hasNext() : boolean
«interface»
Collection
+iterator() : Iterator
IteradorMatriz2D
-filaActual : int
-colActual : int
+next() : Object
+hasNext() : boolean
Matriz2D
1
1
+iterator() : Iterator
El problema con estas soluciones es que no podemos definir más de un iterador por clase debido a
las limitaciones de Java al respecto. Por ejemplo, para la clase Matriz2D podríamos querer recorrer por filas
o por columnas, pero con la solución tradicional no lo podemos hacer. Por eso hasta ahora definimos
solamente el recorrido por filas.
Si queremos algo bien flexible, debemos definir más clases de iteradores para cada tipo de recorrido. Por ejemplo, podemos definir las clases IteradorPorFilas e IteradorPorColumnas. Sin embargo, en
Collection tenemos una única función iterator, y no le podemos hacer devolver dos iteradores. Entiendo que
la mejor solución es definir dos funciones en Matriz2D, una iteratorFilas y una iteratorColumnas. Y para
garantizar el polimorfismo desde una Collection debería existir una función iterator con la funcionalidad más
frecuente. Juntando esta idea con la facilidad de modificar la elección en el futuro, la mejor solución sería
definir en Matriz2D:
public Iterator iteratorFilas() {
return new IteradorPorFilas(this);
}
public Iterator iteratorColumnas() {
return new IteradorPorColumnas(this);
}
public Iterator iterator() {
return iteratorFilas(); // el día de mañana se puede cambiar esta elección
}
-7-
Descargar