Tipos de Datos Abstractos

Hasta el momento, los subproblemas que arroja el proceso de diseño de una solución han sido resueltos mediante funciones o procedimientos. En los lenguajes orientados a objetos, estos problemas también pueden ser resueltos mediante clases de objetos.

La idea es que la resolución de un subproblema se abstrae mediante una clase de objetos. A estas clases se les llama usualmente tipos de datos abstractos (TDAs), por que son tipos de datos al igual que int, double, String, etc. Pero se agrega la palabra abstracto para enfatizar que los detalles de su implementación son desconocidos para el programador que usa el TDA.

Más adelante en el curso veremos cómo se definen nuevas clases de objetos. En la definición se especificarán los detalles de implementación de cada una de las operaciones de sus objetos. Al hacerlo el programador se abstrae del contexto en el que serán usados los objetos.

Por el momento sólo será importante ser capaz de entender la especificación de una clase de objetos para poder usarla en la resolución de un problema complejo.


Ejemplo:

La clase Fecha permite construir objetos que aceptan las siguientes operaciones:

Ejemplo Significado Encabezado
Fecha a= new Fecha(25, 12, 2000); Construye una fecha para el dia 25/12/2000 Fecha(int dd, int mm, int aaaa)
if (a.comparar(b)<0) 0 si a=b, <0 si a<b, >1 si a>b int comparar(Fecha f)
String dia= a.diaSemana() entrega el día de la semana de a: "lunes" String diaSemana()
Fecha m= a.mannana(); entrega la fecha de un día después de a Fecha mannana()
String s= a.toString(); entrega un string en formato "dd/mm/aaaa" String toString()

La columna encabezado se usa para especificar el tipo de los parámetros de cada una de sus operaciones (métodos) y el tipo del resultado de la operación (más adelante veremos que éste es el encabezado que se usa al definir las operaciones en la clase).

Un ejemplo de uso de esta clase es un programa que despliega los días de semana que corresponden a los 7 primeros días del año 2000:

    Fecha dia= new Fecha(1,1,2000);
    Fecha siete= new Fecha(7,1,2000);
    while (dia.comparar(siete)<=0) {
      println(dia.toString()+": "+dia.diaSemana());
      dia= dia.mannana();
    }
Ejercicio:

El archivo feriados.txt contiene las fechas de los días feriados del año 2000. En el archivo viene una línea por cada feriado en el formato dd/mm. Además las fechas viene ordenadas.

    1/1
    21/4
    22/4
    1/5
    21/5
    ...
Escriba un programa que lea este archivo y calcule cuantos días transcurren entre cada día feriado. En pantalla Ud. debe mostrar:

    Entre el Miércoles 1/1/2000 y el Jueves 21/5/2000: xxx dias
    Entre el Jueves 21/5/2000 y el Lunes 4/6/2000: xxx dias
En realidad, la clase Fecha no existe, pues no ha sido programada, pero resuelva el problema abstrayéndose de los detalles de implementación de la clase Fecha.


La clase WordParser

Los objetos de esta clase sirven para descomponer en palabras un String que contiene una frase.

Ejemplo Significado Encabezado
WordParser decod= new WordParser("Esta noche puedo escribir"); Construye un decodificador para "Esta ..." WordParser(String s)
String pal= decod.readString(); Obtiene la primera palabra "Esta". String readString();
String pal= decod.readString(); Obtiene la segunda palabra "noche". Idem
if (decod.hasMoreWords()) ... Determina si quedan más palabras en la frase boolean hasMoreWords()

Ejemplo de uso:

Colocar las primeras 1000 palabras de un archivo en un arreglo:

    TextReader lect= new TextReader("palabras.txt");
    String[] pals= new String[1000]; // No más de 1000
    int i= 0;
    while (i<1000) {
      String lin= lect.readLine();
      if (lect.eofReached()) {
        break;
      }
      WordParser decod= new WordParser(lin);
      while (decod.hasMoreWords() && i<1000) {
        pals[i]= decod.readString();
        i= i+1;
      }
    }
    println("palabras leidas: "+i);

La clase StringMap

La clase StringMap es un arreglo en donde los índices no son enteros, sino que son strings. A este tipo de arreglos también se le denomina arreglos asociativos. A sus índices también se les llama llaves. Sus operaciones son las siguientes:

Ejemplo Significado Encabezado
StringMap map= new StringMap(); Construye un arreglo asociativo StringMap()
map.put("pedro", 23); Asocia el valor 23 a la llave "pedro" void put(String llave, int val)
map.put("pi", 3.14); Asocia el valor 3.14 a la llave "pi" void put(String llave, double x)
void put(String llave, String s)
void put(String llave, boolean b)
int e= map.getInt("pedro"); Obtiene el valor asociado a "pedro" int getInt(String llave)
double getDouble(String llave)
String getString(String llave)
boolean getBoolean(String llave)
if (map.isMapped("pedro")) ... Determina si se le ha asociado un valor a "pedro" boolean isMapped(String llave)
int n= map.size(); Entrega el número de asociaciones en map int size()
StringKeyEnum enum= map.keys() Entrega un enumerador (o iterador) de las llaves en map StringKeyEnum keys()
if (enum.hasMoreKeys()) ... Determina si quedan llaves por visitar boolean hasMoreKeys()
String llave= enum.nextKey(); Entrega una llave no visitada String nextKey()

Ejercicio:

El archivo "votos.txt" contiene las preferencias de los electores en una elección. Cada línea contiene el nombre del candidato preferido por un elector. No se conoce los nombres de los candidatos. Escriba un programa que haga el recuento de votos. La salida del programa debe ser:

    Margaret Tatcher: 20 votos
    Boris Yeltsin: 9 votos
    Ronald Reagan: 17 votos
    ... etc ...
Solución:

Se usa el arreglo asociativo recuento para mantener el número de votos de cada candidato. La idea es que recuento.getInt("xxx") entregará la cantidad de votos percibidos por el candidato "xxx".

Creación

    StringMap recuento= new StringMap();
Crea un arreglo inicialmente vacío.

Consulta

Para obtener la cantidad de votos captados por "Margaret Tatcher" se evalúa:

    int votos= recuento.getInt("Margaret Tatcher");
Es decir, se usa el nombre del candidato como llave en el arreglo. El valor asociado al nombre es entonces su número de votos.

Existencia de una llave

Al obtener el valor asociado a una llave, se debe haber establecido previamente un valor para esa llave en el arreglo. Como éste no será el caso la primera vez que se encuentre el nombre de un candidato en el archivo, se deberá consultar primero si esa llave existe en el arreglo mediante:

    int votos= 0;
    if (recuento.isMapped("Michael Jackson"))
       votos= recuento.getInt("Michael Jackson");
Establecer una asociación

Para establecer un nuevo valor para una llave en el arreglo se usa put:

    recuento.put("Michael Jackson", votos+1);
Si la llave no existía previamente en el arreglo, se crea la llave y se establece la asociación. Si la llave sí existía previamente, se cambia su valor asociado por el resultado de votos+1.

Recorrido de las llaves de un StringMap

Después de leer todo el archivo y contabilizar todos los votos, es necesario conocer todas las llaves presentes en el arreglo. Para esto se obtiene un enumerador de las llaves del arreglo invocando el método keys():

    StringKeyEnum enum= recuento.keys();
    while (enum.hasMoreKeys()) {
      String nombre= enum.nextKey();
      ...
    }
En cada iteración, la variable nombre corresponderá al nombre de un candidato. Cada nombre aparecerá una sola vez. Para averiguar cuantos votos obtuvo se evalúa recuento.getInt(nombre).

Programa final:

    StringMap recuento= new StringMap();
    TextReader lect= new TextReader("votos.txt");
    while (true) { // Cuenta las apariciones
      String nombre= lect.readLine();
      if (lect.eofReached())
        break;
      int votos= 0;
      if (recuento.isMapped(nombre))
        votos= recuento.getInt(nombre);
      recuento.put(nombre, votos+1);
    }
    // Muestra los resultados
    StringKeyEnum enum= recuento.keys();
    while (enum.hasMoreKeys()) {
      String nombre= enum.nextKey();
      println(nombre+": "+recuento.getInt(nombre));
    }
(El programa completo se encuentra en Recuento.java y el archivo de votos en votos.txt.)

Por lo tanto, para la clase StringMap se aplican los mismos patrones de programación que para la clase Map. La única diferencia radica en que las llaves deben ser de tipo String.

Observaciones:


La clase Map

