Sie sind auf Seite 1von 64

Tipos abstractos

de datos
Jordi Àlvarez Canal

PID_00161994
Módulo 1
© FUOC • PID_00161994 • Módulo 1 Tipos abstractos de datos

Índice

Introducción ............................................................................................ 5

Objetivos ................................................................................................... 7

1. Contenedores ...................................................................................... 9
1.1. Dualidad entre especificación/implementación ............................ 9
1.2. Implicaciones matemáticas de los TAD .......................................... 11
1.2.1. TAD como conjunto de valores .......................................... 11
1.2.2. Semántica de un TAD .......................................................... 12
1.2.3. Especificación de un TAD ................................................... 13
1.3. Tipos abstractos de datos en Java ................................................... 14
1.3.1. Bibliotecas en Java ............................................................... 15
1.3.2. Dualidad entre especificación/implementación
en Java ................................................................................. 16

2. Diseño por contrato ......................................................................... 23


2.1. Programación defensiva ................................................................. 24
2.2. Contratos ........................................................................................ 25
2.2.1. Incumplimiento de contrato .............................................. 27
2.3. Invariante de la representación ...................................................... 28
2.4. Contratos en Java ........................................................................... 29
2.4.1. Herramientas de diseño para contrato ................................ 33

3. Desarrollo de una colección ejemplo ........................................... 35


3.1. Operaciones .................................................................................... 35
3.2. Implementación mediante un vector ............................................. 36
3.2.1. Definición de la representación .......................................... 36
3.2.2. Contenedores acotados ........................................................ 37
3.2.3. Implementación de las operaciones ................................... 38
3.2.4. Uso de la colección .............................................................. 40

4. Tipos genéricos o paramétricos ..................................................... 43

5. Biblioteca de colecciones de la asignatura ................................. 48


5.1. Jerarquía de colecciones ................................................................. 48
5.2. Tipos auxiliares ............................................................................... 53

6. Presentación del resto de módulos ............................................... 55

Resumen .................................................................................................... 57

Ejercicios de autoevaluación ............................................................... 59


© FUOC • PID_00161994 • Módulo 1 Tipos abstractos de datos

Solucionario ............................................................................................. 61

Glosario ..................................................................................................... 61

Bibliografía .............................................................................................. 62

Anexo ......................................................................................................... 63
© FUOC • PID_00161994 • Módulo 1 5 Tipos abstractos de datos

Introducción

En este módulo presentamos las nociones básicas que nos proporcionarán el


entorno en el que iremos presentando a lo largo del curso los diferentes tipos
de contenedores.

En primer lugar, desarrollaremos las nociones de contenedor y de tipo abstracto de A lo largo del texto utilizaremos
indistintamente los términos
datos (TAD). La primera es una abstracción que nos permite almacenar un grupo contenedor y colección.

de elementos y realizar las operaciones necesarias para gestionar este grupo: inser-
tar nuevos elementos, borrar existentes o consultar el estado del grupo mismo de
elementos (qué elementos hay...). Esta noción tiene una importancia práctica ca-
pital en el mundo del desarrollo de software, ya que la posibilidad de definir gru-
pos de elementos de diferentes tipos y su gestión es necesaria en cualquier
programa de una mínima complejidad.

La segunda noción, tipo abstracto de datos o TAD, nos propiciará desvincular la


interfaz de un contenedor de su implementación. Esta desvinculación nos per-
mitirá definir dos niveles y aspectos diferentes: la implementación de los con-
tenedores y su uso, totalmente independiente del punto anterior. Y lo mejor
de todo: no necesitaremos mezclar estos dos aspectos.

La noción de TAD se remonta a comienzos de los años setenta, mucho antes TAD es la sigla de
tipo abstracto de datos.
de la aparición de los lenguajes de programación más populares de la actuali-
dad. De hecho, la noción de TAD es completamente independiente del len-
guaje de programación utilizado. Cada lenguaje dispone de un conjunto de
formalismos y construcciones que permiten adaptarse mejor o peor para repre-
sentar la noción de TAD. Como en esta asignatura se utiliza Java, veremos cómo
podemos definir TAD con las herramientas ofrecidas por este lenguaje.

Como hemos comentado, los TAD ofrecen dos aspectos diferentes: la imple-
mentación y el uso. En esta dualidad, ¿cómo definimos la frontera entre lo que
hace el TAD y lo que ha de hacer su usuario (típicamente un programador de
aplicaciones)? La especificación de un TAD es precisamente eso: la definición
de su comportamiento de manera que el usuario pueda programar la aplica-
ción de modo coherente con el comportamiento del TAD.

Ahora bien, existe una técnica que, haciendo uso de la especificación de un


TAD, permite razonar de manera mucho más profunda sobre el comporta-
miento de un TAD y la relación que se establece entre él y sus usuarios. Para
definir bien esta relación, se establece un contrato en el que cada una de las
partes tiene sus responsabilidades y derechos. Este planteamiento ofrece dos
grandes ventajas: por una parte, ayuda a los usuarios de un TAD a tener cla-
ras cuáles son sus responsabilidades y qué pueden esperar de un TAD si las
© FUOC • PID_00161994 • Módulo 1 6 Tipos abstractos de datos

asumen; y, por otra, ayudan a los diseñadores de TAD a definir claramente


el contrato mediante la especificación.

Con todo esto, habremos presentado el marco teórico con el que trabajaremos en
la asignatura. A continuación, pondremos en común todos estos elementos por
medio de un ejemplo de colección, que presentaremos en el mismo formato es-
tándar que utilizaremos para las colecciones de los módulos siguientes.

Esta colección de ejemplo se presentará en dos variantes diferentes: una en la


que no se utilizan tipos paramétricos y otra en la que sí se utilizan. Esta segun-
da variante nos servirá para introducir los tipos paramétricos en Java, una no-
vedad de la versión 1.5 de Java (también denominada Java 5). A lo largo de
toda la asignatura se continuarán utilizando tipos paramétricos para presentar
las diferentes colecciones en el lenguaje Java.

Para acabar, en el módulo se realiza una presentación de la biblioteca de co-


lecciones de la asignatura, que contiene todas las colecciones desarrolladas en
los diferentes módulos. Si bien los detalles se dejan para los módulos siguien-
tes, se efectúa una descripción general de la biblioteca y una presentación del
diseño orientado a objetos de su estructura. Este punto, aparte de proporcio-
naros una impresión general de la biblioteca de colecciones que utilizaréis du-
rante el curso, os servirá de referencia básica cuando la hayáis de utilizar en las
diferentes actividades.
© FUOC • PID_00161994 • Módulo 1 7 Tipos abstractos de datos

Objetivos

Los materiales didácticos de este módulo proporcionan los conocimientos


fundamentales para que alcancéis los siguientes objetivos:

1. Entender las nociones de contenedor y tipo abstracto de datos o TAD; y


como consecuencia de esta segunda, la diferencia entre especificación e
implementación.

2. Saber identificar las responsabilidades del TAD y las de su usuario a partir


de la especificación de un TAD.

3. Ser capaz de asignar –en la tarea de diseño de un TAD– responsabilidades a


un TAD y a sus usuarios a partir de los principales argumentos presentados
por el diseño por contrato. A partir de estas responsabilidades, poder dar la
especificación del TAD en lenguaje natural.

4. Conocer y poder aplicar todos los mecanismos ofrecidos por el lenguaje


Java para representar TAD (interfaces y clases) y colecciones en las que los
elementos son un tipo cualquiera (tipos paramétricos).

5. Entender la organización y la estructura de la biblioteca de colecciones


de la asignatura.
© FUOC • PID_00161994 • Módulo 1 9 Tipos abstractos de datos

1. Contenedores

Esta asignatura versa especialmente sobre cómo representar y gestionar coleccio- En los lenguajes de orientación
a objetos, representaremos un
contenedor con una clase.
nes de datos. Cualquier aplicación mínimamente compleja necesita almacenar En la asignatura Diseño y programación
orientada a objetos ya habéis utilizado las
colecciones de elementos. Por tanto, resulta muy útil disponer de una biblioteca clases java.util.Vector y java.util.ArrayList
para almacenar colecciones.
de contenedores que pueda ser utilizada por cualquier aplicación que hayamos
de desarrollar.

Pero, si se trata únicamente de guardar colecciones de objetos, ¿por qué ha-


blamos de biblioteca de contenedores o de contenedores en plural? ¿Por qué no
tenemos una única clase de contenedor, que utilizaríamos para almacenar
cualquier colección?

La razón es que existen distintas estrategias para almacenar una colección de


objetos. Cada estrategia tiene unas características concretas que determinarán
el uso que podremos hacer de la colección:

• Dependiendo de la estrategia que elijamos, necesitaremos más o menos es-


Espacio
pacio para representar los objetos.
Por espacio entendemos el nú-
mero de bits en la memoria
del ordenador que ocupará
• Las operaciones que podremos realizar variarán ligeramente si utilizamos una colección.
una estrategia u otra. Así pues, una estrategia nos puede ofrecer una opera-
ción determinada que otra no nos ofrece.

• Las mismas operaciones, en ocasiones, se pueden realizar de manera más


eficaz con una estrategia que con otra.

• Algunas estrategias únicamente se pueden utilizar si los objetos almace-


nados cumplen ciertas condiciones, como por ejemplo que se puedan or-
denar.

Normalmente, cada aplicación tendrá una serie de restricciones tanto de efi-


Eficiencia
ciencia (temporal) como de espacio, y en cada una habrá que almacenar un
De entre los recursos utilizados
tipo de objetos determinados, a los cuales necesitaremos acceder también de para una aplicación, nos pode-
mos referir a recursos tempora-
unas maneras determinadas. Estas restricciones harán que no todas las estra- les y a recursos espaciales.
tegias sean aplicables a los mismos casos. Además, entre las estrategias apli- Consecuentemente, hablare-
mos de eficiencia temporal y de
cables en una situación determinada, habrá unas más adecuadas que otras. eficiencia espacial. Muchas ve-
ces, sin embargo, encontrare-
mos el término eficiencia de
manera aislada. En este caso,
normalmente, el término hará
1.1. Dualidad entre especificación/implementación referencia a la eficiencia tem-
poral.

Una vez que la aplicación ya está funcionando, es usual que los requisitos
evolucionen con el tiempo. Por tanto, la aplicación se ha de adaptar a los
© FUOC • PID_00161994 • Módulo 1 10 Tipos abstractos de datos

nuevos requisitos. Estos nuevos requisitos pueden provocar que las estrate-
gias que se utilizaban hasta aquel momento para almacenar colecciones de
objetos dejen de ser óptimas, y sea conveniente sustituirlas por otras. Este
cambio se debería poder realizar fácilmente, y debería tener el menor impac-
to posible en la aplicación.

Por este motivo, y por otros que ya iremos desgranando, cuando desar-
rollemos una aplicación, y ésta haya de utilizar colecciones, es impor-
tante que el código de la aplicación esté tan desvinculado como sea
posible de las estrategias concretas que hemos elegido para almacenar
colecciones de objetos.

Evidentemente, cuando en una aplicación decidimos utilizar un contenedor Lenguaje Java


concreto es porque necesitaremos utilizar algunas (o muchas) de las funciones
El lenguaje de programación
que ofrece este contenedor. Por tanto, la aplicación necesitará como mínimo Java proporciona herramientas
para desvincular las funciones
estar vinculada a las funciones ofrecidas por el contenedor. En cambio, no será ofrecidas por un contenedor
(interfaz) de su implementa-
necesario que la aplicación conozca cómo están implementadas. Es decir, la ción, tal como veremos en el
subapartado 1.2 de esta asig-
aplicación (o resolución del problema) es en realidad independiente de la im- natura.
plementación concreta que estamos utilizando. Dependiendo del lenguaje de
programación utilizado, esta desvinculación será factible o no.

Para llevar a cabo esta desvinculación, cada colección dispondrá de dos partes
bien diferenciadas:

• La especificación, que está compuesta por las signaturas de los métodos Recordad de Fundamentos de
programación que la precondición
(los servicios o funcionalidades que se ofrecen) y la descripción de cada (o poscondición) se expresa con un
predicado booleano.

uno de estos métodos, en forma de precondición y poscondición.

• La implementación, que consiste en asociar un algoritmo a cada una de


las funcionalidades o métodos ofrecidos en la especificación. Cada uno de
los algoritmos asociados ha de garantizar que si al inicio de su ejecución se
cumple la precondición correspondiente, una vez ejecutado el algoritmo,
se cumplirá la poscondición.

Desde el punto de vista conceptual, la especificación de una colección nos dice Esta dualidad entre especificación e
implementación es equivalente a la de
“qué hace la colección” (qué comportamiento tiene); mientras que su imple- especificación-algoritmo que se muestra
en la asignatura Fundamentos de
programación.
mentación nos dice “cómo lo hace” (cómo está implementado este comporta-
miento).

Del mismo modo que podemos tener distintos algoritmos que cumplan
una misma especificación, aquí podremos tener diferentes implemen-
taciones que proporcionen una misma especificación.
© FUOC • PID_00161994 • Módulo 1 11 Tipos abstractos de datos

1.2. Implicaciones matemáticas de los TAD

La dualidad entre especificación e implementación no es aplicable únicamen-


te a las colecciones. De hecho, podemos utilizar esta dualidad en un ámbito
totalmente general, para cualquier clase (o tipo), y no únicamente restringido
a aquellas clases especializadas en guardar colecciones de elementos. Por tanto,
en este subapartado hablaremos de especificación (o, respectivamente, imple-
mentación) de una clase en el mismo sentido que en el subapartado anterior uti-
lizábamos especificación (implementación) de una colección.

Para trabajar de manera conceptual con esta dualidad entre especificación e


Terminología
implementación, en el año 1974, John Guttag y otros investigadores introdu-
La palabra abstracto denota
jeron el concepto tipo abstracto de datos o, abreviado, TAD (en inglés: abstract aquí independencia respecto
data type, ADT). a la implementación concreta
que se utilice.

1.2.1. TAD como conjunto de valores

Un TAD es una abstracción matemática que consiste en un conjunto de valo-


res y operaciones definido a partir de lo que hasta ahora hemos denominado
especificación. Para entender qué significa esto, plantearemos una analogía con
un tipo que nos resulta bien conocido: el de los números naturales (a partir de
ahora utilizaremos Nat para referirnos al tipo de los números naturales).

Un objeto que es de tipo Nat puede tener el valor 0, o bien el valor 1, o bien
El tipo Nat
el valor 2, etc. En cambio, este objeto nunca podrá tener el valor 33,1, ni el
En el caso del tipo Nat,
valor ‘a’, ni el valor ‘casa’. Es decir, el conjunto de valores asociado al tipo sabemos que el conjunto de
valores asociados es infinito.
Nat es {0, 1, 2, 3, 4, ...}. No ha de ser necesariamente
así siempre. Podemos definir
TAD con un conjunto de valo-
Del mismo modo, un objeto que es del tipo correspondiente a un TAD T cual- res finito.
quiera podrá tomar un conjunto de valores determinado por el TAD T mismo: Imaginad, por ejemplo, un
TAD para valores booleanos.
T1, T2, T3...

Sobre un objeto de tipo Nat podemos realizar una serie de operaciones. Así El formalismo que utilizaremos para
definir un TAD y sus operaciones se
muestra en el subapartado 1.2.3
pues, podemos incrementarlo o decrementarlo en una unidad, sumarle o res- de esta asignatura.

tarle una cantidad, etc. La aplicación de cada una de estas operaciones pro-
ducirá una variación en el valor del objeto. Esto es, aplicar una operación
sobre un objeto de tipo Nat que tiene un valor determinado puede hacer (de-
pendiendo de la definición de la operación) que el valor de este objeto se
transforme en otro, que necesariamente ha de estar asociado a Nat. De este
modo, por ejemplo, si incrementamos un objeto que tiene el valor 56, este
objeto pasará a tener el valor 57.

Del mismo modo, si en la especificación de T hay definida una operación de-


terminada, cuando apliquemos esta operación sobre un objeto de tipo T, este
objeto también podrá variar su valor (en función probablemente de su valor
anterior).
© FUOC • PID_00161994 • Módulo 1 12 Tipos abstractos de datos

Ha de quedar claro que el modo conceptual más adecuado de visualizar un


