Guía de Inicialización de Objetos en Java

Guía de Inicialización de Objetos en Java

1. Introducción

En pocas palabras, antes de que podamos trabajar con un objeto en la JVM (Java Virtual Machine), debe inicializarse.

En este tutorial, examinaremos las diversas formas en que podemos inicializar tipos primitivos y objetos.

2. Declaración vs. Inicialización

Comencemos asegurándonos de que estamos en la misma página.

La declaración es el proceso de definir la variable, junto con su tipo y nombre.

Aquí estamos declarando la variable id:

int id;

La inicialización, por otro lado, se trata de asignar un valor:

id = 1;

Para demostrarlo, crearemos una clase Usuario con propiedades de nombre e id:

public class Usuario {
    private String nombre;
    private int id;
    
    // constructor estándar, getters, setters, etc.
}

A continuación, veremos que la inicialización funciona de manera diferente según el tipo de campo que estemos inicializando.

3. Objetos vs. Tipos Primitivos

Java proporciona dos tipos de representación de datos: tipos primitivos y tipos de referencia. En esta sección, discutiremos las diferencias entre los dos en relación con la inicialización.

Java tiene ocho tipos de datos incorporados, conocidos como tipos primitivos de Java; las variables de este tipo mantienen sus valores directamente.

Los tipos de referencia mantienen referencias a objetos (instancias de clases). A diferencia de los tipos primitivos que mantienen sus valores en la memoria donde se asigna la variable, las referencias no mantienen el valor del objeto al que se refieren.

En su lugar, una referencia apunta a un objeto almacenando la dirección de memoria donde se encuentra el objeto.

Ten en cuenta que Java no nos permite descubrir cuál es la dirección de memoria física. En cambio, solo podemos usar la referencia para referirnos al objeto.

Veamos un ejemplo que declara e inicializa un tipo de referencia de nuestra clase Usuario:

@Test
public void cuandoSeInicializaConNew_entoncesLaInstanciaNoEsNula() {
    Usuario usuario = new Usuario();
 
    assertThat(usuario).isNotNull();
}

Como podemos ver, una referencia se puede asignar a un nuevo objeto utilizando la palabra clave new, que es responsable de crear el nuevo objeto Usuario.

Continuemos aprendiendo más sobre la creación de objetos.

4. Creación de Objetos

A diferencia de los tipos primitivos, la creación de objetos es un poco más compleja. Esto se debe a que no estamos agregando simplemente el valor al campo; en su lugar, activamos la inicialización utilizando la palabra clave new. Esto, a su vez, invoca a un constructor e inicializa el objeto en la memoria.

Hablemos más detenidamente sobre los constructores y la palabra clave new.

La palabra clave new es responsable de asignar memoria para el nuevo objeto a través de un constructor.

Normalmente, un constructor se usa para inicializar las variables de instancia que representan las propiedades principales del objeto creado.

Si no proporcionamos un constructor explícitamente, el compilador creará un constructor predeterminado que no tiene argumentos y simplemente asigna memoria para el objeto.

Una clase puede tener muchos constructores, siempre y cuando las listas de parámetros sean diferentes (sobrecarga o overload). Cada constructor que no llama a otro constructor en la misma clase tiene una llamada a su constructor principal, ya sea que se haya escrito explícitamente o que lo haya insertado el compilador a través de super().

Agreguemos un constructor a nuestra clase Usuario:

public Usuario(String nombre, int id) {
    this.nombre = nombre;
    this.id = id;
}

Ahora podemos usar nuestro constructor para crear un objeto Usuario con valores iniciales para sus propiedades:

Usuario usuario = new Usuario("Alice", 1);

5. Alcance de las Variables

En las siguientes secciones, veremos los diferentes tipos de alcance (scope) en los que una variable en Java puede existir y cómo esto afecta al proceso de inicialización.

5.1. Variables de Instancia y de Clase

Las variables de instancia y de clase no requieren que las inicialicemos. Tan pronto como declaramos estas variables, se les asigna un valor predeterminado:

Tipo Valor por defecto
boolean false
byte, short, int, long 0
float, doble 0.0
char '\u0000'
Tipo de referencia null

Ahora intentemos definir algunas variables relacionadas con instancias y clases, y comprobemos si tienen un valor predeterminado o no:

@Test
public void cuandoLosValoresNoSeInicializan_entoncesElNombreDeUsuarioYElIdDevuelvenElPredeterminado() {
    Usuario usuario = new Usuario();
 
    assertThat(usuario.getNombre()).isNull();
    assertThat(usuario.getId() == 0);
}

5.2. Variables Locales

Las variables locales deben inicializarse antes de su uso, ya que no tienen un valor predeterminado y el compilador no nos permitirá usar un valor no inicializado.

Por ejemplo, el siguiente código genera un error del compilador:

public void imprimir(){
    int i;
    System.out.println(i);
}

6. La Palabra Clave final

La palabra clave final aplicada a un campo significa que el valor del campo ya no se puede cambiar después de la inicialización. De esta manera, podemos definir constantes en Java.

Agreguemos una constante a nuestra clase Usuario:

private static final int ANYO = 2000;

Las constantes deben inicializarse ya sea cuando se declaran o en un constructor.

7. Inicializadores en Java

En Java, un inicializador es un bloque de código que no tiene un nombre o tipo de datos asociado y se coloca fuera de cualquier método, constructor o bloque de código.

Java ofrece dos tipos de inicializadores, inicializadores estáticos e inicializadores de instancia. Veamos cómo podemos usar cada uno de ellos.

7.1. Inicializadores de Instancia

Podemos usar estos para inicializar variables de instancia.

Para demostrarlo, proporcionaremos un valor para un id de usuario utilizando un inicializador de instancia en nuestra clase Usuario:

{
    id = 0;
}

7.2. Bloque de Inicialización Estática

Un inicializador estático, o bloque estático, es un bloque de código que se utiliza para inicializar campos static. En otras palabras, es un inicializador simple marcado con la palabra clave static:

private static String foro;
static {
    foro = "Java";
}

8. Orden de Inicialización

Cuando escribimos código que inicializa diferentes tipos de campos, debemos tener en cuenta el orden de inicialización.

En Java, el orden de las declaraciones de inicialización es el siguiente:

  1. Variables estáticas e inicializadores estáticos en orden.
  2. Variables de instancia e inicializadores de instancia en orden.
  3. Constructores

9. Ciclo de Vida del Objeto

Ahora que hemos aprendido cómo declarar e inicializar objetos, descubramos qué sucede con los objetos cuando no se utilizan.

A diferencia de otros lenguajes donde debemos preocuparnos por la destrucción de objetos, Java se encarga de los objetos obsoletos a través de su recolector de basura.

Todos los objetos en Java se almacenan en la memoria de la aplicación. De hecho, el montón representa un gran conjunto de memoria no utilizada asignada para nuestra aplicación Java.

Por otro lado, el 'recolector de basura' (garbage collector) es un programa Java que se encarga de la gestión automática de la memoria eliminando objetos que ya no son accesibles.

Para que un objeto Java se vuelva inaccesible, debe encontrarse en una de las siguientes situaciones:

  • El objeto ya no tiene referencias que lo apunten.
  • Todas las referencias que apuntan al objeto están fuera de alcance.

En conclusión, un objeto primero se crea a partir de una clase, generalmente utilizando la palabra clave new. Luego, el objeto vive su vida y nos proporciona acceso a sus métodos y campos.

Finalmente, cuando ya no es necesario, el recolector de basura lo destruye.

10. Otros Métodos para Crear Objetos

En esta sección, echaremos un breve vistazo a métodos distintos de la palabra clave new para crear objetos, y aprenderemos cómo aplicarlos, específicamente reflexión, clonación y serialización.

La reflexión es un mecanismo que podemos utilizar para inspeccionar clases, campos y métodos en tiempo de ejecución. Aquí tienes un ejemplo de cómo crear nuestro objeto Usuario utilizando reflexión:

@Test
public void cuandoSeInicializaConReflexion_entoncesLaInstanciaNoEsNula() 
  throws Exception {
    Usuario usuario = Usuario.class.getConstructor(String.class, int.class)
      .newInstance("Alicia", 2);
 
    assertThat(usuario).isNotNull();
}

En este caso, estamos utilizando la reflexión para encontrar e invocar un constructor de la clase Usuario.

El siguiente método, la clonación, es una forma de crear una copia exacta de un objeto. Para ello, nuestra clase Usuario debe implementar la interfaz Cloneable:

public class Usuario implements Cloneable { //... }

Ahora podemos usar el método clone() para crear un nuevo objeto clonado usuarioClonado que tenga los mismos valores de propiedad que el objeto usuario:

@Test
public void cuandoSeCopiaConClonación_entoncesSeCreaUnaCoincidenciaExacta() 
  throws CloneNotSupportedException {
    Usuario usuario = new Usuario("Alicia", 3);
    Usuario usuarioClonado = (Usuario) usuario.clone();
 
    assertThat(usuarioClonado).isEqualTo(usuario);
}

También podemos utilizar la clase sun.misc.Unsafe para asignar memoria a un objeto sin llamar a un constructor:

Usuario u = (Usuario) unsafeInstance.allocateInstance(Usuario.class);

11. Conclusión

En este artículo, cubrimos la inicialización de campos en Java. Luego examinamos diferentes tipos de datos en Java y cómo usarlos. También exploramos varias formas de crear objetos en Java.