La clase Map es un arreglo asociativo con llaves de tipo entero al igual que Array. Se diferencia de Array porque con la clase Map no es necesario especificar el tamaño del arreglo. Un simple ejemplo de uso de Map es suficiente para darse cuenta que su uso es idéntico a StringMap:

   Map map= new Map(); // Sin especificar tamaño!
   map.put(5, "cinco");
   map.put(1000, "mil");
   println(map.get(5));
   KeyEnum enum= map.keys();
   int i1= enum.nextKey();    // 5 o 1000, alguno de los dos
   int i2= enum.nextKey();    // 1000 o 5
Recomendación: Utilice la clase Map cuando no sepa el tamaño del arreglo que necesita crear (en vez de Array, int[], double[], etc.).


Contenedores

Los arreglos asociativos corresponden a una categoría de objetos denominados contenedores. Estos objetos almacenan valores (u objetos), pero en sí no realizan trabajo útil. El trabajo útil se realiza con los valores que almacenan. El arreglo asociativo es el más poderoso de los contenedores, por la cantidad de problemas en los que se puede usar. Sin embargo, existen otros contenedores más simples y que en ocasiones resultan más cómodos de usar.

Los contenedores más usados son:

Los nombres se usan como metáforas de estructuras que se dan en la vida real. Por ejemplo, una cola es un remedo de lo que ocurre en una cola de personas esperando ser atendidas en un banco. Cuando llega una persona, se coloca al final de la cola y sólo será atendida cuando hayan sido atendidas todas las personas que llegaron antes que él.

De la misma forma, una pila es un remedo de lo que ocurre con una pila de papeles encima del escritorio. Si se coloca un papel encima de la pila, éste se puede extraer primero. El que está en el fondo de la pila, sólo se podrá extraer cuando se hayan extraído todos los que están encima de él.


La clase Queue

Una cola es un contenedor que ofrece métodos para agregar elementos al final de la cola y para extraer el elemento que se encuentra en primer lugar en la cola.

Ejemplo Significado Encabezado
Queue cola= new Queue(); Construye una cola Queue()
cola.put("pedro"); Agrega "pedro" al final de la cola void put(String s)
cola.put("juan") Agrega "juan" al final de la cola Idem
String s= cola.getString(); Entre los elementos agregados a la cola, extrae y entrega el que está en primer lugar String getString()
if (cola.isEmpty()) ... Determina si la cola está vacía boolean isEmpty()

La siguiente figura muestra el efecto que producen los métodos put y getString:

A la cola se pueden agregar también enteros, reales y booleans, los que se extraen con getInt(), getDouble y getBoolean.

Ejercicio:

En una consulta médica, se necesita un programa que indique el orden de atención de los pacientes. Los pacientes deben atenderse en orden de llegada (el primero en llegar es el primero en ser atendido). Al llegar un paciente, éste indica su nombre a la secretaria que opera el computador. Cuando el médico queda libre, la secretaria consulta en el computador cuál es el próximo paciente que debe ser atendido.

El programa debe responder al siguiente diálogo:

    Ingrese un n para agregar un paciente
    Ingrese un s para atender un paciente
    Ingrese un f para terminar
    Opción ? n
    Nombre del paciente ? juan
    Opción ? n
    Nombre del paciente ? pedro
    Opción ? n
    Nombre del paciente ? diego
    Opción ? s
    Atender al paciente: juan
    Opción ? n
    Nombre del paciente ? ana
    Opción ? s
    Atender al paciente: pedro
    ...
Solución:

    Queue pacientes= new Queue();
    println("Ingrese una n para agregar un paciente");
    println("Ingrese una s para atender un paciente");
    println("Ingrese una f para terminar");
    while (true) {
      // Seleccionar una opcion
      print("opcion ? ");
      String opcion= readLine();
      if (equals(opcion, "n")) {
        // Ha llegado un nuevo paciente.  Se pide el nombre y se agrega
        // a la cola.
        println("Nombre del paciente ? ");
        String nombre= readLine();
        pacientes.put(nombre);
      }
      else if (equals(opcion, "s")) {
        // El medico queda disponible para atender un nuevo paciente.
        // El nombre del proximo paciente es aquel que se encuentra
        // en primer lugar en la cola.
        if (pacientes.isEmpty()) {
          println("No hay pacientes en espera");
        }
        else {
          String nombre= pacientes.getString();
          println("Atender al paciente: "+nombre);
        }
      }
      else if (equals(opcion, "f")) {
        break;
      }
      else {
        println("Ingrese solo n, s o f");
      }
    }
(El programa completo se encuentra en Pacientes.java.)