Saltar al contenido
static-y-final.java · devschool

static y final en Java

Lección 17 de 39 · 13 min de lectura · Actualizado el

En esta lección
  1. Miembros de instancia y miembros de clase
  2. Un contador de instancias con ID automático
  3. Métodos static
  4. Clases de utilidad
  5. Bloques static y orden de inicialización
  6. Constantes: static final
  7. final en todos sus usos
  8. final no es lo mismo que inmutable
  9. Errores frecuentes
  10. Resumen

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 su id propio y lo incrementa para el siguiente.
  • id es final: 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 siempre Clase.miembro para lo static.

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 staticSíSolo a través de un objeto (obj.nombre)
Un método de instanciaSí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()NoCambia 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 final para que nadie herede de ella (no tendría sentido).
  • Todo es static: se usa directamente con Conversor.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 de Socio.

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:

  1. La primera vez que se usa la clase, se inicializan sus atributos static y se ejecutan sus bloques static, de arriba abajo. Solo una vez.
  2. 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:

  • static porque es la misma para todos los objetos: no tiene sentido guardar 300 copias del IVA.
  • final porque 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óndeQué impide
Variable localReasignarla después de darle valor
ParámetroReasignarlo dentro del método
AtributoCambiarlo después del constructor
MétodoQue una subclase lo sobrescriba
ClaseQue 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 static un atributo de cada objeto. Si el nombre del monitor fuera static, 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 static a todo. Compila, pero estropea el diseño. Crea un objeto o mueve el método a la clase correcta.
  • Llamar a lo static a través de un objeto (ana.getTotalSocios()). Funciona, pero engaña al lector.
  • Creer que final hace inmutable un objeto. Solo bloquea la referencia; usa List.of, copias y atributos final.
  • Escribir constantes sin static. Cada objeto tendría su propia copia del mismo valor.
  • Olvidar dar valor a un atributo final en alguno de los constructores: no compila.

Resumen

ConceptoQué hace
Atributo staticUna sola copia para toda la clase
Método staticSe llama con Clase.metodo(); sin this, no ve lo de instancia
Contador de instanciasstatic int que se incrementa en el constructor
Clase de utilidadfinal, constructor privado y métodos static
Bloque staticSe ejecuta una vez, al cargar la clase
static finalConstante: MAYÚSCULAS_CON_GUIONES
Variable finalRecibe valor una sola vez
Efectivamente finalNunca reasignada; requisito para usarla en lambdas
Método / clase finalNo se sobrescribe / no se hereda
InmutableClase 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

[Java] ¿Qué imprime este programa?
class A {
    static { System.out.println("S"); }
    A() { System.out.println("C"); }
}
// en main:
System.out.println("M");
new A();
new A();

[Java] Un compañero declara static el atributo nombre de la clase Monitor. ¿Qué imprime m1.nombre?
class Monitor {
    static String nombre;
    Monitor(String n) { nombre = n; }
}
// en main:
Monitor m1 = new Monitor("Raúl");
Monitor m2 = new Monitor("Sara");
System.out.println(m1.nombre);

[Java] Con final List<String> clases = new ArrayList<>(); ¿cuál de estas líneas NO compila?

[Java] ¿Por qué no compila contador++ dentro de main si contador es un atributo normal (no static)?
public class Mal {
    int contador = 0;
    public static void main(String[] args) {
        contador++;
    }
}

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.