Ámbito de variables en Java

Hace ocho días publiqué el primer reto Bug Master, en el cual se introdujo un error de ámbito de variable. Hay que reconocer que la solución al reto no era muy complicada y la mayoría de vosotros es capaz de resolverlo, pero algo me dice que algunos os estáis preguntando ahora mismo: ¿qué demonios es el ámbito de variable? En este artículo lo vamos a explicar.

Definición de ámbito

El ámbito de una variable define su alcance de uso, o lo que es lo mismo, en que secciones de código una variable estará disponible. Fuera de este ámbito, una variable no podrá ser accedida (no existe).

Tipos de ámbito

En Java tenemos tres tipos de ámbito que pueden aplicar a una variable:

  • Local
  • Global
  • Estático

Veamos un fragmento de código donde aparecen los tres tipos, antes de pasar a explicar con un mínimo de detalle cada uno de ellos:

public class MiClase {
	static int variableEstatica;
	
	int variableGlobal;
	
	void miMetodo(int parametro) {
		int variableLocal;

		// parametro también es local dentro del método
	}
}
	

Variables de ámbito local

Las variables de ámbito local, o de bloque, son aquellas que sólo pueden ser accedidas desde el bloque de código en el que han sido declaradas (que no inicializadas).

void miMetodo(int parametro) {
	int variableLocal = new Random().nextInt();
		
	System.out.println("El valor de variableLocal es: " + variableLocal);
	System.out.println("EL valor de parámetro es: " + parametro);
}
	

En el ejemplo anterior, tenemos dos variables de ámbito local, parametro y variableLocal. Ambas pueden ser accedidas desde cualquier parte del método miMetodo(), ya que es el bloque donde han sido declaradas. Otro ejemplo de variable de ámbito local es aquella declarada dentro de un bucle o un bloque condicional (en cuyo caso sólo existe dentro de éste). En resumen, la variable pertenece al bloque donde ha sido declarada.

Variables de ámbito global

Las variables de ámbito global, o de instancia, son aquellas que pertenecen a cada instancia concreta de la clase donde han sido declaradas, y dependiendo del modificador de visibilidad usado (más tarde hablaremos sobre ésto) podrían ser sólo accedidas desde la propia instancia a la que pertenecen:

public class MiClase {
	int variableGlobal;
	
	void miMetodo() {
		System.out.println("Valor de variableGlobal: " + variableGlobal);
	}
}

public class OtraClase {
	void otroMetodo() {
		// Error de compilación, no es visible
		System.out.println("Valor de variableGlobal: " + variableGlobal);
	}
}
	

Cada instancia de la clase MiClase tendrá su propia copia de la variable variableGlobal, y modificar el valor de dicha variable en una instancia en concreto no afectará a los valores de la misma variable en el resto de (posibles) instancias.

Variables estáticas

Las variables estáticas, o de clase, son aquellas que pertenecen a la propia clase donde han sido declaradas, y dependiendo del modificador de visibilidad usado podrían ser sólo accedidas desde la propia clase en la que han sido declaradas:

public class UnaClase {
	public static int variableEstatica;
}

public class OtraClase {
	void metodo() {
		System.out.println("Valor de UnaClase.variableEstatica:" + UnaClase.variableEstatica);
	}
}
	

UnaClase compartirá una única copia de la variable variableGlobal independientemente del número de inicializaciones que hagamos de la clase, y el último valor de dicha variable será el que esté vigente en cada momento. Al ser una variable de clase, repito, no es necesario crear ninguna instancia de dicha clase para poder usarla (no es necesario invocar a new).

¡Pero yo ya sé todo esto!

Efectivamente, lo que has visto es Java básico, lo que aprendemos el primer día de clase. No tiene mucho misterio, y en caso de que invoquemos una variable fuera de su ámbito seremos advertidos con un error de compilación, por lo que todos estamos a salvo sin tener que preocuparnos demasiado. Pero este artículo no va en esa dirección, si no en la dirección contraria.

Reduciendo el ámbito

La siguiente frase debes grabarla a fuego en tu cabeza desde este mismo momento:

Toda variable debe ser declarada en el ámbito más reducido posible.

Cuando una variable es declarada con un ámbito más extenso del necesario, es sólo cuestión de tiempo que usemos esa variable de forma incorrecta, provocando confusión e invitádonos a introducir bug's (si es que el ámbito incorrecto no es en si mismo ya un bug...). Vamos a ilustrarlo con un ejemplo:

public class MiClase {
	int contarUsuariosPorNombreIncompleto(String fragmentoNombreUsuario, String[] nombreUsuarios) {
		boolean encontrado = false;
		int totalEncontrados = 0;
			
		for (Usuario nombreUsuarioActual : nombreUsuarios) {
			if (nombreUsuarioActual.contains(fragmentoNombreUsuario)) {
				encontrado = true;
			}
			
			if (encontrado) {
				totalEncontrados++;
			}			
		}
		
		return totalEncontrados;
	}

	public static void main(String[] args) {
		String fragmentoNombreUsuario = "Michael";
		String[] nombreUsuarios = {"Jhon Doole", "Michael Fletcher", "James 'Jimmy X' Donald"};
		
		int resultado = new MiClase().contarUsuariosPorNombreIncompleto(fragmentoNombreUsuario, nombreUsuarios);
		System.out.println("Total resultados: " + resultado);
	}
}
	

Al ejecutar la clase anterior, podemos ver por consola la siguiente salida:

Total resultados: 2
	

¿Qué ha ocurrido? Sólo uno de los usuarios tiene en su nombre la cadena Michael, pero se nos informa que hay dos nombres de usuario coincidentes. Depuremos virtualmente la ejecución de la llamada a buscarNumeroUsuarios:

  1. Inicio de la primera iteración:
    • encontrado es false
    • totalEncontrados es 0
  2. Fin de la primera iteración:
    • encontrado es false
    • totalEncontrados es 0
  3. Inicio de la segunda iteración:
    • encontrado es false
    • totalEncontrados es 0
  4. Fin de la segunda iteración:
    • encontrado es true
    • totalEncontrados es 1
  5. Inicio de la tercera y última iteración:
    • encontrado es true (¡pero debería ser false)
    • totalEncontrados es 1
  6. Fin de la tercera y última iteración:
    • encontrado es true
    • totalEncontrados es 2

Sin entrar a valorar que el uso de dos bloques if para la lógica de nuestro problema (buscar un usuario por una fragmento de su nombre) es totalmente innecesario, es el resultado de la variable encontrado la que determina que aumentemos el contador totalEncontrados. El problema es que la variable encontrado debería reiniciarse a false en el inicio (o fin) de cada iteración, o de lo contrario en el momento en que encontremos una coincidencia el resto de iteraciones darán un falso positivo:

public class MiClase {
	int contarUsuariosPorNombreIncompleto(String fragmentoNombreUsuario, String[] nombreUsuarios) {		
		boolean encontrado = false;
		int totalEncontrados = 0;
	
		for (Usuario nombreUsuarioActual : nombreUsuarios) {
			if (nombreUsuarioActual.contains(fragmentoNombreUsuario)) {
				encontrado = true;
			}
			
			if (encontrado) {
				totalEncontrados++;
			}
			
			encontrado = false;		
		}
		
		return totalEncontrados;
	}

	// ...
}
	

Ahora al ejecutar de nuevo nuestra clase obtenemos el resultado esperado para los datos que hemos proporcionado:

Total resultados: 1
	

Bien, ya hemos resuelto nuestro odioso bug. Ahora es el momento de pararnos y evaluar la situación. Si estás pensando en dejar el código tal cual es que: eres inexperto (perdonable), o tienes prisa (no perdonable), o simplemente no te importa (punible).

Olvidando qué tipo de programador quieres ser, vamos a continuar. ¿No te parece que estamos haciendo un uso un poco retorcido de la variable encontrado?:

  • La declaramos como variable de bloque (ámbito local)...
  • ...dentro del método buscarNumeroUsuarios()...
  • ...pero sólo la estamos usando dentro del bloque for (un ámbito local más concreto)

¿Recuerdas aquella frase que debías grabarte a fuego en la cabeza?

Toda variable debe ser declarada en el ámbito más reducido posible.

Nuestra variable está siendo declarada en un ámbito superior a aquel en el que es usada, y por tanto debemos reducir su ámbito:

public class MiClase {
	int contarUsuariosPorNombreIncompleto(String fragmentoNombreUsuario, String[] nombreUsuarios) {
		int totalEncontrados = 0;
		
		for (Usuario nombreUsuarioActual : nombreUsuarios) {
			boolean encontrado = false;
			
			if (nombreUsuarioActual.contains(fragmentoNombreUsuario)) {
				encontrado = true;
			}
			
			if (encontrado) {
				totalEncontrados++;
			}
		}
		
		return totalEncontrados;
	}
	
	// ...
}
	

Ahora que la variable se encuentra en su ámbito adecuado, el código original está libre de errores, y hemos reducido el uso de la variable de tres a dos lugares, lo que significa código más limpio. ¿Podíamos reducir aún más el ámbito de la variable booleana? En este caso concreto sí, eliminándola por completo (no hay variable, no hay ámbito):

public class MiClase {
	int buscarNumeroUsuarios(String nombreParcialUsuarioBusqueda, String[] nombreUsuarios) {
		int totalEncontrados = 0;
		
		for (Usuario nombreUsuarioActual : nombreUsuarios) {
			if (nombreUsuarioActual.contains(nombreParcialUsuarioBusqueda)) {
				totalEncontrados++;
			}
		}
		
		return totalEncontrados;
	}
	
	// ...
}
	

Ahora nuestra lógica de negocio es bastante más sencilla de entender. La eliminación de la variable encontrado no es técnicamente una reducción de ámbito, si no una refactorización (mejora de código).

¿Qué más puedo hacer?

Una vez que hemos reducido el ámbito de nuestras variables hasta el nivel adecuado, existen otras técnicas (no relacionadas con la definición más pura de ámbito) que pueden ayudarnos a evitar que nuestras variables sean usadas forma incorrecta:

  • Reducir la usabilidad
  • Reducir la visibilidad

Veámoslas.

Reducir la usabilidad

Cuando tenemos una variable que sólo debería ser leída, ¿por qué no declararla como de solo lectura?:

final Date fechaDeEjecucion = new Date();
	

Declarando una variable como final, nos aseguramos que sólo pueda ser instanciada una vez. Si intentamos hacer lo siguiente, obtendremos un error de compilación:

fechaDeEjecucion = new Date();  // Error de compilación, fechaDeEjecucion es final
	

Sin embargo, no confundas instanciación (creación de un objeto a través de la palabra reservada new) con modificación:

final Date fechaDeEjecucion = new Date();
// ...

fechaDeEjecucion.setYear(2002);  // Perfectamente legal
	

Por tanto, si lo que queremos es protegernos de la modificación accidental del valor de una variable, la reducción de usabilidad sólo es útil si:

Reducir la visibilidad

La visibilidad de una variable viene definida, como ya sabes, por el uso de los modificadores public, protected, private, y en caso de omisión package-default. El uso de uno u otro operador va a declarar para quién va a ser visible una variable (y para quién no). ¿Recuerdas, de nuevo, aquella frase que debías grabarte a fuego en la cabeza?

Toda variable debe ser declarada en el ámbito más reducido posible

Pues haz sitio en tu cabeza para añadir a fuego una ligera modificación de ella:

Toda variable debe ser declarada con la visibilidad más reducida posible.

Esto es:

  • Declárala privada (private) y deja que sólo sea visible dentro de la clase...
  • ... y sólo en caso de que otras clases dentro de mismo paquete deban acceder a ella. omite cualquier declaración y deja que sea package-protected
  • ...y sólo en caso de que una subclase deba acceder a ella, declárala protegida (protected)...
  • ...y sólo en caso de que cualquier clase deba acceder a ella, declárala pública (public)

¿Estás de acuerdo conmigo? Espero que no. En el último caso, deberías hacer lo siguiente:

  • ...y sólo en caso de que cualquier clase deba acceder a ella, declárala privada y provee un método público para acceder a su valor (un getter)

Los motivos para usar la visibilidad adecuada en cada momento son los mismos que aplican al ámbito de variables: reduciendo el scope, reducimos el abuso y/o mal uso de una variable. Además, la visibilidad también afecta a la declaración de métodos, y todo lo aquí dicho es igualmente válido para éstos.

Resumen

El uso adecuado del ámbito de las variables es tan importante que, además de producir código más limpio, legible y mantenible, puede ahorrarnos muchas (MUCHAS) horas de depuración intentando corregir errores que son consecuencia de su mal uso. El ajuste de la usabilidad y la visibilidad siguen un camino paralelo, así que intenta recorrerlos sin perder de vista los demás. Creeme, tu depurador te lo agradecerá.