Dominio de un TAD
TAD es por referencia a lo siguiente:
Observad que los valores que
pueden tomar las instancias de
1) El conjunto de valores que pueden tener las instancias del TAD. un TAD no han de referirse ne-
cesariamente a tipos básicos
2) La especificación de las operaciones que se pueden aplicar sobre las ins- del lenguaje de programación
usado. Se puede tratar de valo-
tancias del TAD. res de un dominio totalmente
aparte.

De este modo, conseguimos definir el comportamiento del TAD de manera to-


talmente independiente a la implementación (o implementaciones) que se
proporcione.

Con todo esto, podemos definir un TAD como un conjunto de valores


determinado por un conjunto de operaciones y la especificación de
cada una de éstas.

1.2.2. Semántica de un TAD

El conjunto de valores que puede tomar una instancia de un TAD son única-
mente símbolos, pero, de hecho, no tienen ningún significado. Necesitamos
alguna manera más de atribuir un significado o semántica a cada uno de estos
valores. Con el significado podemos, por ejemplo en el caso de los naturales,
saber que el número 2 es 1 + 1, o el doble de 1. En cambio, sin significado, úni-
camente tendríamos los símbolos 2 y 1.

Para dotar de significado al conjunto de valores que puede tomar una instan-
cia de un TAD, la teoría de los tipos abstractos de datos usa la especificación al-
gebraica. En este texto no trataremos con detenimiento este tema, pero sí que
creemos interesante exponerlo brevemente de manera intuitiva, lo que hare-
mos a continuación.

¿Os habéis parado alguna vez a pensar de qué modo se define la semántica
de los números enteros? Si habéis estudiado álgebra, probablemente lo ha-
yáis debido hacer. Los números enteros, junto con las operaciones de suma
y producto, son un anillo conmutativo y unitario. Esto significa que cum-
plen el conjunto de propiedades de un anillo que sea conmutativo y unita-
rio. Estas propiedades están definidas mediante un conjunto de ecuaciones
en las que intervienen las operaciones (en este caso suma y producto) y va-
riables que representan instancias de enteros.

Así, por ejemplo, una de las propiedades de un anillo es la propiedad asociati- Si tenéis curiosidad por el resto
de propiedades o queréis ampliar el
va respecto del producto, expresada por la ecuación: x · (y · z)  (x · y) · z. tema, podéis recurrir a cualquier libro
de álgebra que tengáis a mano.

En el caso de los TAD, se hace algo equivalente mediante la especificación al-


gebraica: se definen un conjunto de ecuaciones que relacionan los valores de
un TAD mediante sus operaciones. Estas ecuaciones permiten dar significado
© FUOC • PID_00161994 • Módulo 1 13 Tipos abstractos de datos

tanto a operaciones como a valores. De hecho, la semántica de unos no es po-


sible sin los otros. Así, en el caso de los enteros no podemos “definir” el 2 a
partir de algún otro valor (1+1) sin la operación de suma.

Por otra parte, el conjunto de ecuaciones que constituye la especificación al-


gebraica de un TAD, al tiempo que da significado a valores y operaciones, de-
fine un conjunto de clases de equivalencia entre los términos que podemos
construir utilizando valores y operaciones. De este modo, por ejemplo, en
el caso de los enteros, la ecuación que nos dice que los enteros cumplen la
propiedad asociativa respecto del producto nos dice también que el término
(3 · 2) · 7 es equivalente al término 3 · (2 · 7).

Todo esto permite definir completamente la semántica de un TAD. Ahora bien, Lecturas
complementarias
para hacerlo, es necesario utilizar un formalismo matemático que no forma par-
te del objetivo de esta asignatura. Así que aquí nos limitaremos a utilizar espe- Si queréis ampliar el tema de
la especificación algebraica y
cificaciones informales, tal como se ve en el subapartado siguiente. la semántica de un TAD,
podéis revisar las obras
siguientes:
X. Franch (2001). Estructuras
de datos. Especificación, diseño
1.2.3. Especificación de un TAD e implementación (4.ª ed.).
Barcelona: Edicions UPC.
Disponible en línea en:
<www.edicionsupc.es>.
Para caracterizar o definir un TAD, hay que proporcionar su especificación.
R. Peña Marí (2000). Diseño
de programas. Formalismo y
abstracción (2.ª ed.).
Prentice Hall.
Advertid que en el subapartado anterior se dice explícitamente que la especi-
ficación de un TAD determina el conjunto de valores. Por tanto, a partir de la
especificación seríamos capaces de obtener este conjunto de valores.

Veamos a continuación cómo definimos unos de los ejemplos de TAD más


sencillos de todos y que ya hemos introducido antes: el TAD Nat. Lo defini-
remos con un constructor y tres operaciones. El constructor generará el valor
0 y, como es el único constructor que definiremos, lo utilizaremos para cons-
truir todas las instancias de Nat. Las tres operaciones definidas modificarán
la instancia del tipo Nat sobre la cual se ejecutarán; la primera y la segunda
operaciones la convertirán respectivamente en el sucesor y predecesor, y la
tercera operación le sumará una cantidad.

TAD Nat {

@pre cierto
@post El natural construido corresponde al número natural 0
Nat();

@pre El valor del número natural representado es X


@post El valor del número natural representado es X + 1
void suc();

@pre El valor del número natural representado es X > 0


© FUOC • PID_00161994 • Módulo 1 14 Tipos abstractos de datos

@post El valor del número natural representado es X – 1


void pred();

@pre El valor del número natural representado es X; y = Y


@post El valor del número natural representado es X + Y
void sumarCantidad(Nat y);

La sintaxis del ejemplo es la que utilizaremos en esta asignatura para especificar


el TAD, y es similar a la que emplearemos más adelante para representar los TAD
en Java. Por una parte, hemos aproximado la sintaxis en la que habitualmente
se presentan los TAD al mundo de la orientación a objetos (OO) y, más concre-
tamente, al mundo Java, que es el lenguaje que hemos elegido para esta asigna-
tura. Por otra parte, hemos utilizado lenguaje natural para especificar la
precondición y la poscondición de las operaciones del TAD.

Hay que remarcar varios puntos:

a) Del mismo modo que sucede con las clases en Java, las operaciones creado-
ras o constructoras tienen el mismo nombre del TAD. En el ejemplo anterior,
disponemos de una operación de creación, Nat(), que crea un objeto de tipo
natural con valor 0.

b) Cada operación del TAD va acompañada por dos etiquetas o tags (@pre y
@post) semejantes a las que utiliza el Javadoc y a las anotaciones del Java 5. Me-
diante estas etiquetas dotamos a cada método de especificación.

c) La precondición y la poscondición son predicados. Tanto en este ejemplo


como en el resto de módulos, se utiliza el lenguaje natural para expresarlas. Sobre la sintaxis
de los TAD

Hemos optado por prescindir de un lenguaje formal en la especificación de los La sintaxis habitual de los TAD
presenta muchas diferencias
TAD con el objetivo de evitar toda la maquinaria matemática que sería necesaria respecto de la que utilizamos
aquí, más enfocada hacia la fa-
si hubiéramos de trabajar con la especificación algebraica de TAD. Como ya he- miliarización con una sintaxis
muy parecida a la que usare-
mos comentado, no es el objetivo de este curso estudiar con detenimiento las im- mos en el resto de la asignatura
plicaciones matemáticas de los TAD ni trabajar con ellas; nuestro objetivo es cuando mezclemos los TAD
con el mundo Java. Podéis en-
trasladar al lenguaje de programación que utilizamos (Java) algunas propiedades contrar la sintaxis original y
una profundización en las no-
de los TAD que nos serán muy útiles: la dualidad entre especificación e implemen- ciones matemáticas de los
TAD en las obras de X. Franch
tación, así como la ocultación de la implementación; y, por tanto, poder utilizar (1999) y R. Peña Marí (2000).
un TAD únicamente haciendo referencia a su especificación.

1.3. Tipos abstractos de datos en Java

En este subapartado trataremos con un poco más de detalle dos aspectos muy
útiles en relación con los tipos abstractos de datos en programación Java: las
© FUOC • PID_00161994 • Módulo 1 15 Tipos abstractos de datos

bibliotecas de clases de Java y la dualidad entre especificación e implementa-


ción que presenta el lenguaje Java.

1.3.1. Bibliotecas en Java

La mayoría de los lenguajes de programación actuales nos permiten encapsu-


lar conjuntos de algoritmos y estructuras reutilizables en lo que se suele deno-
minar bibliotecas. En lenguajes OO hablaremos de bibliotecas de clases. Huelga
OO es acrónimo de orientación
señalar que Java no es una excepción a esto: por un lado, nos permite utilizar a objetos.

bibliotecas desde nuestros programas; por otro, el JDK incorpora herramientas


que nos permiten construir nuevas.

El propio lenguaje incorpora ya una biblioteca con un extenso conjunto de clases


definidas. Por ejemplo, podemos encontrar en ella desde clases que nos permiten
trabajar con horas y calendarios (java.util.Date, java.util.GregorianCalendar), hasta
clases que nos posibilitan interaccionar con ficheros (java.io.FileInputStream, ja-
va.io.File y otras), pasando por clases que sirven para almacenar colecciones de
objetos (todas las que implementan la interfaz java.util.Collection, como la pro-
bablemente conocida java.util.Vector).

Así, por ejemplo, utilizando la biblioteca de clases que nos proporciona el len-
guaje Java, podemos construir fácilmente un programa que nos muestre por
la salida estándar la hora y el día en curso:

HoraActual.java

package uoc.ei.ejemplos.modulo1;
import java.util.Date;
public class HoraActual {
public static void main(String[] args) {
Date ahora = new Date();
System.out.println(ahora);
}
}

El ejemplo anterior define una clase denominada HoraActual en el paquete


Paquetes de software
uoc.ei.ejemplos.modulo1. En los ejemplos de código que aparecen en este
Resulta muy recomendable el
texto se hace un uso extensivo de los paquetes Java. Todos los ejemplos es- uso de paquetes (packages) que
tán localizados en subpaquetes del paquete uoc.ei.ejemplos, y todos ellos los nos permitan ordenar las clases
que desarrollamos de una
podéis encontrar también en formato electrónico en el aula. manera coherente, sobre todo
cuando el número de clases con
el que trabajamos comienza a
El código del ejemplo se reduce a dos líneas: en la primera se crea un objeto ser elevado.
de la clase java.util.Date para obtener la hora en curso (esta clase está conteni-
da en la biblioteca proporcionada por el mismo Java); en la segunda se escribe
la representación en texto de este objeto por la salida estándar (se llama de ma-
nera implícita el método ahora.toString(), definido en la classe java.util.Date).
© FUOC • PID_00161994 • Módulo 1 16 Tipos abstractos de datos

Aparte de la biblioteca integrada en el mismo entorno Java, es posible utilizar


otras bibliotecas, o construir nuevas. Todas las bibliotecas en Java están repre-
sentadas por ficheros con extensión .jar. Esta especie de ficheros nos permiten
agrupar conjuntos de clases en un único fichero del mismo modo que un fi-
chero .zip nos permite agrupar otros ficheros.

La variable CLASSPATH
Asignación del valor
de CLASSPATH
Para poder utilizar una biblioteca (que no sea la propia de Java), debéis incluir el fichero
.jar correspondiente en la variable de entorno del sistema operativo denominada CLAS- Para dar un valor a la variable
SPATH. Así pues, si la variable CLASSPATH es igual a .;c:\ei\tad.jar significará que cuando CLASSPATH, se ha de llevar a
compilemos un programa en Java (y también cuando lo ejecutemos), tenemos acceso, cabo desde el sistema operati-
vo. Cada versión de sistema
primero, a las clases que están situadas en el subárbol de directorios que cuelga del direc- operativo tiene su propio
torio actual (la parte correspondiente al ‘.’); y, en segundo lugar, a todas las clases que modo de hacerlo.
contiene el fichero c:\ei\tad.jar.

1.3.2. Dualidad entre especificación/implementación en Java

Para poder representar adecuadamente un TAD en un lenguaje de pro-


gramación, es necesario que el lenguaje permita la especificación y la
implementación de manera separada. En Java utilizaremos una interfaz
(construcción interface) para representar la especificación de un TAD, y cla-
ses para representar sus implementaciones.

Especificación

Como sabéis, la construcción interface nos permite especificar un conjunto de


métodos y describir para cada uno de ellos la signatura del método.

El lenguaje Java no dispone de ningún mecanismo para dotar a los métodos


de una interfaz de precondición o poscondición. El concepto de especifica-
ción de un TAD va, por tanto, bastante más lejos de lo que Java nos ofrece
para representarla.

Como uno de los objetivos principales de la especificación es documentar el Recordad, de Diseño y programación
orientada a objetos, que el término
comportamiento de los métodos correspondientes, habitualmente veremos Javadoc se refiere al formato estándar
para comentar clases Java.

que la especificación de un método en forma de precondición y poscondición


se ha sustituido por una documentación detallada en formato Javadoc del
comportamiento del método. Podemos comprobar esto en la misma docu-
mentación del API de Java.

No obstante, tal como veremos, en esta asignatura seguiremos utilizando pre-


condiciones y poscondiciones. Esto, sin embargo, no siempre será posible para
las interfaces. En estos casos, optaremos por proporcionar una buena docu-
mentación en lenguaje natural que informe de una manera clara de qué hace
cada uno de los métodos.
© FUOC • PID_00161994 • Módulo 1 17 Tipos abstractos de datos

Veamos a continuación cómo traducir la especificación del TAD Nat a Java en


forma de interfaz. Como podéis comprobar, la traducción es bien directa. No-
tad que hemos añadido una operación adicional denominada consultar(), que
nos permite acceder al número natural representado (esta operación no tiene
ningún sentido en el TAD original, ya que el valor representado es de hecho
la misma instancia del TAD).

Nat.java
En la definición de la interfaz Nat se
ha omitido expresamente cualquier
package uoc.ei.ejemplos.modulo1; referencia a la especificación del TAD, que
se tratará en el apartado 2 de este módulo.
public interface Nat {
/** Este método incrementa el valor del natural. */
void suc();

/** Este método decrementa el valor del natural. */


void pred();

/** Este método suma un natural al objeto.


* @param y El natural a sumar. */
void sumarCantidad(Nat y);

/** Método consultor que devuelve el valor del


* natural como un entero.
* @return el valor del objeto. */
int consultar();
}

La interfaz anterior también se diferencia de la especificación del TAD Nat por


el hecho de que no proporciona ninguna operación de creación. Al traducir
los TAD a Java, las operaciones de creación únicamente estarán asociadas con
la implementación. Esto es así porque, en Java, en el momento de crear una
instancia de un TAD, hemos de especificar a partir de cuál de sus implemen-
taciones la queremos crear. Por tanto, no tiene utilidad asociar las operaciones
creadoras con la interfaz si después hemos de hacer referencia a la implemen-
tación elegida cada vez que las utilicemos.

Así pues, asociaremos las operaciones de creación de un TAD, exclusivamente,


con las implementaciones.

Implementación

Una vez definida la interfaz del TAD, podemos proporcionarle una imple-
mentación definiendo una clase que proporcione la definición de cada uno
de los métodos definidos en la interfaz. A continuaión tenéis una implemen-
tación del TAD Nat.
© FUOC • PID_00161994 • Módulo 1 18 Tipos abstractos de datos

NatiImpl.java

package uoc.ei.ejemplos.modulo1;

public class NatiImpl implements Nat {

/** Esta clase representa el natural


* mediante un atributo entero. */
private int nat;
/** Crea una instancia de Nat con valor 0. */
public NatiImpl() { nat = 0; }

public void suc() { nat++; }


public void pred() { nat--; }
public void sumarCantidad(Nat y) {
nat+= y.consultar();
}
public int consultar() { return nat; }

/** Devuelve una representación del natural


* en forma de texto. */
public String toString() {
return Integer.toString(consultar());
}
}

TAD Nat en Java

Como podéis comprobar, la implementación de Nat es totalmente directa. Esto es así por-
que el tipo int de Java ya dispone de operaciones equivalentes a las que estamos intentando
definir en el TAD Nat. Por tanto, únicamente se tratará de utilizar las operaciones homólo-
gas del tipo int. De hecho, esto nos indica que la definición del TAD Nat en Java no nos
resultará útil en realidad. La especificación y la implementación aquí del TAD Nat está úni-
camente justificada por motivos didácticos y tiene el objetivo de introducir, mediante un
ejemplo sencillo, todos los conceptos que utilizaremos con TAD a partir de ahora.

Las implementaciones de un TAD siempre contendrán un conjunto de atri-


butos privados o protegidos con los cuales habremos de poder representar
los diferentes valores que pueda tomar una instancia del TAD. En este caso,
representamos un número natural mediante un atributo entero (Nat).

En la cabecera de la clase ha de constar que la clase implementa la interfaz


correspondiente a la especificación del TAD. Y, de igual modo como cuando
una clase implementa a cualquier interfaz, se ha de proporcionar una imple-
mentación para cada uno de los métodos que aparecen en ella. Cada una de
estas implementaciones accederá y modificará adecuadamente el conjunto
de atributos que se corresponden a la representación (en el caso de la clase
NatiImpl, el atributo Nat).

La implementación proporcionada por la clase NatiImpl es probablemente la


más sencilla que se puede hacer del TAD Nat. Pero no es la única. Por ejemplo,
© FUOC • PID_00161994 • Módulo 1 19 Tipos abstractos de datos

es posible representar un natural de manera alternativa mediante una tabla de


booleanos. En esta representación, un natural N estaría representado por una
tabla de booleanos en la que las primeras N posiciones de la tabla (de 0 a N – 1)
tienen el valor ‘cierto’ (true), y la posición N-ésima tiene valor ‘falso’ (false).

Únicamente con fines didácticos disponéis a continuación de una implemen-


tación de Nat que utiliza esta representación.

NatbImpl.java

package uoc.ei.ejemplos.modulo1;

public class NatbImpl implements Nat {

/** Esta clase representa el natural


* mediante una tabla de booleanos. La representación
* de un natural N se corresponde con una tabla
* en la que las primeras posiciones de la tabla tienen
* el valor true y la posició N + 1 tiene
* el valor false. */
private boolean[] nat;

/** Crea una instancia de natural con valor 0. */


public NatbImpl() {
nat = new boolean[10];
}

public void suc() {


int i = buscarUltimaPosicion();
marcarPosicion(i + 1,true);
}

public void pred() {


int i = buscarUltimaPosicion();
marcarPosicion(i,false);
}

public void sumarCantidad(Nat y) {


int i = buscarUltimaPosicion();
int j = y.consultar();
while (j-->0)
marcarPosicion(++i,true);
}

public int consultar() {


return buscarUltimaPosicion() + 1;
}
© FUOC • PID_00161994 • Módulo 1 20 Tipos abstractos de datos

/** Busca la última posición con valor true


* del vector y la devuelve */
private int buscarUltimaPosicion() {
int i = 0;
while (i<nat.length && nat[i])
i++;
return i-1;
}

/** Marca una posición del vector de booleanos


* con valor. En caso de que la posición ultrapase
* el final del vector (i>=nat.length),
* duplica la medida del vector nat. */
private void marcarPosicion(int i,boolean valor) {
if (i>=nat.length)
duplicarVector();
nat[i] = valor;
}

/** Duplica la medida del vector nat. */


private void duplicarVector() {
boolean[] nuevo = new boolean[nat.length*2];
for(int i = 0;i<nat.length;i++)
nuevo[i] = nat[i];
nat = nuevo;
}
}

La representación de los naturales que usábamos en NatiImpl (un entero) es


La utilidad del diseño
mucho más cercana al modelo original de los naturales que la representación descendente
utilizada aquí (un vector de booleanos). Por este motivo, en esta implementa- Manejar directamente el vec-
tor de booleanos dentro de los
ción alternativa, la implementación de los métodos que ofrece el TAD resulta métodos públicos del TAD se-
bastante más compleja, y necesitaríamos definir métodos privados adicionales ría una tarea demasiado com-
pleja. Por ello, hemos de tener
que se encarguen de manejar el vector de booleanos. siempre bien presente la técni-
ca de diseño descendente que
aprendisteis en Fundamentos
de programación y Prácticas de
Ahora ya disponemos de un TAD –Nat– y de dos implementaciones diferentes: programación.

NatiImpl y NatbImpl. Es importante que en los algoritmos que desarrollemos en


los que utilicemos el TAD Nat, hagamos referencia a la implementación el me-
nor número de veces posible. En principio, esto debería ser únicamente en el
momento de crear instancias del TAD. En el resto del algoritmo haremos refe-
rencia únicamente a la interfaz. Ésta es una buena práctica de programación que
nos permitirá obtener un código más claro e inteligible; y, además, incrementa-
rá la mantenibilidad del código, al tiempo que facilitará posibles cambios de im-
plementación en el futuro si es necesario.
© FUOC • PID_00161994 • Módulo 1 21 Tipos abstractos de datos

No es necesario realizar un análisis exhaustivo para darse cuenta de que la im-


plementación proporcionada por NatiImpl es claramente mejor en todos los
aspectos:

• El código es más compacto y claro. Por tanto, el mantenimiento de la im-


plementación resulta mucho más fácil.

• La implementación de las operaciones proporcionada en NatiImpl es más Coste temporal


eficiente que la proporcionada por NatbImpl. Advertid que mientras que las del método

implementaciones proporcionadas en NatiImpl se limitan a utilizar los ope- En el módulo “Complejidad al-
gorítmica” aprenderemos a
radores del tipo int de Java, todas las operaciones de NatbImpl contienen calcular de manera rigurosa lo
que tarda un método en ejecu-
una llamada al método privado buscarUltimaPosicion, en el que se hace una
tarse. Lo haremos únicamente
búsqueda en el vector de booleanos. a partir de las instrucciones
que aparecen en la implemen-
tación del método. Al resulta-
• El espacio ocupado por una instancia de NatiImpl (un atributo de tipo int) es do lo denominaremos coste
temporal del método, y nos
más pequeño que el espacio ocupado por una instancia de NatbImpl (un vector permitirá elegir de una manera
objetiva, de entre distintos al-
de booleanos).
goritmos o métodos, aquel
que sea más eficiente.
Todos estos factores son muy importantes a la hora de elegir una implementación
u otra. Si nos ponemos sólo en el papel de usuario de un TAD, nos interesarán so-
bre todo los dos últimos puntos: la eficiencia temporal y la eficiencia espacial.

En este caso, la implementación proporcionada por NatiImpl es claramente


mejor en todos los aspectos. En otros casos encontraremos situaciones en las
que unas operaciones del TAD son más eficientes en una implementación,
mientras que otras lo son en otra. Por tanto, para elegir entre una y otra, de-
beremos evaluar el uso que hacemos del TAD en la aplicación desarrollada y,
a partir de ello, determinar qué métodos interesará que se ejecuten de manera
más eficiente.

Como ejemplo de aplicación usuaria del TAD Nat a continuación mostramos


una aplicación que calcula los N primeros números de Fibonacci utilizando el
TAD Nat. Advertid que podéis intercambiar las dos implementaciones de Nat
modificando una sola línea. El resultado será el mismo en todos los casos.

Fibonacci.java

package uoc.ei.ejemplos.modulo1;

public class Fibonacci {

protected static Nat crearNat() {


return new NatiImpl(); // o bien NatbImpl()
}
public static void muestraNumeros(int n) {
Nat fibn = crearNat(); // f[0] = 0
Nat fibn2 = crearNat();
fibn2.succ // f[1] = 1
Nat fibn3 = crearNat();
fibn3.suc(); // f[2] = 1
© FUOC • PID_00161994 • Módulo 1 22 Tipos abstractos de datos

int i = 0;
while (i<n) {
fibn = fibn2;
fibn2 = fibn3;
fibn3 = crearNat();
fibn3.sumarCantidad(fibn);
fibn3.sumarCantidad(fibn2);
i++;
}
System.out.println("El número es: "+fibn);
}

public static void main(String[] args) {


if (args.length!= 1)
System.out.println("¡Has de proporcionar
un solo parámetro!");
else {
try {
int n = Integer.parseInt(args[0]);
muestraNumeros(n);
} catch (NumberFormatException e) {
System.out.println("¡El parámetro ha de ser
un entero!");
}
}
}

}
© FUOC • PID_00161994 • Módulo 1 23 Tipos abstractos de datos

2. Diseño por contrato

Al final del apartado anterior, hemos dejado de lado todo lo referente a la es-
pecificación de manera expresa. ¿Qué sucede si se ejecuta uno de los métodos
ofrecidos por la interfaz del TAD Nat en una situación que no está prevista en
el TAD? Esto podría producirse por distintos motivos. Por ejemplo, el usuario
de la aplicación final (que es usuaria de nuestro TAD) puede introducir en ella
un dato erróneo. O bien, una situación también común, la aplicación final tie-
ne algún defecto que hace que se llame a algún método de un TAD en una si-
tuación anormal.

El programa siguiente genera un número natural –que inicialmente tendrá el Observad que la precondición
de pred (podéis ver el
valor 0– y ejecuta el método pred(). El predecesor de 0 no existe, y ninguna de subapartado 1.2) no se cumple
si el natural es 0.
las implementaciones que hemos visto está preparada para tratar esta situación.

PruebaErrorNat.java

package uoc.ei.ejemplos.modulo1.defensivo;
import uoc.ei.ejemplos.modulo1.*;

public class PruebaErrorNat {


public static void main(String[] args) {
Nat n = new NatiImpl(); // o bien NatbImpl
n.pred();
System.out.println("El valor del natural es: "+n);
}
}

Según la especificación que hemos dado del TAD Nat tal como aparece en
Precondición
el apartado 1.2, el comportamiento de la operación pred del TAD Nat en el de un algoritmo

caso de que el natural sea 0 no está definido. Es decir, la operación no está Recordad que, tal como estudi-
asteis en Fundamentos de pro-
preparada para recibir un 0 como parámetro y el resultado devuelto no está gramación, en un algoritmo
(en nuestro caso el correspon-
definido, ya que la poscondición no se ha de cumplir necesariamente en diente a una operación de un
esta situación. Si probamos el programa anterior con NatiImpl, veremos có- TAD), su precondición deter-
minará si una llamada es cor-
mo la salida estándar muestra el resultado –1, que no es un número natural. recta o no. El algoritmo
únicamente es responsable de
Si, en cambio, usamos NatbImpl, la ejecución se parará y se generará una tratar aquellas situaciones en
las que la precondición se
ArrayIndexOutOfBoundException, porque la operación pred estará intentan- cumple.
do acceder a la posición –1 del vector de booleanos.

En este caso, la precondición de la operación pred del TAD Nat nos informa de
que PruebaErrorNat es responsable de garantizar que n no es el natural 0 cuan-
do se hace la llamada a pred. Pero la precondición es un predicado que, de mo-
mento, no comprobamos en ningún lugar al hacer la llamada. ¿Tiene sentido,
© FUOC • PID_00161994 • Módulo 1 24 Tipos abstractos de datos

por tanto, mantener la precondición como está? ¿No hará esto que el TAD sea
poco robusto?

Para llegar al fondo del problema, dejemos de lado momentáneamente la pre-


condición actual de la operación pred y analicemos la situación desde un pun-
to de vista de asignación de responsabilidades, es decir: qué cosas se han de
garantizar desde cada una de las partes y quién ha de gestionar los errores. Pos-
teriormente, veremos cómo todo eso está completamente ligado con la espe-
cificación del TAD.

Por lo que respecta a la asignación de responsabilidades, se entreven dos posi-


bilidades:

1) Asignar la responsabilidad de comprobar las situaciones anómalas en el


mismo TAD. Esto significa que el código del método del TAD será responsable
de comprobar si el valor actual del TAD y los parámetros recibidos por el mé-
todo son los adecuados para ejecutar el método.

2) Asignar la responsabilidad a los usuarios del TAD de garantizar que siempre


que llamen a un método del TAD, lo harán en las condiciones adecuadas para
que se pueda ejecutar.

Elegir entre una u otra opción es una decisión que habremos de tomar siempre
que diseñemos un nuevo TAD, o, de hecho, cualquier clase.

2.1. Programación defensiva

Asignar la responsabilidad al TAD supone utilizar lo que comúnmente se de-


nomina programación defensiva. En este caso, deberemos incorporar a nuestros
TAD líneas de código dedicadas exclusivamente a comprobar condiciones de
error y a tratarlas adecuadamente.

El siguiente fragmento de código muestra cómo podemos modificar la imple-


Disponéis del conjunto completo
de clases Java para este ejemplo
mentación del método pred() de NatiImpl añadiendo la comprobación necesaria. en formato electrónico (paquete
uoc.ei.ejemplos. modulo1.defensivo).

NatiDefensivoImpl.java

package uoc.ei.ejemplos.modulo1.defensivo;
...
public void pred() throws ExcepcionNat {
if (nat==0)
throw new ExcepcionNat(“No existe el predecesor de 0”);
nat--;
}
...
© FUOC • PID_00161994 • Módulo 1 25 Tipos abstractos de datos

En este caso la comprobación es sencilla. Pero para otras situaciones, el código


correspondiente a la comprobación es, en ocasiones, bastante más complejo.
Adicionalmente, en el mismo TAD, normalmente no sabremos cómo hemos Recordad el mecanismo de
lanzamiento y captura de excepciones
del lenguaje Java presentado en la
de tratar la situación anómala. En general, el tratamiento del error se realizará asignatura Diseño y programación orientada
a objetos.
en la aplicación usuaria del TAD. Por este motivo, la programación defensiva
se ha de limitar, también en general, a comprobar la condición de error y en
caso de detectar una situación anómala, lanzar una excepción que posterior-
mente alguien tratará. En este caso, hemos introducido el uso de una excep-
ción nueva: ExcepcionNat.

Si hacemos responsables a los usuarios del TAD de garantizar que los paráme- Responsabilidad
tros recibidos son correctos, nos ahorraremos la programación defensiva. En y verificación

cambio, esta opción es menos sólida frente a posibles usos incorrectos. Por este Hacer responsables a los usua-
rios de un TAD de garantizar su
motivo, el uso de la programación defensiva podría parecer en un primer mo- uso adecuado no significa que
éstos hayan de hacer las
mento más adecuada. Pero esto no es exactamente así: el uso de la programa- comprobaciones defensivas.
ción defensiva tiene también desventajas importantes. Si bien es cierto que
puede hacer el código más sólido, su presencia hace también al código más
complejo y difícil de leer, en definitiva, menos mantenible.

Para aplicaciones y TAD de tamaño pequeño, podemos asumir sin problemas


esta complejidad, pero para aplicaciones de tamaño considerable es muy de-
seable no arrastrar este coste. Esto no impide, sin embargo, que en algún caso
concreto nos resulte más interesante que el propio TAD se encargue de las
comprobaciones pertinentes.

Las dos opciones también se pueden combinar. Esto puede suceder cuando en
un mismo método se dan distintas situaciones de error. Para algunas, nos puede
interesar que el propio TAD tenga la responsabilidad de comprobarlas, mientras
que para otras nos puede interesar que sea el cliente quien garantice al TAD que
la situación es la correcta para realizar la operación.

En cualquier caso, siempre que decidimos utilizar la programación defensiva


es deseable que el código correspondiente al algoritmo que queremos “prote-
ger” quede lo más aislado posible del código que lo “protege”.

2.2. Contratos

Con el objetivo de asignar de una manera clara las responsabilidades de cada


DBC es la sigla de la expresión
inglesa design by contract.
una de las clases/TAD que intervienen en una aplicación, utilizaremos lo que
se denomina diseño por contrato, en ocasiones conocido por su sigla inglesa
DBC.

Hablaremos de contrato tanto


El diseño por contrato (a partir de ahora, DBC) introduce un nuevo elemento en el ámbito de clase/interfaz,
como en el de TAD.
en el diseño de aplicaciones OO: el contrato entre una clase y sus usuarios.
© FUOC • PID_00161994 • Módulo 1 26 Tipos abstractos de datos

El contrato entre una clase y sus usuarios no es nada más que la espe-
cificación de esta clase, que representa el acuerdo formal entre la clase
y sus usuarios, en la que quedan definidos los derechos y obligaciones
de cada parte.

Así, pues, la precondición de un método de una clase (o TAD):

1) “Compromete” al usuario de la clase a garantizar que la precondición se


cumpla cuando ejecute el método.

2) Como consecuencia, “garantiza” que cuando el método en cuestión sea lla-


mado, la precondición de la clase será cierta. Por tanto, el desarrollador de un
TAD o una clase puede asumir que, siempre que un método sea ejecutado, su
precondición será cierta.

Por otra parte, la poscondición de un método de una clase:

1) “Compromete” a la clase a que, una vez ejecutado el método, se cumplirá


la poscondición (y siempre que al comienzo de la ejecución se cumpla la pre-
condición, lo que la clase asume).

2) Como consecuencia, “garantiza” a los usuarios de la clase que, una vez eje-
cutado el método, se cumplirá la poscondición.

Del mismo modo que los contratos legales que podemos firmar (por ejemplo,
para comprar un piso u obtener un crédito), el contrato que nos ocupa vincula
a dos partes: por un lado, a una clase y, por otro, a sus usuarios. También, como
en los contratos legales, define una serie de compromisos y garantías para cada
una de las partes. Evidentemente, los compromisos de una parte son las garan-
tías de la otra.

En definitiva, el diseño por contrato (DBC) no es otra cosa que englobar


bajo el concepto de contrato los diferentes elementos que aparecen en la
especificación. La noción de contrato nos ayuda, mediante la especifica-
ción, a establecer una relación clara entre una clase y sus usuarios.

El uso de contratos en el desarrollo del software orientado a objetos (OO)


Abreviamos las expresiones
permite definir de una manera clara las responsabilidades en cada una de las orientación a objetos u orientado
a objetos con la sigla OO.
interacciones entre las clases que conforman la aplicación OO. Esto hace que
el software desarrollado a partir de esta técnica sea en general más fiable, ya
que proporciona una herramienta que nos permite descomponer la respon-
sabilidad global de hacer funcionar la aplicación entre el conjunto de clases
(y TAD) que participan en la aplicación, y nos separa de manera clara las res-
ponsabilidades de cada una.
© FUOC • PID_00161994 • Módulo 1 27 Tipos abstractos de datos

2.2.1. Incumplimiento de contrato


La precondición
de un método
¿Qué sucede si el usuario de una clase no cumple su parte de contrato? Pues, Comprobar la precondición de
simplemente, que el método no está obligado a cumplir la suya. Ni tan siquie- un método dentro de la imple-
mentación de éste no es una
ra está obligado a informar de la situación anómala. En esta situación, es libre buena práctica en situaciones
normales, ya que uno de los
de hacer “lo que le dé la gana”. Para estas situaciones, diremos que el compor- objetivos principales al
tamiento del método no está definido. establecer contratos es
eliminar el código defensivo
correspondiente a las situacio-
nes erróneas capturadas por la
El DBC es compatible con la programación defensiva, ya que no existe nin- precondición.
gún problema para incluir en el contrato las comprobaciones que se deriven
de él. Esto es, el DBC nos ofrece la posibilidad de definir contratos más o me-
nos defensivos, según lo creamos conveniente en cada situación. El uso de
contratos poco (o nada) defensivos será lo más habitual y nos permitirá cen-
trarnos exclusivamente en el desarrollo del algoritmo o TAD. Normalmente,
esto será preferible y más beneficioso que “sobreproteger” nuestros métodos
con programación defensiva que haga más complejo el código. Esto no qui-
ta, como ya hemos comentado antes, que queramos utilizar la programación
defensiva para situaciones concretas.

Esta estrategia tiene, no obstante, un inconveniente importante. La programa-


ción defensiva permite detectar situaciones de error de manera fiable. En cam-
bio, el uso de contratos poco defensivos hará que estas situaciones de error no
se detecten hasta que se produzca un error detectado directamente por el len-
guaje de programación o el sistema operativo. Así, por ejemplo, si habéis hecho
desarrollo en Java, seguro que os habrá aparecido alguna vez un NullPointerEx-
ception, o un ArrayIndexOutOfBoundException. Este error difícilmente nos dirá
algo sobre la situación conceptual de error que se ha producido. En cambio, el
uso de programación defensiva habría detectado esta situación con anteriori-
dad, diciendo que, por ejemplo, un contenedor no tiene un elemento determi-
nado; o un entero debería estar dentro de un cierto rango. Esta información, sin
duda, será más útil al desarrollador para encontrar el error de programación de
manera más rápida.

Existen soluciones que permiten mantener tanto la fiabilidad proporcionada


por la programación defensiva, como la mayor mantenibilidad del uso de con-
tratos que responsabilicen a la parte cliente de un uso correcto de un TAD o
clase. Estas soluciones consisten en disponer de algún sistema para –una vez
definido el contrato de una manera rigurosa– comprobarlo automáticamente.
Estos sistemas no proliferan, y son específicos para cada plataforma de desarro-
llo concreta. Para el caso de Java, existen distintos sistemas de este tipo; y para
esta asignatura se ha desarrollado uno que se comenta más adelante, y con más Podéis ver el subapartado 2.4.1 de
este módulo.
detenimiento en los recursos electrónicos de la asignatura.

Ejemplo

En la especificación del TAD Nat vista en el subapartado 1.2, se establece que la precondición
de la operació pred es que el valor del número natural representado sea mayor que 0: “X > 0”;
y la poscondición, que el valor del número natural representado sea “X – 1”. El programa
PruebaErrorNat que hemos visto al comienzo del apartado 2 llama al método pred con
© FUOC • PID_00161994 • Módulo 1 28 Tipos abstractos de datos

un natural que tiene valor 0. Por tanto, PruebaErrorNat está incumpliendo su parte del con-
trato. Este incumplimiento hace que ninguna de las implementaciones de Nat esté obligada
a garantizar que la poscondición de pred se cumpla.

Por tanto, el resultado –1 obtenido si se utiliza NatiImpl como implementación de Nat, y


el lanzamiento de la excepción ArrayIndexOutOfBoundsException si se utiliza NatbImpl se de-
ben a un incumplimiento del contrato por parte de PruebaErrorNat.

2.3. Invariante de la representación

Una implementación de un TAD utilizará un conjunto de atributos para repre- Podéis ver NatbImpl en el
subapartado 1.3.2 de este módulo.
sentar el valor concreto que toma la instancia del TAD. A este conjunto de atri-
butos lo denominaremos representación del TAD. Así, en el caso de NatbImpl, la
representación consta de un único atributo, que es un vector de booleano. Observación

Por comodidad en la explica-


ción, utilizaremos el término
Como es lógico, para cada uno de los valores posibles del TAD, tendremos (co- valor de la representación para
mo mínimo) un valor de la representación. En el caso de NatbImpl, tenemos referirnos a una combinación
de valores para todos los atri-
que los valores del TAD Nat –que son cada uno de los números naturales (0, 1, butos de la representación.

2, 3, ...)– son los de la representación de NatbImpl que los representa. Así, por
ejemplo, el natural 5 se puede representar de la siguiente manera:

Ahora bien, a menudo nos encontraremos con casos en los que representacio-
nes diferentes representan el mismo valor del TAD. Observad, por ejemplo, los
dos vectores siguientes. Ambos son representaciones diferentes. En cambio,
ambos representan al natural 5.

Adicionalmente, también hay situaciones en las que las representaciones no Podéis ver el TAD Nat en el
subapartado 1.2.3 de este módulo.
equivalen a ningún valor del TAD. Por ejemplo, fijémonos ahora en la otra im-
plementación del TAD Nat que hemos visto: NatiImpl. En esta implementación,
la representación está constituida por el atributo entero nat. La representación
nat = –3 es una representación posible, ya que nat es un int y, como tal, puede
tomar valores negativos. En cambio, la mencionada representación no se corres-
ponde con ningún valor del TAD Nat.

Así pues, en general, únicamente un subconjunto de todas las representacio-


nes posibles tendrán un equivalente como valor del TAD. Lógicamente, éstas
serán las únicas representaciones del TAD que consideraremos válidas.

El invariante de la representación es una propiedad que definimos con cada


implementación de un TAD, y que es cierta para todos los valores válidos de
© FUOC • PID_00161994 • Módulo 1 29 Tipos abstractos de datos

la representación, y falsa para aquellos valores de la representación que no lo


son. Esta propiedad define todo aquello que tienen en común los estados vá-
lidos de la representación.

Por ejemplo, en el caso de NatiImpl, el invariante de la representación se corres-


ponde con la propiedad nat 0. En cambio, en el caso de NatbImpl, todas las re-
presentaciones se corresponden con valores del TAD y, por tanto, el invariante
de la representación es ‘cierto’. Dependiendo de la implementación y del TAD,
el invariante de la representación puede llegar a ser realmente complejo.

El invariante de la representación es una noción especialmente útil para propor- En estos materiales, no trabajaremos
más este tema. Sí que os lo
encontraréis, pero de manera implícita,
cionar la especificación (o contrato) de un TAD para una implementación concre- en las especificaciones de las
implementaciones de la biblioteca
ta. Para cada una de las operaciones del TAD, el invariante de la representación se de colecciones de la asignatura.

ha de cumplir tanto al principio como al final de la ejecución de la operación. Es


decir, las operaciones siempre parten de una representación válida (que represen-
ta un valor del TAD), que convierten en otra representación válida. Por tanto, una
vez definido el invariante de la representación, éste formará parte de la precondi-
ción y de la poscondición de todas la operaciones.

2.4. Contratos en Java

La mayoría de cursos y libros de texto existentes en el área de los TAD y las estruc-
turas de datos no establecen una vinculación directa entre el marco conceptual
que acabamos de definir y la parte más práctica, correspondiente al desarrollo y
uso de implementaciones de TAD. Es decir, no suele haber ningún vínculo entre
la especificación formal del comportamiento de un TAD y su implementación.

En esta asignatura, adoptamos un punto de vista ligeramente diferente. Ha-


biendo presentado el marco conceptual sin utilizar ningún formalismo mate-
mático, no nos interesa trabajar la especificación formal de los TAD; sí, en
cambio, pretendemos establecer una vinculación directa entre la implementa-
ción y la especificación de un TAD en el lenguaje de programación.

Así pues, todas las implementaciones de TAD que introducimos en esta asig-
Precondiciones
natura irán acompañadas de su contrato. Definiremos el contrato de una im- y poscondiciones en Java
plementación de la siguiente manera: A pesar de que en Java repre-
sentamos las precondiciones y
poscondiciones como expre-
• Expresaremos la precondición y la poscondición de cada uno de los métodos siones booleanas, debéis tener
presente que no forman parte
de la clase, así como el invariante de su representación mediante expresiones del algoritmo y que no tienen
ningún efecto en él.
booleanas, utilizando una extensión de la misma sintaxis proporcionada por
el lenguaje Java (más adelante veremos en qué consiste esta extensión).

Este modo de definir el contrato no es


estándar de Java.
• Definiremos la precondición y la poscondición de un método en su docu- Es una extensión especialmente definida
para esta asignatura.
mentación Javadoc mediante unas etiquetas especiales @pre y @post.
© FUOC • PID_00161994 • Módulo 1 30 Tipos abstractos de datos

• Del mismo modo, el invariante de la representación estará incluido en el Java-


doc de la clase mediante una etiqueta especial denominada @inv.

Observad que en los párrafos anteriores hemos hablado de contratos para las
implementaciones de los TAD, y no para los TAD en sí mismos, representados
en Java mediante interfaces. Siempre que podamos, intentaremos proporcio-
nar el contrato de manera genérica para la interfaz que representa al TAD. No
obstante, esto no siempre será posible y, en ocasiones, estaremos limitados a
proporcionar únicamente contratos para las implementaciones.

El motivo por el que no siempre podremos proporcionar un contrato para las


interfaces es que en Java no podemos proporcionar una traducción directa de
la especificación algebraica. Por tanto, muchas veces será necesario acceder al
estado (contenido) de la instancia del TAD para construir la precondición o la
poscondición.

No siempre será posible acceder a este estado desde la misma interfaz; depende-
rá de si ésta ofrece las operaciones adecuadas de consulta (algo que no siempre
será así). En cambio, hacerlo desde la implementación concreta misma de un
TAD siempre será posible, ya que los atributos que definen este estado están de-
finidos en la misma implementación (y claramente tenemos acceso a ellos).

Veamos cómo podríamos proporcionar un contrato en el caso del TAD Nat.


En este caso, sí que podemos proporcionar el contrato por lo que se refiere a
la interfaz. Así pues, vemos cómo queda la interfaz Nat:

Nat.java

package uoc.ei.ejemplos.introduccion;

public interface Nat {

/** Este método incrementa el valor del natural.


* @pre true
* @post consultar() == $old(consultar())+1 */
void suc();

/** Este método decrementa el valor del natural.


* @pre consultar()>0
* @post consultar() == $old(consultar())-1 */
void pred();

/** Este método suma un natural al objeto.


* @pre true
* @post consultar() == $old(consultar())+y.consultar()
* @param y El natural a sumar. */
void sumarCantidad(Nat y);
© FUOC • PID_00161994 • Módulo 1 31 Tipos abstractos de datos

/** Método consultor que devuelve el valor del natural como un entero.
* @post $return == consultar() && consultar() == $old(consultar())
* @return el valor del objeto. */
int consultar();
}

