La Palabra Clave static en Java

La Palabra Clave static en Java

1. Introducción

En este tutorial, exploraremos en detalle el modificador static del lenguaje Java.

Descubriremos cómo podemos aplicar la palabra clave static a variables, métodos, bloques y clases anidadas, y qué diferencia marca.

2. La Anatomía de la Palabra Clave static

En el lenguaje de programación Java, la palabra clave static significa que el miembro en particular pertenece a un tipo en sí mismo, en lugar de pertenecer a una instancia de ese tipo.

Esto significa que crearemos solo una instancia de ese miembro estático que se comparte entre todas las instancias de la clase.

Variables estáticas compartidas en Java

Podemos aplicar la palabra clave a variables, métodos, bloques y clases anidadas.

3. Los Campos static (o Variables de Clase)

En Java, cuando declaramos un campo como static, se crea exactamente una copia de ese campo y se comparte entre todas las instancias de esa clase.

No importa cuántas veces instanciemos una clase. Siempre habrá una sola copia del campo static perteneciente a ella. El valor de este campo static se comparte entre todos los objetos de la misma clase.

Desde la perspectiva de la memoria, las variables estáticas se almacenan en la memoria heap.

3.1. Ejemplo del Campo static

Supongamos que tenemos una clase Coche con varios atributos (variables de instancia).

Cada vez que creamos nuevos objetos a partir de esta plantilla Coche, cada nuevo objeto tendrá su copia distintiva de estas variables de instancia.

Sin embargo, supongamos que queremos una variable que mantenga el conteo del número de objetos Coche instanciados y se comparta entre todas las instancias para que puedan acceder a ella e incrementarla al inicializarse.

Ahí es donde entran en juego las variables static:

public class Coche {
    private String nombre;
    private String motor;
    
    public static int cantidadDeCoches;
    
    public Coche(String nombre, String motor) {
        this.nombre = nombre;
        this.motor = motor;
        cantidadDeCoches++;
    }

    // getters y setters
}

Ahora, por cada objeto de esta clase que instanciemos, la misma copia de la variable cantidadDeCoches se incrementará.

Entonces, para este caso, esto será cierto:

@Test
public void cuandoSeInicializanObjetosDeCoche_LaCuentaEstaticaAumenta() {
    new Coche("Seat", "Panda");
    new Coche("Porsche", "Taycan");
 
    assertEquals(2, Coche.cantidadDeCoches);
}

3.2. Razones para Usar Campos static

Aquí hay algunas razones por las cuales querríamos usar campos static:

  • Cuando el valor de la variable es independiente de los objetos.
  • Cuando se supone que el valor debe ser compartido entre todos los objetos.

3.3. Puntos Clave a Recordar

Dado que las variables static pertenecen a una clase, podemos acceder a ellas directamente utilizando el nombre de la clase. Por lo tanto, no necesitamos ninguna referencia a un objeto.

Solo podemos declarar variables static a nivel de clase.

Podemos acceder a campos static sin inicializar un objeto.

Finalmente, podemos acceder a campos static utilizando una referencia a un objeto (como ford.cantidadDeCoches++). Pero debemos evitar esto porque se vuelve difícil determinar si es una variable de instancia o una variable de clase. En su lugar, siempre debemos referirnos a las variables static utilizando el nombre de la clase (Coche.cantidadDeCoches++).

4. Los Métodos static (o Métodos de Clase)

Al igual que con los campos static, los métodos static también pertenecen a una clase en lugar de a un objeto. Por lo tanto, podemos llamarlos sin crear un objeto de la clase en la que residen.

4.1. Ejemplo del Método static

Generalmente, usamos métodos static para realizar una operación que no depende de la creación de una instancia.

Para compartir código entre todas las instancias de esa clase, lo escribimos en un método static:

static void establecerCantidadDeCoches(int cantidadDeCoches) {
    Coche.cantidadDeCoches = cantidadDeCoches;
}

También comúnmente usamos métodos static para crear clases de utilidad o ayudantes para que podamos obtenerlos sin crear un nuevo objeto de esas clases.

Como ejemplos, podemos observar las clases de utilidad de Collections o Math en JDK, StringUtils de Apache o CollectionUtils del framework Spring y notar que todos sus métodos de utilidad son static.

