Desmistificando a Certificação SCJP6 - Parte VI - Parte 3/3

Nesta última parte do artigo trataremos conceitos de variável de referência, construtores, modificador static, acoplamento/coesão, coleta de lixo e aplicaremos um mini-teste.

CONVERSÃO DE VARIÁVEL DE REFERÊNCIA

package devmedia; class Mamifero{ public void comunicar(){ System.out.println("Comunicação generalizada"); } } class Homem extends Mamifero{ public void comunicar(){ System.out.println("Homem fala"); } public void andar(String texto){ System.out.println("Bípede"); } } public class Teste { public static void main(String args[]){ Mamifero[]mamiferos = {new Homem(), new Mamifero()}; for(Mamifero m: mamiferos){ m.comunicar(); if(m instanceof Homem){ //m.andar(); erro de compilação } } } }

Saída:
Homem fala
Comunicação generalizada

O compilador nos avisa que o erro potencial, uma vez que exista um comentário, foi criado porque a classe Mamifero não tem nenhum método andar(). Vamos modificar a linha problemática

Homem h = (Homem) m; h.andar();//Código compilando ((Homem)m).andar();//Equivale ao código acima

O que falamos ao compilador é que a variável se refere a um objeto Homem, sendo assim não existe problemas em criar uma nova variável de referência Homem e apontar para este objeto.

É o inverso da conversão redutora, a conversão generalizadora, que consiste na conversão para cima na árvore de herança, ou seja, de um tipo mais especifico para um tipo mais genérico, funciona implicitamente.

Isso quer dizer que não existe necessidade de incluir nada no código, uma vez que quando uma conversão generalizadora é feita, você está implicitamente restringindo a quantidade de métodos ou métodos que pode usar, ao contrário de uma conversão redutora.

package devmedia; class Mamifero{} class Homem extends Mamifero{} public class Teste { public static void main(String args[]){ Homem homem = new Homem(); Mamifero mamifero = homem;//conversão implícita Mamifero m = (Mamifero)homem;//conversão explícita } }

Quando uma subclasse quiser modificar a implementação de um método herdado, ou seja, uma sobrescrição, terá que definir um método que coincida exatamente com a versão herdada. Ou então, existe a possibilidade de modificar o tipo de retorno do método sobrescritor, desde que o novo tipo de retorno seja um subtipo(subclasse) do tipo de retorno declarado no método da superclasse. Observe o retorno covariante em ação.

package devmedia; class X{ protected Object verificar(){ return "X"; } } class Y extends X{ public String verificar(){ return "Y"; } } public class Executavel { public static void main (String args[]){ X x = new Y(); System.out.println(x.verificar()); } }

Existem 6 regras de retorno que você deve estar ciente.

  1. O array é um tipo de retorno válido.
  2. Você poderá retornar null em um método que tem como tipo de retorno a referencia a um objeto.
  3. Em um método com tipo de retorno primitivo você pode retornar qualquer valor ou variável que possa ser explicitamente convertido para o tipo de retorno declarado.
  4. Em um método com tipo de retorno primitivo você pode retornar qualquer valor ou variável que possa ser implicitamente convertido para o tipo de retorno declarado.
  5. Em um método que tenha como tipo de retorno a referência a um objeto, você pode retornar qualquer tipo de objetos que possa ser implicitamente convertido para o tipo de retorno declarado.
  6. Você não deve retornar nada em um método com tipo de retorno void.

ASPECTOS BÁSICOS DO CONSTRUTOR

Qualquer classe, incluindo as classes abstract, precisa possuir um construtor. Memorize isso. Entretanto, porque a classe precisa ter um construtor, não significa que o programador tenha que digitá-lo. Observe o exemplo:

package devmedia; public class Veiculo{ String pintura, marca; int capacidadePassageiros; public Veiculo(String pintura, String marca, int capacidadePassageiros) { this.pintura = pintura; this.marca = marca; this.capacidadePassageiros = capacidadePassageiros; } } Avalie o exemplo a seguir. package devmedia; class SerVivo{ private String name; SerVivo(){ name="Rex"; } protected String getName(){ return name; } } class Cachorro extends SerVivo{ private String tipo; Cachorro(){ super();//Chamada implícita, se não houver sobrecarga de construtores tipo="Vira-lata"; } public String toString(){ return super.getName()+" "+tipo; } } public class Construtor { public static void main(String args[]){ Cachorro cachorro = new Cachorro(); System.out.println(cachorro); } }

Saída:
Rex Vira-lata

Analise esse outro exemplo, onde a hierarquia é baseada em 3 classes. O construtor Z() invoca Y(), que invoca X(). Se acha que terminou, ainda não, pois X() invoca Object().

package devmedia; class X{ X() { System.out.println("X"); } } class Y extends X{ Y() { System.out.println("Y"); } } class Z extends Y{ Z(){ System.out.println("Z"); } } public class ConstrutorTeste { public static void main(String args[]){ Z z = new Z(); } }

Saída:
X Y Z

Os construtores podem utilizar qualquer modificador de acesso, incluindo private. Um construtor com modificador de acesso private indica que apenas o código que estiver dentro da própria classe poderá instanciar um objeto desse tipo.

É possível, porém não muito útil, possuir um método com o mesmo nome da classe, entretanto isso não o tornara um construtor. Caso exista um tipo de retorno, será um método, em vez de um construtor.

Se quisermos o construtor sem argumentos em uma classe e existir outros construtores no código, o compilador não fornecerá o construtor padrão, assim como qualquer outro.

O construtor-padrão é o que o fornecido pelo compilador. Você não pode acessar uma variável de instância, ou fazer uma chamada ao método de uma instância, até que tenha executado um construtor da superclasse. Afinal, o construtor é que fornece o estado de um objeto.

Somente variáveis e métodos static podem ser acessados como parte de uma chamada this() ou super(). Avalie o exemplo abaixo e perceba a presença de construtores sobrecarregados na classe Animal.

package devmedia; class Animal { private String name; private int age; Animal() { name = "Tom"; } Animal(int age) { this.age = age; } protected String getName(){ return name; } } class Gato extends Animal{ static int a = 10; private String tipo; Gato() { super(Gato.a); tipo = "Siamês"; } public String toString(){ return super.getName()+" "+tipo; } }

Existindo a necessidade de um construtor sem argumentos que sobrecarregue um construtor que tenha argumento, lembre-se de que você mesmo tem que adicioná-lo.

MODIFICADOR STATIC

A palavra-chave static serve na declaração de uma variável dentro de uma classe para se criar uma variável que poderá ser acessada por todas as instâncias de objetos desta classe como uma variável comum.

Independente de uma instancia da classe execute um metodo, ele se comportará da mesma forma. Ou seja, o comportamento do método não depende do estado de um objeto.

Os métodos e atributos declarados como static pertencem à classe, em vez de pertencer a uma instancia especifíca. Na verdade, é possível utilizar um membro (método ou atributo) static sem ter nenhuma instância dessa classe criada. Porém, havendo várias instâncias, haverá apenas uma cópia do atributo static na memória. Veja o seguinte exemplo:

package devmedia; class Tigre{ static int contadorTigres = 0; static int getContadorTigres() { return contadorTigres; } public Tigre(){ contadorTigres +=1; } } public class StaticTeste { public static void main(String[]args){ System.out.println("Tigres - "+Tigre.contadorTigres); new Tigre(); System.out.println("Tigres - "+Tigre.contadorTigres); new Tigre(); System.out.println("Tigres - "+Tigre.contadorTigres); } }

Saída:
Tigres - 0
Tigres - 1
Tigres - 2

Um método static não pode ter acesso a um atributo não-static porque não existe uma instância. Um método static não pode usar diretamente um método não-static.

Outro ponto essencial é que um método static não podem ser sobrecarregado e nem sobrescrito.

ACOPLAMENTO E COESÃO

Acoplamento é quanto um elemento: método, classe, módulo, função, componente, basicamente qualquer coisa, depende e conhece do outro. Elementos bastante acoplados normalmente são muito dependentes, isto quer dizer, alterou um, você com certeza vai ter que mudar o outro.

Coesão é o quanto as tarefas que o elemento faz estão relacionadas com o mesmo conceito. Baixa coesão teria uma classe que, por exemplo, exibe um formulário e imprime um documento para o usuário, neste caso geralmente você deveria dividir essa classe em mais do que uma.

Os 2 tópicos, acoplamento e coesão tem a ver com o nível de qualidade de um projeto OO. Normalmente, um bom projeto OO necessita de baixo acoplamento e alta coesão.

Acoplamento é um grau em que uma classe sabe da outra. Se o único conhecimento que a classe A tem sobre a classe B é o que a classe B expôs por meio de seus métodos públicos, então podemos dizer que o acoplamento entre ambas é fraco. Isso é o correto. O fato de se ter acesso a uma pequena quantidade de métodos implica em um acoplamento limitado.

A expressão coesão é utilizada para indicar o grau em que uma classe tem um único e focado propósito.

O benefício primordial da alta coesão é que tais classes geralmente são de manutenção muito mais fácil e são alteradas com bem menos frequência do que as classes com baixa coesão. Outro importante beneficio da alto coesão é que as classes com propósito bem focado tendem a ser mais reutilizáveis do que outras.

COLETA DE LIXO

O coletor de lixo da linguagem Java nos fornece uma solução de forma automática para o gerenciamento da memória. Na maioria das situações, ele o livrará de ter que adicionar alguma lógica de gerenciamento de memória. A desvantagem dessa coleta automática é a imcapacidade de controlar exatamente quando ela será ou não executada.

Todas as vezes que um programa for executado ele utilizará a memória de várias formas. Heap é a área da memória em que residem os objetos. É o único local que, de algum modo, está envolvido na coleta de lixo.

Sendo assim, todo assunto de coleta de lixo consiste em assegurar que o heap tenha tanto espaço disponível quanto for possível.

O coletor de lixo é gerenciado pela VM. Então, a JVM decide quando executá-lo. Dentro do programa Java, você pode solicitar à JVM que execute o coletor, mas não existem garantias de que ele será executado. Geralmente ele é executado quando a VM verifica que existe pouca memória.

Todo programa Java tem de um a vários threads. Cada thread tem a sua própria pilha de execução.

Na maioria das vezes, você fará com que ao menos um thread seja executado em um programa: o que tiver o método main(), no final da pilha. Cada thread tem seu próprio ciclo de vida. Os threads podem estar, ou não, ativos. Com essas informações auxiliares já podemos falar que um objeto está qualificado para o coletor de lixo quando nenhum thread ativo puder ter acesso a ele. Nesse caso o objeto não está alcançável.

A coleta de lixo não deve ser forçada. Entretanto, Java possui alguns métodos que permitirão a solicitação à JVM que execute a coleta de lixo. Por exemplo, caso você tenha que executar certas operações limitadas pelo tempo, possivelmente irá querer mitigar(minimizar) as probabilidades de um atraso ocasionado pela coleta de lixo. Esses métodos pedem, não mandam. Então, não existem garantias de que a coleta de lixo de fato ocorrerá.

As rotinas de coletas de lixo que Java oferece são membros da classe Runtime. Runtime é uma classe especial que tem apenas um objeto (padrão de projeto Singleton), para cada programa principal. O objeto de Runtime proporciona um mecanismo de comunicação direta com maquina virtual. Para capturar a instância de Runtime, é possível utilizar o método Runtime.getRuntime(), que retornará o objeto Singleton. Assim que tiver o Singleton, você pode invocar o coletor de lixo através do método gc()-garbage collector.

Uma outra alternativa é invocar o método da classe System e, assim, obter o Singleton(System.gc()). Teoricamente após chamar System.gc(), você terá tanto espaço livre quanto possível. Falamos teoricamente porque essa rotina nem sempre funciona desse modo. Primeiro porque a JVM que você estiver utilizando pode não tê-la implementado; a implementação da linguagem permite que essa rotina não tenha nenhuma função. Em segundo, o thread pode executar uma alocação substancial de memória logo após você processar a coleta do lixo.

Mini-teste

  1. Quais regras de sobrescrição de métodos podem impedir que um código seja compilado? (Marque todas as alternativas corretas)
    1. Um método sobrescrito não pode ter tipo de retorno diferente.
    2. Um método sobrescrito não pode lançar exceções não verificadas novas.
    3. Um método sobrescrito não pode ter uma lista de argumento diferente.
    4. Um método sobrescrito não pode ser declarado com um modificador de acesso mais restritivo.
    5. Um método sobrescrito não pode lançar exceções verificadas novas.
  2. Dado o seguinte código:
    package devmedia; class Funcionario{ Funcionario() { System.out.print("Funcionário "); } void pagar(){ System.out.println("Funcionário pagando"); } } class Gerente{ Gerente(){ System.out.println("Gerente"); } Funcionario f = new Funcionario(){ void pagar(){ System.out.println("Gerente pagando"); } }; } public class Testador { public static void main (String[]args){ Gerente g = new Gerente(); } }
    Qual é a saída no console:
    1. A compilação falha.
    2. É lançada uma exceção.
    3. Funcionário Gerente
    4. Funcionário
    5. Gerente
  3. Com o trecho de código abaixo:
    package devmedia; class Dog{} class Pitbull extends Dog{} public class Carrocinha { public static void main(String args[]){ Pitbull p = new Pitbull(); Dog d = new Dog(); Dog dog = p; // coloque código aqui } }
    Qual opção inserida no local do comentário irá funcionar sem erro de compilação?
    1. Pitbull pit = (Pitbull)dog;
    2. Pitbull pit = d;
    3. Nenhuma das instruções anteriores irá compilar.
  4. Dado o código a seguir:
    package pck1; public class Funcionario{ protected String nome; protected void alteraNome(){} } package pck2; import pck1.*; public class Chefe extends Funcionario {?}
    Quais códigos podem ser colocados corretamente dentro de Chefe?
    1. protected void alteraNome() throws RuntimeException{}
    2. {(new Funcionario().nome = “Davi”;)}
    3. void alteraNome(){}
    4. {nome = “Davi”;}
    5. protected int alteraNome(){}
  5. Qual instrução, quando colocada na posição indicada no código abaixo, causará o lançamento de uma exceção em tempo de execução?
    class X{} class Y extends X{} class Z extends X{} class Main{ public static void main(String args[]){ X x = new X(); Y y = new Y(); Z z = new Z(); //inserir código aqui } }
    Selecione uma opção:
    1. y=(X)y;
    2. z=(Z)y
    3. y=(Y)x;
    4. z=x;
    5. x =y;

Gabarito comentado

  1. Resposta correta: D e E

    É preciso testar códigos envolvendo sobreposição com exaustão. Desse modo sim, você perceberá que um método sobrescrito não pode lançar exceções verificadas novas e que um método sobrescrito não é declarado com um modificador de acesso mais restritivo.

    A opção C não impede a compilação. Mas o caso deixa de ser uma sobreposição e passa a ser uma sobrecarga. A opção B está errada porque um método sobrescrito pode lançar exceções não-verificadas novas. Finalizando, a opção A, que também está errada, faz referência a covariância. Sendo assim, um método sobrescrito pode ter um tipo de retorno diferente desde que ele pertença a uma mesma hierarquia.

  2. Resposta correta: C

    Essa é uma questão interessante, uma vez que trata de diversos assuntos: construtores e classe interna anônima. A resposta consiste em saber que, mesmo sem haver a palavra-chave extends presente na classe Gerente, Gerente extends Funcionario, de forma que, ao criar um gerente, as regras de invocação de construtores devem que ser aplicadas. Assim, primeiro o construtor da classe Pai é chamado e depois o construtor da classe Filho é chamado.

  3. Resposta correta: A

    A única conversão possível é de dog em um Pitbull. Desse modo, pitbull poderá acessar os métodos específicos de Pitbull. Para que a opção B funcione é necessário uma conversão explícita forçada.

  4. Resposta correta: A e D

    A variável protected nome na classe Funcionario é acessível na classe Chefe apesar de estar em um outro pacote, desde que Chefe estenda Funcionario. Então, a alternativa D está certa e a B não irá compilar porque ela acessa o atributo nome de uma instância da classe Funcionario, o que é proibido.

    A opção C não irá compilar porque métodos protected não podem ser sobrepostos por métodos com acesso-padrão. Por fim, a opção E está errada porque não é possível sobrepor um método por relacionamento de herança modificando o tipo de retorno.

  5. Resposta correta: C

    A opção E executa e compila sem erros pois é permitido atribuir o objeto da subclasse para uma variável de referência da superclasse.

    As opções A e D causarão erro de compilação, visto que elas tentam atribuir um tipo incompatível a referência. A alternativa B causará um erro de compilação, visto que as classes Z e Y não possuem uma relação de herança. A opção C compilará, mas lançará um ClassCastException em tempo de execução, uma vez que o objeto é convertido para Y não é uma instância de Y.

Esperamos que possam aproveitar mais esse artigo da série e que venham os próximos!


Leia todos artigos da série

Artigos relacionados