Tutorial de Java

Objetos

Anterior | Siguiente
Creación de Objetos

Un objeto es (de nuevo) una instancia de una clase. Tanto en Java como en C++, la creación de un objeto se realiza en tres pasos (aunque se pueden combinar):

  • Declaración, proporcionar un nombre al objeto
  • Instanciación, asignar memoria al objeto
  • Inicialización, opcionalmente se pueden proporcionar valores iniciales a las variables de instancia del objeto

Cuando se trata de Java, es importante reconocer la diferencia entre objetos y variables de tipos básicos, porque en C++ se tratan de forma similar, cosa que no ocurre en Java.

Tanto en Java como en C++ la declaración e instanciación de una variable de tipo básico, utiliza una sentencia que asigna un nombre a la variable y reserva memoria para almacenar su contenido:

    int miVariable;

En los dos lenguajes se puede inicializar la variable con un valor determinado en el momento de declararla, es decir, podemos resumir los tres pasos anteriormente citados de declaración, instanciación e inicialización, en una sola sentencia:

    int miVariable = 7;

Y, lo más importante, este sucede en tiempo de compilación.

C++ puede declarar e instanciar variables de tipo primitivo u objetos, desde clases en tiempo de ejecución, de forma que el compilador no sabe, en tiempo de compilación, dónde va a estar almacenado el objeto o la variable. Esto es lo que se llama instanciación de un objeto o variable en memoria dinámica.

El ejemplo java501.cpp muestra la instanciación e inicialización de variables y objetos en memoria dinámica. También muestra la instanciación e inicialización de objetos y variables estáticas y la instanciación de arrays en memoria dinámica. En C++ cuando se instancia un array en memoria dinámica no se permite la inicialización.

Por suerte o por desgracia, Java es mucho más restrictivo que C++. Por ejemplo, Java no permite la instanciación de variables de tipos básicos en memoria dinámica, aunque hay una serie de objetos que coinciden con los tipos básicos y que pueden utilizarse para este propósito. Java tampoco permite la instanciación de objetos en memoria estática.

Cuando C++ instancia un array de objetos en memoria dinámica, lo que hace es instanciar el array de objetos y devolver un puntero al primer objeto. En Java, cuando se instancia un array (que siempre ha de ser en memoria dinámica), se instancia un array de referencias, o punteros (llámese como se quiera) a los objetos. En Java, es necesario utilizar múltiples veces el operador new para instanciar un array de objetos, mientras que en C++ sólo es necesario su uso una única vez. El siguiente código de ejemplo, java501.java, corresponde a la aplicación Java que realiza el mismo trabajo que el programa C++ visto antes, pero con las restricciones que se acaban de enumerar. Se ha incorporado la presentación en pantalla no sólo del dato al que apunta un objeto, sino también de la dirección en la que se encuentra almacenado ese dato (la dirección del objeto). La salida del programa sería:

C:\> java java501
miVariableInt contiene 6
miPunteroObj contiene java501@208741
miPunteroObj apunta a 12
miArrayPunterosObj contiene java501@20875f java501@208760 java501@208761
miArrayPunterosObj apunta a 13 14 15

El formato en que se presenta un objeto en Java, cuando el compilador no conoce el tipo de que se trata, consiste en el identificador de la clase y el valor hexadecimal de la dirección.

En Java no siempre es necesaria la declaración de un objeto (darle un nombre). En el siguiente ejemplo, java502.java, se instancia un nuevo objeto que se usa en una expresión, sin previamente haberlo declarado.

import java.util.*;
        
class java502 { 
    public static void main( String args[] ) {
        System.out.println( new Date() );
        }
    }

La inicialización de un objeto, tanto en C++ como en Java, se puede realizar utilizando las constantes de la clase, de forma que un objeto de un mismo tipo puede ser declarado e inicializado de formas diferentes.

Utilización de Objetos

Tanto en Java como en C++, una vez que se tiene declarado un objeto con sus variables y sus métodos, podemos acceder a ellos para que el uso para el que se ha creado ese objeto entre en funcionamiento. No hay diferencias apreciables entre los dos lenguajes a la hora de la invocación de los métodos de un objeto. Sin embargo, algunos métodos o variables pueden estar ocultos y el acceso a ellos resultar imposible.

Para acceder a variables o métodos en Java o en C++, se especifica el nombre del objeto y el nombre de la variable, o método, separados por un punto (.). En C++, si un puntero contiene la dirección de un objeto, se puede acceder a los miembros de ese objeto utilizando el separador flecha (->).

En las siguientes sentencias, se muestran las formas de acceso en Java y en C++:

cout << "miObjeto contiene " << miObjeto.getData() << endl;
cout << "miPuntero apunta a " << miPuntero->getData() << endl;
System.out.println( "miObjeto apunta a "+miObjeto.getData() );
cout << "La fecha es " << fecha.dataArray << endln;
fecha.displayFecha( dateContainer( "29/07/97" );

Destrucción de Objetos

En C++, el programador es el responsable de la destrucción de objetos y liberación de los recursos que esos objetos estaban ocupando, como son: la memoria dinámica arrebatada al sistema operativo, el cierre de ficheros, la desconexión de canales de comunicación, etc. Para este propósito, C++ dispone de destructores. El destructor es un método especial o una función miembro de la clase que siempre se ejecutará cuando el objeto se destruya. Por tanto, se podría colocar el código necesario para realizar las acciones anteriores en este método y confiar en que se ejecutará.

En el ejemplo java503.cpp, se muestra la acción automática del destructor. En él, se instancia un objeto en memoria dinámica dentro de un bloque de ámbito limitado. Cuando el control del programa se sale fuera del ámbito del bloque, se ejecuta el destructor, devolviendo la memoria al sistema operativo y presentando en pantalla un chivato de la invocación.

Aunque es responsabilidad del programador en C++ el proporcionar el código necesario para devolver la memoria al sistema operativo, volvemos a reiterar. El programador puede estar completamente seguro de que el código que coloque en el destructor se ejecutará siempre que ese objeto se salga del ámbito de ejecución del programa y que puede incluir el código que desee dentro del destructor.

En Java, las cosas cambian para mejor. Aquí, la buena noticia es que el programador Java no necesitará preocuparse más de devolver la memoria, ese bien tan preciado, al sistema operativo. Eso se realizará automáticamente de la mano del reciclador de basura, que también es bien conocido como garbage collector.

Sin embargo, hay una mala noticia. Y es que en Java no hay soporte para algo parecido a un destructor, que pueda garantizar su ejecución cuando el objeto deje de existir. Por lo tanto, excepto la devolución de memoria al sistema operativo; la liberación de los demás recursos que el objeto utilizase vuelve a ser responsabilidad del programador. Pero, algo se ha ganado, ya no debería aparecer más el fatídico mensaje de "No hay memoria suficiente", o su versión inglesa de "Out of Memory".

Liberación de Memoria

El único propósito para el que se ha creado el reciclador de memoria es para devolver al sistema operativo la memoria ocupada por objetos que no es necesaria. Un objeto es blanco del garbage collector para su reciclado cuando ya no hay referencias a ese objeto. Sin embargo, el que un objeto sea elegido para su reciclado, no significa que eso se haga inmediatamente.

El garbage collector de Java comprueba todos los objetos, y aquellos que no tienen ninguna (esto es, que no son referenciados por ningún otro objeto) son marcados y liberada la memoria que estaban consumiendo.

El garbage collector es un thread, o hilo de ejecución, de baja prioridad que puede ejecutarse síncrona o asíncronamente, dependiendo de la situación en que se encuentre el sistema Java. Se ejecutará síncronamente cuando el sistema detecta poca memoria o en respuesta a un programa Java. El programador puede activar el garbage collector en cualquier momento llamando al método System.gc(). Se ejecutará asíncronamente cuando el sistema se encuentre sin hacer nada (idle) en aquellos sistemas operativos en los que para lanzar un thread haya que interrumpir la ejecución de otro, como es el caso de Windows 95/NT.

No obstante la llamada al sistema para que se active el garbage collector no garantiza que la memoria que consumía sea liberada.

A continuación se muestra un ejemplo sencillo de funcionamiento del garbage collector:

String s;                // no se ha asignado memoria todavía
s = new String( "abc" ); // memoria asignada
s = "def";               // se ha asignado nueva memoria 
                         // (nuevo objeto)

Más adelante veremos en detalle la clase String, pero una breve descripción de lo que hace esto es; crear un objeto String y rellenarlo con los caracteres "abc" y crear otro (nuevo) String y colocarle los caracteres "def".

En esencia se crean dos objetos:

  • Objeto String "abc"
  • Objeto String "def"

Al final de la tercera sentencia, el primer objeto creado de nombre s que contiene "abc" se ha salido de alcance. No hay forma de acceder a él. Ahora se tiene un nuevo objeto llamado s y contiene "def". Es marcado y eliminado en la siguiente iteración del thread reciclador de memoria.

El Método finalize()

Antes de que el reciclador de memoria reclame la memoria ocupada por un objeto, se puede invocar el método finalize(). Es te método es miembro de la clase Object, por lo tanto, todas las clase lo contienen. La declaración, por defecto, de este método es:

    protected void finalize() {
        }

Para utilizar este método, hay que sobrecargarlo, proporcionando el código que contenga las acciones que se desee ejecutar antes de liberar la memoria consumida por el objeto. Este método no sustituye a los destructores de C++. Aunque se puede asegurar que el método finalize() se invocará antes de que el garbage collector reclame la memoria ocupada por el objeto, no se puede asegurar cuando se liberará esa memoria; e incluso, si esa memoria será liberada.

El ejemplo java504.java trata de ilustrar todo esto. Este programa instancia una gran cantidad de objetos y monitoriza la actividad del garbage collector. En concreto, el programa instancia los objetos y los hace seleccionables inmediatamente por el garbage collector para su reciclado (no asignándolos a variable alguna, con lo cual no hay ninguna referencia a ellos, que es la condición para que sean marcados para su reciclaje). Cuando el objeto 1000 es finalizado, se fija el flagSalida, que está monitorizado en el thread principal, para terminar el programa cuando sea true. Sin embargo, si se observa la ejecución, el garbage collector finaliza 86 objetos más, después de haberse fijado el flagSalida a true antes de que se devuelva el control al thread principal y pueda concluir el programa. La salida por pantalla de la ejecución sería:

%java java504
Iniciado el reciclado en el Objeto numero 0
Creado el objeto #1000
Finalizado el objeto #1000. Fija la salida a true
Objetos totales creados = 1087
Objetos totales finalizados = 1086
86 objetos fueron finalizados despues de fijar la Salida

Si se experimenta con distintos valores para el número de objetos creados, se podrá observar un funcionamiento diferente del garbage collector y además, de forma impredecible.

Reiteramos que el método finalize() no es un destructor al uso como en C++. Nunca se puede asegurar cuando se ejecutará, o incluso, si será ejecutado. Por tanto, si se necesita que un objeto libere los recursos que estaba consumiendo, antes de que el programa finalice, no se puede confiar esa tarea al método finalize(), sino que hay que llamar a métodos que realicen esas tareas explícitamente.

Además del método System.gc(), que permite invocar al garbage collector en cualquier instante, también puede se posible forzar la ejecución de los métodos finalize() de los objetos marcados para reciclar, llamando al método:

System.runFinalization();

Pero, la circunstancia explicada anteriormente sigue vigente, es decir, nadie garantiza que esto libere todos los recursos y, por lo tanto, que se produzca la finalización del objeto.

Si se modifica el programa del ejemplo anterior, para incluir la finalización, tal como se muestra en el ejemplo java505.java, se podrá observar esta circunstancia. La clase principal del ejemplo quedaría como muestra el trozo de código que se reproduce a continuación.

public class java505 {
    volatile static int objContador = 0; 
        
    public static void main( String args[] ) {
        while( objContador < 1200 ) {
            // Solicita el reciclado cada 120 objetos
            if( (objContador % 120 ) == 0 )
                System.gc();
            new Obj( objContador++ ); 
            }
        
        System.out.println( "Objetos totales creados = " + Obj.objCreados );
        System.out.println( "Objetos totales finalizados = " + Obj.objFinalizados );
        // Intenta forzar la finalizacion de los objetos
        System.out.println( "Intenta forzar la finalizacion." );
        System.runFinalization();
        System.out.println( "Objetos totales finalizados = " + Obj.objFinalizados );
        }
    }

La salida del programa por pantalla en esta ocasión sería:

%java java505
Iniciado el reciclado en el Objeto numero 0
Creado el objeto #1000
Finalizado el objeto #1000. Fija la salida a true
Objetos totales creados = 1200
Objetos totales finalizados = 1010
Intenta forzar la finalizacion.
Objetos totales finalizados = 1089

Como se puede observar, solamente se han finalizado 1080 objetos de los 1200 instanciados. 1010 de ellos fueron finalizados cuando se instanciaron, por lo tanto, parece una acción del garbage collector. Posteriormente, se finalizaron 79, posiblemente debido a la invocación del método de finalización, System.runFinalization().

Navegador

Home | Anterior | Siguiente | Indice | Correo