4.2. Razones para Usar Métodos static

Echemos un vistazo a algunas razones por las que querríamos usar métodos static:

  • Para acceder/manipular variables estáticas y otros métodos estáticos que no dependen de objetos.
  • Los métodos static se usan ampliamente en clases de utilidad y ayudantes.

4.3. Puntos Clave a Recordar

Los métodos static en Java se resuelven en tiempo de compilación. Dado que la sobrescritura de métodos es parte del polimorfismo en tiempo de ejecución, los métodos static no pueden ser sobrescritos.

Los métodos abstractos no pueden ser static.

Los métodos static no pueden usar las palabras clave this o super.

Las siguientes combinaciones de métodos y variables de instancia y de clase son válidas:

  • Los métodos de instancia pueden acceder directamente tanto a los métodos de instancia como a las variables de instancia.
  • Los métodos de instancia también pueden acceder directamente a variables y métodos static.
  • Los métodos static pueden acceder a todas las variables static y a otros métodos static.
  • Los métodos static no pueden acceder directamente a variables de instancia y métodos de instancia. Necesitan alguna referencia de objeto para hacerlo.

4.4. Llamar a un Método No static en un Método static en Java

Para llamar a un método no estático en un método static, debemos usar una instancia de la clase que contiene el método no estático. Este es un caso de uso común al llamar a un método no estático en el método static main(), por ejemplo.

Consideremos un ejemplo de la clase Coche que presentamos anteriormente en este artículo y que define los siguientes métodos:

public String getNombre() {
    return nombre;
}

public String getMotor() {
    return motor;
}

public static String getInformacionDeCoches(Coche coche) {
    return coche.getNombre() + "-" + coche.getMotor();
}

Como podemos ver, estamos llamando a los métodos getNombre() y getMotor(), que son métodos no estáticos, en el método static getInformacionDeCoches(). Esto solo es posible porque usamos una instancia del objeto Coche para acceder a estos métodos. De lo contrario, obtendríamos este mensaje de error: "Non-static method ‘getNombre()’ cannot be referenced from a static context" ("Método no estático 'getNombre()' no puede ser referenciado desde un contexto estático").

5. Bloques static

Usamos un bloque static para inicializar variables static. Aunque podemos inicializar variables static directamente durante la declaración, hay situaciones en las que necesitamos realizar un procesamiento multilineal. En tales casos, los bloques static son útiles.

5.1. El Ejemplo del Bloque static

Por ejemplo, supongamos que queremos inicializar un objeto List con algunos valores predefinidos.

Esto se vuelve fácil con bloques static:

public class DemoBloqueEstatico {
    public static List<String> rangos = new LinkedList<>();

    static {
        rangos.add("Oro");
        rangos.add("Platino");
        rangos.add("Diamante");
    }
    
    static {
        rangos.add("Master");
        rangos.add("Grand Master");
    }
}

No sería posible inicializar un objeto List con todos los valores iniciales junto con la declaración. Por eso hemos utilizado el bloque static aquí.

5.2. Razones para Usar Bloques static

A continuación, algunas razones para usar bloques static:

  • Si la inicialización de variables static requiere alguna lógica adicional aparte de la asignación.
  • Si la inicialización de variables static propensas a errores y necesita manejo de excepciones.

5.3. Puntos Clave a Recordar

Una clase puede tener varios bloques static.

Los campos static y los bloques static se resuelven y ejecutan en el mismo orden en que están presentes en la clase.

6. Una Clase static

Java nos permite crear una clase dentro de otra clase. Proporciona una forma de agrupar elementos que solo utilizaremos en un solo lugar. Esto ayuda a mantener nuestro código más organizado y legible.

En general, la arquitectura de clases anidadas se divide en dos tipos:

  • Las clases anidadas que declaramos como static se llaman clases anidadas static.
  • Las clases anidadas que no son static se llaman clases internas.

La diferencia principal entre estas dos es que las clases internas tienen acceso a todos los miembros de la clase contenedora (incluidos los private), mientras que las clases anidadas static solo tienen acceso a miembros static de la clase externa.