Como podemos ver, para cada método de la interfaz Nat definimos la precon-
El método consultar
dición y poscondición mediante las etiquetas @pre y @post. Los valores aso-
Observad que para el caso de
ciados a estas etiquetas son expresiones booleanas de Java con algún elemento la interfaz Nat es posible pro-
porcionar un contrato para la
adicional. Veamos cuáles son estos elementos adicionales: interfaz, ya que disponemos
del método consultar, que nos
permite consultar el ”estado”
• $old(expresión). Permite hacer referencia al valor de la expresión justo antes del natural (de hecho su valor).
Si el TAD no dispusiera de este
de ejecutar el método. La expresión se puede corresponder con toda la re- método, sólo podríamos defi-
presentación del TAD (en este caso, encontraríamos $old(this)), con un atri- nir los contratos en cada una
de sus implementaciones.
buto del TAD (sólo cuando se trata de una implementación, en el ejemplo
anterior este uso no es posible porque una interfaz no tiene atributos) o,
incluso, con cualquier expresión evaluable antes de ejecutar el método (por
ejemplo, $old(consultar()).

• $return. Permite referirse al valor de retorno del método.

• $all(i:tabla,expr-bool). Es una expresión booleana con tres parámetros:

– i: cualquier nombre, se utilizará dentro de expr-bool. Los elementos $all


y $exists...
– tabla: hace referencia a un atributo de tipo tabla; es decir, definido en el ... son la traducción directa de
los cuantificadores universal y
TAD como tipo[].
existencial de la lógica de pri-
mer orden. Resultan útiles para
expresar condiciones sobre
– expr-bool: es una expresión booleana que se verificará para cada elemento conjuntos de elementos. Por
de tabla. Dentro de expr-bool podemos utilizar tanto tabla como i para refe- eso, los necesitaremos para ex-
presar los contratos. Encontra-
rirnos a cada uno de los elementos. réis ejemplos de uso de éstos
en los recursos electrónicos
de la asignatura.
El resultado de la expresión $all será ‘cierto’ si expr-bool se cumple para to-
dos los elementos de tabla y ‘falso’ en caso contrario.

• $exists(i:tabla,expr-bool). Es una expresión booleana con los tres mis-


mos parámetros que $all. El resultado de la expresió $exists será ‘cierto’
si expr-bool se cumple como mínimo para uno de los elementos de la ta-
bla, y ‘falso’ en caso contrario. La precondición
y la poscondición

Repasemos algunas de las precondiciones y poscondiciones para entender Normalmente, en la precondi-


ción se definen un conjunto de
bien cómo funciona la definición de contrato. Fijémonos, por ejemplo, en el condiciones que se deben
cumplir para el correcto funci-
método pred. La precondición es consultar() > 0. Eso signfica que la precondi- onamiento del método; y en la
ción se cumplirá cuando la llamada al método consultar devuelva un valor más poscondición se describe cuál
es el resultado de la ejecución
grande que 0. Es decir, si un algoritmo llama al método pred para un Nat que del método en función del es-
tado de la instancia del TAD.
tiene valor 0, estará incumpliendo su parte del contrato. a
© FUOC • PID_00161994 • Módulo 1 32 Tipos abstractos de datos

En cuanto a la poscondición de pred, nos dice cuál es el estado de la instancia de


Nat (su valor en este caso) según su estado anterior. Nos dice que el valor del
natural (denotado por consultar()) es igual al valor del natural antes de ejecutar
el método menos 1. Fijaos cómo para hacer la referencia al valor del natural an-
tes de ejecutar el método se utiliza $old de la siguiente manera: $old(consultar()).

Revisemos ahora la precondición y la poscondición del método suc. La pos-


condición de este método es muy parecida a la poscondición de pred, y nos
dice que el nuevo valor del natural después de ejecutar el método suc es igual
al valor antiguo más 1.

En cambio, su precondición es una expresión muy simple: true. ¿Qué significa


eso? Simplemente, que sea cual sea el estado de la instancia de Nat y sean los
que sean los parámetros de suc (en este caso no tenemos parámetros), la pre-
condición siempre se cumplirá. Es decir, el contrato de Nat no impone ningu-
na condición a los usuarios del TAD Nat, que siempre podrán llamar al método
suc sin necesidad de garantizar nada. a
Como hemos visto, la sintaxis de las precondiciones y de las poscondiciones se
corresponde con expresiones booleanas Java donde podemos utilizar algunos
elementos adicionales. Como en cualquier expresión booleana Java, podemos
realizar llamadas a métodos. Ahora bien, como la evaluación de precondición y
poscondición no forma parte, de hecho, del algoritmo o programa que se está
definiendo, no tiene sentido llamar a métodos que modifiquen el estado del
TAD.

Así pues, en las precondiciones y poscondiciones se permite llamar a


métodos, pero sólo a aquellos que únicamente calculen o devuelvan un
valor, y que en ningún caso modifiquen los objetos o atributos que ac-
cedan a él.

A continuación, tenéis un programa que ejemplifica una ejecución en la que


veréis en qué momento se comprueban la precondición y la poscondición.

public void static main(String[] args) {


(1) Nat n = new Natilmpl();
(2)comprobamos post del constructor
(3)comprobamos pre de suc
(4) n.succ();
(5)post de suc
(6)pre de pred
(7) n.pred();
(8)post de pred
(9) System.out.printin(n);
}
© FUOC • PID_00161994 • Módulo 1 33 Tipos abstractos de datos

2.4.1. Herramientas de diseño por contrato

Ha quedado claro que –como en cualquier otro contrato– el incumplimiento Podéis ver las repercusiones del
incumplimiento del contrato en el
subapartado 2.2.1 de este módulo.
del contrato por parte de los usuarios de un TAD exime al TAD de cumplir su
parte del contrato. Sin embargo, en el proceso de desarrollo de una aplicación,
es normal que se cometan errores. Por lo tanto, puede ocurrir que la aplicación
usuaria de un TAD incumpla su parte de contrato en alguna de las llamadas a
métodos de este TAD. Por motivos obvios, nos interesará detectar esta situa-
ción y localizar el error lo antes posible.

La programación por contrato exime al TAD de detectar el incumplimiento


por parte de los usuarios. Por este motivo, al no detectar la situación anómala
de entrada, se puede producir un error a partir del cual será más difícil detectar
el código fuente responsabe de esta situación .

Tal y como también hemos comentado, en una situación ideal querríamos te- En los recursos electrónicos
de la asignatura, encontraréis
documentación sobre el sistema de
ner la capacidad de detectar los incumplimientos de contrato sin haber de DBC que emplearemos en este curso.

“ensuciar” el código con comprobaciones de programación defensiva que no


forman realmente parte del algoritmo, sino del contrato mismo. Para conse-
guirlo, utilizaremos un sistema de DBC para Java elaborado especialmente
para esta asignatura.

Con esta herramienta podemos:

• Especificar el contrato de una clase utilizando las etiquetas @pre, @post y


@inv. Es aconsejable hacerlo para cualquier clase que desarrollemos, sobre
todo si forma parte de una biblioteca que será utilizada por otros módulos
o aplicaciones.

• A partir de un conjunto de clases para las cuales se ha especificado el con-


trato, generar una biblioteca (en formato .jar) que contenga este conjunto
de clases y que compruebe de manera automática los contratos definitivos.

La biblioteca jar generada con la herramienta de DBC la podemos emplear


como cualquier otro jar. De esta manera, cuando estemos desarrollando una
aplicación que hace uso de una biblioteca que tiene contratos definidos, po-
demos utilizar el jar generado con la herramienta de DBC. Eso hará que pro-
bemos las funcionalidades que vamos desarrollando, se comprueben los
contratos definitivos en la biblioteca y se pueda detectar rápidamente si hay
alguno que no se cumple.

Una vez se haya acabado la fase de desarrollo, si queremos, podemos sustituir Encontraréis una explicación más
detallada sobre el funcionamiento
de la herramienta de DBC en los
el fichero jar generado por la herramienta de DBC por otro jar que podemos recursos electrónicos del aula.

generar con las herramietnas estándar proporcionadas por el entorno Java y


que no comprobará ningún contrato.
© FUOC • PID_00161994 • Módulo 1 34 Tipos abstractos de datos

Debéis tener en cuenta, sin embargo, que no será necesario que utilicéis esta
herramienta en este curso; ya que, en principio, vosotros no deberéis desarro-
llar ninguna biblioteca. A lo largo de todo el curso, trabajaréis con la biblioteca
de TAD de la asignatura, de la que tendréis dos versiones diferentes (es decir,
dos ficheros jar): una generada con la herramienta de DBC, que hará las com-
probaciones de los contratos, y otra generada con las herramientas estándar
del entorno Java, que no comprobará ningún contrato.
© FUOC • PID_00161994 • Módulo 1 35 Tipos abstractos de datos

3. Desarrollo de una colección de ejemplo

Veamos un primer ejemplo de colección. El objetivo principal de este apartado


no es el estudio de esta colección concreta de ejemplo, sino la presentación
mediante un ejemplo práctico de los diferentes elementos que aparecen en el
desarrollo y uso de una colección. La colección que definiremos y empleare-
mos de ejemplo en este apartado es la de los conjuntos.

Un conjunto es un grupo de elementos no ordenados y sin repetición. Repre-


sentaremos un conjunto enumerando sus elementos, separándolos por comas
y poniendo la mencionada enumeración entre llaves. Así, por ejemplo, repre-
sentaremos un conjunto con los números naturales 1, 5 y 8 como: {1, 5, 8}.

Decimos que los elementos del conjunto no están ordenados porque no nos Elección de colecciones
importa el orden en el que se hayan añadido, ni tampoco cualquier otro orden
Cuando conozcáis más tipos
que podamos definir entre los elementos del conjunto (por ejemplo, en el caso de colecciones y estéis en
proceso de resolución de un
de los números naturales, el orden ‘<’ dentro de los mismos números natura- problema, un paso muy impor-
tante de este proceso será ele-
les). Con eso queremos decir, por ejemplo, que {1, 5, 8} y {8, 1, 5} son en rea- gir las colecciones que nos
lidad el mismo conjunto. permitan representar de mane-
ra adecuada el dominio del
problema. Dado que en un
Por otro lado, decimos que un conjunto es un grupo de elementos sin repeti- conjunto no se establece nin-
gún orden entre los elementos
ción, ya que los elementos sólo aparecen en él una vez. Es decir, a partir de un que lo forman, la colección
Conjunto no nos servirá para
elemento e y de un conjunto C, sólo existen dos situaciones posibles: o bien e representar, por ejemplo, una
cola de personas que están es-
pertenece a C o bien no pertenece a éste. perando para ser atendidas en
una gestoria. En cambio, sí que
nos servirá para representar el
conjunto de gestiones que se
pueden realizar en la gestoría.
3.1. Operaciones

Una vez que tenemos conceptualmente claro en qué consiste y cuáles son las
propiedades de la colección que hemos elegido de ejemplo, veremos cuáles son
las operaciones que nos interesará proporcionar sobre la colección Conjunto:
Representación
de colecciones
colección Conjunto es Ésta es la forma en la que pre-
sentaremos habitualmente
cada una de las colecciones
constructor() que vayan apareciendo a lo lar-
go de los módulos. Para cada
Crea un conjunto vacío. operación que se pueda efec-
tuar sobre la colección tene-
@pre cierto.
mos: su signatura en negrita,
@post Devuelve un conjunto sin ningún elemento ($this es el conjunto una breve explicación, y la pre-
condición y poscondición, que
vacío). estarán precedidas de las eti-
quetas @pre y @post. En el
caso de las colecciones (que
se corresponderán a interfaces
void insertar(Object elem) Java), la precondición y la
poscondición se expresarán
Añade un elemento al conjunto. Si el elemento ya existe, no hace nada. habitualmente de manera
@pre cierto. informal.

@post elem pertenece al conjunto ($this.esta(elem)).


© FUOC • PID_00161994 • Módulo 1 36 Tipos abstractos de datos

boolean esta(Object elem)


Comprueba si un objeto pertenece al conjunto.
@pre ‘cierto’.
@post El valor vuelve ($return) es ‘cierto’ si elem pertenece al conjunto
($this). Y ‘falso’ en caso contrario.

Object borrar(Object elem)


Borra un elemento del conjunto. Si el elemento no está en éste, no hace
nada.
@pre ‘cierto’.
@post elem no pertenece al conjunto (!$this.esta(elem)). El valor devuelto
($return) es el elemento borrado, o ‘nulo’ si el elemento no estaba en él.

Las operaciones previas corresponden a las operaciones más elementales que Encontraréis la interfaz Java
correspondiente a esta colección
en los ejemplos disponibles en
podemos realizar sobre los conjuntos. Aunque las colecciones como tales no tie- los recursos electrónicos
de la asignatura (interfaz Conjunto
nen constructor, normalmente les añadiremos operaciones denominadas cons- en el paquete uoc.ei.ejemplos.modulo1.
Conjunto).

tructor, de manera que las diferentes maneras de construir la colección se


identifiquen fácilmente. Tened en cuenta, sin embargo, que estas operaciones
no proceden de ningún método de la interfaz Java correspondiente, sino direc-
tamente de los métodos constructores de las implementaciones de la interfaz.

La versión de los conjuntos que proporcionamos en este apartado nos permite En el apartado “Ejercicios de
autoevaluación” del módulo,
crear un conjunto vacío, añadirle y borrarle elementos, y consultar si un obje- encontraréis material adicional
relacionado con la ampliación de las
funcionalidades de los conjuntos
to concreto le pertenece. Sería interesante disponer de operaciones adiciona- presentados en este apartado.

les. Por ejemplo, poder unir dos conjuntos, saber cuántos elementos tiene, o
poder recorrer los elementos. Por otro lado, en otro módulo, se estudia una
versión mucho más completa de los conjuntos, también con implementacio- En el módulo “El TAD Tabla”, de
nes más eficientes de las que veremos a continuación. a esta asignatura, se estudia una
versión mucho más completa de los
conjuntos.

3.2. Implementación mediante un vector

Una vez vistas las operaciones, y definido de manera clara su comportamien-


to, pasemos a ver cómo podemos proporcionar una implementación, hemos
de definir una clase con los elementos siguientes:

1) Un conjunto de atributos que nos permitan representar cualquier conjunto.


Denominaremos este conjuto de atributos representación de la colección o TAD.

2) Una vez tengamos definida la representación, debemos proporcionarle im-


plementaciones para las operaciones de la colección que satisfagan el contrato
impuesto por la colección misma (etiquetas @pre y @post).

3.2.1. Definición de la representación

Vayamos pues al primer paso: elegir una representación que nos permita re- La eficiencia se estudia en el módulo
“Complejidad algorítmica” de esta
asignatura.
presentar los conjuntos. Normalmente, elegiremos la representación en la que
© FUOC • PID_00161994 • Módulo 1 37 Tipos abstractos de datos

la realización de cada una de las operaciones sea la más eficiente. De momen-


to, dejaremos de lado el tema de la eficiencia, y en este subapartado propor-
cionaremos una representación en la que dominará la sencillez.

Un modo sencillo de representar un conjunto de elementos es mediante dos


atributos: un vector de tipo Object donde guardaremos los elementos y un atri-
buto entero (int) que utilizaremos para guardar el número de elementos del
conjunto. Así, por ejemplo, podremos representar el conjunto {1, 5, 8, 2, 7}
mediante los atributos especificados de la siguiente manera:

Medida de los vectores


en Java

Recordad que en el lenguaje


Java el tamaño de los vectores
es accesible a partir del mismo
vector (en el ejemplo sería
necesario acceder a ele-
En las representaciones en las que aparezcan vectores, normalmente, encon- ments.length). Por lo tanto,
en el caso concreto de Java, no
traremos un trozo del vector que contendrá los elementos que guardamos (pa- hace falta tener un atributo
especial para guardar el ta-
ra abreviar, lo denominaremos trozo lleno), y un trozo del vector libre que maño del vector. Otros lengua-
emplearemos cuando necesitemos almacenar un número superior de elemen- jes como C requerirían un
atributo adicional.
tos. En el ejemplo, el trozo completo consta de las cinco primeras posiciones
del vector, y el resto está libre. La variable numeroElementosActual indica la me-
dida del trozo lleno.

3.2.2. Contenedores acotados

Las representaciones que utiliza un vector para almacenar los elementos que
constituyen una colección tienen una particularidad: en el momento de la
creación de la colección, es necesario establecer el número máximo de ele-
mentos. El motivo está muy claro: necesitamos definir el tamaño del vector
en el momento de la creación de la colección, y el tamaño es el que determi-
nará, en principio, el número de elementos que podremos guardar como
máximo en esta colección .

Eso tiene varias implicaciones:

• En primer lugar, será necesario modificar ligeramente el contrato de la co-


lección para tener en cuenta este máximo.

• Como consecuencia de esto, sólo podremos utilizar una representación


que utilice vectores para aquellos casos en los que conozcamos el número
máximo de elementos.

En otro módulo estudiaremos alternativas a la representación con vectores En el módulo “Contenedores


secuenciales” de esta asignatura se
estudian algunas alternativas
que esquivan estas implicaciones. Estas representaciones alternativas tienen a a la representación con vectores.

menudo un coste adicional tanto en la programación como en el espacio de


memoria ocupado. Por ello, a veces interesará utilizar una representación con
© FUOC • PID_00161994 • Módulo 1 38 Tipos abstractos de datos

vectores. Estudiaremos también una técnica que permite redefinir el número


máximo de elementos de una representación con vectores con posterioridad
al momento de la creación.

De momento, trabajaremos con este máximo. Con el fin de establecer de una


manera clara y limpia el nuevo contrato, que tiene en cuenta el máximo de
elementos, definiremos una nueva colección que adaptará este contrato para
la operación añadir y añadirá una operación nueva denominada estaLleno. Ha-
remos eso de manera específica para la colección Conjunto:

colección ConjuntoAcotado extiende Conjunto es Una sola colección general

Para una biblioteca de colec-


void insertar(Object elem) ciones como la de nuestra
@pre o bien ‘elem’ ya está en el conjunto o bien hay espacio para añadir un asignatura (podéis ver el apar-
tado 5), será más conveniente
nuevo elemento. definir una colección general
única, que denominaremos
ContenedorAcotado. Esta
colección general será imple-
boolean estaLleno() mentada por todas las
Comprueba si el conjunto está lleno. implementaciones que
proporcionan una representa-
@pre ‘cierto’. ción nivelada.
@post el valor devuelto ($return) es ‘cierto’ si el número de elementos del
conjunto es el máximo posible, y ‘falso’ en el caso contrario.

Por convención, marcamos la signatura de la operación añadir en cursiva


para indicar que estamos redefiniendo el contrato. Si redefinimos el contra-
to, únicamente especificaremos los elementos que cambian (en este caso, la
precondición).

3.2.3. Implementación de las operaciones

Una vez aclaradas las implicaciones de utilizar una representación con vecto-
res, veremos cómo se lleva a cabo la implementación de las operaciones a par-
tir de la representación elegida. En primer lugar, mostremos un esquema
algorítmico de cada una de las operaciones:

colección ConjuntoVectorImpl implementa ConjuntoAcotado

• constructor()
Operación de insertar
– Crea el vector de elementos.
– Asigna el número de elementos actual a 0. Observad que en la operación
de insertar, es el usuario quien
ha de garantizar que el conjun-
• void insertar(Object elem) to tiene espacio suficiente para
añadir el elemento. Por tanto,
– Asigna ‘elem’ a la primera posición libre del vector de elementos. la implementación no necesita
realizar ninguna comproba-
– Incrementa el número de elementos actual. ción.

• boolean esta(Object elem)


– Busca ‘elem’ en el trozo del vector de elementos lleno.
– Devuelve ‘cierto’ si ‘elem’ se ha encontrado, y ‘falso’ en caso contrario.
© FUOC • PID_00161994 • Módulo 1 39 Tipos abstractos de datos

• Object borrar(Object elem)


Reciclaje de la memoria
– Busca ‘elem’ en el trozo del vector de elementos ocupado.
Con vistas al correcto reciclaje
– Si se ha encontrado de la memoria (garbage
i. Si ‘elem’ no es el último elemento: collection), es muy importante
desasignar los objetos que ya
toma el último elemento del trozo ocupado del vector y lo pone en la no necesitamos guardar, y so-
breescribir en ellos la referencia
posición de ‘elem’. con ‘null’, tal como se hace en
ii. Asigna ‘null’ a la última posición ocupada del vector. el paso (ii). En el módulo “Con-
tenedores secuenciales”, se ex-
iii. Decrementa el número de elementos actual. plica el reciclaje de memoria en
Java, y allá se entenderá la im-
iv. Devuelve el ‘elem’ encontrado al vector. portancia de esta acción y el
motivo.
– Si no se ha encontrado, devuelve ‘null’.

Una vez visto el esquema de implementación, veremos cómo se traduce esto


En el aula de la asignatura
al lenguaje Java. A continuación tenéis las partes más representativas de la disponéis de los ficheros Java
correspondientes a los ejemplos
de este apartado. En el paquete
clase ConjuntoVectorImpl: edu.uoc.ei.ejemplos.modulo1
encontraréis las interfaces Conjunto
y ConjuntoAcotado, la clase
ConjuntoVectorImpl, y un programa
de ejemplo (PruebaConjunto).
ConjuntoVectorImpl.java

package uoc.ei.ejemplos.modulo1;

public class ConjuntoVectorImpl implements ConjuntoAcotado {

/** Tabla que guarda los elementos del conjunto. */


private Object[] elementos;

/** Número de elementos que el conjunto contiene actualmente.*/


private int numElemsActual;

public ConjuntoVectorImpl(int n) {
elementos = new Object[n];
numElemsActual = 0;
}
...
public Object borrar(Object elem) {
Object elementoBorrado = null;
int posicion = buscarPosicionElemento(elem);
if (posicion! = -1) {
elementoBorrado = elementos[posicion];
numElemsActual--;
if (posicion<numElemsActual)
elementos[posicion] = elementos[numElemsActual];
// importante para la recogida de basura
elementos[numElemsActual] = null;
}
return elementoBorrado;
}
© FUOC • PID_00161994 • Módulo 1 40 Tipos abstractos de datos

/** Método privado que busca la posición de un elemento dentro de la tabla


* en la que se guardan todos los elementos que se han añadido
* al conjunto. */
protected int buscarPosicionElemento(Object elem) {
boolean encontrado = false;
int i = 0;
while (i<numElemsActual && !encontrado) {
encontrado = elementos[i].equals(elem);
if (!encontrado) i++;
}
return encontrado ? i : -1;
}
...
}

En el fragmento de código inmediatamente anterior se pueden apreciar los si-


guientes aspectos:

• La definición de los dos atributos que conforman la representación de la


colección (el vector elementos y el entero numElemsActual).

• El constructor.

• El método borrar, que corresponde al seudocódigo que hemos presenta-


do anteriormente.

• Un método privado de la clase denominado buscarPosicionElem que busca la Observad cómo


buscarPosicionElemento proviene de
una esmerada aplicación de la técnica
posición de un elemento en el trozo ocupado del vector elementos, y devuelve del diseño descendiente, aprendida
en las asignaturas Fundamentos
su posición o –1 si el elemento no está en él. Este método es utilizado por dis- de programación y Prácticas de
programación. No hay que olvidar las
técnicas aprendidas en esta asignatura,
tintos métodos públicos de la colección (esta, insertar y borrar), que necesitan ni en Diseño y programación orientada
a objetos.
buscar elementos dentro del trozo lleno del vector de elementos.

La explicación que daremos a lo largo de los distintos módulos de esta asigna- Los conceptos básicos para analizar
la eficiencia de un algoritmo se
presentan en el módulo “Complejidad
tura sobre la implementación de las operaciones de una colección irá siempre algorítmica” de esta asignatura.

acompañada de un análisis de la eficiencia de las operaciones. De momento,


en este módulo introductorio no hablaremos de este tema. a

3.2.4. Uso de la colección

A continuación tenéis un fragmento de un programa que utiliza ConjuntoVec-


torImpl. Este programa muestra el uso de la colección. Permite crear conjuntos
de hasta 10 elementos añadiéndolos uno a uno. Una vez tenemos el conjunto,
los muestra por la salida estándar.
© FUOC • PID_00161994 • Módulo 1 41 Tipos abstractos de datos

PruebaConjunto.java

package uoc.ei.ejemplos.modulo1;
...
public class PruebaConjunto {
...
public static void main(String[] args) {
boolean salir = false;
ConjuntoAcotado cjt = new ConjuntoVectorImpl(10);
while (!salir) {
try {
String elem = readLine("Introduce un núm (o \"fin\"): ",System.in);
salir = elem.equalsIgnoreCase("fin");
cjt.insertar(new Integer(Integer.parseInt(elem)));
if (cjt.estaLleno()) {
System.out.println("El conjunto está lleno. No se pueden añadir más
elementos.");
salir = true;
}
} catch (NumberFormatException e) {
System.out.println("El texto introducido no es un número vuélvelo a
probar.");
} catch (IOException e) {
System.out.println("Problema de E/S, saliendo");
salir = true;
}
}
System.out.println("El conjunto resultante es: "+cjt);
}
}

Como podéis apreciar, en primer lugar se crea la colección, y después se le aña- Como siempre, encontraréis
el programa completo como
recurso electrónico en el aula.
den elementos uno a uno hasta que o bien el conjunto está lleno, o bien el
usuario decide no llenarlo más (y entra el valor especial ‘fin’).

Se trata de un programa sencillo que no tiene más interés que mostrar el


uso de la colección que nos ha ocupado en este apartado. Hay que remarcar
algunos aspectos de la programación que es importante respetar cuando
utilicemos una colección o TAD:

• Para la creación de la colección es imprescindible hacer referencia a la im- La mayoría de estos aspectos
ya han sido comentados con
anterioridad. Sin embargo, dada su
plementación de la colección (ConjuntoVectorImpl). Ahora bien, una vez importancia, no está de más recordarlos
una vez ya tenemos una visión más clara
creada, es una buena práctica de programación hacer referencia única- de lo que es un TAD y de su uso.

mente a la interfaz, siempre que eso sea posible. En el programa de ejem-


plo, la variable cjt es de tipo ConjuntoAcotado. Por tanto, en el programa
siempre hacemos referencia a esta interfaz.
© FUOC • PID_00161994 • Módulo 1 42 Tipos abstractos de datos

• Recordad que el programa usuario de un TAD es responsable de garantizar


Podéis ver las herramientas de
que, cuando llama una operación, se cumpla la precondición. En este caso, diseño por contrato en el
subapartado 2.4.1 de este módulo.
PruebaConjunto es responsable de garantizar las precondiciones de los mé-
todos que ha llamado. Por tanto, nuestro programa de prueba es responsa-
ble de garantizar que, cuando se llama al método insertar, los elementos
añadidos quepan en él. Esto, en este caso, se hace con la instrucción if pos-
terior a la llamada insertar, que comprueba si el conjunto está lleno y, si lo Razonamiento
está, ya no deja seguir añadiendo elementos. Fijaos también en que es ne- para el lector

cesario utilizar la interfaz ConjuntoAcotado, porque la interfaz Conjunto no Queda como razonamiento
para el lector deducir cómo, con
dispone del método estaLleno y, en consecuencia, no nos permitiría realizar las esta instrucción if posterior a la
comprobaciones necesarias para garantizar la precondición del método in- llamada a insertar, se garantiza
que el método insertar no se lla-
sertar. mará nunca cuando el conjunto
esté lleno.

• Para acabar –y esto no se refiere al uso de un TAD en concreto, sino al len-


guaje Java en general–, recordad que mientras un método se está ejecutan-
do se puede producir una situación de error que normalmente provoca el
lanzamiento de una excepción. Es importante capturar estas excepciones en
el contexto adecuado, en el cual sabemos cómo tratar la situación excepcional.
Ved cómo en PruebaConjunto se capturan dos situaciones excepcionales. Una
de ellas, correspondiente a la excepción IOException, se puede producir al leer
una línea de la entrada estándar. En este caso, si detectamos un error de este
tipo, decidimos acabar el programa. La otra situación excepcional se produce
al convertir una cadena de texto (String) en un número entero, en la llamada
al método Integer.parseInt. En este caso, se debe a un error producido por el
usuario cuando teclea. Por tanto, el tratamiento propuesto es escribir un men-
saje explicativo y volver a pedir el entero.
© FUOC • PID_00161994 • Módulo 1 43 Tipos abstractos de datos

4. Tipos genéricos o paramétricos

En el apartado anterior hemos desarrollado un ejemplo de colección. Fijaos en


que hay un tema que hemos obviado hasta este momento: ¿qué tipo de ele-
mentos guarda la colección? En el apartado anterior hemos asumido de ma-
nera implícita que los elementos de las colecciones eran de tipo Object, tal
como se puede comprobar revisando las signaturas de los métodos de los TAD
vistos como ejemplos hasta ahora.

Esto significa que podremos utilizar como elementos de los conjuntos del aparta- Como ya sabéis de Diseño y
programación orientada a objetos, el
do anterior cualquier objeto que sea instancia de Object o de una subclase de ésta. tipo Object es el tipo más general en Java.
Todo el resto de tipos de Java heredan
directa o indirectamente del tipo Object
Es decir, podremos utilizar como elemento cualquier objeto, dejando de lado los (excepto los primitivos, que son int, char,
boolean, float y double).
tipos Java primitivos. Por tanto, los conjuntos del apartado anterior pueden con-
tener instancias de Integer, tal como se muestra en el ejemplo al final del apartado,
pero también podríamos poner en ellos instancias de String, Boolean, o cualquier
clase definida por nosotros mismos. Incluso, nadie nos impide añadir a un con-
junto elementos que sean instancia de Conjunto (y, por tanto, podríamos tener fá-
cilmente conjuntos de conjuntos) tampoco existe ningún impedimento para que
en un mismo conjunto pueda haber elementos de distintas clases. La única res-
tricción impuesta por la firma de Conjunto, y que comprobará el compilador de
Java, es que todos los elementos sean instancia de Object.

A primera vista, ésta puede parecer la situación ideal (porque es la más gene-
ral). Pero en muchas ocasiones –de hecho es lo más habitual– nos interesará
definir conjuntos en los que todos los elementos sean del mismo tipo. En estas
situaciones, sería muy difícil estar seguros de que todos los elementos añadi-
dos a un conjunto son instancias de este tipo concreto.

Esto no se puede hacer con las interfaces y la implementación vistas en el apar- No podríamos aprovechar las
implementaciones existentes
mediante herencia; pero sí utilizando otras
tado anterior, que aceptan elementos del tipo mas general que existe en Java técnicas de la orientación a objetos (podéis
ver el ejercicio de autoevaluación 11).
(Object). Para hacerlo, habría que definir de nuevo las interfaces correspon-
dientes, con una nueva signatura para cada uno de los métodos, y también ha-
bría que proporcionar una nueva implementación. En estas interfaces, habría
que modificar las firmas de los métodos, en las cuales sustituiríamos el tipo
Object de los elementos por el tipo concreto que necesitáramos. En cuanto a la
implementación, como las signaturas de los métodos diferirían de la imple-
mentación del apartado anterior, habría que redefinir las implementaciones
de todos los métodos, sin poder reutilizar ninguna de las existentes.

Los tipos paramétricos proporcionan una solución elegante a este proble-


ma, ya que permiten definir tipos genéricos que tienen como parámetros
otros tipos, que no es necesario concretar en el momento de la definición
del tipo genérico, ya que se concretarán más adelante, en el momento de
la instanciación.
© FUOC • PID_00161994 • Módulo 1 44 Tipos abstractos de datos

En esta asignatura, esta técnica nos permitirá definir colecciones de elementos


sin concretar el tipo de elementos en el momento de proporcionar la imple-
mentación. Será posteriormente, en el momento de utilizar la colección, cuan-
do concretaremos el tipo de los elementos.

El concepto de tipos paramétricos, también denominados genéricos, ya se intro- Los tipos paramétricos se comentan
en el subapartado 6.2 del módulo 3
de la asignatura Diseño y programación
duce en la asignatura Diseño y programación orientada a objetos. En este aparta- orientada a objetos, a pesar de que no
se estudian con detenimiento.
do, veremos cómo utilizar tipos paramétricos en Java.

La utilización de tipos paramétricos no resulta muy compleja. En este aparta- Los tipos paramétricos
do, la presentaremos mediante un ejemplo en el que incorporaremos el uso de en Java

tipos paramétricos a la colección Conjunto del apartado anterior. La incorporación de los tipos
paramétricos en Java se ha rea-
lizado en su versión 1.5, dispo-
nible desde bien avanzado
Definiremos y utilizaremos un tipo paramétrico en dos fases: el 2004.
Otros lenguajes como C++,
Eiffel y Ada también disponen
1) Definición del tipo paramétrico en función de uno o más paráme- de tipos paramétricos.

tros que corresponden a otros tipos que no se concretan en el momento


de la definición.

2) Sustitución de los parámetros por tipos concretos, y conversión del


tipo paramétrico en un tipo “normal”, del cual podremos crear instanci-
as. Normalmente, este paso se realiza en el mismo momento de la
instanciación, pero en algunos casos nos interesará hacerlo en algún otro
momento (por ejemplo, cuando estemos definiendo otro tipo).

Veamos estas dos fases para la colección Conjunto. En primer lugar definimos
la interfaz Conjunto con un parámetro que corresponde al tipo de los elemen-
tos, tal como se muestra a continuación:

Conjunto.java

package uoc.ei.ejemplos.modulo1.genericos;

public interface Conjunto<E> {


public void insertar(E elem);
public boolean esta(E elem);
public E borrar(E elem);
}

Fijaos en los aspectos siguientes:

• Se especifica el parámetro al final del nombre de la interfaz (o clase), y se


delimita mediante los símbolos ‘<’ y ‘>’. Podríamos tener más de un pará-
metro. En este caso, utilizaríamos la misma técnica y separaríamos los pa-
rámetros con comas (por ejemplo, podríamos definir la clase Par<A,B>).
© FUOC • PID_00161994 • Módulo 1 45 Tipos abstractos de datos

• Un vez especificados los parámetros, los podemos utilizar dentro del cuer-
po de la clase o de la interfaz en cualquier momento, como cualquier otro
tipo que tengamos definido. Fijaos cómo las firmas de los métodos de la
interfaz Conjunto son idénticas a las del apartado anterior y han sustitui-
do Object por E.

Además de la interfaz Conjunto, e igual que hemos hecho en el apartado an-


terior, hay que definir la interfaz ConjuntoAcotado (que no mostraremos
Encontraréis la interfaz
ConjuntoAcotado en los recursos
aquí) y la implementación ConjuntoVectorImpl, de la cual tenéis un trozo a electrónicos asociados a la asignatura.

continuación.

ConjuntoVectorImpl.java

package uoc.ei.ejemplos.modulo1.genericos;

public class ConjuntoVectorImpl<E> implements ConjuntoAcotado<E> {

/** Tabla que guarda los elementos del conjunto. */


private E[] elementos;

/** Número de elementos que el conjunto contiene actualmente. */


private int numElemsActual;

public ConjuntoVectorImpl(int n) {
elementos = (E[])new Object[n];
numElemsActual = 0;
}
...
public E borrar(E elem) {
E elementoBorrado = null;
int posicion = buscarPosicionElemento(elem);
if (posicion! = -1) {
elementoBorrado = elementos[posicion];
numElemsActual--;
if (posicion<numeroElementosActual)
elementos[posicion] = elementos[numeroElementosActual];
}
return elementoBorrado;
}
}

Del uso de los tipos paramétricos en esta implementación hay que remarcar
varias cosas:

a) La definición de un tipo paramétrico como una extensión de otro tipo pa-


ramétrico. En este ejemplo definimos ConjuntoVectorImpl<E> como una exten-
sión de ConjuntoAcotado<E>.
© FUOC • PID_00161994 • Módulo 1 46 Tipos abstractos de datos

b) La aparición del parámetro con diferentes funciones. Por una parte, aparece en
la signatura de los métodos, tal como ya habíamos visto en Conjunto, pero tam-
bién en la definición del atributo elementos, o en la variable local del método bo-
rrar denominada elementoBorrado; y también en la operación de casting del
constructor. No está de más volver a recalcar que podemos utilizar el parámetro
(o parámetros) del mismo modo como podríamos utilizar cualquier otro tipo.

