JavaRush /Blog Java /Random-ES /Pausa para el café #128. Guía de registros de Java

Pausa para el café #128. Guía de registros de Java

Publicado en el grupo Random-ES
Fuente: abhinavpandey.dev En este tutorial, cubriremos los conceptos básicos del uso de Registros en Java. Los registros se introdujeron en Java 14 como una forma de eliminar el código repetitivo relacionado con la creación de objetos de valor y, al mismo tiempo, aprovechar los objetos inmutables. Pausa para el café #128.  Guía de registros Java - 1

1. Conceptos básicos

Antes de entrar en las entradas, veamos el problema que resuelven. Para ello, tendremos que recordar cómo se creaban los objetos de valor antes de Java 14.

1.1. Objetos de valor

Los objetos de valor son una parte integral de las aplicaciones Java. Almacenan datos que deben transferirse entre capas de aplicaciones. Un objeto de valor contiene campos, constructores y métodos para acceder a esos campos. A continuación se muestra un ejemplo de un objeto de valor:
public class Contact {
    private final String name;
    private final String email;

    public Contact(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

1.2. Igualdad entre objetos de valor

Los objetos de valor también pueden proporcionar una forma de compararlos para determinar su igualdad. De forma predeterminada, Java compara la igualdad de objetos comparando su dirección de memoria. Sin embargo, en algunos casos, los objetos que contienen los mismos datos pueden considerarse iguales. Para implementar esto, podemos anular los métodos iguales y .hashCode . Implementémoslos para la clase Contacto :
public class Contact {

    // ...

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Contact contact = (Contact) o;
        return Object.equals(email, contact.email) &&
                Objects.equals(name, contact.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, email);
    }
}

1.3. Inmutabilidad de objetos de valor

Los objetos de valor deben ser inmutables. Esto significa que debemos limitar las formas en que podemos cambiar los campos de un objeto. Esto es aconsejable por las siguientes razones:
  • Para evitar el riesgo de cambiar accidentalmente el valor del campo.
  • Para conseguir que los objetos iguales sigan siendo los mismos durante toda su vida.
Dado que la clase Contact ya es inmutable, ahora hacemos lo siguiente:
  1. hizo que los campos fueran privados y definitivos .
  2. proporcionó solo un captador para cada campo (sin definidores ).

1.4. Registrar objetos de valor

A menudo necesitamos registrar los valores contenidos en los objetos. Esto se hace proporcionando un método toString . Siempre que se registra o imprime un objeto, se llama al método toString . La forma más sencilla aquí es imprimir el valor de cada campo. He aquí un ejemplo:
public class Contact {
    // ...
    @Override
    public String toString() {
        return "Contact[" +
                "name='" + name + '\'' +
                ", email=" + email +
                ']';
    }
}

2. Reducir plantillas con Registros

Dado que la mayoría de los objetos de valor tienen las mismas necesidades y funcionalidades, sería bueno simplificar el proceso de creación. Veamos cómo las grabaciones ayudan a lograrlo.

2.1. Convirtiendo la clase Persona a Registro

Creemos una entrada de clase de Contacto que tenga la misma funcionalidad que la clase de Contacto definida anteriormente.
public record Contact(String name, String email) {}
La palabra clave record se utiliza para crear una clase Record . La persona que llama puede procesar los registros de la misma manera que una clase. Por ejemplo, para crear una nueva instancia de entrada, podemos usar la nueva palabra clave .
Contact contact = new Contact("John Doe", "johnrocks@gmail.com");

2.2. Comportamiento por defecto

Hemos reducido el código a una línea. Enumeremos lo que incluye:
  1. Los campos de nombre y correo electrónico son privados y finales de forma predeterminada.

  2. El código define un "constructor canónico" que toma campos como parámetros.

  3. Se puede acceder a los campos a través de métodos similares a getter: name() y email() . No existe un definidor de campos, por lo que los datos del objeto se vuelven inmutables.

  4. Implementé el método toString para imprimir los campos tal como lo hicimos para la clase Contacto .