De hecho, las clases anidadas static se comportan exactamente como cualquier otra clase de nivel superior, pero están contenidas en la única clase que las accederá, para proporcionar una mejor comodidad en el empaquetado.

6.1. Ejemplo de Clase static

El enfoque más ampliamente utilizado para crear objetos singleton es a través de una clase anidada static:

public class Singleton  {
    private Singleton() {}

    private static class SingletonHolder {
        public static final Singleton instancia = new Singleton();
    }

    public static Singleton obtenerInstancia() {
        return SingletonHolder.instancia;
    }
}

Usamos este método porque no requiere sincronización y es fácil de aprender e implementar.

Otro ejemplo de clase anidada static, donde se muestra la visibilidad entre los miembros de la clase principal y anidada, y viceversa:

public class Pizza {

    private static String cantidadCocinada;
    private boolean esMasaDelgada;

    public static class ContadorVentasDePizza {

        private static String cantidadOrdenada;
        public static String cantidadEntregada;

        ContadorVentasDePizza() {
            System.out.println("Campo estático de la clase contenedora es "
              + Pizza.cantidadCocinada);
            System.out.println("Campo no estático de la clase contenedora es "
              + new Pizza().esMasaDelgada);
        }
    }

    Pizza() {
        System.out.println("Campo no estático público de la clase estática es "
          + ContadorVentasDePizza.cantidadEntregada);
        System.out.println("Campo no estático privado de la clase estática es "
          + ContadorVentasDePizza.cantidadOrdenada);
    }

    public static void main(String[] a) {
        new Pizza.ContadorVentasDePizza();
    }
}

El resultado cuando ejecutamos el método principal es:

Campo estático de la clase contenedora es null
Campo no estático de la clase contenedora es false
Campo no estático público de la clase estática es null
Campo no estático privado de la clase estática es null

6.2. Razones para Usar una Clase static

Veamos algunas razones para usar clases anidadas static en nuestro código:

  • Agrupar clases que solo se usarán en un lugar aumenta la encapsulación.
  • Acercamos el código al único lugar que lo utilizará. Esto aumenta la legibilidad y facilita el mantenimiento.
  • Si una clase anidada no requiere acceso a los miembros de instancia de su clase contenedora, es mejor declararla como static. De esta manera, no estará acoplada a la clase externa y, por lo tanto, será más óptima, ya que no requerirá memoria heap o memoria stack.

6.3. Puntos Clave a Recordar

Básicamente, una clase anidada static no tiene acceso a ningún miembro de instancia de la clase contenedora. Solo puede acceder a través de una referencia de objeto.

Las clases anidadas static pueden acceder a todos los miembros estáticos de la clase contenedora, incluidos los privados.

La especificación de programación de Java no nos permite declarar la clase de nivel superior como static. Solo las clases dentro de las clases (clases anidadas) pueden ser declaradas como static.

7. Comprendiendo el Error "Non-static variable cannot be referenced from a static context"

Por lo general, este error ocurre cuando usamos una variable no estática dentro de un contexto estático.

Como vimos anteriormente, las variables estáticas pertenecen a la clase y se cargan en el momento de la carga de la clase. Por otro lado, necesitamos crear un objeto para hacer referencia a las variables no estáticas.

Por lo tanto, el compilador de Java se queja porque se necesita un objeto para llamar o usar variables no estáticas.

Ahora que sabemos qué causa el error, ilustrémoslo con un ejemplo:

public class MiClase { 
    int variableDeInstancia = 0; 
    
    public static void metodoEstatico() { 
        System.out.println(variableDeInstancia); 
    } 
    
    public static void main(String[] args) {
        MiClase.metodoEstatico();
    }
} 

Como podemos ver, estamos usando variableDeInstancia, que es una variable no estática, dentro del método estático metodoEstatico.

Como resultado, obtendremos el error "Non-static variable cannot be referenced from a static context" (No se puede hacer referencia a una variable no estática desde un contexto estático).

8. Conclusión

En este artículo, vimos en acción la palabra clave static.

También discutimos las razones y ventajas de usar campos estáticos, métodos estáticos, bloques estáticos y clases anidadas estáticas.

Finalmente, aprendimos qué provoca que el compilador falle con el error "No se puede hacer referencia a una variable no estática desde un contexto estático".