Definición de Clases - 2da. Parte

Objetivos: Conocer como se escriben los programas cuando no se dispone de la biblioteca de cc10a. Esta materia no es esencial, pero es importante conocerla porque permite comprender por qué los programas estándares de Java se escriben con un procedimiento main que lleva el atributo static.

Temas:


Supresión del identificador this

Al definir una clase, en la mayoría de las ocasiones no es necesario colocar el identificador this porque el compilador lo puede agregar automáticamente. La clase Post se podría reescribir como:

    class Post extends Program {
      String ci;
      String nombre;
      Post(String ci, String nombre) {
        this.ci= ci;
        this.nombre= nombre;
      }
      Post(TextReader lect) {
        if (lect.eofReached())
          ci= null;
        else {
          ci= lect.readString();
          nombre= lect.readString();
          lect.skip();
        }
      }
      void escribir(TextWriter escr) {
        escr.print(ci);
        escr.println(nombre);
      }
      int compararCon(Post post) {
        return compare(nombre, post.nombre); // (*)
      }
    }
Observe que en (*) se necesita accesar el nombre de un objeto que no es this. En este caso es necesario entonces colocar el prefijo post. para diferenciarlo del acceso a las variables de this.


Invocación de métodos de la clase

Supongamos que se está definiendo una clase Robot con métodos orden1 y orden2. Al programar orden1, nos damos cuenta que necesitamos invocar orden2. Esto se puede hacer utilizando this:

    class Robot extends Program {
      void orden1(...) {
        ...
        this.orden2(...); // o también orden2(...);
        ...
      }
      void orden2(...) { ... }
      ...
    }
Al igual que en las variables de instancia, se puede suprimir this e invocar orden2 simplemente con:

    orden2(...);
El compilador agrega automáticamente el prefijo this.


¿Funciones y procedimientos en Java?

Cuando en este curso definimos un procedimientos como:

    class Tarea extends Program {
      void run() {
        ... 
        ordenar(...);
        ...
      }
      void ordenar(int[] a, int n) {
        ...
      }
      ...
    }
¿Cómo distingue Java que Tarea es un programa y que run y ordenar son procedimientos? ¿No podría interpretar que Tarea es una clase de objetos y que run y ordenar son los métodos de la clase?

La respuesta es que Java no lo distingue: Java no posee funciones ni procedimientos. En realidad Tarea sí es una clase como cualquier otra, y run y ordenar son métodos de la clase Tarea. Es perfectamente legal invocar:

		this.ordenar(...);
De hecho, cuando no se pone, el compilador lo agrega automáticamente. ¿Y qué objeto referencia this en este caso? Es un objeto que es creado por la clase Run. En realidad la forma estándar de invocar programas en Java es:

java una clase argumentos

Y no es obligatorio que la clase sea Run. Pero en este curso hemos estado colocando siempre la clase Run en primer lugar. Esta clase se encarga de crear un objeto de la clase que se suministra como argumento. Por ejemplo al invocar:

java Run Tarea

la clase Run pertenece a la biblioteca del curso. Ejecutará las siguientes instrucciones:

    Tarea tarea= new Tarea();
    tarea.run();
o simplemente new Tarea().run();. Es decir, crea un objeto de la clase Run y luego invoca el método run.

Por lo tanto, dado que Java no posee funciones ni procedimientos, se simulan mediante métodos. El lector se preguntará por qué entonces se estudiaron los métodos como funciones y procedimientos cuando en realidad no existen.


Lenguajes Orientados a Objetos

Los lenguajes que permiten definir clases de objetos y asociar métodos a estas clases se denominan lenguajes orientados a objetos (lenguajes O-O). Ejemplos de este tipo de lenguajes son Java, C++, Eiffel, Smalltalk. El punto central de la programación O-O es que las clases sirven para implementar TDAs y por lo tanto los programas deben abstraerse de cómo se implementan las clases que usan. En particular, los programas no deben accesar directamente las variables de instancia de los objetos, porque éstas son parte de la implementación. La variables de instancia sólo se accesan al definir la implementación de la clase.

Los lenguajes O-O se usan desde hace relativamente poco tiempo. Solo en los 90 se comenzaron a usar a nivel de empresas. En los 70 y 80 se usaban lenguajes tradicionales (no orientados a objetos). Estos lenguajes sólo ofrecen mecanismos para definir funciones, procedimientos y records. Por ejemplo Fortran, Pascal, Cobol y C.

Un record es la agrupación de un conjunto de datos en una sola estructura que puede ser manipulada como un solo dato o también a través de sus partes. Típicamente, en los lenguajes tradicionales, no era posible definir métodos asociados a los records y por lo tanto los records son un mecanismo de abstracción más débil que los objetos.

Uso de objetos como records

A pesar de que Java no posee un mecanismo para definir records, éstos se pueden simular mediante clases: basta accesar directamente las variables de instancia como se hizo en las clases de apoyo para almacenar la información contenida en una línea de un archivo.