c) La forma como se crea el atributo elementos. La manera que podría parecer


más intuitiva para crear un vector de dimensión n de E sería mediante la ins-
trucción new E[n]. Esto no es posible en Java. Por tanto, hemos de recurrir a la
alternativa que se muestra en el código: crear un vector de Object y después
convertir su tipo en un vector de E mediante la operación de casting adecuada.

Para acabar, nos queda ver cómo concretamos el tipo paramétrico Conjun-
to<E> en un tipo que podamos instanciar para trabajar con conjuntos, tal
como hemos hecho al final del apartado anterior. Adaptaremos a continua-
ción el ejemplo del apartado anterior para utilizar los conjuntos paramétricos
definidos en este apartado.

PruebaConjunto.java

package uoc.ei.ejemplos.modulo1.genericos;
...
public class PruebaConjunto {
...
public static void main(String[] args) {
System.out.println("Introduce numeros para añadir al conjunto");
boolean salir = false;
ConjuntoAcotado<Integer> cjt = new ConjuntoVectorImpl<Integer>(10);
while (!salir) {
try {
...
cjt.insertar(Integer.parseInt(elem));
...
}
...
}
}
System.out.println("El conjunto resultante es: "+cjt);
}
}

En el código anterior se observa lo siguiente:

• La sustitución de los parámetros por tipos concretos. Únicamente es ne-


cesario repetir el nombre del tipo y sustituir los nombres de los paráme-
© FUOC • PID_00161994 • Módulo 1 47 Tipos abstractos de datos

tros por los tipos concretos deseados (en este caso Integer). Fijaos en que
hay dos sustituciones diferentes: por un lado, la del tipo de cjt, que es
ConjuntoAcotado<Integer>; y, por otra, tenemos la clase que se instancia,
que es ConjuntoVectorImpl<Integer>.

• Cómo los usos posteriores a la creación del conjunto se realizan exacta-


mente de la misma manera que en el apartado anterior.

La biblioteca de TAD de la asignatura hace un uso extensivo de los tipos para-


métricos. Por tanto, será necesario que tengáis instalada la versión 1.5 de Java
(también denominada Java 5) o superior.
© FUOC • PID_00161994 • Módulo 1 48 Tipos abstractos de datos

5. Biblioteca de colecciones de la asignatura

Como parte muy importante del material docente del curso disponéis de una
biblioteca de colecciones que se ha diseñado e implementado especialmente
para esta asignatura. Esta biblioteca, a la cual también nos referiremos como bi-
blioteca de TAD de la asignatura, contiene:

• Una jerarquía de TAD con todas las colecciones que iréis viendo a lo largo
del curso. Se ha intentado que la jerarquía sea sencilla pero, a la vez, sufi-
cientemente potente y flexible para que resulte efectiva en un amplio ran-
go de situaciones.

• Todas las implementaciones de estos TAD que aparecen en el texto. De


esta manera, podréis examinar el código Java de cada una de las imple-
mentaciones.

La biblioteca os acompañará a lo largo de todo el curso. Por una parte, estudia-


réis la signatura y el comportamiento de diferentes TAD y, a continuación, su
implementación. Encontraréis definidas ambas cosas en la biblioteca, a la cual
podréis recurrir para completar la información suministrada en el material en
papel. Además, en la biblioteca encontraréis la definición de los contratos para
cada una de las implementaciones. Por otra parte, en el curso se enfatiza el uso
de colecciones mediante ejercicios. En esta otra vertiente, actuaréis como usua-
rios de la biblioteca, como cualquier desarrollador Java cuando utiliza las clases
definidas en la Java Collections del JDK.

Seguidamente, se describen la estructura y los elementos principales de la bi-


blioteca de TAD de la asignatura, de modo que cuando hayáis de utilizar o exa-
minar alguna colección concreta, ya tengáis una visión general de éstas.

5.1. Jerarquía de colecciones

Las colecciones de la biblioteca están estructuradas en una jerarquía. La raíz de


la jerarquía es la interfaz Contenedor, que define los métodos básicos que toda
colección proporcionará:

• elementos(): permite aceder a cada uno de los elementos almacenados en la Podéis ver los iteradores
en el módulo “Contenedores
secuenciales” de esta asignatura.
colección mediante una construcción auxiliar denominada Iterador.

• estaVacio(): devuelve ‘cierto’ si la colección está vacía y ‘falso’ en caso con-


trario.

• numeroElems(): devuelve el número de elementos de la colección.


© FUOC • PID_00161994 • Módulo 1 49 Tipos abstractos de datos

Los tipos de contenedor básicos que aparecen a lo largo de los módulos se definen
como interfaces que extienden la interfaz Contenedor directamente. Así, extien-
den directamente la raíz de la jerarquía: ContenedorAcotado, Pila, Cola, Lista, Arbol,
Diccionario y Conjunto. Con el fin de proporcionar una biblioteca sencilla, las úni-
cas interfaces de la biblioteca que representan colecciones serán éstas. Cada una
de estas interfaces proporciona la signatura directamente utilizable de los respec-
tivos TAD (excepto la base, Contenedor y ContenedorAcotado, que define únicamen-
te un aspecto de los contenedores). Podéis ver esta jerarquía en la figura 1.

Esta “minijerarquía” de interfaces se completa con una jerarquía de clases que


tiene un doble objetivo:

Por un lado, tiene el objetivo obvio de proporcionar las implementaciones de


las diferentes colecciones.

Por otro, nos sirve para proporcionar estas implementaciones de manera


orientada a objetos y así permitir definir el comportamiento más general y
reutilizable en el nivel más alto de la jerarquía y el comportamiento específico
de una implementación concreta en los niveles más bajos.

Figura 1

En las figuras que aparecen a continuación se muestran distintas partes de la Trataremos con detenimiento las
colecciones mencionadas en los
módulos “Contenedores secuenciales”
jerarquía de implementaciones de la biblioteca. En la figura 3 podéis obser- y “Colas con prioridad”,
en el que se estudia con detalle esta
var las implementaciones de los TAD secuenciales: las dos implementaciones implementación concreta.

proporcionadas para el TAD Lista y las implementaciones acotadas de los


TAD Pila y Cola.
© FUOC • PID_00161994 • Módulo 1 50 Tipos abstractos de datos

Figura 2

Por lo que respecta a los árboles, la biblioteca tiene una jerarquía de implementa- Si bien no es tarea de esta
asignatura tratar la reutilización del
código en una jerarquía
ciones moderadamente profunda, motivada principalmente para maximizar la de clases, cuando estudiéis las
implementaciones arbóreas, sería muy
reutilización del código. En el ámbito funcional, nos encontramos con un con- interesante que revisarais el código de la
biblioteca teniendo en cuenta este
aspecto.
junto de TAD más reducido: principalmente árboles binarios, árboles n-arios y ár-
boles ordenados.
© FUOC • PID_00161994 • Módulo 1 51 Tipos abstractos de datos

Figura 3

Así, por ejemplo, el ArbolBinario es una clase abstracta que implementa la in-
terfaz Arbol. En esta clase, se definen el conjunto de métodos de los árboles bi-
© FUOC • PID_00161994 • Módulo 1 52 Tipos abstractos de datos

narios y el comportamiento de estos árboles, que es independiente de la


representación. Esto permite reutilizar este comportamiento tanto para los ár- Los árboles binarios se explican en
el módulo “Árboles”. Los árboles de
búsqueda se ven en el módulo “Árboles
boles de búsqueda como para las colas con prioridad. En la figura 3 se muestra de búsqueda”. Las colas con prioridad se
estudian en el módulo “Colas con
la jerarquía de implementaciones referente a los árboles. prioridad”.

Una tercera parte de las implementaciones proporcionadas en la biblioteca Los TAD Diccionario y Conjunto, así
como sus implementaciones se
estudian en los módulos “El TAD Tabla”
consiste en implementaciones de las interfaces Diccionario y Conjunto. A dife- y “Árboles de busca”.

rencia de las implementaciones introducidas en los diagramas anteriores, éstas


no definen una estructura para los elementos, sino que se limitan a definir una
funcionalidad implementable mediante diferentes tipos de estructuras. La bi-
blioteca proporciona distintas implementaciones para cada una de estas inter-
faces. Estas implementaciones son funcionalmente equivalentes, si bien
difieren en la organización de los datos que permite proporcionar el compor-
tamiento deseado. La mayoría delegan en otra colección.

En la figura 4 se muestra la parte de la jerarquía relacionada.

Figura 4
© FUOC • PID_00161994 • Módulo 1 53 Tipos abstractos de datos

Para acabar, la biblioteca también proporciona la posibilidad de trabajar con


Las implementaciones de Grafo se
grafos. A causa de su naturaleza, el TAD Grafo no está clasificado como subcla- trabajan en el módulo “Grafos” de
esta asignatura.
se de Contenedor. La jerarquía correspondiente queda aislada del resto de TAD
presentados, y tiene como base la misma interfaz Grafo. En la figura 5 podéis
apreciar la parte de la jerarquía relacionada.

Figura 5

5.2. Tipos auxiliares

Aparte de las interfaces y clases que representarán las colecciones y sus imple-
mentaciones, se necesitan también algunos tipos auxiliares que nos permiti-
rán trabajar de una manera homogénea con las colecciones.
© FUOC • PID_00161994 • Módulo 1 54 Tipos abstractos de datos

En concreto, en la biblioteca de colecciones de la asignatura necesitaremos ha-


cer referencia a los aspectos siguientes:

• Los elementos de la colección. Dado que la biblioteca hace un uso exten-


sivo de tipos paramétricos, podremos instanciar cualquier colección con
cualquier tipo Java, incluyendo los tipos básicos. Hay colecciones, sin
embargo, que imponen alguna condición sobre el tipo de los elementos
como, por ejemplo, que implementen alguna interfaz (Comparable).

• Claves. Algunas colecciones permiten acceder a los elementos que almacenan


mediante claves. Por ejemplo, podemos querer acceder a los datos de una per-
sona por su DNI. Como sucede con los elementos, las claves también pueden
ser de cualquier tipo. Ahora bien, a menudo las colecciones imponen condi-
ciones sobre los tipos semejantes a las comentadas en el punto anterior para
los elementos.

• Posiciones. Hay colecciones que están organizadas posicionalmente, de El concepto de posición aparece en
las colecciones de secuencias y
también en los árboles, y se trabaja
manera que cada elemento está almacenado en una posición concreta. Este exhaustivamente en el módulo
“Contenedores secuenciales” de esta
tipo de colecciones ofrecen un conjunto de operaciones para trabajar con asignatura.

posiciones. La biblioteca contiene una interfaz Posicion que representa esta


abstracción.

• Iteradores y recorridos. Frecuentemente nos interesará recorrer los ele-


mentos de una colección. La noción de iterador permite hacerlo de una ma-
nera homogénea para todas las colecciones de la biblioteca. La biblioteca
define una interfaz Iterador. De un modo similar, también define una inter-
faz Recorrido, análoga a Iterador, pero para las posiciones de un contenedor.

• Comparadores. Algunas colecciones necesitan comparar sus elementos


de manera interna. Por ejemplo, si la colección almacena los elementos
en un cierto orden, será necesario poder comparar dos elementos para de-
cidir cuál de los dos es más pequeño. Todas las colecciones de la bibliotea
que tienen este requisito ofrecen dos maneras de hacer esta comparación:
o bien el tipo de los elementos implementa la interfaz Comparable, o bien
en el momento de la creación de la instancia de la colección se propor-
ciona un objeto comparador que hace el trabajo. En el primer caso, se de-
lega la tarea a los mismos elementos; en el segundo, proporcionamos un
objeto independiente que sabe cómo comparar los elementos. En este ca-
so, en lugar de proporcionar las interfaces a la biblioteca de la asignatura,
se utilizan las interfaces java.util.Comparable y java.util.Comparator defini-
das en el JDK.
© FUOC • PID_00161994 • Módulo 1 55 Tipos abstractos de datos

6. Presentación del resto de módulos

En el módulo “Complejidad algorítmica”, se estudia la noción de coste asintó-


tico. Esta noción es capital en esta asignatura y nos permite hacernos una idea
de la eficiencia de las implementaciones tanto en el ámbito temporal (tiempo
necesario para realizar las operaciones de los TAD), como espacial (espacio
ocupado por una representación). La noción de coste asintótico nos acompaña-
rá a lo largo de toda la asignatura y nos servirá para decidir la representación
o implementación más conveniente en cada momento, según cada situación.

Tal como ya se ha comentado en la presentación de la biblioteca de TAD de la


asignatura, en los módulos “Contenedores secuenciales”, “Árboles”, “Colas
con prioridad”, “El TAD Tabla”, “Árboles de búsqueda” y “Grafos” se presen-
tan los diferentes TAD que se estudian en la asignatura (uno o más en cada
módulo). Para cada uno de estos TAD se explica, en primer lugar, la interfaz
propuesta en la biblioteca de colecciones de la asignatura junto con su com-
portamiento y, después, se discuten una o más implementaciones.

En el módulo “Contenedores secuenciales”, se presentan las colecciones con


una organización secuencial. Su estudio sirve para introducir también algunos
conceptos que nos servirán en otros módulos, como el concepto de iterador,
las representaciones encadenadas y la amortización de costes.

En el módulo “Árboles”, se estudian los árboles y se introduce la recursividad,


una noción algorítmica importante que resulta muy útil, entre otras cosas,
para definir algoritmos sobre estructuras arbóreas. En el módulo “Colas con
prioridad” se ven las colas prioritarias y se estudia una implementación muy
eficiente basada en una estructura arbórea.

El TAD Diccionario se estudia entre los módulos “El TAD Tabla” y “Árboles de La memoria secundaria
búsqueda”. Un Diccionario es un TAD que nos permite acceder a un elemento
Este tipo de memoria normal-
a partir de una clave. En el módulo “El TAD Tabla” se presenta el TAD y se mente hace referencia al disco,
y es mucho más lenta que la
estudia una implementación eficiente basada en lo que se denomina tablas memoria primaria. En la imple-
mentación de bases de datos,
de dispersión. En el módulo “Árboles de búsqueda” se estudia otra implemen- se ha de tener en cuenta el fac-
tación basada en árboles. En este módulo se presentan también algunas va- tor de la memoria secundaria,
ya que el tamaño de los datos
riantes de los árboles de búsqueda especialmente diseñadas para almacenarlas hace normalmente inviable te-
nerlos todos a la vez en memo-
en la memoria secundaria (en lugar de en la primaria, como las estructuras que ria primaria.
se habrán presentado hasta entonces). Estas variantes se utilizan en la imple-
mentación de bases de datos.

El módulo “Grafos” presenta distintas versiones del TAD Grafo. Los módulos
anteriores se centran en TAD que permiten representar colecciones de elemen-
tos y que tienen una estructura bastante rígida. El objetivo de un grafo no es
© FUOC • PID_00161994 • Módulo 1 56 Tipos abstractos de datos

tanto representar un conjunto de elementos, sino más bien definir una estructu-
ra en la que participen un conjunto de elementos, sin ninguna otra restricción en
su forma. Este TAD es la base para un conjunto de algoritmos extrapolables a mu-
chas situaciones del mundo real que nos permiten llevar a cabo una diversidad de
cálculos complejos. En esta asignatura, sin embargo, nos centraremos estricta-
mente en temas de representación e implementación de las operaciones bási-
cas del TAD, ya que los algoritmos aplicables sobre grafos se estudian en la
asignatura Grafos y complejidad.

Para acabar, una vez vistos todos los TAD que se estudian en el curso, el mó-
dulo “Bibliotecas de colecciones” explica cómo diseñar nuevos TAD basán-
donos en un conjunto de colecciones básicas (las que habréis visto en los
otros módulos). Aparte de esto, el módulo habla sobre el diseño de bibliote-
cas de colecciones, y analiza los factores que hacen que una biblioteca sea
más cómoda de usar que otra, o más potente. Finalmente, se presentan algu-
nas de las bibliotecas más comúnmente usadas en el mundo del desarrollo
orientado a objetos.
© FUOC • PID_00161994 • Módulo 1 57 Tipos abstractos de datos

Resumen

En este módulo hemos presentado las nociones básicas y el marco teórico que
nos acompañará a lo largo del resto del curso. En primer lugar, hemos estudia-
do la noción de contenedor, un elemento básico en el desarrollo de cualquier
aplicación informática. Seguidamente, hemos visto la noción de TAD, que uti-
lizamos como abstracción para separar la especificación de la implementación
de los contenedores.

Otra de las nociones básicas introducidas en este módulo es la del diseño por
contrato (DBC), que se ha presentado en el apartado 2. La noción de contrato
nos hace razonar sobre qué es responsabilidad del TAD y qué es responsabili-
dad del usuario del TAD. El diseñador del TAD es quien establece el contrato
y quien decide quién asume cada responsabilidad. Dependiendo de las deci-
siones que tome el diseñador, el TAD será más o menos usable, y más o menos
mantenible. Una vez definido, el contrato es inamovible y el usuario del TAD
se ha de adaptar a él.

Posteriormente, se ha definido una colección de ejemplo, usando el formato


que se utilizará en los módulos siguientes para presentar cada una de las colec-
ciones estudiadas en el curso. Se han presentado los tipos paramétricos en Java
y se ha vuelto a definir la colección de ejemplo, con el fin de sacarle partido.

Finalmente, se ha presentado la biblioteca de colecciones de la asignatura.


© FUOC • PID_00161994 • Módulo 1 59 Tipos abstractos de datos

Ejercicios de autoevaluación

1. Explicad la diferencia entre la noción de contenedor y la de tipo abstracto de datos.

2. Explicad con vuestras palabras las ventajas principales de desvincular la especificación de


la implementación en el caso de las colecciones o contenedores.

3. ¿Qué relación existe entre la especificación de un algoritmo vista en la asignatura Funda-


mentos de programación y la especificación de un TAD vista en este módulo?

4. Revisad la especificación del TAD Nat del subapartado 1.2.3 y la de la interfaz Nat del
subapartado 2.4. ¿Dicen lo mismo? Razonad las diferencias y el motivo.

5. Incorporad dos nuevas operaciones al TAD Nat: una operación denominada multiplicar
que multiplique el natural por otro natural, y otra operación denominada dividir, que haga
la división entera. Se os pide lo siguiente:
• Definid la signatura de las dos operaciones de manera similar a la de la operación sumar
(con un parámetro de tipo Nat).
• Estableced el contrato para estas dos nuevas operaciones. Pensad en definirlo siguiendo
las recomendaciones del apartado 2. Proporcionad la especificación de las nuevas ope-
raciones del TAD en lenguaje natural.
• Incorporad las dos operaciones a las implementaciones vistas en el texto:
– Definid la especificación.
– Codificad las operaciones en lenguaje Java.

6. Definid un TAD denominado Hora, que represente un momento del día constituido por hora
(0-23), minuto (0-59) y segundo (0-59), con las cuatro operaciones siguientes:
• constructor(int h,int m,int s): crea una hora a partir de tres enteros que representan hora,
minuto y segundo.
• tick: avanza la hora un segundo.
• sumar(Hora h): suma otra hora.
• restar(Hora h): resta otra hora.
La especificación del TAD no se os proporciona de manera expresa.
a) Evaluad diferentes posibilidades a la hora de definir el contrato del TAD, y razonad sus
ventajas y desventajas.
b) Elegid una y proporcionad la especificación del TAD.
c) Definid el TAD en Java.
d) Proponed una representación para el TAD Hora en Java.
e) Proporcionad el invariante de la representación.
f) Especificad (usando el sistema DBC de la asignatura) e implementad el TAD Hora utili-
zando la representación propuesta (si queréis, podéis definir métodos adicionales co-
mo, por ejemplo, toString).

7. Desarrollad una aplicación usuaria del TAD Hora que calcule el día y la hora de llegada de un
vuelo, a partir del día y la hora de salida, el tiempo de vuelo y el número de franjas horarias
que cruza. Por ejemplo, en un vuelo Barcelona-Ciudad de México que sale a las 10 de la ma-
ñana, tarda 14 horas (con escala) y cruza 6 franjas horarias, la aplicación debería dar como re-
sultado las 18 horas.
• ¿Qué problemática veis en el uso del TAD Hora para esta aplicación? ¿Se podría producir
incumplimiento del contrato en alguna situación?
• En caso de que el contrato y/o la signatura del TAD Hora que hayáis diseñado en el ejer-
cicio anterior no se adapte a esta aplicación, haced las modificaciones necesarias y razo-
nad los motivos de éstas.

8. Revisad la documentación Javadoc de las clases del JDK y, más en concreto, la interfaz
Collection y la clase abstracta AbstractCollection:
• Echad un vistazo a la jerarquía de interfaces y clases que heredan de las dos e identificad
la representación Java de, al menos, dos TAD. Es decir, os pedimos que identifiquéis dos
interfaces y, para cada una de ellas, al menos una clase que la implemente.
• Buscad al menos un TAD que tenga más de una implementación.

9. Queremos añadir, a la colección Conjunto desarrollada en el apartado 3, la operación


union(Conjunto c). Esta operación ha de añadir al conjunto los elementos de c que no estén en
él. Se os pide lo siguiente:
• Especificad la operación unión en un nivel de TAD en lenguaje natural.
• Especificadla con el sistema de DBC de la asignatura para la representación del subapar-
tado 3.2.1.
• Añadid la operación a la implementación del subapartado 3.2.
Haced lo mismo con las operaciones respectivas de intersección y diferencia de conjuntos.
© FUOC • PID_00161994 • Módulo 1 60 Tipos abstractos de datos

10. Dad una explicación informal sobre la eficiencia temporal de las tres operaciones del ejer-
cicio 9 (¡después de estudiar el módulo “Complejidad algorítmica” podréis hacer este ra-
zonamiento de una manera totalmente precisa!). Adicionalmente, se os pide:
• Buscad alguna forma de modificar la representación (o sus propiedades) con el objeti-
vo de hacer más eficientes las implementaciones del ejercicio anterior.
• Contestad si después de esta modificación es necesario modificar la especificación del
TAD. En caso afirmativo, decid qué partes.
• Contestad si sucede lo mismo con la especificación de la implementación con el siste-
ma DBC de la asignatura. En caso afirmativo, decid qué partes.

11. Imaginad que no existen los tipos paramétricos en Java y que queremos definir un con-
junto de strings. Además, queremos estar seguros en tiempo de compilación de que todos
los elementos que se añaden al conjunto son del tipo String. Para esto se os pide lo si-
guiente:
• Definid en Java una interfaz denominada ConjuntoString con la signatura adaptada
para forzar a los elementos del conjunto a ser del tipo String.
• Decid si podéis heredar la signatura de Conjunto vista en el apartado 3 y por qué.
• Dad una implementación de esta interfaz. ¿Podéis reaprovechar de alguna manera la
implementación dada en el subapartado 3.2? En caso afirmativo, ¿os puede ayudar al-
guna técnica de la orientación a objetos?

12. Trasladad las implementaciones del ejercicio 9 a la variante de TAD Conjunto que utiliza
tipos paramétricos definida en el apartado 4.

13. Buscad semejanzas entre los TAD que habéis encontrado en el ejercicio 8 y los de la bi-
blioteca de TAD de la asignatura. Guiaos únicamente por la funcionalidad básica del
TAD. Tened en cuenta que TAD conceptualmente semejantes pueden diferir en el núme-
ro de operaciones y la signatura de éstas según los criterios utilizados en el diseño de la
biblioteca de la que forman parte.

14. ¿Existe en la biblioteca de TAD de la asignatura un equivalente para la interfaz Collection


del JDK? ¿Y para AbstractCollection? ¿Cuáles creéis que son los motivos para esto?

15. Echad un vistazo al código de la biblioteca de TAD de la asignatura. Intentad descubrir


para qué implementaciones se emplea delegación; es decir, qué implementaciones dele-
gan la definición del comportamiento del TAD en otra implementación (muy probable-
mente de otro TAD).

16. Revisad el Javadoc de la clase java.util.Comparator del JDK. Proporcionad una implemen-
tación para la implementación del TAD Hora que habéis definido en el ejercicio 7.
© FUOC • PID_00161994 • Módulo 1 61 Tipos abstractos de datos

Solucionario

1. Cuando hablamos de contenedor, definimos la funcionalidad de una entidad: almacenar


elementos. En cambio, cuando hablamos de tipo abstracto de datos o TAD, hacemos refe-
rencia a una abstracción que nos permite desvincular la definición de su comportamiento
(especificación) del método concreto que se utiliza para proporcionar este comportamien-
to (implementación). Un TAD es una buena herramienta para definir contenedores; ahora
bien, su uso es más amplio.

3. La especificación de un algoritmo vista en la asignatura Fundamentos de programación es


equivalente a la especificación de una operación de un TAD. Aquí definimos la especifi-
cación de un TAD como un conjunto de especificaciones para sus operaciones.

4. Sí, no podría ser de otra manera. La única diferencia es el formalismo que se utiliza: en
el primer caso se utiliza lenguaje natural y, en el segundo, el sistema DBC de la asignatu-
ra. No siempre será posible especificar los TAD utilizando el sistema DBC de la asignatura.
Las implementaciones, en cambio, sí, ya que siempre dispondremos de la representación,
a la cual podremos hacer referencia en las expresiones Java que el sistema DBC usa.

7. Problemática del uso del TAD Hora: se puede dar la situación de que un avión salga de su
origen un día y llegue el día siguiente a su destino. Si el contrato establecido en el ejerci-
cio 6 responsabiliza al usuario del TAD de hacer que las operaciones ejecutadas no cam-
bien de día, o bien se produciría incumplimiento de contrato, o bien se requeriría un
tratamiento laborioso de la aplicación usuaria. Este tratamiento laborioso replicaría en
parte el comportamiento definido en el TAD Hora, algo no deseable.

Modificaciones necesarias

El contrato se debería modificar de manera que fuera responsabilidad del TAD tratar el
paso de un día a otro. Esto se puede hacer de varias maneras:
a) Ampliando el conjunto de valores representados, es decir, pasando del TAD Hora a un
TAD que podríamos denominar Instante y que tuviera en cuenta también el día, y modi-
ficara su valor si la suma o resta de horas hiciera cambiar de día.
b) Modificando la firma de las operaciones relacionadas del TAD Hora de manera que “se avi-
se” al usuario de alguna manera de que se ha cambiado el día. Por ejemplo, devolviendo un
booleano que sería ‘cierto’ si se cambia de día y, ‘falso’ en caso contrario.
c) Notificando al usuario del TAD la situación excepcional de sobreseimiento mediante
el lanzamiento de una excepción. El usuario del TAD debería capturar la excepción, que
contendría información necesaria sobre la situación excepcional.
Podríamos pensar que éste es un ejemplo en el que la programación defensiva es una bue-
na solución. En realidad, se trata más de un tema funcional en el que la aplicación desa-
rrollada delega el control sobre el cambio de día al propio TAD que se encarga de calcular
las horas, ya que ambas tareas están muy relacionadas.

8. La jerarquía de subinterfaces de java.util.Collection proporcionada en el JDK es muy sencilla.


Podemos identificar la interfaz List y la Set como en los TAD Lista y Conjunto (todas en el pa-
quete java.util, como las clases comentadas a continuación).
El JDK proporciona distintas implementaciones para cada una de ellas. En el caso de ja-
va.util.List encontramos 3 implementaciones diferentes: ArrayList, LinkedList yVector. En el
caso de Set, tenemos HashSet, LinkedHashSet y TreeSet (advertid que esta última implementa
también una subinterfaz de Set: SortedSet).

13. El TAD representado en el JDK mediante la interfaz java.util.List sería conceptualmente pa-
recido al representado por la interfaz de la biblioteca de TAD de la asignatura uoc.ei.tad.Lis-
ta. ¡Advertid, no obstante, que hay muchas diferencias por lo que respecta a la signatura!
Estas diferencias se deben principalmente a la diferencia de criterios usados en el diseño de
ambas bibliotecas (este tema se comenta delladamente en el módulo “Bibliotecas de colec-
ciones”). El TAD representado en el JDK por java.util.Set correspondería a la interfaz
uoc.ei.tad.Conjunt de la biblioteca de colecciones de la asignatura.
Por lo que respecta a las implementaciones, la interfaz Lista tiene dos implemen-
taciones: ListaEncadenada y ListaDoblementeEncadenada. Y Conjunto tiene también dos
implementaciones: ConjuntoAVLImpl y ConjuntoTablaImpl. Todas las implementaciones
están ubicadas también en el paquete uoc.ei.tads.

Glosario

biblioteca f Conjunto de clases, módulos, funciones o rutinas que se encuentran habitual-


mente empaquetadas en un único fichero (.lib, .dll, .jar, etc.) y que se ponen a disposición de
un conjunto de desarrolladores (usuarios de la biblioteca).
© FUOC • PID_00161994 • Módulo 1 62 Tipos abstractos de datos

contenedor m Abstracción que permite almacenar y gestionar un grupo de elementos.

contenedor acotado m Contenedor que tiene una capacidad máxima.

contrato m Definición de las responsabilidades y derechos que tiene un TAD, clase, método
u operación, por una parte, y sus usuarios por la otra.

diseño por contrato m Técnica de diseño basada en la definición de contratos para separar
las responsabilidades de una clase de las de sus usuarios.

encapsulación f Característica ofrecida por algunos lenguajes de programación que permi-


te definir entidades (módulos, clases, etc.) en las que los detalles de implementación quedan
escondidos de sus usuarios.

especificación de un TAD f Descripción del comportamiento del TAD correspondiente.

implementación de un TAD f Conjunto de algoritmos presentados en una signatura


igual que la del TAD y que, además, cumple la especificación de éste.

invariante de la representación m Propiedad que es cierta para cualquiera de las repre-


sentaciones válidas de una implementación de un TAD, y que es falsa para representaciones
no válidas.

operación de un TAD f Unidad de comportamiento básica de un TAD. Proporciona la úni-


ca vía de interacción entre el TAD y sus usuarios.

programación defensiva f Técnica de programación que consiste en proteger un algorit-


mo contra cualquier mal uso que se pueda hacer de él (habitualmente mediante parámetros
no adecuados).

representación (de una implementación) de un TAD f Conjunto de datos que permi-


te almacenar el estado de una instancia del TAD para una implementación de éste.

tipo genérico m Véase tipo paramétrico.

tipo paramétrico m Definición de tipo en la que uno o más elementos no se han concre-
tado y permanecen como parámetros.

Bibliografía

Bibliografía básica

Meyer, B. (1999). Construcción de software orientado a objetos. Madrid: Prentice Hall.

Franch, X. (2001). Estructuras de datos. Especificación, diseño e implementación (4.ª ed.). Bar-
celona: Edicions UPC. Disponible en línea en: <www.edicionsupc.es>

Peña Marí, R. (2000). Diseño de programas. Formalismo y abstracción (2.ª ed.). Prentice Hall.

Goodrich, M.; Tamassia, R. (2001). Data structures and algorithms in Java (2.ª ed.). John
Wiley and Sons.
© FUOC • PID_00161994 • Módulo 1 63 Tipos abstractos de datos

Anexo

Para saber más


En este módulo hemos presentado el marco teórico en el cual de- En cuanto al diseño por contrato, os puede resultar muy intere-
sarrollaremos el curso. Se han presentado un par de temas teóri- sante revisar la obra de Meyer (1999), que contiene una descrip-
cos de manera intuitiva y sin entrar en detalles de formalismos ción detallada y muy argumentada sobre el diseño por contrato,
matemáticos: los tipos abstractos de datos (TAD) y el diseño por su motivación y sus ventajas.
contrato.
Los puntos referentes al lenguaje Java –tipos paramétricos o ge-
Respecto a los TAD, existen muchos temas que se han mencio- néricos, y sistemas de diseño por contrato– son, o bien demasia-
do nuevos (el primero), o bien demasiado concretos (el segundo),
nado sin entrar en detalles: la especificación algebraica, las ál-
para encontrar bibliografía al respecto en el momento de escribir
gebras que son modelos de la anterior, los diferentes tipos de
este texto. Donde sí que podréis encontrar más información re-
semánticas que se pueden utilizar para unir los modelos de ferente a los tipos paramétricos para Java es en el sitio web de Sun
una especificación con esta, etc. Todos estos temas los podéis (<http://java.sun.com>).
encontrar explicados de manera más detallada en las obras de
X. Franch (1999) y R. Peña Marí (2000).

Das könnte Ihnen auch gefallen