Tema 4. Diseño de Tipos Autor: Miguel Toro. Revisión: José C. Riquelme 1. El tipo Object En Java existe una clase especial llamada Object. Todas las clases que definamos y las ya definidas heredan de Object. Por otra parte Object (como ocurre con todas las clases) define también un tipo. Los métodos de este tipo son los métodos públicos no static de Object. Veamos en primer lugar algunos de esos métodos públicos: equals, hashCode y toString. Aprenderemos sus propiedades, las restricciones entre ellos y la forma de rediseñarlos para que se ajusten a nuestras necesidades. La signatura de estos métodos es: boolean equals(Object o); int hashCode(); String toString(); Como el tipo Object es ofrecido por todos los objetos que creemos, los métodos anteriores están disponibles en todos los objetos. El método equals(Object o) se utiliza para decidir si el objeto es igual al que se le pasa como parámetro. Recordamos que para decidir si dos objetos son idénticos se usa el operador ==. El método hashCode() devuelve un entero que es el código hash del objeto. Todo objeto tiene, por lo tanto, un código hash asociado. El método toString() devuelve una cadena de texto que es la representación exterior del objeto. Cuando el objeto se imprima en la pantalla se mostrará como indique su método toString correspondiente. Todos los objetos ofrecen estos tres métodos. Por lo tanto es necesaria una buena comprensión de sus propiedades y un buen diseño de los mismos. Propiedades y restricciones. Los métodos hashCode, toString y equals deben cumplir las siguientes propiedades y restricciones: Ninguno de los tres métodos puede disparar excepciones. . Si dos objetos son iguales, sus representaciones en forma de cadena también deben serlo. Es decir para dos objetos cualquiera x, y distintos de null se debe cumplir x.equals(y)-> x.toString().equals(y.toString()). Si dos objetos son iguales, sus códigos hash tienen que coincidir. La inversa no tiene por qué ser cierta. Es decir para dos objetos cualquiera x, y distintos de null se debe cumplir x.equals(y)-> x.hashCode()==y.hashCode(). Sin embargo no se exige que dos objetos no iguales produzcan códigos hash desiguales aunque hay que ser consciente 2 Introducción a la Programación que se puede ganar mucho en eficiencia si en la mayoría de los casos objetos distintos tienen códigos hash distintos. Veamos un ejemplo con la implementación en el tipo Punto del tema 1: public String toString(){ String s; s="("+this.getX()+","+getY()+")"; return s; } public boolean equals(Object o){ boolean r = false; if(o instanceof Punto){ Punto p = (Punto) o; r = this.getX().equals(p.getX()) && this.getY().equals(p.getY()); } return r; } public int hashCode(){ return getX().hashCode()*31+getY().hashCode(); } Algunos comentarios al código anterior: Podemos estar seguros que el casting a Punto no disparará excepciones puesto que se hace después de comprobar que objeto ofrece ese tipo. Evidentemente si p no es de tipo Punto el resultado es false. Hemos supuesto que sus propiedades no pueden tomar el valor null. Si esto no fuera así se dispararía una excepción en getX().equals(p1.getX()) puesto que se intentaría invocar el método equals sobre un objeto null. La cadena resultante en el método toString se debe calcular a partir de los resultados devueltos por los correspondientes toString de las propiedades involucradas en la igualdad y, posiblemente, otras propiedades derivadas de las mismas. En este caso no es necesario invocar el método toString porque cuando un tipo objeto está dentro de una operación con otros operandos de tipo String el compilador llama automáticamente al método. Es decir, en ese contexto el compilador convierte automáticamente el tipo dado a String. El hashCode se calcula a partir de las propiedades involucradas en la igualdad. Si hay más de una podemos seguir la siguiente regla. El hashCode resultante es igual al hashCode de la primera propiedad más 31 por el hashCode de la segunda propiedad más 31*31 por el hashCode de la tercera propiedad etc. Alternativamente al 31 se podría haber escogido otro número primo no muy grande. El escoger un número primo hace que el hashCode calculado se disperse adecuadamente. Es decir, que los hashCode de dos objetos distintos sean en la mayoría de los casos distintos. 4. Diseño de tipos 2. Los tipos Comparable y Comparator Los tipos Comparable y Comparator sirven para proporcionar un orden a los objetos de un tipo creado por el usuario. Java ya proporciona un orden natural a los tipos envoltorio como Integer y Double o al tipo inmutable String (que heredan de Comparable) permitiendo que, por ejemplo, se puedan ordenar cuando están sobre una Lista. Los tipos Comparable y Comparator están definidos como: package java.lang; public interface Comparable<T>{ int compareTo(T o); } package java.util; public interface Comparator<T>{ int compare(T o1, T o2); } El tipo Comparable está definido en Java en el paquete java.lang y se compone de un sólo método: el método compareTo. El tipo Comparator está definido en el paquete java.util y se compone también de un único método llamado compare. Ambos son tipos genéricos y sirven para definir relaciones de orden total sobre los objetos de un tipo dado. El tipo Comparable sirve para establecer el orden natural de un tipo dado. El tipo Comparator sirve para definir un orden alternativo sobre los objetos de un tipo. El orden natural (Comparable) compara el objeto this con otro que toma como parámetro el método compareTo. Los órdenes alternativos (Comparator) comparan los dos objetos que toma como parámetros el método compare. Ambos tipos tienen un conjunto de requisitos: El método compareTo debe disparar la excepción NullPointerException cuando toma como parámetro un valor null. compareTo y compare comparan dos objetos p1 y p2 (en el caso de compareTo p1 es this) y devuelve un entero que es: ◦ Negativo si p1 es menor que p2 ◦ Cero si p1 es igual a p2 ◦ Positivo si p1 es mayor que p2 equals/compareTo: El orden natural definido debe ser coherente con la definición de igualdad. Si equals devuelve true compareTo debe devolver cero. Aquí también incluimos, tal como se recomienda en la documentación de Java, la inversa. Es decir, que si compareTo devuelve cero entonces equals devuelve true. Esto lo podemos enunciar diciendo que la expresión siguiente es verdadera para cualquier par de objetos x, y: (x.compareTo(y) == 0)(x.equals(y)). equals/compare: Para el diseño de un Comparator Java propone los siguiente: 3 4 Introducción a la Programación http://docs.oracle.com/javase/7/docs/api/java/util/Comparator.html It is generally the case, but not strictly required that (compare(x,y)==0) == (x.equals(y)). Generally speaking, any comparator that violates this condition should clearly indicate this fact. The recommended language is "Note: this comparator imposes orderings that are inconsistent with equals." Esta recomendación tiene el problema de que si hacemos un Comparator coherente con equals y éste debe ser coherente con compareTo, entonces el orden natural y el orden alternativo definido por el Comparator serían equivalentes salvo en el sentido del orden. Es decir, que los órdenes alternativos (definidos mediante un Comparator) al orden natural (definido por compareTo) estarían limitados a cambiar el sentido de la ordenación. Este problema lo estudiaremos en este mismo tema un poco más adelante. En general, para implementar el método compareTo usaremos los métodos compareTo de las propiedades involucradas en la igualdad o algunas otras derivadas. Un orden natural adecuado puede ser comparar en primer lugar por una propiedad elegida arbitrariamente, si resulta cero comparar por la segunda propiedad, etc. Como ejemplo vamos a suponer un tipo Persona con tres propiedades: nombre, DNI y edad. Su interfaz sería pues la siguiente (Nótese que hereda del tipo Comparable): public interface Persona extends Comparable<Persona> { public String getNombre(); public String getDNI(); public Integer getEdad(); //... } Supongamos que queremos definir la igualdad entre dos objetos de tipo Persona por sus propiedades nombre y DNI, de forma que dos personas son iguales si tienen el mismo nombre y DNI. Esto implica que sus métodos que heredan de Object serían codificados en PersonaImpl de la siguiente forma: public String toString() { String s="["+this.getNombre()+" DNI: "+ this.getDNI()+"]"; return s; } public boolean equals(Object o){ boolean r = false; if(o instanceof Persona){ Persona p = (Persona) o; r = this.getNombre().equals(p.getNombre()) && this.getDNI().equals(p.getDNI()); } return r; } public int hashCode(){ return getNombre().hashCode()*31+getDNI().hashCode(); } 4. Diseño de tipos Si queremos establecer un orden natural para el tipo Persona deberá ser compatible con equals y por tanto deberá ordenar primero por nombre y si éste es el mismo por DNI: public int compareTo(Persona p) { int r; if(p==null){ throw new NullPointerException(); } r = getNombre().compareTo(p.getNombre()); if(r == 0) r = getDNI().compareTo(p.getDNI()); return r; } Como en el caso del tipo Persona hay muchos tipos cuyo orden natural se establece secuencialmente ordenando primero por una propiedad, simple o derivada, si son iguales ordenando por una segunda, etc. Como hemos visto arriba el tipo Comparator establece un orden alternativo al natural. Para su implementación es necesario implementar una nueva clase. La implementación del orden de Persona por edad podría ser: import java.util.Comparator; public class ComparatorPersonaEdad implements Comparator<Persona> { public int compare(Persona p1, Persona p2){ Integer r; r = p1.getEdad().compareTo(p2.getEdad()); return r; } } Como podemos comprobar la implementación anterior no es completamente consistente con la igualdad definida para el tipo Persona. Si queremos conseguir al menos la consistencia de que si el comparador devuelve cero es porque son iguales, hacemos una comparación adicional, si el resultado es cero, con el orden natural del tipo. Con esto conseguimos, al menos que si compare da cero entonces equals es true. public class ComparatorPersonaEdad2 implements Comparator<Persona> { public int compare(Persona p1, Persona p2){ Integer r; r = p1.getEdad().compareTo(p2.getEdad()); if (r==0){ r=p1.compareTo(p2); } return r; } } 5 6 Introducción a la Programación La clase que implementa el tipo Comparator puede tener atributos privados y el o los constructores de esta clase pueden tener parámetros para inicializar esos atributos. Esto será muy útil para construir órdenes que dependan de parámetros. Por ejemplo, si quisiéramos construir un orden configurable mediante un parámetro de tipo int que si fuera positivo indicaría un orden ascendente (de menor a mayor edad) y negativo para un orden descendente. El código sería: public class ComparatorPersonaEdadConfigurable implements Comparator<Persona> { private int sentido; public ComparatorPersonaEdadConfigurable(int s){ sentido=s; } public int compare(Persona p1, Persona p2){ Integer r; if(sentido >=0) r = p1.getEdad().compareTo(p2.getEdad()); else r = p2.getEdad().compareTo(p1.getEdad()); return r; } } Ejercicio: cómo se implementaría un Comparator que a partir de un parámetro de tipo Integer indicando una edad ordenara de menor a mayor distancia a esa edad. Esto es, si el parámetro fuera 18, primero estarían los objetos Persona de 18 años (distancia cero), después los de 17 ó 19 años (distancia uno) y así sucesivamente. 3. Uso de Comparable y Comparator 3.1 Clase Collections Uno de los usos habituales de los tipos Comparable y Comparator es ordenar los objetos que estén organizados en colecciones o agregados. Como ya sabemos del tema anterior el tipo List añade los elementos a la lista en un determinado orden. Así el método add sin argumento de posición añade al final de la lista y mediante add con posición puede añadir al principio (posición 0) o en medio (otra posición menor del tamaño dado por size). Para modificar el orden de los elementos en una lista java proporciona la clase de utilidad Collections con los siguientes métodos estáticos. Puede consultarse la lista completa en http://docs.oracle.com/javase/7/docs/api/java/util/Collections.html int binarySearch(List<T> list, T key) Searches the specified list for the specified object using the binary search algorithm. int binarySearch(List<T> list, T key, Comparator<T> c) Searches the specified list for the specified object using the binary search algorithm. 4. Diseño de tipos T max(Collection<T> coll) Returns the maximum element of the given collection, according to the natural ordering of its elements. T max(Collection<T> coll, Comparator<T> comp) Returns the maximum element of the given collection, according to the order induced by the specified comparator. T min(Collection<T> coll) Returns the minimum element of the given collection, according to the natural ordering of its elements. T min(Collection<T> coll, Comparator<T> comp) Returns the minimum element of the given collection, according to the order induced by the specified comparator. void reverse(List<T> list) Reverses the order of the elements in the specified list. void sort(List<T> list) Sorts the specified list into ascending order, according to the natural ordering of its elements. void sort(List<T> list, Comparator<? super T> c) Sorts the specified list according to the order induced by the specified comparator. El siguiente código nos da algunos ejemplos de uso: List<Persona> listaclase = new ArrayList<Persona>(); Persona p = new PersonaImpl("Pedro Gómez","11111111A", 23); Persona p1 = new PersonaImpl("Luisa Espinel","222222222B", 24); Persona p2 = new PersonaImpl("Pedro Gómez","33333333C", 24); Persona p3 = new PersonaImpl("Mariana Guerrero","44444444D",28); listaclase.add(p); listaclase.add(p1); listaclase.add(p2); listaclase.add(p3); mostrar("lista: ",listaclase); Comparator<Persona> cmp_edad = new ComparatorPersonaEdad(); Collections.sort(listaclase); mostrar("lista ordenada natural: ",listaclase); Collections.sort(listaclase,cmp_edad); mostrar("lista ordenada edad: ",listaclase); Persona mayoredad=Collections.max(listaclase,cmp_edad); mostrar("persona mayor edad ",mayoredad); 7 8 Introducción a la Programación Otros métodos útiles de Collections aunque sin relación con el orden son: void copy(List<T> dest, List<T> src) Copies all of the elements from one list into another. boolean disjoint(Collection<T> c1, Collection<T> c2) Returns true if the two specified collections have no elements in common. void fill(List<T> list, T obj) Replaces all of the elements of the specified list with the specified element. int frequency(Collection<T> c, Object o) Returns the number of elements in the specified collection equal to the specified object. int indexOfSubList(List<T> source, List<T> target) Returns the starting position of the first occurrence of the specified target list within the specified source list, or -1 if there is no such occurrence. int lastIndexOfSubList(List<T> source, List<T> target) Returns the starting position of the last occurrence of the specified target list within the specified source list, or -1 if there is no such occurrence. List<T> nCopies(int n, T o) Returns an immutable list consisting of n copies of the specified object. boolean replaceAll(List<T> list, T oldVal, T newVal) Replaces all occurrences of one specified value in a list with another. void shuffle(List<T> list) Randomly permutes the specified list using a default source of randomness. Set<T> singleton(T o) Returns an immutable set containing only the specified object. List<T> singletonList(T o) Returns an immutable list containing only the specified object. void swap(List<T> list, int i, int j) Swaps the elements at the specified positions in the specified list. 3.2 Tipo SortedSet Como ya se señaló en el tema 3, el tipo Set no permite ordenar sus elementos. Para que los elementos de un conjunto tengan orden, Java proporciona un tipo SortedSet que hereda de Set. La clase que implementa SortedSet se llama TreeSet y tiene dos posibles constructores: si el SortedSet se inicializa mediante el constructor sin parámetros, el orden de los elementos será el orden natural (el impuesto por compareTo) y si se invoca al constructor con un objeto Comparator como argumento, el orden será el impuesto por el correspondiente método 4. Diseño de tipos compare. La definición de un tipo SortedSet mediante un Comparator no coherente con equals tiene un problema importante que advierte la documentación de Java: http://docs.oracle.com/javase/7/docs/api/java/util/TreeSet.html Note that the ordering maintained by a set (whether or not an explicit comparator is provided) must be consistent with equals if it is to correctly implement the Set interface. (See Comparable or Comparator for a precise definition of consistent with equals.) This is so because the Set interface is defined in terms of the equals operation, but a TreeSet instance performs all element comparisons using its compareTo (or compare) method, so two elements that are deemed equal by this method are, from the standpoint of the set, equal. The behavior of a set is well-defined even if its ordering is inconsistent with equals; it just fails to obey the general contract of the Set interface. Veamos un ejemplo con los objetos de tipo Persona Persona Persona Persona Persona p = new PersonaImpl("Pedro Gómez","11111111A", 23); p1 = new PersonaImpl("Luisa Espinel","222222222B", 24); p2 = new PersonaImpl("Pedro Gómez","33333333C", 24); p3 = new PersonaImpl("Mariana Guerrero","44444444D",28); Set<Persona> conjunto = new HashSet<Persona>(); conjunto.add(p);conjunto.add(p1);conjunto.add(p2);conjunto.add(p3); mostrar("conjunto ",conjunto); Comparator<Persona> cmp_edad = new ComparatorPersonaEdad(); Comparator<Persona> cmp_edad2 = new ComparatorPersonaEdad2(); SortedSet<Persona> conj_ord = new TreeSet<Persona>(cmp_edad); SortedSet<Persona> conj_ord2 = new TreeSet<Persona>(cmp_edad2); conj_ord.addAll(conjunto); conj_ord2.addAll(conjunto); mostrar("conjunto ordenado 1 ",conj_ord); mostrar("conjunto ordenado 2 ",conj_ord2); La salida del código anterior es la siguiente: conjunto [[Pedro Gómez DNI: 11111111A], [Luisa Espinel DNI: 222222222B], [Mariana Guerrero DNI: 44444444D], [Pedro Gómez DNI: 33333333C]] conjunto ordenado 1 [[Pedro Gómez DNI: 11111111A], [Luisa Espinel DNI: 222222222B], [Mariana Guerrero DNI: 44444444D]] conjunto ordenado 2 [[Pedro Gómez DNI: 11111111A], [Luisa Espinel DNI: 222222222B], [Pedro Gómez DNI: 33333333C], [Mariana Guerrero DNI: 44444444D]] Se puede observar que con ComparatorPersonaEdad que no rompe el empate por el orden natural, esto es que ordena de manera exclusiva por edad, al añadir los elementos en el 9 10 Introducción a la Programación SortedSet “desaparece” el objeto p2, ya que no se permitirían dos objetos con la misma edad en conj_ord. Sin embargo, si rompemos el empate de edad por el orden natural (ComparatorPersonaEdad2) ya no se “pierde” ningún objeto al insertarlos en un SortedSet. Este segundo comparator no es totalmente coherente con equals ya que ahora se cumple compare == 0 equals==true, pero no la implicación al contrario. Sin embargo, con este segundo comparator si quisiéramos comprobar si dos objetos de tipo Persona tienen la misma edad, al poner una sentencia cómo la siguiente: if (cmp_edad2.compare(p1,p2)==0) mostrar("tienen la misma edad"); else mostrar("no tienen la misma edad"); El resultado sería que no tienen la misma edad, ya que el empate en edad de p1 y p2 es roto por el orden natural. Por tanto, al diseñar una clase Comparator el programador tiene que tener en cuenta estas circunstancias. El tipo SortedSet además de los métodos de Collection que hereda de Set tiene un conjunto de métodos propios: http://docs.oracle.com/javase/7/docs/api/java/util/SortedSet.html E first() Returns the first (lowest) element currently in this set. SortedSet<E> headSet(E toElement) Returns a view of the portion of this set whose elements are strictly less than toElement. E last() Returns the last (highest) element currently in this set. SortedSet<E> subSet(E fromElement, E toElement) Returns a view of the portion of this set whose elements range from fromElement, inclusive, to toElement, exclusive. SortedSet<E> tailSet(E fromElement) Returns a view of the portion of this set whose elements are greater than or equal to fromElement. 4. Modificadores de atributos y métodos Como hemos visto anteriormente Java nos permite escribir algunas palabras reservadas que modifican las características de acceso o comportamiento del elemento al que preceden. Hay dos tipos de modificadores: De acceso: Son los que permiten modificar la visibilidad del elemento, es decir, desde qué puntos de un programa Java son accesibles. 4. Diseño de tipos De comportamiento: Son los que permiten modificar el funcionamiento y la manera de uso de los elementos. Modificadores de acceso para atributos: public: El atributo es accesible desde cualquier punto del programa en el que se disponga de un objeto de la clase. private: El atributo es accesible sólo desde los métodos de la propia clase. protected: El atributo es accesible desde los métodos de la propia clase, desde los de las clases que hereden de ésta y desde los de las clases del mismo paquete (aunque no hereden de ésta). Modificadores de comportamiento para atributos: static: Todas las instancias de la clase comparten el mismo valor para el atributo. Los atributos declarados static “pertenecen a” un objeto especial que tiene el mismo nombre que la clase. También se llaman atributos de clase frente a los no static que se denominan atributos de instancia. final: No se permite cambiar el valor inicial del atributo (atributo constante). Modificadores de acceso de métodos: public: El método es visible desde cualquier punto del programa en el que se disponga de un objeto de la clase. private: El método es visible sólo desde los métodos de la propia clase. protected: El método es visible desde los métodos de la propia clase, desde los de las clases que hereden de ésta y desde los de las clases del mismo paquete (aunque no hereden de ésta). Modificadores de comportamiento para métodos: static: El método puede ser invocado sin necesidad de crear una instancia de la clase. Los métodos declarados static deben ser invocados sobre un objeto especial que tiene el mismo nombre que la clase. Desde ellos sólo se permite el acceso a atributos o métodos declarados static. También se llaman métodos de clase frente a los no static que se denominan métodos de instancia. final: No se permite que las clases hijas redefinan este método. Criterios para elegir los modificadores de atributos y métodos En general, declararemos los atributos con el modificador private. Los métodos a implementar definidos en la interfaz, los declararemos con el modificador public. En ocasiones, es útil definir constantes de clase: valores constantes para todos los objetos de un tipo. Para ello definiremos un atributo con los modificadores private o public, static y final. 11 12 Introducción a la Programación Usaremos el modificador static para los atributos que guarden información relativa a la población de objetos del tipo, y no al estado de un objeto particular. Por ejemplo, para contar el número de instancias creadas del tipo o una propiedad común a todos los objetos como el origen de coordenadas en el tipo Punto. Usaremos el modificador static para los métodos funcionales: aquellos que se comportan de manera funcional, recibiendo datos por parámetro y devolviendo el resultado, sin involucrar a ningún atributo del objeto implícito. Por ejemplo, en las clases de utilidad (como la clase Rectas de prácticas). Los métodos static pueden acceder y modificar el valor de los atributos static. Por cada clase existe un objeto especial, con el mismo nombre de la clase, cuyos atributos y métodos son los etiquetados con static. Un método declarado static no puede utilizar atributos ni métodos de la clase que no hayan sido declarados static. Un método no static sí puede usar un atributo o un método static. Un método static puede ser invocado (junto con el objeto especial cuyo nombre es el de clase) sobre cualquier elemento de la clase. Si un método es static se puede invocar sobre el nombre de la clase: Math.sqrt(2.0); 5. Reutilización de Código Una diseñado un tipo tenemos que implementarlo. Al implementar es muy importante reutilizar al máximo el código ya disponible. Usamos la reutilización de código en la fase de implementación de un tipo sea o no subtipo de otro. Hay diferentes formas de reutilizar código: Uso: Al implementar una clase podemos declarar variables de tipos ya implementados. Decimos que estamos usando esos tipos. Herencia de Clases: Cuando una clase extiende otra tiene disponible el código de los métodos de la clase padre. Composición de Clases: Cuando implementamos una clase podemos declarar uno o varios objetos privados de otros tipos ya implementados. Tenemos entonces disponibles todos los métodos de los atributos declarados. El uso de la herencia o la composición como mecanismos de reutilización si requiere algunos comentarios adicionales. La herencia es un mecanismo de reutilización sencillo pero no recomendable en muchos casos. La composición es, en general, más recomendable que la herencia. La herencia, como mecanismo de reutilización de código, sólo es conveniente cuando vamos a implementar un tipo S que es un subtipo de T del que ya disponemos de una implementación en una clase dada. En el resto de los casos se recomienda usar composición de clases. 4. Diseño de tipos 5.1 Herencia Al diseñar un tipo nuevo mediante herencia debemos partir de los ya existentes. La primera decisión a tomar es qué tipos, de entre los anteriores y otros que iremos viendo, debe extender el tipo diseñado. Todo tipo tiene, además de las heredadas de los tipos que refina, unas propiedades. Cada propiedad tiene un nombre, un tipo, puede ser consultada y además modificada o sólo consultada, y puede ser una propiedad simple o una propiedad derivada. Además las propiedades pueden ser individuales y compartidas. Las propiedades pueden tener parámetros y una precondición. Los tipos pueden tener adicionalmente operaciones que sirven para modificar el estado el objeto. Según sean modificables o sólo consultables, deduciremos un conjunto de métodos. Además de estos, los tipos pueden tener otros métodos. Para diseñar un tipo podemos seguir la siguiente plantilla: NombreDeTipo extiende T1, T2 … Propiedades o NombreDePropiedad, Tipo, Consultable o no, derivada o no, compartida o individual. o … Operaciones o … Propiedades heredadas o Criterio de Igualdad: detalles o Representación como cadena: detalles o Orden natural: si lo tiene especificar detalles o … Operaciones Heredadas o … La propiedades y operaciones heredadas son aquellas propiedades u operaciones heredadas de los que tipos que refina pero a las que se les han añadido más requisitos. La siguiente tarea es implementar el tipo y el conjunto de excepciones que son usadas por el tipo. Para ello hay que tomar algunas decisiones: ¿Cuántos atributos y de qué tipos? Una primera idea es poner un atributo por cada propiedad no derivada. Cada atributo tiene como nombre el de la propiedad pero empezando por minúscula y el tipo de la misma. Si la propiedad asociada es compartida por toda la población del tipo entonces el atributo llevará el modificador static. ¿Cuántos constructores? Uno con todos los datos suficientes para dar valor a los atributos, otro sin parámetros y en muchos casos uno que tome un String como parámetro. 13 14 Introducción a la Programación Los constructores deben disparar excepciones si no pueden construir un objeto en un estado válido con los parámetros dados. Los constructores deben dar valor a todos los atributos. En general, como podemos observar, es recomendable hacer los cálculos necesarios en variables locales adecuadas y posteriormente asignar el valor calculado a los atributos. Detalles de la herencia entre clases Una clase hereda de otra cuando consta en la cabecera de la misma la palabra reservada extends seguida del nombre de la clase de la que se hereda. Cada clase sólo puede heredar de una única clase. Si la clase B hereda de la clase A, implica que todos los atributos y métodos declarados public o protected contenidos en la clase A pasan a estar disponibles en la clase B. public class A { public String atr1; protected Integer atr2; private Boolean atr3; // Falta constructor public void metodo1(){…} private void metodo2(){…} protected void metodo3(){…} } public class B extends A{ private String atr4; // Falta constructor public void metodo4(); } Atributos accesibles en la clase B: atr1, atr2 y atr4. Métodos accesibles en la clase B: metodo1(), metodo3() y metodo4(). Una clase B que herede de otra A puede reutilizar, también sus constructores. Para ello la clase invocará en la primera línea del cuerpo de su constructor al constructor apropiado de la clase padre. Esto se consigue mediante el uso de la palabra reservada super, seguida de los parámetros reales separados por comas y encerrados entre paréntesis. Igualmente una clase puede reutilizar otros constructores propios ya definidos. Esto se consigue mediante el uso de la palabra reservada this, seguida de los parámetros reales separados por comas y encerrados entre paréntesis. La palabra reservada this tiene otros usos. Designa el objeto actual por una parte y por otra this.a, this.m(e) designan el atributo a y la invocación al método m de la clase que estamos definiendo. 4. Diseño de tipos public class A { public String atr1; protected Integer atr2; private Boolean atr3; public A(String a1, Integer a2, (…) Boolean a3){…} } public class B extends A{ private String atr4; public B(String a1, Integer a2, super(a1,a2,a3); atr4=a4; } (…) Boolean a3, String a4){ } Asimismo, los métodos heredados pueden ser redefinidos, siendo común utilizar en dicha redefinición una llamada al método de la clase padre. Para invocar los métodos de la superclase se utiliza la palabra reservada super seguida de un punto, el nombre del método a invocar y los parámetros reales entre paréntesis. public class A { (…) public void metodo1(){ super.metodo1(); (…) } } Como ejemplo de herencia entre clases se ha estudiado en la asignatura el tipo Punto y el tipo Pixel o el tipo Persona y el tipo Médico. 5.2 Composición de clases Como hemos explicado arriba la composición de clases consiste en construir una clase a partir de la funcionalidad parcial o total ofrecida por otras. Para implementarla se declaran tantos atributos como clases a componer y se delegan los métodos a implementar de la nueva clase en alguno de los objetos declarados. La relación de composición no crea relación tipo-subtipo entre la clase construida y las reutilizadas. Esto hace que este mecanismo sea el más indicado para la reutilización de código en la mayoría de los casos. Sea la clase A que queremos implementar componiendo las clases C1, C2, C3. Sean m1, n1, … los métodos a reutilizar de C1 (usualmente serán un subconjunto de todos los métodos públicos que ofrece). Igualmente para C2, C3. El esquema de implementación es: public class A { private C1 c1; private C2 c2; private C3 c3; 15 16 Introducción a la Programación … public A(){ c1 = new C1(); c2 = new C2(); c3 = new C3(); … } public tm1 m1() { c1.m1();} public tn1 n1() { c1.n1();} … public tm2 m2() { c2.m2();} … } Ejemplo de reutilización mediante composición Como ejemplo de composición se han estudiado en la asignatura el tipo Circulo o el tipo Hospital. Apéndice: Paso de parámetros en Java Desde el principio de la programación la construcción de software mediante la división del problema en subproblemas independientes ha sido una constante. Las unidades independientes de instrucciones que resuelven una tarea específica han recibido a lo largo del tiempo distintos nombres: rutinas, subrutinas, subprogramas, procedimientos, funciones, métodos, etc. Esta forma de programar presenta importantes ventajas: Facilita la construcción del software al dividir un problema complejo en subproblemas más simples, de forma que el programador puede concentrarse en resolver una tarea específica sin tener en cuenta el problema entero. Facilita la revisión y prueba del código. Facilita la organización y comprensión al mejorar la legibilidad del software. Permite la reutilización y el intercambio de software. Esta forma de organizar el código necesita de una estructura en las que estas unidades de código independientes, se declaran una sola vez pero pueden ser utilizadas, mediante llamadas, todas las veces que se quiera en un programa. Estas llamadas tienen que tener un mecanismo de intercambio de información entre la unidad que llama y la rutina invocada. El mecanismo de paso de valores a un módulo hace una abstracción de los valores concretos que se proporcionan al módulo en cada llamada, los parámetros actuales, mediante la declaración de variables que servirán para referenciar esos valores de manera única, los parámetros formales. De esta forma es posible diseñar un módulo de manera independiente de los valores que le serán proporcionados en la entrada. Además, ocultamos los datos e instrucciones que 4. Diseño de tipos maneja el subprograma de manera que mejoramos la estructura del programa y facilitamos la abstracción a la hora de programar. En Java se definen métodos y recordemos del tema 1 que los parámetros formales son variables que aparecen en la signatura del método en el momento de su declaración. Los parámetros reales son expresiones que se colocan en el lugar de los parámetros formales en el momento de la llamada al método: Un ejemplo para diferenciar parámetros reales y formales. En PuntoImpl escribimos: public void setX(Double neox){ x=neox; } En TestPunto escribimos: Double a=3.0; Punto p = new PuntoImpl(2., 3.); p.setX(a); En este ejemplo a es un parámetro real y neox un parámetro formal. Este mecanismo de paso de parámetros varía de unos lenguajes a otros. En C teníamos funciones y parámetros de entrada o parámetros de entrada/salida y en la llamada a una función se asignan los parámetros reales a los formales, se ejecuta el cuerpo del método llamado y se devuelve el resultado al llamador. Si el parámetro era de entrada su valor devuelto era el mismo aunque el parámetro formal hubiese cambiado en la función. Si el parámetro era de entrada/salida, los parámetros formales y reales compartían memoria y por tanto, el cambio en el parámetro formal dentro de la función implicaba un cambio en el valor del parámetro real. En Java formalmente no se distingue por su sintaxis entre un tipo de parámetro u otro. Esto es, no existe nada parecido a los * y & de C ó C++. En Java la distinción entre un tipo u otro viene dada por el tipo del parámetro. Así si el tipo de parámetro formal es un tipo primitivo o un tipo objeto inmutable el parámetro real no cambiara de valor en el método, aunque el parámetro formal lo haga, comportándose por lo tanto como si fuera un parámetro sólo de entrada. Por el contrario si el parámetro formal es un objeto mutable, un cambio dentro del método implica un cambio en el parámetro real, ya que parámetro real y formal son el mismo objeto en el momento de la llamada. Veamos un ejemplo: 17 18 Introducción a la Programación public class TestPasoParámetros extends Test{ public static void main(String[] args) { Punto p = new PuntoImpl(1.,0.); mostrar("Punto p antes ",p); metodo_Punto(p); mostrar("Punto p después ",p); int j=0; mostrar("int j antes ",j); metodo_int(j); mostrar("int j después ",j); Integer n=0; mostrar("Integer n antes ",n); metodo_Integer(j); mostrar("Integer n después ",n); String cad="Hola"; mostrar("String cad antes ",cad); metodo_String(cad); mostrar("String cad después ",cad); List<Integer> li = new ArrayList<Integer>(); li.add(1); li.add(2);li.add(3); mostrar("Lista li antes ",li); metodo_Lista(li); mostrar("Lista li despues ",li); } public static void metodo_Punto(Punto q){ q.setX(3.0); } public static void metodo_int(int i){ i=1; } public static void metodo_String(String s){ s.concat(s); } public static void metodo_Integer(Integer i){ i++; } public static <T> void metodo_Lista(List <T> l){ l.addAll(l); } En este código se han implementado cinco métodos que modifican sus parámetros formales: uno es un tipo primitivo (int), dos son objetos inmutables (Integer y String) y dos objetos mutables (Punto y List). Ejecútelo y compruebe qué sucede. Escriba una explicación al por qué unos parámetros cambian y otros no.