  5. Se implementaron métodos iguales y .hashCode . Incluyen todos los campos, al igual que la clase Contacto .

2.3 Constructor canónico

El constructor predeterminado toma todos los campos como parámetros de entrada y los establece en campos. Por ejemplo, el constructor canónico predeterminado se muestra a continuación:
public Contact(String name, String email) {
    this.name = name;
    this.email = email;
}
Si definimos un constructor con la misma firma en la clase de grabación, se usará en lugar del constructor canónico.

3. Trabajar con registros

Podemos cambiar el comportamiento de la entrada de varias formas. Veamos algunos casos de uso y cómo lograrlos.

3.1. Anulación de implementaciones predeterminadas

Cualquier implementación predeterminada se puede cambiar anulándola. Por ejemplo, si queremos cambiar el comportamiento del método toString , podemos anularlo entre llaves {} .
public record Contact(String name, String email) {
    @Override
    public String toString() {
        return "Contact[" +
                "name is '" + name + '\'' +
                ", email is" + email +
                ']';
    }
}
De manera similar, podemos anular los métodos iguales y hashCode .

3.2. Kits de construcción compactos

A veces queremos que los constructores hagan más que simplemente inicializar campos. Para hacer esto, podemos agregar las operaciones necesarias a nuestra entrada en el Constructor Compacto. Se llama compacto porque no necesita definir la inicialización de campo o la lista de parámetros.
public record Contact(String name, String email) {
    public Contact {
        if(!email.contains("@")) {
            throw new IllegalArgumentException("Invalid email");
        }
    }
}
Tenga en cuenta que no hay una lista de parámetros y que la inicialización del nombre y el correo electrónico se produce en segundo plano antes de realizar la verificación.

3.3. Agregar constructores

Puede agregar varios constructores a un registro. Veamos un par de ejemplos y limitaciones. Primero, agreguemos nuevos constructores válidos:
public record Contact(String name, String email) {
    public Contact(String email) {
        this("John Doe", email);
    }

    // replaces the default constructor
    public Contact(String name, String email) {
        this.name = name;
        this.email = email;
    }
}
En el primer caso, se accede al constructor predeterminado utilizando la palabra clave this . El segundo constructor anula el constructor predeterminado porque tiene la misma lista de parámetros. En este caso, la entrada en sí no creará un constructor predeterminado. Existen varias restricciones para los constructores.

1. El constructor predeterminado siempre debe llamarse desde cualquier otro constructor.

Por ejemplo, el siguiente código no se compilará:
public record Contact(String name, String email) {
    public Contact(String name) {
        this.name = "John Doe";
        this.email = null;
    }
}
Esta regla garantiza que los campos siempre estén inicializados. También se garantiza que siempre se ejecuten las operaciones definidas en el constructor compacto.

2. No es posible anular el constructor predeterminado si se define un constructor compacto.

Cuando se define un constructor compacto, se crea automáticamente un constructor predeterminado con inicialización y lógica de constructor compacto. En este caso, el compilador no nos permitirá definir un constructor con los mismos argumentos que el constructor predeterminado. Por ejemplo, en este código no se realizará la compilación:
public record Contact(String name, String email) {
    public Contact {
        if(!email.contains("@")) {
            throw new IllegalArgumentException("Invalid email");
        }
    }
    public Contact(String name, String email) {
        this.name = name;
        this.email = email;
    }
}

3.4. Implementación de interfaces

Como ocurre con cualquier clase, podemos implementar interfaces en los registros.
public record Contact(String name, String email) implements Comparable<Contact> {
    @Override
    public int compareTo(Contact o) {
        return name.compareTo(o.name);
    }
}
Nota IMPORTANTE. Para garantizar una inmutabilidad total, los registros no se pueden heredar. Las entradas son definitivas y no se pueden ampliar. Tampoco pueden ampliar otras clases.

3.5. Agregar métodos

Además de los constructores, que anulan métodos e implementaciones de interfaz, también podemos agregar cualquier método que queramos. Por ejemplo:
public record Contact(String name, String email) {
    String printName() {
        return "My name is:" + this.name;
    }
}
También podemos agregar métodos estáticos. Por ejemplo, si queremos tener un método estático que devuelva una expresión regular con la que podamos comprobar el correo electrónico, podemos definirlo como se muestra a continuación:
public record Contact(String name, String email) {
    static Pattern emailRegex() {
        return Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE);
    }
}

3.6. Agregar campos

No podemos agregar campos de instancia a un registro. Sin embargo, podemos agregar campos estáticos.
public record Contact(String name, String email) {
    private static final Pattern EMAIL_REGEX_PATTERN = Pattern
            .compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE);

    static Pattern emailRegex() {
        return EMAIL_REGEX_PATTERN;
    }
}
Tenga en cuenta que no existen restricciones implícitas en los campos estáticos. Si es necesario, podrán estar disponibles públicamente y no ser definitivos.

Conclusión

Los registros son una excelente manera de definir clases de datos. Son mucho más convenientes y poderosos que el enfoque JavaBeans/POJO. Debido a que son fáciles de implementar, deberían preferirse a otras formas de crear objetos de valor.
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION