Construtores / Destrutores

Computação II - Ciência da Computação


Prof.: Danilo S. Carvalho

Nessa aula, trataremos do processo de construção e destruição dos objetos.


Ao final da aula, teremos como garantir a inicialização apropriada do estado das instâncias de uma classe e também a liberação ou transferência de recursos usados pelas instâncias quando estas não são mais necessárias.

Aprendemos até aqui que quando queremos criar um novo objeto, usamos a palavra chave new (um operador) e o chamado método construtor da classe, que tem o mesmo nome da classe.

Isso faz com que a memória para o novo objeto seja alocada, resultando em uma referência que é passada para a variável que está sofrendo a atribuição.

Vamos olhar esse processo em mais delahes agora.

                        
                            Pessoa usuarioBiblioteca = new Pessoa();
                        
                    

Quando um objeto é criado, duas coisas são feitas em sequência:

  1. Alocar memória para a estrutura que vai armazenar os dados do objeto, de acordo com o tipo.
  2. Inicializar os campos do objeto, usando valores padrão ou definidos através de um método construtor.

No primeiro passo, o espaço alocado para a estrutura do objeto é definido em função dos campos da classe, sendo tipicamente: 16 bytes (metadados) + 4 bytes * Nº de campos complexos + soma dos tamanhos dos tipos primitivos.

No segundo passo, os campos do novo objeto são inicializados primeiramente com valores padrão:

  • Valor nulo para tipos primitivos: 0 para números, \u0000 para char e false para boolean.
  • null para tipos complexos. Indicando ausência de uma referência.

Em seguida, o método construtor que foi chamado é executado, onde os campos podem ser preenchidos com valores escolhidos pelo programador, tipicamente passados como argumentos do construtor.

Por fim, a referência é atribuída à variável.

O método construtor funciona de forma similar a um método normal de instância, mas não possui retorno definido, pois é usado apenas com o operador new.

Também pode ser sobrecarregado, permitindo diversas maneiras de inicializar um objeto.

                        
                            package br.ufrj.dcc.comp2.ple.aula_danilo.exemplos.biblioteca;
                            import java.time.LocalDateTime;

                            public class Pessoa {
                                String cpf;
                                String nome;
                                LocalDateTime dataNascimento;
                                String endereço;
                                String telefone;
                                int livrosLidos;
                                int paginasLidas;

                                public Pessoa(String cpf, String nome) {
                                    this.cpf = cpf;
                                    this.nome = nome;
                                }

                                public void ler(Livro livro) {
                                    this.livrosLidos += 1;
                                    this.paginasLidas += livro.numPaginas;
                                }

                                public void ler(Revista revista) {
                                    this.paginasLidas += revista.numPaginas;
                                }
                            }
                        
                    
                        
                            package br.ufrj.dcc.comp2.ple.aula_danilo.exemplos.biblioteca;
                            import java.time.LocalDateTime;

                            public class Pessoa {
                                String cpf;
                                String nome;
                                LocalDateTime dataNascimento;
                                String endereço;
                                String telefone;
                                int livrosLidos;
                                int paginasLidas;

                                public Pessoa() {

                                }

                                public Pessoa(String cpf, String nome) {
                                    this.cpf = cpf;
                                    this.nome = nome;
                                }

                                public Pessoa(String cpf, String nome, LocalDateTime dataNascimento) {
                                    this.cpf = cpf;
                                    this.nome = nome;
                                    this.dataNascimento = dataNascimento;
                                    livrosLidos = 0;  // Uso do this não é obrigatório se não há conflito de nomes.
                                    paginasLidas = 0;
                                }

                                public void ler(Livro livro) {
                                    this.livrosLidos += 1;
                                    this.paginasLidas += livro.numPaginas;
                                }

                                public void ler(Revista revista) {
                                    this.paginasLidas += revista.numPaginas;
                                }
                            }
                        
                    

Em Java, quando um objeto sai do escopo de uso, ou não possui mais referências apontando para ele, é colocado em uma lista de "coleta de lixo".


Assim que possível, a JVM executa um processo chamado coletor de lixo (garbage collector, ou GC), que desaloca os objetos descartados da memória.

Dessa forma, não é necessário o programador desalocar manualmente os objetos.

Até a versão 9 do Java, o coletor de lixo executava o método finalize() do objeto antes de desalocá-lo.

A intenção desse método era liberar recursos, como arquivos abertos e conexões com bancos de dados, de forma segura.

                        
                            package br.ufrj.dcc.comp2.ple.aula_danilo.exemplos.biblioteca;
                            import java.time.LocalDateTime;

                            public class Pessoa {
                                String cpf;
                                String nome;
                                LocalDateTime dataNascimento;
                                String endereço;
                                String telefone;
                                int livrosLidos;
                                int paginasLidas;

                                public Pessoa() {

                                }

                                public Pessoa(String cpf, String nome) {
                                    this.cpf = cpf;
                                    this.nome = nome;
                                }

                                public Pessoa(String cpf, String nome, LocalDateTime dataNascimento) {
                                    this.cpf = cpf;
                                    this.nome = nome;
                                    this.dataNascimento = dataNascimento;
                                    livrosLidos = 0;  // Uso do this não é obrigatório se não há conflito de nomes.
                                    paginasLidas = 0;
                                }

                                public void finalize() {
                                    // Fazer a liberação de recursos aqui.
                                }

                                public void ler(Livro livro) {
                                    this.livrosLidos += 1;
                                    this.paginasLidas += livro.numPaginas;
                                }

                                public void ler(Revista revista) {
                                    this.paginasLidas += revista.numPaginas;
                                }
                            }
                        
                    

Nas versões mais recentes (>= 9) isso não acontece mais e a liberação dos recursos deve ser feita usando um bloco try-catch-finally (no finally), ou usando um bloco try-with-resources, como mostrado abaixo:

                        
                            // try-catch-finally
                            try {
                                Scanner scanner = new Scanner(System.in);

                                String linha = scanner.nextLine();
                            }
                            catch (NoSuchElementException) {
                                System.out.println("Acabaram as linhas...");
                            }
                            finally {
                                scanner.close();
                            }
                        
                    

O bloco try-with-resources libera automaticamente os recursos alocados em sua cláusula.

                        
                            // try-with-resources
                            try (Scanner scanner = new Scanner(System.in)) {
                                String linha = scanner.nextLine();
                            }
                            catch (NoSuchElementException) {
                                System.out.println("Acabaram as linhas...");
                            }
                        
                    

Agora sabemos como inicializar apropriadamente nossos objetos, e como liberar recursos de forma correta.


Que tal definir os construtores da classe Livro e da classe Revista?

Perguntas:

  1. Por que é uma boa ideia prencher valores iniciais para os campos de uma classe?
  2. Posso ter um campo que é do próprio tipo da classe? Que cuidados devo tomar ao inicializá-lo?

Exercício:

  1. Expandindo a ideia da conta bancária, conceitualize e escreva as classes para operação de um sistema bancário simples, incluindo os construtores e métodos, que devem ser sobrecarregados quando apropriado.

Até a próxima aula!