La clase Object

Objetivos: Mostrar que en ocasiones no se necesita saber nada de un objeto y que en esos casos es útil trabajar con él, como objeto pertenciente a la clase Object.

Temas:


La clase Object

Todas las clases son en realidad subclases de una clase más amplia: la clase Object. Esta clase incluye todos los objetos (los lectores de archivos, las tortuga, los arreglos, los glyphs, etc.). Por lo tanto siempre es posible colocar cualquier objeto en donde se espera un expresión de tipo Object. Por ejemplo:

    Object o1= "Hola";
    Object o2= new TextReader("datos.txt");
    Object o3= new Nodo(3, "hola", null, null);
    Object o4= new Box( ... );
Recuerde que la relación ser subclase de es transitiva: como Box es subclase de Glyph y Glyph es subclase de Object entonces Box es subclase de Object también y por eso es válido realizar la última operación del ejemplo.

El único problema es que no hay muchas operaciones que se puedan realizar con una variable de tipo Object. Quizas la más importante es que se puede obtener un string que describa el objeto:

    println(o1.toString());  // despliega "Hola" en pantalla
    println(o3.toString());  // despliega un mensaje no muy útil
Este método se puede redefinir en las subclases de modo que se entregue alguna información más util.

De todas formas algunas de las clases contenedoras que hemos visto durante el curso aceptan objetos como parámetros. Por ejemplo se puede construir una cola con objetos:

    Queue cola= new Queue();
    cola.put(o1);
    cola.put(o2);
    cola.put(o3);
    cola.put(o4);
En realidad este último ejemplo debe ser visto como una humorada porque rara vez es útil juntar elementos tan diferentes en un mismo contenedor. Por otra parte, para extraer los objetos de la cola se debe usar el método get que retorna un objeto:

    Object stringO= cola.get();
Sería un error tratar de almacenar la referencia del objeto entregada por get en una variable de tipo String, aún cuando se sabe que el primer objeto de la cola era un string. Para poder ver el objeto como string, se debe aplicar el cast a String:

     String s= (String)stringO;
     String starS= "*** "+s+" ***"; // Ahora sí se puede trabajar
     println(starS);                // como un string
Como se sabe que el siguiente objeto de la cola es un lector de archivos, se puede aplicar directamente el cast al momento de extraerlo:

    TextReader lect= (TextReader)cola.get();
    String lin= lect.readLine();
    ...
Por lo tanto la clase Object es importante porque evita tener que programar contenedores para cada posible tipo de objetos. Por ejemplo, la clase PilaNodos del capítulo anterior no es necesaria, porque se puede usar la clase más general Stack para almacenar todo tipo de objetos. El único cuidado es que al extraer con la operación pop, se debe aplicar el cast a Nodo.


Implementación de contenedores de objetos

Para implementar una cola de objetos con una lista enlazada, basta cambiar los datos contenidos en los eslabones. En vez de colocar una referencia de un String, se coloca una referencia de un objeto:

    class EslabonCola {
      Object o;
      EslabonCola prox;
      EslabonCola(Objecto o, EslabonCola prox) {
        this.o= o;
        this.prox= prox;
      }
    }
La implementación es la misma sólo que cambia el tipo del dato que se manipula:

    class Queue extends Program {
      EslabonCola primero;
      EslabonCola ultimo;
      ...
      Object get() {
        Object o= primero.prox.o;
        primero.prox= primero.prox.prox;
        return o;
      }
      void put(Object o) {
        ultimo.prox= new EslabonCola(o, null);
        ultimo= ultimo.prox;
      }
    }
El problema de esta implementación de Queue es que no acepta colocar enteros o números reales:

    cola.put(4); // error en tiempo de compilación
    cola.put(3.14); // idem
    cola.put(true); // idem
En todos estos caso el compilador entregará un error diciendo que la operación put no recibe enteros, reales o valores de verdad como parámetro. Esto se debe a que estos tipos de datos no son objetos. En Java se dice que son tipos de datos primitivos. (En realidad este es un error de diseño del lenguaje: no hay ninguna razón de fondo para que no sean objetos, como sí lo son los Strings.)


Mezclando objetos con tipos primitivos

Para solucionar el problema mencionado más arriba Java ofrece la clase Integer que sí es una subclase de Object. La operaciones que acepta esta clase son las siguientes:

Ejemplo Significado Declaración
Integer objent= new Integer(1); Construye un objeto Integer que almacena el 1 Integer(int i)
int i= objent.intValue() Entrega el valor entero almacenado en objent int intValue()

El objeto objent es un objeto y por lo tanto puede ser colocado en una cola:

    q.put(objent); // Correcto
Sin embargo, al ser un objeto pierde sus operaciones como entero y no se puede operar como tal:

    objent= objent+1; // Error de tipos en tiempo de compilación
Con la clase Integer ahora es posible almacenar cualquier entero en una cola. Supongamos que necesitamos agregar el entero e en una cola y posteriormente extraerlo. Entonces el siguiente código es una receta para lograr esto:

    q.put(new Integer(e));  // Coloca el entero e en la cola
    ...
    int f= ((Integer)q.get()).intValue(); // Recupera el valor e de la cola
Java también ofrece las clases estándares Double y Boolean para trabajar con los reales y los valores de verdad como objetos. Observe que se diferencian únicamente de los tipos de datos primitivos double y boolean porque Double y Boolean comienzan con una letra mayúscula y porque Double y Boolean son subclases de Object.

De todas formas, esta solución todavía no es satisfactoria porque obliga a los programadores usuarios de la clase Queue a crear estos objetos adicionales. La siguiente implementación de Queue acepta enteros primitivos en la operación put:

    class Queue extends Program {
      EslabonCola primero;
      EslabonCola ultimo;
      ...
      void put(int i) {
        put(new Integer(i)); // (*)
      }
      int getInt() {
        return ((Integer)get()).intValue();
      }
    }
Ahora cuando se invoque put con un entero, el compilador determina que el parámetro es entero y que hay una versión especial de put que recibe un entero. Observe que la definición de put no es recursiva en ningún caso puesto que en (*) se invoca la otra versión de put: aquella que recibe un objeto como parámetro.

En el caso general, en Java se puede definir varios métodos con el mismo nombre, siempre y cuando los tipos de los parámetros sean diferentes. Durante la compilación, Java determina el método que mejor se aproxima a los argumentos especificados en la invocación del método.

Ejercicio: modifique esta implementación para que acepte números reales y valores booleanos en el put.