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-