En este punto, es importante mencionar la disyuntiva existente en la actualidad con respecto a la enseñanza de Computación utilizando un lenguaje O-O como primer lenguaje. Hay profesores que opinan que es conveniente enseñar métodos y clases desde un comienzo, sin pasar por funciones y procedimientos. Otros profesores, como el autor de este documento, opinan que resulta más sencillo comenzar por programación no orientada a objetos y por ello se han simulado las funciones y procedimientos con los métodos de Java.


Métodos estáticos

Cuando se invoca un método normal siempre se suministra un objeto de la invocación que aparece a la izquierda. Por ejemplo:

   t.sumar(t2);
Cuando se diseñan los métodos que necesita un clase, inevitablemente se llega a casos en que no se necesita un objeto de la invocación. Por ejemplo, podríamos agregar a la clase Tiempo un método horaActual que entrega la hora del día.

Para este tipo de caso, Java ofrece los métodos estáticos. Este tipo de métodos no va asociado a ningún objeto. Se invocan colocando al lado izquierdo el nombre de la clase (y no un objeto):

   Tiempo ahora= Tiempo.horaActual();
Este tipo de método se define en la clase agregándoles el atributo static:

    class Tiempo extends Program {
      ...
      static Tiempo horaActual() {
        ... averiguar horas y minutos actuales ...
        return new Tiempo(horas, minutos);
      }
    }
Al programar métodos estáticos se debe tener mucho cuidado, porque dado que no van asociados a un objeto, no se puede usar el identificador this:

    class Ejemplo extends Program {
      int vari; // una variable de instancia
      static void mets(String s) { // Un método estático
        ... s ... // se pueden usar argumentos
        XXX this.vari XXX // no se puede usar this
        XXX vari XXX      // tampoco, porque esto equivale a this.vari
        XXX meti() XXX    // no se pueden invocar métodos normales
      }
      void meti() {       // un método normal
        mets("hola");     // se puede invocar Ejemplo.mets(...)
      }
    }
Explicación:

Los métodos normales también se llaman métodos de instancia (porque van asociados a una instancia de la clase). Desde un método de instancia sí se puede invocar un método estático. Si el método estático está en la misma clase, no es necesario especificar el nombre de la clase a la izquiera. El compilador lo coloca automáticamente. Por ejemplo:

    mets(...); // o también Ejemplo.mets(...)
Si se trata de un método estático de otra clase, siempre hay que colocar explícitamente el nombre de la clase.


Ejecución de programas sin la biblioteca del curso

En este curso, hemos usado sistemáticamente una biblioteca de clases escrita en su mayor parte por el autor de este documento. El objetivo ha sido simplificar la escritura de los programas. Sin embargo, en ocasiones no se podrá utilizar esta biblioteca porque no se encuentra disponible en todas las instalaciones.

Por lo tanto, en esta parte veremos cuál es la forma estándar de escribir aplicaciones en Java. Es decir, sin recurrir a bibliotecas especiales.

Como se dijo previamente, la forma estándar de ejecutar programas en Java es por medio del comando:

java nombre de clase

sin especificar Run. En estos casos, el comando java busca algún método estático que se llame main y que reciba como argumento un arreglo de strings y lo invoca. Por ejemplo, un programa que despliega Hola Mundo en la pantalla se escribiría:

    class Jalisco {
      static void main(String[] args) {
        System.out.println("Hola Mundo");
      }
    }
Observaciones:

En este curso, no será obligatorio escribir programas en la forma estándar de Java. Ud. podrá contar siempre con la biblioteca del curso. Pero para los curiosos se entregarán los conocimientos necesarios para programar sin ella.


Diseño y abstracción

Como hemos insistido, el diseño de una solución consiste en descomponer el problema en subproblemas más simples. Una forma de realizar esta descomposición es asignar cada subproblema a una clase. Los programadores que usan una clase aplican abstracción, despreocupándose de los detalles de como se implementan los métodos que invocan.

Cuando los proyectos son grandes, como para que el desarrollo de un programa abarque varios meses e involucre muchos programadores, la metodología puede fallar por falta de abstracción por parte de algunos programadores. Para entender mejor el problema examinaremos el siguiente escenario: un programador requiere realizar una operación con un objeto, pero esta operación no ha sido definida en la clase a la que pertenece ese objeto, aunque se puede llevar a cabo con la información que se mantiene en las variables de instancia.

Como normalmente los programadores pueden examinar el código que ha escrito otro programador, el programador que usa una clase se ve tentado por realizar la operación accesando directamente las variables de instancia del objeto. Este acceso podría ocurrir en cualquier parte del programa completo. Este es un atajo que permite resolver rápidamente el problema. Sin embargo, más tarde el programador que ha implementado la clase podría decidir introducir cambios en las variables de instancia, provocando que el código del primer programador deje de funcionar.

Si se abusa de este tipo de atajos en un programa de gran envergadura, éste se puede tornar incomprensible para todo el grupo. Simplemente, ningún programador puede modificar el código que ya escribió, porque esto haría que el todo deje de funcionar.

La forma correcta de proceder en estos casos es agregar la operación a la clase y en lo posible accesar las variables de instancia en la misma clase, de modo que si se introducen modificaciones en las variables de instancia, será evidente que hay que cambiar el nuevo método, gracias a su cercanía con el resto de los métodos. Una de las gracias del mecanismo de clases es que permite concentrar todos los posibles usos de las variables de instancia en un solo archivo, evitando así que queden diseminados por todas partes. La única forma de operar con las variables de instancia debería ser por medio de los métodos de la clase.

La modificación de una clase la puede realizar tanto el programador que usa la clase como el que la definió, pero lo importante es que la operación quede definida en la misma clase en donde se han declarado las variables de instancia.


Encapsulación

Para encausar a los programadores hacia el correcto uso del mecanismo de clases, algunos lenguajes de programación ofrecen herramientas para prohibir el acceso a las variables de instancia de una clase. La idea es que los programadores agreguen el atributo private a las variables de instancia:

    class Tiempo extends Program {
      private int horas;
      private int min;
      ...
    }
Con este atributo, si un programador quisiera accesar las variables de instancia horas y min fuera de la clase, se producirá un error de compilación:

    class Uso extends Program {
      void run() {
        Tiempo t= new Tiempo(10, 20);
        t.horas= -4; // error en tiempo de compilación
        ...
      }
    }
De esta forma, a futuro se podrá cambiar la representación de los objetos de la clase Tiempo, asegurándose que ninguna clase usuaria de Tiempo se verá afectada por el cambio.


Encapsulación por medio de paquetes de clases

El mecanismo de encapsulación por medio del atributo de privacidad no es suficiente. Existen muchísimos problemas en donde se hace necesario definir dos clases que se accesan mutuamente las variables de instancia. Esto impide que se puedan declarar sus variables de instancia como privadas. Y por lo tanto un programador usuario de estas clases podría accesar esas variables desde una tercera clase, lo cual nos gustaría evitar.

Para ello Java ofrece el mecanismo de encapsulación por medio de paquetes de clases. La idea es que fuera de un paquete de clases sólo es posible accesar las variables de instancia y métodos que hayan sido definidos con el atributo public. Los programadores del paquete de clases especifican como públicas sólo aquellas clases y operaciones contempladas para los usuarios del paquete.

A modo de ejemplo, supongamos que se desea empaquetar las clases para realizar animaciones en un paquete llamado anim. Para lograr esto será necesario indicar al comienzo de cada archivo que las clases contenidas pertenecen al paquete anim:

    package anim;
Luego, se definen las clases señalando las clases y métodos que son públicas. Es decir que se pueden usar fuera del paquete:

    package anim;

    public class Glyph extends Program {
      int x, y;
      ...
      public int getX() { ... }
      public int getY() { ... }
      public void moveTo(int x, int y) { ... }
      void draw(Pizarra p) { ... }
    }
Observe que sólo se declaran públicos los métodos getX, getY y moveTo, por lo que podrán ser usados fuera del paquete (o desde otros paquetes). En cambio el método draw no es público y por lo tanto no puede ser usado por clases que están fuera del paquete anim.

    package anim;

    public class Animator extends Program {
      Pizarra p;
      public void drawAndSleep(...) {
        ...
        gl.draw(p);
        ...
      }
      ...
    }
Aunque un programador puede examinar el código y saber que existe el método draw y las variables de instancia x, y y p, no podrá utilizarla porque el compilador se lo impedirá, entregando un mensaje de error en caso que se usen fuera del paquete.

La variables de instancia pueden ser declaradas como públicas pero un buen estilo de programación es que no lo sean. Si se requiere conocer su valor desde fuera del paquete, se definen métodos que entregan su valor (como getX y getY). Pero como no hay método para obtener la pizarra asociada a un objeto de la clase Animator, no será posible dibujar cosas distintas de un glyph en esa pizarra.


Uso de paquetes de clases

Para usar las clases que se ubican en un paquete, normalmente se usa la directiva import:

    import anim.*;

    ...
Observación:

Como no es un objetivo de este curso que los alumnos realicen programas de gran envergadura como para que se hagan necesarios los mecanismos de encapsulación, no seguiremos profundizando al respecto. Sólo utilizaremos la palabra public a título de comentario para señalar que un método puede ser invocado por los usuarios de la clase.

Debe quedar claro que el uso de public es inútil cuando las clases no se han organizado en paquetes. Por otra parte, en programas pequeños resulta sencillo ser disciplinados en cuanto a la abstracción y por lo tanto los mecanismos de encapsulación pierden efectividad.