static y final en Java
En esta lección
static y final son dos palabras que aparecen en casi cualquier programa Java, empezando por public static void main. La primera decide a quién pertenece algo: a cada objeto o a la clase entera. La segunda decide si puede cambiar. En la lección de encapsulación ya las viste de pasada; aquí las estudiarás a fondo, con sus reglas, el orden en que Java inicializa las cosas y los errores que más se repiten en los exámenes.
Trabajaremos con el programa de gestión de un gimnasio: socios, clases dirigidas y tarifas.
Miembros de instancia y miembros de clase
Hasta ahora, cada atributo que declarabas en una clase pertenecía a cada objeto: cada socio tiene su nombre. Son atributos de instancia. Si creas 300 socios, hay 300 nombres en memoria.
Un atributo static (o atributo de clase) pertenece a la clase, no a los objetos. Existe una sola copia, compartida por todos, y existe aunque no hayas creado ningún objeto.
Clase Socio
┌──────────────────────┐
│ static siguienteId=4 │ ← una sola copia
│ static totalSocios=3 │
└──────────────────────┘
▲ ▲ ▲
┌────┴───┐ ┌──┴─────┐ ┌┴───────┐
│ id=1 │ │ id=2 │ │ id=3 │ ← cada objeto, la suya
│ Ana │ │ Pablo │ │ Irene │
└────────┘ └────────┘ └────────┘
Para saber si algo debe ser static, pregúntate: ¿este dato es de cada socio o del gimnasio entero? El nombre es de cada socio; el número total de socios es del gimnasio.
Un contador de instancias con ID automático
El ejemplo clásico: cada socio recibe un número de socio correlativo, y la clase lleva la cuenta de cuántos se han creado.
public class Socio {
private static int siguienteId = 1; // compartido por todos
private static int totalSocios = 0;
private final int id; // propio de cada socio
private String nombre;
public Socio(String nombre) {
this.id = siguienteId;
siguienteId++;
totalSocios++;
this.nombre = nombre;
}
public static int getTotalSocios() {
return totalSocios;
}
@Override
public String toString() {
return "Socio #" + id + " (" + nombre + ")";
}
}
public class Main {
public static void main(String[] args) {
System.out.println(Socio.getTotalSocios());
Socio ana = new Socio("Ana");
Socio pablo = new Socio("Pablo");
Socio irene = new Socio("Irene");
System.out.println(ana);
System.out.println(irene);
System.out.println(Socio.getTotalSocios());
}
}
// 0
// Socio #1 (Ana)
// Socio #3 (Irene)
// 3
Fíjate en tres detalles:
Socio.getTotalSocios()se llama con el nombre de la clase, y funciona antes de crear ningún socio (devuelve 0).- Cada constructor lee el valor compartido
siguienteId, lo copia en suidpropio y lo incrementa para el siguiente. idesfinal: una vez asignado, el número de socio no cambia nunca.
Cuidado: Java también te deja escribir
ana.getTotalSocios(), y funciona, pero confunde: parece que el dato sea de Ana. Los IDE muestran un aviso. Usa siempreClase.miembropara lostatic.
Métodos static
Un método static (o método de clase) se ejecuta sin objeto. Por eso no tiene this y no puede usar atributos ni métodos de instancia directamente. Es la regla que más errores provoca:
public class Mal {
int contador = 0;
public static void main(String[] args) {
contador++;
}
}
Mal.java:5: error: non-static variable contador cannot be referenced from a static context
main es static, así que no ve contador, que es de cada objeto (¿de cuál, si no hay ninguno?). Tienes dos salidas: crear un objeto y usar su atributo, o hacer el atributo static si de verdad es de la clase. La regla resumida:
| Desde… | ¿Puede usar miembros static? | ¿Puede usar miembros de instancia? |
|---|---|---|
Un método static | Sí | Solo a través de un objeto (obj.nombre) |
| Un método de instancia | Sí | Sí, directamente |
Por eso, en los primeros programas, todos los métodos que llamabas desde main llevaban static (lo viste en métodos).
Cuándo un método debe ser static
Un método debe ser static cuando no necesita datos de ningún objeto: todo lo que usa le llega por parámetro. Si un método usa this o algún atributo de instancia, no puede serlo.
| Método | ¿static? | Por qué |
|---|---|---|
socio.renovarCuota() | No | Cambia los datos de un socio concreto |
Socio.getTotalSocios() | Sí | El total es de la clase |
Conversor.millasAKm(26.2) | Sí | Solo usa su parámetro |
Math.max(a, b) | Sí | Solo usa sus parámetros |
Clases de utilidad
Una clase de utilidad agrupa métodos static relacionados que no necesitan objetos. Java tiene varias: Math, Arrays, Collections, Objects. Para hacer la tuya, sigue el patrón:
public final class Conversor {
public static final double KM_POR_MILLA = 1.609344;
private Conversor() { } // impide hacer new Conversor()
public static double millasAKm(double millas) {
return millas * KM_POR_MILLA;
}
public static double kmAMillas(double km) {
return km / KM_POR_MILLA;
}
public static String ritmo(double km, int minutos) {
double minPorKm = minutos / km;
int min = (int) minPorKm;
int seg = (int) Math.round((minPorKm - min) * 60);
return String.format("%d:%02d min/km", min, seg);
}
}
System.out.printf("%.2f km%n", Conversor.millasAKm(26.2)); // 42.16 km
System.out.printf("%.2f millas%n", Conversor.kmAMillas(10)); // 6.21 millas
System.out.println(Conversor.ritmo(10, 52)); // 5:12 min/km
- El constructor privado evita que alguien cree objetos de una clase que no los necesita.
- La clase es
finalpara que nadie herede de ella (no tendría sentido). - Todo es
static: se usa directamente conConversor.metodo(...).
Consejo: no conviertas todo en clases de utilidad. Si te descubres pasando el mismo objeto como parámetro a muchos métodos
static(calcularCuota(socio),renovar(socio)), esos métodos deberían ser de instancia dentro deSocio.
Si usas mucho una clase de utilidad, puedes ahorrarte el nombre con import static, que se explica en paquetes.
Bloques static y orden de inicialización
Un bloque static es código que se ejecuta una sola vez, cuando Java carga la clase (la primera vez que la usas). Sirve para inicializar atributos static que necesitan más de una línea: leer una configuración, rellenar una tabla, hacer un cálculo.
public class Orden {
static class Tarifas {
static final double CUOTA_BASE;
static {
System.out.println("Bloque static de Tarifas");
CUOTA_BASE = 35.0;
}
private final String plan;
Tarifas(String plan) {
System.out.println("Constructor: " + plan);
this.plan = plan;
}
}
public static void main(String[] args) {
System.out.println("Inicio de main");
new Tarifas("Mañanas");
new Tarifas("Completo");
System.out.println(Tarifas.CUOTA_BASE);
}
}
// Inicio de main
// Bloque static de Tarifas
// Constructor: Mañanas
// Constructor: Completo
// 35.0
El orden es siempre este:
- La primera vez que se usa la clase, se inicializan sus atributos
staticy se ejecutan sus bloquesstatic, de arriba abajo. Solo una vez. - Cada vez que haces
new, se inicializan los atributos de instancia y se ejecuta el constructor.
Por eso “Bloque static” sale una sola vez, aunque creemos dos objetos. (static class Tarifas es una clase dentro de otra; las verás en clases internas.)
Constantes: static final
Una constante es un valor con nombre que no cambia nunca. Se declara static final:
staticporque es la misma para todos los objetos: no tiene sentido guardar 300 copias del IVA.finalporque no se puede modificar.
public static final double IVA = 0.21;
public static final int MAX_PLAZAS_CLASE = 25;
public static final String NOMBRE_GIMNASIO = "Gimnasio Atlántico";
Por convenio se escriben en MAYÚSCULAS_CON_GUIONES_BAJOS. Sirven para eliminar los números mágicos: if (inscritos < MAX_PLAZAS_CLASE) se entiende; if (inscritos < 25) obliga a adivinar qué es ese 25, y si cambia tendrás que buscarlo por todo el código.
Java está lleno de constantes: Math.PI (3.141592653589793), Integer.MAX_VALUE (2147483647). Si un grupo de constantes representa opciones cerradas (planes, días, estados), un enum es mejor opción.
final en todos sus usos
final significa “esto no se puede cambiar después”, pero lo que queda bloqueado depende de dónde lo pongas:
| Dónde | Qué impide |
|---|---|
| Variable local | Reasignarla después de darle valor |
| Parámetro | Reasignarlo dentro del método |
| Atributo | Cambiarlo después del constructor |
| Método | Que una subclase lo sobrescriba |
| Clase | Que otra clase herede de ella |
Variables locales y parámetros
final int x = 3;
x = 4;
error: cannot assign a value to final variable x
Una variable final no tiene que recibir su valor en la misma línea: basta con que lo reciba exactamente una vez, y el compilador lo comprueba en todos los caminos:
final int plazas;
boolean temporadaAlta = true;
if (temporadaAlta) {
plazas = 20;
} else {
plazas = 30;
}
System.out.println(plazas); // 20
En parámetros, final evita reasignarlos por error dentro del método:
static double precioConDescuento(final double precio, final int porcentaje) {
// precio = 0; no compilaría
return precio - precio * porcentaje / 100;
}
// precioConDescuento(40, 15) devuelve 34.0
Variables “efectivamente final” y lambdas
Una variable que nunca se reasigna, aunque no lleve final, es efectivamente final (effectively final). Importa porque las lambdas y las clases anónimas solo pueden usar variables locales que lo sean:
String saludo = "Hola";
saludo = "Buenas";
Runnable r = () -> System.out.println(saludo);
error: local variables referenced from a lambda expression must be final or effectively final
Quita la reasignación (o usa otra variable) y compila.
Atributos final
Un atributo final debe recibir su valor en la declaración o en el constructor, y ya no cambia. Es perfecto para lo que identifica al objeto: el id del socio, un DNI, la fecha de alta. Si olvidas asignarlo en algún constructor, el compilador avisa con variable id might not have been initialized.
Métodos y clases final
Un método final no se puede sobrescribir en una subclase, y de una clase final no se puede heredar (la herencia se explica en su lección):
class Cuota {
public final double calcularIva(double base) { return base * 0.21; }
}
class CuotaJoven extends Cuota {
@Override
public double calcularIva(double base) { return 0; } // error
}
final class Plan { }
class PlanVip extends Plan { } // error
error: calcularIva(double) in CuotaJoven cannot override calcularIva(double) in Cuota
overridden method is final
error: cannot inherit from final Plan
Se usa cuando cambiar ese comportamiento rompería algo. String, Integer y Math son clases final.
final no es lo mismo que inmutable
Esta es la confusión más habitual. En una variable de tipo objeto, final congela la referencia (a qué objeto apunta), no el contenido del objeto:
final List<String> clases = new ArrayList<>();
clases.add("Spinning"); // permitido: cambia el contenido
clases.add("Pilates");
System.out.println(clases); // [Spinning, Pilates]
// clases = new ArrayList<>(); // esto sí da error
Si quieres una lista que no se pueda modificar, necesitas una lista inmodificable, como las que crea List.of:
List<String> fijas = List.of("Yoga", "Zumba");
fijas.add("Boxeo"); // lanza UnsupportedOperationException
Clases inmutables
Un objeto inmutable no cambia nunca después de crearse. Para conseguirlo, la clase es final, todos sus atributos son private final, no hay setters y, si algo tiene que “cambiar”, se devuelve un objeto nuevo:
public final class Clase {
private final String nombre;
private final int plazas;
public Clase(String nombre, int plazas) {
if (plazas <= 0) throw new IllegalArgumentException("Plazas no válidas");
this.nombre = nombre;
this.plazas = plazas;
}
public String getNombre() { return nombre; }
public int getPlazas() { return plazas; }
public Clase conPlazas(int nuevas) {
return new Clase(nombre, nuevas); // no modifica: crea otra
}
@Override
public String toString() { return nombre + " (" + plazas + " plazas)"; }
}
Clase spinning = new Clase("Spinning", 20);
Clase ampliada = spinning.conPlazas(25);
System.out.println(spinning); // Spinning (20 plazas)
System.out.println(ampliada); // Spinning (25 plazas)
Los objetos inmutables se pueden compartir entre partes del programa (o entre hilos) sin miedo a que alguien los altere. String, Integer y las fechas de java.time son inmutables. Si la clase solo guarda datos, un record te da todo esto escrito en una línea.
Errores frecuentes
- Hacer
staticun atributo de cada objeto. Si elnombredel monitor fuerastatic, al crear “Raúl” y luego “Sara”, los dos se llamarían Sara: solo hay una copia. - Arreglar el error non-static … static context poniendo
statica todo. Compila, pero estropea el diseño. Crea un objeto o mueve el método a la clase correcta. - Llamar a lo
statica través de un objeto (ana.getTotalSocios()). Funciona, pero engaña al lector. - Creer que
finalhace inmutable un objeto. Solo bloquea la referencia; usaList.of, copias y atributosfinal. - Escribir constantes sin
static. Cada objeto tendría su propia copia del mismo valor. - Olvidar dar valor a un atributo
finalen alguno de los constructores: no compila.
Resumen
| Concepto | Qué hace |
|---|---|
Atributo static | Una sola copia para toda la clase |
Método static | Se llama con Clase.metodo(); sin this, no ve lo de instancia |
| Contador de instancias | static int que se incrementa en el constructor |
| Clase de utilidad | final, constructor privado y métodos static |
Bloque static | Se ejecuta una vez, al cargar la clase |
static final | Constante: MAYÚSCULAS_CON_GUIONES |
Variable final | Recibe valor una sola vez |
| Efectivamente final | Nunca reasignada; requisito para usarla en lambdas |
Método / clase final | No se sobrescribe / no se hereda |
| Inmutable | Clase final, atributos private final, sin setters, devuelve copias |
Ahora que sabes controlar a quién pertenece cada cosa, toca organizar las clases en carpetas: sigue con paquetes.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.