Modificadores de no acceso en Java

Los modificadores son palabras clave que nos permiten afinar el acceso a nuestra clase y sus miembros, su alcance y comportamiento en determinadas situaciones. Por ejemplo, podemos controlar...

Introducción

Modificadores son palabras clave que nos permiten afinar el acceso a nuestra clase y sus miembros, su alcance y comportamiento en determinadas situaciones. Por ejemplo, podemos controlar qué clases/objetos pueden acceder a ciertos miembros de nuestra clase, si una clase se puede heredar o no, si podemos anular un método más tarde, si debemos anular un método más tarde, etc.

Las palabras clave modificadoras se escriben antes del tipo y el nombre de la variable/método/clase (retorno), p. private int myVar o public String toString().

Los modificadores en Java se dividen en uno de dos grupos: acceso y sin acceso:

native no se trata con más detalle a continuación, ya que es una palabra clave simple que marca un método que se implementará en otros lenguajes, no en Java. Funciona junto con la interfaz nativa de Java (JNI). Se usa cuando queremos escribir secciones de código críticas para el rendimiento en lenguajes más amigables con el rendimiento (como C).

¿Quiere obtener más información sobre los modificadores de acceso, a diferencia de los modificadores de no acceso? Si es así, consulta nuestro artículo Modificadores de acceso en Java.

Modificadores sin acceso

Estos tipos de modificadores se usan para controlar una variedad de cosas, como las capacidades de herencia, si todos los objetos de nuestra clase comparten el mismo valor de miembro o tienen sus propios valores de esos miembros, si un método puede anularse en una subclase, etc.

Una breve descripción de estos modificadores se puede encontrar en la siguiente tabla:

Descripción general del nombre del modificador


  static      The member belongs to the class, not to objects of that class.
   final      Variable values can\'t be changed once assigned, methods can\'t be overriden, classes can\'t be inherited.
 abstract     If applied to a method - has to be implemented in a subclass, if applied to a class - contains abstract methods

sincronizado Controla el acceso de subprocesos a un bloque/método. volatile The variable value is always read from the main memory, not from a specific thread's memory. transient The member is skipped when serializing an object.

El modificador estático

El modificador static hace que un miembro de clase sea independiente de cualquier objeto de esa clase. Hay algunas características a tener en cuenta aquí:

  • Las variables declaradas estáticas se comparten entre todos los objetos de una clase (ya que la variable pertenece esencialmente a la clase misma en este caso), es decir, los objetos no tienen sus propios valores para esa variable, sino que todos comparten uno solo.
  • Se puede acceder a las variables y métodos declarados static a través del nombre de la clase (en lugar de la referencia de objeto habitual, por ejemplo, MyClass.staticMethod() o MyClass.staticVariable), y se puede acceder a ellos sin la clase que se instancia.
  • Los métodos ’estáticos’ solo pueden usar variables ’estáticas’ y llamar a otros métodos ’estáticos’, y no pueden hacer referencia a ’esto’ o ‘super’ de ninguna manera (es posible que ni siquiera exista una instancia de objeto cuando llamamos a un método ’estático’ , entonces ’esto’ no tendría sentido).

Nota: Es muy importante tener en cuenta que las variables y métodos estáticos no pueden acceder a variables y métodos no estáticos (de instancia). Por otro lado, las variables y métodos no estáticos pueden acceder a variables y métodos estáticos.

Esto es lógico, ya que los miembros estáticos existen incluso sin un objeto de esa clase, mientras que los miembros instancia existen solo después de que se haya instanciado una clase.

Variables estáticas {#variables estáticas}

Para las variables, usamos static si queremos que la variable sea común/compartida para todos los objetos.

Echemos un vistazo a cómo las variables estáticas se comportan de manera diferente a las variables de instancia regulares:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class StaticExample {
    public static int staticInt = 0;
    public int normalInt = 0;
    
    // We'll use this example to show how we can keep track of how many objects
    // of our class were created, by changing the shared staticInt variable
    public StaticExample() {
        staticInt++;
        normalInt++;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// No instances of StaticExample have been created yet
System.out.println(StaticExample.staticInt); // Prints: 0
// System.out.println(StaticExample.normalInt); // this won't work, obviously

// Let's create two instances of StaticExample
StaticExample object1 = new StaticExample();
// We can refer to static variables via an object reference as well, 
// however this is not common practice, we usually access them via class name
// to make it obvious that a variable/method is static
System.out.println(object1.staticInt); // Prints: 1
System.out.println(object1.normalInt); // Prints: 1

StaticExample object2 = new StaticExample();
System.out.println(object2.staticInt); // Prints: 2
System.out.println(object2.normalInt); // Prints: 1

// We can see that increasing object2's staticInt 
// increases it for object1 (and all current or future objects of that class)

object1.staticInt = 10;
object1.normalInt = 10;
System.out.println(object2.staticInt); // Prints: 10
System.out.println(object2.normalInt); // Prints: 1 (object2 retained its own value for normalInt as it depends on the class itself)

Métodos estáticos

El ejemplo más común de uso de static es el método main(), se declara como static porque debe llamarse antes de que exista cualquier objeto. Otro ejemplo común es la clase Math ya que usamos los métodos de esa clase sin crear una instancia de ella primero (como Math.abs()).

Una buena manera de pensar en los métodos estáticos es "¿Tiene sentido usar este método sin crear primero un objeto de esta clase?" (por ejemplo, no es necesario crear una instancia de la clase Math para calcular el valor absoluto de un número).

Los métodos estáticos se pueden utilizar para acceder y modificar miembros “estáticos” de una clase. Sin embargo, se usan comúnmente para manipular parámetros de métodos o calcular algo y devolver un valor.

Estos métodos se denominan métodos de utilidad:

1
2
3
static int average(int num1, int num2) {
    return (num1+num2)/2;
}

Este método de utilidad se puede utilizar para calcular el promedio de dos números, por ejemplo.

Como se mencionó anteriormente, la clase ‘Math’ se usa a menudo para llamar a métodos ’estáticos’. Si miramos el código fuente, podemos notar que en su mayoría ofrece métodos de utilidad:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public static int abs(int i) {
    return (i < 0) ? -i : i;
}

public static int min(int a, int b) {
    return (a < b) ? a : b;
}

public static int max(int a, int b) {
    return (a > b) ? a : b;
}

Bloques estáticos

También hay un bloque estático. Un bloque ’estático’ se ejecuta solo una vez cuando se crea una instancia de la clase por primera vez (o se llama a un miembro ’estático’, incluso si la clase no está instanciada), y antes del resto del código.

Agreguemos un bloque ’estático’ a nuestra clase ‘StaticExample’:

1
2
3
4
5
6
7
class StaticExample() {
    ...
    static {
        System.out.println("Static block");
    }
    ...
}
1
2
StaticExample object1 = new StaticExample(); // "Static block" is printed
StaticExample object2 = new StaticExample(); // Nothing is printed

Independientemente de su posición en la clase, los bloques estáticos se inicializan antes que cualquier otro bloque no estático, incluidos los constructores:

1
2
3
4
5
6
7
8
9
class StaticExample() {
    public StaticExample() {
        System.out.println("Hello from the constructor!");
    }

    static {
        System.out.println("Hello from a static block!");
    }
}

Crear una instancia de esta clase daría como resultado:

1
2
Hello from a static block!
Hello from the constructor!

Si hay varios bloques estáticos, se ejecutarán en su orden respectivo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class StaticExample {
    
    static {
        System.out.println("Hello from the static block! 1");
    }
    
    public StaticExample() {
        System.out.println("Hello from the constructor!");
    }
    
    static {
        System.out.println("Hello from the static block! 2");
    }
}

Crear una instancia de esta clase daría como resultado:

1
2
3
Hello from the static block! 1
Hello from the static block! 2
Hello from the constructor!

Importaciones estáticas

Como ya se mencionó, es mejor llamar a los miembros estáticos con el prefijo del nombre de la clase, en lugar del nombre de la instancia. Además, en algunos casos, nunca instanciamos realmente una clase con métodos estáticos, como la clase Math, que ofrece numerosos métodos de utilidad relacionados con las matemáticas.

Dicho esto, si usamos una clase de miembros ’estáticos’ a menudo, podemos importar miembros individuales o todos usando una ‘importación estática’. Esto nos permite omitir el prefijo de sus llamadas con el nombre de la clase:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package packageOne;

public class ClassOne {
    static public int i;
    static public int j;

    static public void hello() {
        System.out.println("Hello World!");
    }
}
1
2
3
4
5
6
7
8
9
package packageTwo;

static import packageOne.ClassOne.i;

public class ClassTwo {
    public ClassTwo() {
        i = 20;
    }
}

O, si quisiéramos importar todos los miembros estáticos de ClassOne, podríamos hacerlo así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package packageTwo;

import static packageOne.ClassOne.*;

public class ClassTwo {
    public ClassTwo() {
        i = 20;
        j = 10;
    }
}

Lo mismo se aplica a los métodos:

1
2
3
4
5
6
7
8
9
package packageTwo;

import static packageOne.ClassOne.*;

public class ClassTwo {
    public ClassTwo() {
        hello();
    }
}

Ejecutar esto generaría:

1
Hello World!

Esto puede no parecer tan importante, pero ayuda cuando llamamos a muchos miembros estáticos de una clase:

1
2
3
4
5
6
7
8
9
public int someFormula(int num1, int num2, int num3) {
    return Math.ceil(Math.max(Math.abs(num1), Math.abs(num2))+Math.max(Math.abs(num2), Math.abs(num3)))/(Math.min(Math.abs(num1), Math.abs(num2))+Math.min(Math.abs(num2), Math.abs(num3)));
}

// Versus...
import static java.lang.Math.*;
public int someFormula(int num1, int num2, int num3) {
    return ceil(max(abs(num1), abs(num2))+max(abs(num2), abs(num3)))/(min(abs(num1), abs(num2))+min(abs(num2), abs(num3)));
}

El modificador final

La palabra clave final puede tener uno de tres significados:

  • para definir constantes con nombre (variables cuyos valores no pueden cambiar después de la inicialización)
  • para evitar que un método sea anulado
  • para evitar que una clase sea heredada

Constantes con nombre

Agregar el modificador final a una declaración de variable hace que esa variable no se pueda cambiar una vez que se inicializa.

El modificador final se usa a menudo junto con el modificador static si estamos definiendo constantes. Si solo aplicamos static a una variable, todavía se puede cambiar fácilmente. También hay una convención de nomenclatura vinculada a esto:

1
static final double GRAVITATIONAL_ACCELERATION = 9.81;

Variables como estas a menudo se incluyen en clases de utilidad, como la clase Math, acompañadas de numerosos métodos de utilidad.

Aunque, en algunos casos, también garantizan sus propias clases, como Constants.java:

1
2
3
public static final float LEARNING_RATE = 0.3f;
public static final float MOMENTUM = 0.6f;
public static final int ITERATIONS = 10000;

Nota: cuando utilice final con variables de referencia de objetos, tenga cuidado con el tipo de comportamiento que espera. Considera lo siguiente:

1
2
3
4
5
6
7
8
9
class MyClass {
    int a;
    int b;

    public MyClass() {
        a = 2;
        b = 3;
    }
}
1
2
    final MyClass object1 = new MyClass();
    MyClass object2 = new MyClass();

La variable de referencia ‘objeto1’ es de hecho ‘final’ y su valor no puede cambiar, pero ¿qué significa eso para las variables de referencia de todos modos? Significa que object1 ya no puede cambiar a qué objeto apunta, pero podemos cambiar el objeto en sí. Esto es algo que a menudo confunde a la gente:

1
2
    // object1 = object2; // Illegal!
    object1.a = 5; // Perfectly fine

Los parámetros del método también se pueden declarar finales. Esto se usa para asegurarse de que nuestro método no cambie el parámetro que recibe cuando se llama.

Las variables locales también se pueden declarar finales. Esto se usa para asegurarse de que la variable reciba un valor solo una vez.

Evitar la anulación

Si especifica el modificador final mientras define un método, cualquier subclase futura no puede anularlo.

1
2
3
4
5
class FinalExample {
    final void printSomething() {
        System.out.println("Something");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class ExtendsFinalExample extends FinalExample {
    // This would cause a compile-time error
    //void printSomething() {
    //  System.out.println("Some other thing");
    //}
    
    // However, you are perfectly free to overload this method
    void printSomething(String something) {
        System.out.println(something);
    }
}

Una pequeña ventaja de declarar métodos verdaderamente finales como “finales” es un ligero aumento de rendimiento cada vez que llamamos a este método. Por lo general, Java resuelve las llamadas a métodos dinámicamente en tiempo de ejecución, pero con los métodos declarados finales, Java puede resolver una llamada en tiempo de compilación, o si un método es realmente pequeño, simplemente puede hacer llamadas en línea a ese método ya que " sabe" que no se anulará. Esto elimina la sobrecarga asociada con una llamada de método.

Prevención de la herencia

Este uso de final es bastante sencillo, una clase definida con final no se puede heredar. Esto, por supuesto, también declara implícitamente todos los métodos de esa clase finales (no se pueden anular si la clase no se puede heredar en primer lugar).

1
final class FinalExample {...}

El modificador abstracto

El modificador abstracto se usa para definir métodos que se implementarán en una subclase más adelante. La mayoría de las veces se usa para sugerir que alguna funcionalidad debería implementarse en una subclase, o (por alguna razón) no puede implementarse en la superclase. Si una clase contiene un método abstracto, también debe declararse abstracto.

Nota: No puede crear un objeto de una clase abstracta. Para hacerlo, debe proporcionar una implementación para todos los métodos abstractos.

Un ejemplo sería si tuviéramos una clase simple llamada Empleado que encapsula datos y métodos para un empleado. Digamos que no a todos los empleados se les paga de la misma manera, a algunos tipos de empleados se les paga por hora ya otros se les paga un salario fijo.

1
2
3
4
5
6
7
8
abstract class Employee {
    int totalHours; // In a month
    int perHour;    // Payment per hour
    int fixedRate;  // Fixed monthly rate
    ...
    abstract int salary();
    ...  
}
1
2
3
4
5
6
7
8
class Contractor extends Employee {
    ...
    // Must override salary if we wish to create an object of this class
    int salary() {
        return totalHours*perHour; 
    }
    ...
}
1
2
3
4
5
6
7
class FullTimeEmployee extends Employee {
    ...
    int salary() {
        return fixedRate; 
    }
    ...
}
1
2
3
4
5
6
7
class Intern extends Employee {
    ...
    int salary() {
        return 0; 
    }
    ...
}

Si una subclase no proporciona una implementación para todos los métodos “abstractos” en la superclase, también debe declararse como “abstracto” y no se puede crear un objeto de esa clase.

Nota: ‘abstracto’ se usa mucho con polimorfismo, p. diríamos ArrayList<Employee> employee = new ArrayList();, y le agregaríamos los objetos Contractor, FullTimeEmployee y Intern. Aunque no podemos crear un objeto de la clase Empleado, aún podemos usarlo como un tipo de variable de referencia.

El modificador sincronizado

Cuando dos o más subprocesos necesitan usar el mismo recurso, de alguna manera debemos asegurarnos de que solo uno de ellos tenga acceso a él a la vez, es decir, debemos sincronizarlos.

Esto se puede lograr de varias maneras, y una forma simple y legible (aunque con un uso algo limitado) es mediante el uso de la palabra clave “sincronizado”.

Un concepto importante que debe comprender antes de ver cómo usar esta palabra clave es el concepto de monitor. Cada objeto en Java tiene su propio monitor implícito asociado. Un monitor es un bloqueo "mutuamente exclusivo", lo que significa que solo un subproceso puede "poseer" un monitor a la vez. Cuando un subproceso ingresa al monitor, ningún otro subproceso puede ingresar hasta que salga el primer subproceso. Esto es lo que hace synchronized.

Los subprocesos están más allá del alcance de este artículo, por lo que me centraré únicamente en la sintaxis de sincronizado.

Podemos sincronizar el acceso a métodos y bloques de código. La sincronización de bloques de código funciona proporcionando una instancia de objeto a la que queremos sincronizar el acceso y el código que queremos ejecutar relacionado con ese objeto.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class SynchronizedExample {

    ...
    SomeClass object = new SomeClass();
    ....

    synchronized(object) {
         // Code that processes objects
         // only one thread at a time
    }
    
    // A synchronized method
    synchronized void doSomething() {
         ...
    }
    ...
}

El modificador volátil

El modificador volátil le dice a Java que una variable puede ser cambiada inesperadamente por alguna otra parte del programa (como en la programación multiproceso), por lo que el valor de la variable siempre se lee desde la memoria principal (y no desde la memoria caché de la CPU), y que cada cambio en la variable volátil se almacena en la memoria principal (y no en la memoria caché de la CPU). Con esto en mente, volátil solo debe usarse cuando sea necesario, ya que leer/escribir en la memoria cada vez es más costoso que hacerlo con el caché de la CPU y solo leer/escribir en la memoria cuando sea necesario.

En términos simplificados, cuando un subproceso lee un valor de variable “volátil”, se garantiza que leerá el valor escrito más recientemente. Básicamente, una variable volátil hace lo mismo que los métodos/bloques sincronizados, simplemente no podemos declarar una variable como sincronizada.

El modificador transitorio

Cuando una variable se declara como “transitoria”, eso significa que su valor no se guarda cuando el objeto se almacena en la memoria. transient int a; significa que cuando escribimos el objeto en la memoria, el contenido de "a" no se incluirá. Por ejemplo, se usa para asegurarnos de que no almacenemos información privada/confidencial en un archivo.

Cuando tratamos de leer un objeto que contiene variables “transitorias”, todos los valores de las variables “transitorias” se establecerán en “nulo” (o valores predeterminados para tipos primitivos), sin importar cuáles fueran cuando escribimos el objeto en el archivo. Otro ejemplo de uso sería cuando el valor de una variable debe derivarse en función de otros datos (como la edad actual de alguien) y no forma parte del estado del objeto persistente.

Nota: Algo muy interesante sucede cuando usamos transient y final juntos. Si tenemos una variable transitoria final que se evalúa como una expresión constante (cadenas o tipos primitivos), la JVM siempre la serializará, ignorando cualquier posible modificador transitorio. Cuando se usa transient final con variables de referencia, obtenemos el comportamiento predeterminado esperado de transient.

Conclusión

Los modificadores son palabras clave que nos permiten afinar el acceso a nuestra clase y sus miembros, su alcance y comportamiento en determinadas situaciones. Proporcionan rasgos fundamentales para nuestras clases y sus miembros. Cada desarrollador debe estar completamente familiarizado con ellos para hacer el mejor uso de ellos.

Como ser consciente de que el control de acceso “protegido” se puede omitir fácilmente, o el modificador “transitorio final” cuando se trata de expresiones constantes.

Licensed under CC BY-NC-SA 4.0