Apuntes Java: Clases – Otros aspectos

Inicialización de variables

Desde el punto de vista del lugar donde se declaran existen dos tipos de variables:

  • Variables miembro: Se declaran en una clase, fuera de cualquier método.
  • Variables locales: Se declaran y usan en un bloque de código dentro de un método.

Las variables miembro son inicializadas automáticamente, de la siguiente forma:

  • Las numéricas a 0.
  • Las booleanas a false.
  • Las char al carácter nulo (hexadecimal 0).
  • Las referencias a null. (null es un literal que indica referencia nula, que no apunta a ningún objeto.)

Las variables miembro pueden inicializarse con valores distintos de los anteriores en su declaración.

Las variables locales no se inicializan automáticamente. Se debe asignarles un valor antes de ser usadas. Si el compilador detecta una variable local que se usa antes de que se le asigne un valor produce un error. Por ejemplo:

int p;
int q = p;    // error

El compilador también produce un error si se intenta usar una variable local que podría no haberse inicializado, dependiendo del flujo de ejecución del programa. Por ejemplo:

int p;
if (. . . ) {
     p = 5 ;
}
int q = p;    // error

El compilador produce un error del tipo 'La variable podría no haber sido inicializada', independientemente de la condición del if.

Ámbito de las variables

El ámbito de una variable es el área del programa donde la variable existe y puede ser utilizada. Fuera de ese ámbito la variable, o bien no existe o no puede ser usada (que viene a ser lo mismo).

El ámbito de una variable miembro (que pertenece a un objeto) es el de la usabilidad de un objeto. Un objeto es utilizable desde el momento en que se crea y mientras existe una referencia que apunte a él. Cuando la última referencia que lo apunta sale de su ámbito el objeto queda 'perdido' y el espacio de memoria ocupado por el objeto puede ser recuperado por la JVM cuando lo considere oportuno. Esta recuperación de espacio en memoria se denomina 'recogida de basura' (Garbage collector) y se describe un poco más adelante.

El ámbito de las variables locales es el bloque de código donde se declaran. Fuera de ese bloque la variable es desconocida. Por ejemplo:

{
    int x;     // empieza el ámbito de x. (x es conocida y utilizable)
    {
        int q;    // empieza el ámbito de q. x sigue siendo conocida.
        . . .
    }            // finaliza el ámbito de q (termina el bloque de código)
     . . .        // q ya no es utilizable 
}                // finaliza el ámbito de x

Recogida de basura (Garbage collector)

Cuando ya no se necesita un objeto simplemente puede dejar de referenciarse. No existe una operación explícita para 'destruir' un objeto o liberar el área de memoria usada por él. Por otra parte cuando un objeto no está apuntado por ninguna referencia, no existe ningún mecanismo Java que permita recuperarlo. Es decir, se ha convertido en basura.  

La liberación de memoria la realiza el recolector de basura (garbage collector) que es una función de la JVM. El recolector revisa toda el área de memoria del programa y determina que objetos pueden ser borrados porque ya no tienen referencias activas que los apunten. El recolector de basura actúa cuando la JVM lo determina (tiene un mecanismo de actuación no trivial).

En ocasiones es necesario realizar alguna acción asociada a la acción de liberar la memoria asignada al objeto (como por ejemplo liberar otros recursos del sistema, como descriptores de ficheros). Esto puede hacerse codificando en la clase correspondiente un método finalize que debe declararse como

protected void finalize() throws Throwable { 
}

las clausulas protected y throws se explican en capítulos posteriores.

El método finalize de cada clase es invocando por la JVM antes de liberar la memoria por el recolector de basura, o antes de terminar la JVM. No existe un momento concreto en que las áreas de memoria son liberadas, sino que lo determina en cada momento la JVM en función de sus necesidades de espacio.

Sobrecarga de métodos

Una misma clase puede tener varios métodos con el mismo nombre siempre que se diferencien en el tipo o número de los argurmentos. Cuando esto sucede se dice que el método está sobrecargado. Por ejemplo, una misma clase podría tener los métodos:

int metodoSobrecargado() { . . .}
int metodoSobrecargado(int x) { . . .}

 Sin embargo no se puede sobrecargar cambiando sólo el tipo del valor devuelto. Por ejemplo:

int metodoSobrecargado() { . . .}
void metodoSobrecargado() { . . .}  // error en compilación

con esta definición, en la expresión y.metodoSobrecargado() la JVM no sabría que método invocar.

Se puede sobrecargar cualquier método miembro de una clase, así como el constructor.

La referencia this

En ocasiones es conveniente disponer de una referencia que apunte al propio objeto que se está manipulando. Esto se consigue con la palabra reservada this. this es una referencia implicita que tienen todos los objetos y que apunta a si mismo. Por ejemplo:

class Circulo {
    Punto centro;
    int radio;
    . . .
    Circulo elMayor(Circulo c) {
        if (radio > c.radio) return this;
        else return c;
    }
}

El método elMayor devuelve una referencia al círculo que tiene mayor radio, comparando los radios del Circulo c que se recibe como argumento y el propio. En caso de que el propio resulte mayor el método debe devolver una referencia a si mismo. Esto se consigue con la expresión return this.

También es típico el uso de this para diferenciar una variable local de una variable miembro, por ejemplo:

class Punto {
    int x , y;
    Punto (int x, int y) {
        this.x = x; // se asigna el valor de la variable x a la variable miembro x.
        this.y = y;
    }
}

La referencia null

Para asignar a una referencia el valor nulo se utiliza la constante null. El ejemplo del caso anterior se podría completar con:

class Circulo {
    Punto centro;
    int radio;
    . . .
    Circulo elMayor(Circulo c) {
         if (radio > c.radio) return this;
         else if (c.radio > radio) return c;
              else return null;
    }
}

Por otra parte, tener referencias con el valor null es una fuente de error, en tiempo de ejecución, muy frecuente. Por ejemplo;

Circulo c1 = new Circulo(new Punto(0 , 0), 2) ;
Circulo c2 = new Circulo(new Punto(3 , 3), 2) ;
Circulo mayor = c1.elMayor(c2);
int radioMayor = mayor.radio;

El ejemplo es sintácticamente correcto y se compila sin errores. Sin embargo, en ejecución, el valor devuelto por el método c1.elMayor(c2) es null, por lo que la expresión mayor.radio provoca una excepción de puntero nulo (null pointer exception), ya que mayor contiene la referencia null y no está apuntando a ningún objeto.

Ocultamiento de variables

Puede ocurrir que una variable local y una variable miembro reciban el mismo nombre (en muchos casos por error). Cuando se produce esto la variable miembro queda oculta por la variable local, durante el bloque de código en que la variable local existe y es accesible. Cuando se sale fuera del ámbito de la variable local, entonces la variable miembro queda accesible. Obsérvese esto en el ejemplo siguiente:

. . . 
String x = "Variable miembro";
. . .
void variableOculta() {
    System.out.println(x);
    {
        String x = "Variable local";
        System.out.println(x);
    }
    System.out.println(x);
}

Nota: El uso de Strings se verá en un capítulo posterior, aunque su uso aquí resulta bastante intuitivo. La llamada System.out.println envia a la consola (la salida estándar habitual) las variables que se pasan como argumentos.

La llamada al método variableOculta() producirá la siguiente salida:

Variable miembro
Variable local
Variable miembro

Se puede acceder a la variable miembro oculta usando la referencia this. En el ejemplo anterior la expresión:

System.out.println(this.x);

siempre producirá la salida 'Variable miembro', puesto que this.x se refiere siempre a la variable miembro.

Los entornos de desarrollo como Netbeans o Eclipse avisan cuando se produce un ocultamiento de variables, ya que normalmente es un error, díficil de detectar a veces.

Última actualización: 1/11/2016