JavaRush /Blogue Java /Random-PT /Pausa para café #128. Guia de registros Java

Pausa para café #128. Guia de registros Java

Publicado no grupo Random-PT
Fonte: abhinavpandey.dev Neste tutorial, abordaremos os fundamentos do uso de registros em Java. Os registros foram introduzidos no Java 14 como uma forma de remover o código clichê em torno da criação de objetos Value e, ao mesmo tempo, aproveitar as vantagens de objetos imutáveis. Pausa para café #128.  Guia de registros Java - 1

1. Conceitos básicos

Antes de entrarmos nas entradas em si, vejamos o problema que elas resolvem. Para fazer isso, teremos que lembrar como os objetos de valor eram criados antes do Java 14.

1.1. Objetos de valor

Objetos de valor são parte integrante dos aplicativos Java. Eles armazenam dados que precisam ser transferidos entre camadas de aplicação. Um objeto de valor contém campos, construtores e métodos para acessar esses campos. Abaixo está um exemplo de um 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. Igualdade entre objetos Value

Os objetos de valor também podem fornecer uma maneira de compará-los quanto à igualdade. Por padrão, Java compara a igualdade dos objetos comparando seus endereços de memória. Contudo, em alguns casos, objetos contendo os mesmos dados podem ser considerados iguais. Para implementar isso, podemos substituir os métodos equals e .hashCode . Vamos implementá-los para a classe Contact :
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. Imutabilidade de objetos de valor

Os objetos de valor devem ser imutáveis. Isso significa que devemos limitar as maneiras pelas quais podemos alterar os campos de um objeto. Isto é aconselhável pelos seguintes motivos:
  • Para evitar o risco de alterar acidentalmente o valor do campo.
  • Para garantir que objetos iguais permaneçam os mesmos ao longo de suas vidas.
Como a classe Contact já é imutável, agora:
  1. tornou os campos privados e finais .
  2. forneceu apenas um getter para cada campo (sem setters ).

1.4. Registrando objetos Value

Muitas vezes precisamos registrar os valores contidos nos objetos. Isso é feito fornecendo um método toString . Sempre que um objeto é registrado ou impresso, o método toString é chamado . A maneira mais fácil aqui é imprimir o valor de cada campo. Aqui está um exemplo:
public class Contact {
    // ...
    @Override
    public String toString() {
        return "Contact[" +
                "name='" + name + '\'' +
                ", email=" + email +
                ']';
    }
}

2. Reduza modelos com registros

Como a maioria dos objetos de valor tem as mesmas necessidades e funcionalidades, seria bom simplificar o processo de criação deles. Vejamos como as gravações ajudam a conseguir isso.

2.1. Convertendo a classe Person em Record

Vamos criar uma entrada da classe Contact que tenha a mesma funcionalidade da classe Contact definida acima.
public record Contact(String name, String email) {}
A palavra-chave record é usada para criar uma classe Record . Os registros podem ser processados ​​pelo chamador da mesma forma que uma classe. Por exemplo, para criar uma nova instância de entrada, podemos usar a palavra-chave new .
Contact contact = new Contact("John Doe", "johnrocks@gmail.com");

2.2. Comportamento padrão

Reduzimos o código para uma linha. Vamos listar o que inclui:
  1. Os campos de nome e e-mail são privados e finais por padrão.

  2. O código define um “construtor canônico” que utiliza campos como parâmetros.

  3. Os campos são acessíveis através de métodos do tipo getter - name() e email() . Não há setter para campos, portanto os dados no objeto tornam-se imutáveis.

  4. Implementado o método toString para imprimir os campos assim como fizemos para a classe Contact .

  5. Métodos equals e .hashCode implementados . Eles incluem todos os campos, assim como a classe Contact .

2.3 Construtor canônico

O construtor padrão pega todos os campos como parâmetros de entrada e os define como campos. Por exemplo, o Construtor Canônico padrão é mostrado abaixo:
public Contact(String name, String email) {
    this.name = name;
    this.email = email;
}
Se definirmos um construtor com a mesma assinatura na classe de gravação, ele será usado no lugar do construtor canônico.

3. Trabalhando com registros

Podemos alterar o comportamento da entrada de diversas maneiras. Vejamos alguns casos de uso e como alcançá-los.

3.1. Substituindo implementações padrão

Qualquer implementação padrão pode ser alterada substituindo-a. Por exemplo, se quisermos alterar o comportamento do método toString , podemos substituí-lo entre chaves {} .
public record Contact(String name, String email) {
    @Override
    public String toString() {
        return "Contact[" +
                "name is '" + name + '\'' +
                ", email is" + email +
                ']';
    }
}
Da mesma forma, podemos substituir os métodos equals e hashCode .

3.2. Kits de construção compactos

Às vezes queremos que os construtores façam mais do que apenas inicializar campos. Para fazer isso, podemos adicionar as operações necessárias à nossa entrada no Construtor Compacto. É denominado compacto porque não necessita definir inicialização de campo ou lista de parâmetros.
public record Contact(String name, String email) {
    public Contact {
        if(!email.contains("@")) {
            throw new IllegalArgumentException("Invalid email");
        }
    }
}
Observe que não há lista de parâmetros e a inicialização do nome e do email ocorre em segundo plano antes da verificação ser realizada.

3.3. Adicionando Construtores

Você pode adicionar vários construtores a um registro. Vejamos alguns exemplos e limitações. Primeiro, vamos adicionar novos construtores 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;
    }
}
No primeiro caso, o construtor padrão é acessado usando a palavra-chave this . O segundo construtor substitui o construtor padrão porque possui a mesma lista de parâmetros. Neste caso, a entrada em si não criará um construtor padrão. Existem várias restrições aos construtores.

1. O construtor padrão deve sempre ser chamado de qualquer outro construtor.

Por exemplo, o código abaixo não será compilado:
public record Contact(String name, String email) {
    public Contact(String name) {
        this.name = "John Doe";
        this.email = null;
    }
}
Esta regra garante que os campos sejam sempre inicializados. Também é garantido que as operações definidas no construtor compacto sejam sempre executadas.

2. Não é possível substituir o construtor padrão se um construtor compacto for definido.

Quando um construtor compacto é definido, um construtor padrão é criado automaticamente com inicialização e lógica de construtor compacto. Neste caso, o compilador não nos permitirá definir um construtor com os mesmos argumentos do construtor padrão. Por exemplo, neste código a compilação não acontecerá:
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. Implementando Interfaces

Como acontece com qualquer classe, podemos implementar interfaces em 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 garantir a imutabilidade completa, os registros não podem ser herdados. As inscrições são finais e não podem ser expandidas. Eles também não podem estender outras classes.

3.5. Adicionando Métodos

Além dos construtores, que substituem métodos e implementações de interface, também podemos adicionar quaisquer métodos que desejarmos. Por exemplo:
public record Contact(String name, String email) {
    String printName() {
        return "My name is:" + this.name;
    }
}
Também podemos adicionar métodos estáticos. Por exemplo, se quisermos ter um método estático que retorne uma expressão regular com a qual possamos verificar o e-mail, podemos defini-lo conforme mostrado abaixo:
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. Adicionando campos

Não podemos adicionar campos de instância a um registro. No entanto, podemos adicionar 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;
    }
}
Observe que não há restrições implícitas em campos estáticos. Se necessário, podem estar disponíveis publicamente e não ser definitivos.

Conclusão

Os registros são uma ótima maneira de definir classes de dados. Eles são muito mais convenientes e poderosos do que a abordagem JavaBeans/POJO. Por serem fáceis de implementar, devem ser preferidos a outras formas de criação de objetos de valor.
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION