Tema 4

Anuncio
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.
Descargar