Tema 3. Tipos List y Set Autor: Miguel Toro. Revisión: José C. Riquelme 1. Definiciones Java proporciona además del tipo array, los tipos List y Set para gestionar agregados de elementos del mismo tipo. Los objetos de los tipos array y List son agregados lineales de elementos que pueden ser accedidos (indexados) a través de la posición que ocupan en la colección. El tipo Set no es indexable. Para poderlos usar debe importarse el paquete java.util. Los elementos que puede contener un de agregado de tipo List o Set tienen que ser obligatoriamente objetos, por el contrario de los arrays que podían contener también tipo primitivos. Esto quiere decir que no se puede definir un List<int> o un Set<double> y se ha de recurrir a los tipos envoltorios. En resumen: Pueden contener tipos primitivos array Sí List Set String No No Sólo char Pueden contener Se puede tipos objeto modificar el tamaño Sí No Sí Si No Sí Si No Puede modificarse el contenido de una celda Sí Sí No No La inicialización para los casos de List<T> y Set<T> se hace con el operador new y el constructor de la clase adecuado. Hay que tener en cuenta que List y Set son interfaces y que Java nos proporciona clases que implementas esos tipos. Las clases que implementan List son ArrayList y LinkedList que tienen características de eficiencia diferente según cuál sea el uso mayoritario que se vaya a hacer de la lista. Si preferiblemente se va a usar para acceder a un elemento mediante índice o buscarlo es recomendable ArrayList. Por el contrario, si la mayoría de las operaciones son de inserción y borrado es preferible LinkedList. Al final del tema se le propone un ejercicio sobre las diferencias entre ambas implementaciones. Finalmente, la clase más usada para implementar un objeto de tipo Set es HashSet, aunque hay algunas implementaciones especiales como EnumSet o LinkedHashSet. Ejemplos: List<Integer> listanumeros = new ArrayList<Integer>(); Set<Punto> nube = new HashSet<Punto>(); La diferencia entre List y Set es que en este último tipo no se permiten elementos repetidos, esto es, si dos elementos x e y son tales que x.equals(y) devuelve true, al insertar los dos sólo uno estará en el Set. Hay que tener en cuenta que al redefinir el método equals Java obliga a 2 Introducción a la Programación redefinir el método de Object denominado hashCode. Este método asigna a cada objeto de cualquier clase un único número entero de identificador, que es usado por la clase HashSet para insertar elementos en un conjunto. Por tanto, si el método hashCode de Punto no está implementado, el siguiente código añadiría al conjunto nube 4 puntos aunque haya dos iguales. Set<Punto> nube = new HashSet<Punto>(); Punto Punto Punto Punto p = new PuntoImpl(2.,0.); p1 = new PuntoImpl(2.,0.); p2 = new PuntoImpl(3.,0.); p3 = new PuntoImpl(4.,0.); nube.add(p);nube.add(p1);nube.add(p2);nube.add(p3); mostrar(nube); El método hashCode debe redefinirse siempre invocando a los métodos hashCode de sus propiedades, normalmente mediante una suma de éstos multiplicados por algún número primo. Por ejemplo, el hashCode de Punto quedaría así: public int hashCode(){ return getX().hashCode()*31+getY().hashCode(); } 2. Métodos comunes de List y Set Tanto List como Set heredan en Java de la interfaz Collection. Por tanto los métodos de Collection son comunes a ambos tipos. A continuación presentamos los más usados. La lista completa de métodos puede consultarse en: http://docs.oracle.com/javase/7/docs/api/java/util/Collection.html boolean add(E e) Añade un elemento a la colección, devuelve false si no se añade. boolean addAll(Collection c) Añade todos los elementos de c a la colección que invoca. Es eel operador unión void clear() Borra todos los elementos de la colección boolean contains(Object o) Devuelve true si o está en la colección invocante. boolean containsAll(Collection c) Devuelve true si la colección que invoca contiene todos los elementos de c 2. Elementos del lenguaje Java boolean isEmpty() Devuelve true si la colección no tiene elementos boolean remove(Object o) Borra el objeto o de la colección que invoca, si no estuviera se devuelve false boolean removeAll(Collection c) Borra todos los objetos de la colección que invoca que estén en c boolean retainAll(Collection c) En la colección que invoca sólo se quedarán aquellos objetos que están en c. Por tanto, es la intersección entre ambas colecciones. int size() Devuelve el número de elementos T[] toArray(T[] a) Devuelve un array con la colección 3. Métodos específicos de List Además de los métodos anteriores, como se ha señalado anteriormente List es un tipo indexado, lo que permite añadir o borrar el elemento de una determinada posición. Más en http://docs.oracle.com/javase/7/docs/api/java/util/List.html void add(int index, E element) Inserta el elemento especificado en la posición especificada boolean addAll(int index, Collection c) Inserta todos los elementos de c en la posición especificada E get(int index) Devuelve el elemento de la lista en la posición especdificada. int indexOf(Object o) Devuelve el índice donde ser encuentra por primera vez el elemento o (si no está devuelve -1) int lastIndexOf(Object o) Devuelve el índice donde ser encuentra por última vez el elemento o (si no estuviera devuelve -1) E remove(int index) Borra el elemento de la posición especificada E set(int index, E element) 3 4 Introducción a la Programación Remplaza el elemento de la posición indicada por el que se da como argumento List<E> subList(int fromIndex, int toIndex) Devuelve una vista de la porción de la lista entre fromIndex, inclusive, and toIndex, sin incluir. 4. Anidamiento de agregados Los elementos de un List pueden ser a su vez también List o un Set. Pensemos por ejemplo en cualquier organización, por ejemplo un Hospital. Los médicos de un Hospital se organizan en (una lista de) departamentos, y cada uno de ellos podría ser un conjunto de médicos. De esta manera: List<Set <Medico>> hospital = new ArrayList<Set<Medico>>(); Set<Medico> dep1= new HashSet<Medico>(); Set<Medico> dep2= new HashSet<Medico>(); .... dep1.add(medico1); .... depn.add(medicom); .... hospital.add(0,dep1); hospital.add(1,dep2); .... De la misma forma el tipo para guardar los valores de expresión genética del ADN de un conjunto de pacientes, se definiría de la siguiente manera. Set<List<Double>> experimento=new HashSet<List<Double>>(); List<Double> muestraADN1= new ArrayList<Double>(); List<Double> muestraADN2= new ArrayList<Double>(); .... muestraADN1.add(0.78); .... muestraADNn.add(-0.56); .... experimento.add(muestraADN1); experimento.add(muestraADN2); .... 5. Tipos genéricos y métodos genéricos En Java existe la posibilidad de usar tipos genéricos. Estos son tipos que dependen de parámetros formales que serán instanciados posteriormente por tipos concretos. Los tipos List y Set son tipos genéricos y por eso en su definición entre los símbolos <> se pone el tipo real que van a guardar. 2. Elementos del lenguaje Java En cualquier clase puede haber métodos genéricos. La declaración de los parámetros genéricos va entre el tipo devuelto por el método y los modificadores del mismo. Por ejemplo, supongamos que queremos diseñar un método que rellene y devuelva una colección de objetos iguales, pero que sea “genérica”. Esto es, queremos que sirva para devolver una lista de Punto, de Integer o de Circulo. La construcción del método ha de ser genérica, y el tipo es sustituido por una T. Así el método recibe un entero con el número de elementos que tendrá la lista y un objeto a de tipo T sin definir. Su código es: public static <T> List<T> nCopias(int n, T a){ List<T> v = new ArrayList<T>(); for(int i=0; i<n; i++){ v.add(a); } return v; } En el programa principal es donde los tipos y métodos deben instanciarse antes de ser usados. Es decir, los parámetros genéricos deben sustituirse por parámetros concretos. Así a la hora de usar el método nCopias podríamos poner: public static void main(String[] args) { Integer a = 14; List<Integer> v; v = nCopias(10,a); mostrar(v); Punto p = new PuntoImpl(1.,0.); List<Punto> lp; lp=nCopias(5,p); mostrar(lp); } En el ejemplo se ha instanciado el tipo genérico List<T> a List<Integer>. Igualmente se ha instanciado el método nCopias. Ahora la instanciación ha sido deducida por el compilador. El mecanismo por el cual el compilador induce la instanciación necesaria se denomina inferencia de tipos. Java tiene esta característica. 6. ANEXO I Implemente en una clase TestLista el siguiente código. Este código crea dos listas usando cada una de las dos implementaciones existentes. A continuación hace lo siguiente en las dos listas: inserta 1.000.000 de enteros aleatorios secuencialmente; inserta 10.000 elementos en la posición 0; ; inserta 10.000 elementos en una posición intermedia al azar; hace 5.000 búsquedas de elementos elegidos al azar; borra 5.000 elementos de índices elegidos al azar; por último, accede a 5.000 posiciones (mediante su índice) elegidas al azar. Para cada una de 5 6 Introducción a la Programación esas operaciones muestra el tiempo, en milisegundos, empleado en cada una de las dos implementaciones. Interprete los resultados. Para ello, tenga en cuenta que debajo de la implementación mediante ArrayList hay un array de longitud fija en el que se accede a los elementos mediante el índice; cuando el array se llena, se mueven todos sus elementos a un nuevo array del doble del tamaño del anterior, y así sucesivamente cada vez que se “necesita más espacio”. Insertar en un punto del array que no sea el final implica que el elemento que está en la posición en la que se inserta y superiores deben ser movidos un lugar hacia delante. Lo mismo ocurre cuando se borra: los que están a la derecha del que se elimina tienen que ser desplazados un lugar hacia la izquierda. En la implementación mediante LinkedList, los elementos están formando una cadena en la que cada uno sabe quién es el anterior y el siguiente. Para acceder a uno en concreto (que no sea el último, que está localizado por la propia lista), debe recorrerse la cadena desde el principio avanzando por los enlaces hacia delante tantas veces como indique el índice. Para borrar un elemento simplemente se elimina el eslabón de la cadena, pero para saber cuál es el que hay que borrar hay que localizarlo, lo que exige recorrer la cadena como en los accesos. import import import import public java.util.ArrayList; java.util.LinkedList; java.util.List; java.util.Random; class TestLista extends Test { private static final int NUM_INSERCIONES = 1000000; private static final int NUM_INSERCIONES_PRINCIPIO = 10000; private static final int NUM_INSERCIONES_POS = 10000; private static final int NUM_BUSQUEDAS = 5000; private static final int NUM_BORRADOS = 5000; private static final int NUM_ACCESSOS = 5000; private static final int MAX_VALUE = 100000; private static Random rand = new Random(); private static long tiempoAntes, tiempoDespues; public static void main(String[] args) { List<Integer> arrayList, linkedList; arrayList = new ArrayList<Integer>(); linkedList = new LinkedList<Integer>(); mostrar("TEST DE INSERCIÓN SECUENCIAL========================"); mostrar("Vamos a insertar " + NUM_INSERCIONES+ " elementos secuencialmente al final..."); testInsercion(arrayList); testInsercion(linkedList); mostrar("TEST DE INSERCIÓN AL PRINCIPIO======================"); mostrar("Vamos a insertar " + NUM_INSERCIONES_PRINCIPIO+ " elementos al principio..."); testInsercionPrincipio(arrayList); testInsercionPrincipio(linkedList); mostrar("TEST DE INSERCIÓN INTERMEDIA======================"); mostrar("Vamos a insertar " + NUM_INSERCIONES_POS+ " elementos intermedios..."); testInsercionPrincipio(arrayList); testInsercionPrincipio(linkedList); mostrar("TEST DE BÚSQUEDA AL AZAR============================"); mostrar("Vamos a buscar " + NUM_BUSQUEDAS + " elementos al azar..."); 2. Elementos del lenguaje Java testBusqueda(arrayList); testBusqueda(linkedList); mostrar("TEST DE ACCESO AL AZAR=============================="); mostrar("Vamos a acceder a " + NUM_ACCESSOS + " elementos al azar..."); testAcceso(arrayList); testAcceso(linkedList); mostrar("TEST DE BORRADO ELEMENTOS INICIALES==================="); mostrar("Vamos a borrar " + NUM_BORRADOS+ " elementos del principio..."); testBorrado(arrayList); testBorrado(linkedList); } private static void testInsercion(List<Integer> l) { tiempoAntes = System.currentTimeMillis(); for (int i = 0; i < NUM_INSERCIONES; i++) { l.add(rand.nextInt(MAX_VALUE)); } tiempoDespues = System.currentTimeMillis(); mostrar(l.getClass().getName() + ":\t" + (tiempoDespues tiempoAntes) + " milisegundos"); } private static void testInsercionPrincipio(List<Integer> l) { tiempoAntes = System.currentTimeMillis(); for (int i = 0; i < NUM_INSERCIONES_PRINCIPIO; i++) { l.add(0, rand.nextInt(MAX_VALUE)); } tiempoDespues = System.currentTimeMillis(); mostrar(l.getClass().getName() + ":\t" + (tiempoDespues - tiempoAntes) + " milisegundos"); } private static void testInsercionPosicion(List<Integer> l) { tiempoAntes = System.currentTimeMillis(); for (int i = 0; i < NUM_INSERCIONES_POS; i++) { l.add(rand.nextInt(MAX_VALUE),rand.nextInt(MAX_VALUE)); } tiempoDespues = System.currentTimeMillis(); mostrar(l.getClass().getName() + ":\t" + (tiempoDespues - tiempoAntes) + " milisegundos"); } private static void testBusqueda(List<Integer> l) { tiempoAntes = System.currentTimeMillis(); for (int i = 0; i < NUM_BUSQUEDAS; i++) { l.contains(rand.nextInt(MAX_VALUE)); } tiempoDespues = System.currentTimeMillis(); mostrar(l.getClass().getName() + ":\t" + (tiempoDespues - tiempoAntes) + " milisegundos"); } private static void testAcceso(List<Integer> l) { tiempoAntes = System.currentTimeMillis(); Integer n = 0; for (int i = 0; i < NUM_ACCESSOS; i++) { n = l.get(rand.nextInt(NUM_INSERCIONES)); n++; // por hacer algo con la variable } 7 8 Introducción a la Programación tiempoDespues = System.currentTimeMillis(); mostrar(l.getClass().getName() + ":\t" + (tiempoDespues - tiempoAntes) + " milisegundos"); } private static void testBorrado(List<Integer> l) { tiempoAntes = System.currentTimeMillis(); for (int i = 0; i < NUM_BORRADOS; i++) { l.remove(i); } tiempoDespues = System.currentTimeMillis(); mostrar(l.getClass().getName() + ":\t" + (tiempoDespues - tiempoAntes) + " milisegundos"); } } Una posible ejecución del código anterior (depende de la maquina y de los números aleatorios generados) TEST DE INSERCIÓN SECUENCIAL======================== Vamos a insertar 1000000 elementos secuencialmente al final... java.util.ArrayList: 47 milisegundos java.util.LinkedList: 172 milisegundos TEST DE INSERCIÓN AL PRINCIPIO====================== Vamos a insertar 10000 elementos al principio... java.util.ArrayList: 3635 milisegundos java.util.LinkedList: 0 milisegundos TEST DE INSERCIÓN INTERMEDIA====================== Vamos a insertar 10000 elementos intermedios... java.util.ArrayList: 3027 milisegundos java.util.LinkedList: 0 milisegundos TEST DE BÚSQUEDA AL AZAR============================ Vamos a buscar 5000 elementos al azar... java.util.ArrayList: 609 milisegundos java.util.LinkedList: 2028 milisegundos TEST DE ACCESO AL AZAR============================== Vamos a acceder a 5000 elementos al azar... java.util.ArrayList: 0 milisegundos java.util.LinkedList: 9010 milisegundos TEST DE BORRADO ELEMENTOS INICIALES=================== Vamos a borrar 5000 elementos del principio... java.util.ArrayList: 1764 milisegundos java.util.LinkedList: 63 milisegundos 7. ANEXO II En este experimento vamos a comprobar que los objetos que se incluyen en una colección (List, Set, array) no son copias de éstos, sino que son el mismo objeto. Para ello vamos a realizar el siguiente experimento. Copie en la clase TestCopias el siguiente código. En él, se crea una lista lp1 con tres puntos y se asigna a otra lista lp2 los dos primeros elementos mediante el método subList (el resultado del experimento no cambia si se hace lp1=lp2). En el experimento 1 se modifican los elementos de la segunda y vemos qué ocurre con los elementos de la primera (se modifican). En el segundo experimento se crea una nueva lista y 2. Elementos del lenguaje Java se asignan a ésta los elementos de la primera; luego se modifican los elementos de la segunda y vemos cómo quedan los elementos de la primera (también se modifican). Por último, en el tercer experimento se crea una nueva lista en la que se insertan copias de los elementos de la primera; se modifican los de la segunda y vemos cómo quedan los de la primera (no se modifican): package test; import java.util.LinkedList; import java.util.List; import punto.Punto; import punto.PuntoImpl; public class TestCopia extends Test { public static void main(String[] args) { List<Punto> lp1; List<Punto> lp2; lp1 = new LinkedList<Punto>(); Punto p1 = new PuntoImpl(2.0, 3.0); Punto p2 = new PuntoImpl(3.0, 2.5); Punto p3 = new PuntoImpl(1.0, 3.6); lp1.add(p1); lp1.add(p2); lp1.add(p3); mostrar("lp1: " + lp1); // Experimento 1 mostrar("Experimento 1"); lp2 = lp1.subList(0, 2); mostrar("lp2: " + lp2); for (Punto p : lp2) { p.setY(5.5); } mostrar("lp1 después de modificar lp2: " + lp1); // Experimento 2 mostrar("Experimento 2"); lp2 = new LinkedList<Punto>(); lp2.addAll(lp1); for (Punto p : lp2) { p.setY(6.5); } mostrar("lp1 después de modificar lp2: " + lp1); // Experimento 3 mostrar("Experimento 3"); lp2 = new LinkedList<Punto>(); for (Punto punto : lp1) { Punto p = punto.copia(); p.setY(7.5); lp2.add(p); } mostrar("lp2: " + lp2); mostrar("lp1 después de modificar lp2: " + lp1); } 9 10 Introducción a la Programación Para hacer este experimento deberá añadir al tipo Punto de un método de copia con el siguiente código: public Punto copia(){ return new PuntoImpl(getX(),getY()); }