Java com café: sobrescrita
Mostrando postagens com marcador sobrescrita. Mostrar todas as postagens
Mostrando postagens com marcador sobrescrita. Mostrar todas as postagens

Sobrescrita e Exceções

Introdução

Hoje vou escrever sobre um assunto que está presente no objetivo 1.5 da SCJP. O objetivo determina que o candidato saiba identificar se a sobrescrição de um método está correta. Eu já falei sobre sobrescrita num outro post, mas fiquei devendo uma explicação melhor sobre exceções.


Exceções Verificadas (checked)

As exceções verificadas são exceções que devem ser tratadas por você no seu código. O compilador irá forçá-lo a lidar adequadamente com essas exceções. Você pode capturar essas exceções ou declará-las na cláusula throws do método.
Geralmente, as exceções verificadas ocorrem devido a condições fora de seu controle, como IOException ou uma falha na conexão de rede. Mas sua aplicação deve prever essas condições e tratá-las corretamente para que não venha travar. É por isso que você é forçado pelo compilador a lidar com essas exceções. Se a conexão de rede é interrompida durante uma transferência de arquivo, o aplicativo deve lidar com isso e permitir que o usuário tente novamente.


Exceções Não verificadas (unchecked)

Por outro lado, exceções não-verificadas ocorrem devido a erros de codificação por parte do programador. Como uma exceção NullPointerException que pode ser evitada através de uma verificação para saber se a referência é nula.
Existem também algumas exceções unchecked que podem ocorrer que não são culpa do programador. Estas são as exceções que são subclasses da classe java.lang.Error. Quando essas exceções ocorrem, não há muito o que fazer, então você não é obrigado a lidar com elas já que não pode prever quando irão ocorrer. Como um OutOfMemoryError pode ocorrer a qualquer momento que um objeto é instanciado, você não pode colocar cada instanciação de objeto em blocos try-catch.

Exemplos

Vamos criar um exemplo para ver como o compilador nos força a tratar exceções verificadas.
class ExcecaoNaoTratada {
public static void main(String[] args) {
 throw new Exception();
}
}

O código acima não vai compilar porque não estamos tratando a exceção verificada. Para tornar o código compilável podemos colocar a instrução num bloco try-catch afim de capturá-la ou declarar a exceção numa cláusula throws do método main que irá lançá-la.

No caso de exceções não verificadas, se o método lançar uma exceção não verificada, o compilador não vai reclamar se ela não for tratada. Então o seguinte código compila normalmente:
class ExcecaoNaoTratada {
public static void main(String[] args) {
 throw new RuntimeException();
}
}


Hierarquia de classes

A classe java.lang.Throwable é uma exceção verificada. A API Java define duas subclasses de Throwable. Estes são java.lang.Exception e java.lang.Error. Error é uma classe de exceção não verificada, enquanto Exception é uma exceção verificada. A classe Exception em si tem a subclasse java.lang.RuntimeException (não verificada). As exceções não verificadas como NullPointerException, ClassCastException etc, são todas as sub-classes de RuntimeException. Todas as subclasses de RuntimeException e Error são exceções não verificadas. Outras classes são exceções verificadas.


Se você criar sua própria classe de exceção, se ela é verificada ou não, depende de qual classe Exception ela é subclasse. Se a sua classe estende uma classe de exceção verificada, então sua classe será uma exceção verificada. E se a sua classe estende uma classe de exceção não verificada, então a sua classe será uma exceção não verificada.

Existe uma regra com exceções verificadas que às vezes confunde as pessoas. Se no seu bloco catch você está capturando uma exceção verificada, então deve haver a possibilidade de sua ocorrência no bloco try. Assim, o código a seguir não vai compilar:
try {
System.out.println("...");
} catch(java.io.IOException ioe) {
}

O compilador irá informar que IOException nunca é lançada no bloco try, então você não pode capturá-la. Esta regra não se aplica às exceções não verificadas. Se você está capturando uma exceção não verificada, que não é lançada no bloco try, o compilador não vai reclamar. Agora fica um pouco confuso com as classes Exception e Throwable. Ambas são classes de exceções verificadas, mas você pode capturá-las sem qualquer exceção sendo lançada no bloco try. Então, esse código será compilado sem erros:
try {
System.out.println("...");
} catch(Exception ex) {
}

As regras são diferentes para Throwable e Exception e outras classes de exceção verificada. Isto porque tanto Exception e Throwable tem exceções não verificadas como subclasses. Assim, o compilador permite capturá-los pensando que você pode estar tentando pegar uma exceção não verificada. Lembre-se que o compilador não está preocupado com as exceções não verificadas. RuntimeException é uma subclasse de Exception e Error é uma subclasse de Throwable, e ambos RuntimeException e Error são classes de exceção não verificada. Então, quando escrever o código:
try {
System.out.println ("...");
} Catch (Exception ex) {
}

O compilador não vai reclamar porque Exception também pode capturar exceções não verificadas (RuntimeException e suas subclasses). Da mesma forma o código a seguir é permitido porque Throwable pode capturar exceções não verificadas (RuntimeException e Error e suas subclasses).
try {
System.out.println ("...");
} Catch (Throwable ex) {
}


Regras

Antes de continuar quero adiantar que os tópicos anteriores foram traduzidos deste site. Na verdade isso fugiu um pouco ao objetivo do exame mas serve como um esclarecimento sobre exceções. Agora vejamos ao que realmente importa no exame:
  1. O método novo não pode lançar exceções verificadas novas ou mais abrangentes;
  2. O método novo pode lançar exceções verificadas mais restritivas ou menos abrangentes;
  3. O método novo pode lançar qualquer exceção não verificada;
A maioria dos programadores Java utilizam alguma IDE para desenvolver. É muito mais produtivo. As vezes utilizamos Notepad só para fazer alguns testes, mas normalmente alguém em boas condições mentais não programaria no bloco de notas. As IDE´s quase sempre sabem quando estamos fazendo algo errado. Aí então damos um clique e está resolvido. As regras acima são exemplos de coisas que podemos deixar por conta da IDE, mas no exame não! Para o exame você deve ter um compilador rodando em seu cérebro. A cada código analisado, você deverá saber se está certo ou não. Então vamos treinar um pouco pra isso.
A primeira regra diz que os métodos novos não podem lançar excessões novas ou mais abrangentes (menos restritivas). Abaixo temos um código que será usado como exemplo, nele temos uma sobrescrição simples:
public class Excecoes extends Exemplo{

void A (){
// Sobrescreveu..
}

}

class Exemplo{

void A (){

}

}

A primeira coisa é que não se pode fazer isso:
void A () throws Exception {
// Sobrescreveu..
}

O método novo tentou lançar uma exceção verificada nova. O código não compila. Agora analise este caso:
import java.io.IOException;

public class Excecoes extends Exemplo{

void A () throws Exception { //Tentou lançar uma exeção mais abrangente...
// Sobrescreveu..
}

}

class Exemplo{

void A () throws IOException{

}

}

O código acima também não compila. O método novo tentou lançar uma exceção mais abrangente. Com isso vimos como funciona a primeira regra. Vamos agora pra segunda:
import java.io.IOException;

public class Excecoes extends Exemplo{

void A () throws IOException { //Excesão menos abrangente e mais restritiva
// Sobrescreveu..
}

}

class Exemplo{

void A () throws Exception{

}

}

O código acima compila. O novo método lançou uma exceção que é subclasse de Exception, por isso ela é menos abrangente. O método sobrescrito se arrisca a lançar qualquer tipo de Exception, mas o método novo não precisa declarar nada que nunca lançará.
E a terceira e última regra diz que o método novo pode lançar qualquer exceção não verificada. Isso significa que o seguinte código é válido:
public class Excecoes extends Exemplo{

void A () throws RuntimeException {
// Sobrescreveu..
}

}

class Exemplo{

void A (){

}

}

Outro detalhe importante também é que o método novo pode resolver não lançar nenhuma exceção. Portanto, se ver um método sendo sobrescrito e não lançar nenhuma exceção (mesmo que o método da superclasse o faça) saiba que isto é válido.

Conclusão

Espero ter deixado mais claro este assunto que é muito importante não só pra quem vai tirar uma certificação mas também pra quem está programando no dia a dia e sabe que o negócio não é só ficar lançando exceção ou capturando sem nenhuma codificação no bloco Catch.

Até a próxima!

Sobrescrita / Sobercarga (Overriding / Overloading) - Java

O assunto de hoje é sobre o objetivo 1.5 e 5.4 do exame SCJP. Os objetivos são basicamente determinar se um método esta corretamente sendo sobrescrito ou sobrecarregado e sobre os tipos de retorno válidos para o método.
Também fala sobre desenvolver código que declare e chame métodos sobrescritos ou sobrecarregados, e códigos que chamem construtores da superclasse sendo eles sobrescritos ou sobrecarregados.

Métodos sobrescritos

Sempre que herdamos um método da superclasse, podemos sobrescrevê-lo. Fazendo isto temos a oportunidade de determinar um comportamento específico do método para a subclasse. Então estamos dizendo "Sei que existe um método genérico herdado da superclasse para fazer isto, mas este método não me serve e quero que esta subclasse tenha o seu próprio". Tudo bem, mas não esqueça que métodos marcados como final, private ou static não são herdados e por este motivo não podem ser sobrescritos.
Para demonstrar como os métodos são sobrescritos vou usar o mesmo código que usei no tópico sobre casting:
//Superclasse
public class Animal {

void comer(){
 System.out.println("Animal comendo...");
}

}
//Subclasse
class Dog extends Animal{

void rolar(){
 System.out.println("Dog rolando...");
}
//Dog sobrescreveu o método comer
void comer(){
 System.out.println("Dog comendo...");
}
}

Dog herdou o método comer de animal mas por algum motivo teve que escrever seu próprio método comer. Então se num determinado momento quisermos chamar:
new Dog().comer();

Teremos o resultado Dog comendo.... Sem problemas.
Vamos analisar alguns casos agora. Se você herdou métodos abstratos de uma classe abstrata, então é obrigatório sobrescrevê-los (na prática você estaria implementando porque não tem nada pra sobrescrever). Mas se sua classe também for abstrata não precisa, você pode simplente "passar a vez" para a última classe concreta da árvore. Só não esqueça que em algum momento você será obrigado a sobrescrever os métodos abstract.
Uma questão interessante que também foi abordada no post sobre casting é a seguinte: Quando sobrescrevemos um método e depois fazemos o upcast, qual método será chamado? Seguindo o mesmo exemplo do código acima, se fizermos a seguinte chamada:
Animal a = new Dog();
a.comer();

Qual versão do método será executada? Sabemos que quando convertemos pra cima da árvore de herança, os métodos do subtipo são perdidos mas neste caso a versão executada será a do subtipo. Porque? O compilador só verifica o tipo da variável e não da da instância. A referência é do tipo Animal? Então só métodos de Animal poderão ser executados. Mas em tempo de execução, o objeto real que está sendo executado é Dog, então o método de Dog será chamado.
Parece confuso mas pense: Estamos criando uma referência Animal para Dog. O que o compilador vai fazer? Ele vai pensar "Vou criar um objeto tipo Dog na memória e uma variável x tipo Animal que aponte pra este objeto." A variável sendo do tipo Animal não tem conhecimento dos métodos em Dog. Acontece que quando o programa for executado a JVM vai pensar "A variável x está chamando o método em Animal, mas o objeto sobrescreveu? Então execute o método do objeto!".

Outro ponto importante é que os métodos sobrescritos não podem declarar um modificador de acesso mais restritivo do que o que foi sobrescrito. Por exemplo:
//Superclasse
public class Animal {

protected void comer(){
 System.out.println("Animal comendo...");
}

}
//Subclasse
class Dog extends Animal{

void comer(){ //Problema!!!
 System.out.println("Dog comendo...");
}
}

Declaramos o método como protected mas depois tentamos deixá-lo default. Isto não compila porque default é mais restritivo que protected. Como expliquei antes, em tempo de execução, a JVM chama a versão do método sobrescrito, e o modificador de acesso ficou mais restrito. Se ele estivesse em um pacote diferente não poderia ser chamado.
Vamos ver agora umas regras importantes:
A lista de argumentos deve se manter a mesma!. Se ela mudar o método não será mais sobrescrito e sim sobrecarregado. Lembre-se que sobrescrever não é nada mais que usar o mesmo nome do método herdado.
O modificador de acesso do novo método não pode ser mais restritivo (menos pode). Já falei sobre isto!
Métodos só podem ser sobrescritos se forem herdados. Ou seja, métodos final, private ou static não podem. E classes fora do pacote só podem sobrescrever métodos public ou protected (métodos protected são herdados).
O tipo de retorno deve ser o mesmo ou um subtipo do método original. Isto chama-se retornos covariantes ou covariant returns. Este é um assunto a parte mas apenas lembre-se que o tipo de retorno pode ser o mesmo ou um subtipo. Não tem problema em retornar um subtipo porque com certeza ele "cabe" no supertipo.
O método que sobrescreveu pode lançar qualquer exceção de tempo de execução (não verificada / unchecked), mesmo que o método original não tenha lançado.
O método que sobrescreveu não pode lançar exceções verificadas (checked) novas ou mais abrangentes que as declaras pelo método original. Este é um assunto para outro post.
Pra finalizar a parte de sobrescrita vou fazer só mais duas observações importantes: Podemos chamar a versão da superclasse do método usando a palavra chave super:
//Superclasse
public class Animal {

void comer() {
 System.out.println("Animal comendo...");
}

public static void main(String[] args) {
 Dog g = new Dog();
 g.comer();
}
}

// Subclasse
class Dog extends Animal {

void rolar() {
 System.out.println("Dog rolando...");
}

// Dog sobrescreveu o método comer
void comer() {
 super.comer(); //Chamou a versão da superclasse
 System.out.println("Dog comendo...");
}

}

E outra, mesmo não tento falado sobre exceções vou insistir em colocar uma questão aqui que poderá cobrada no exame:
//Superclasse
public class Animal {

void comer() throws Exception {
 //Lança uma exceção
}
public static void main(String[] args) {
 Animal a = new Dog(); //Referência polimórfica
 a.comer();
}
}

// Subclasse
class Dog extends Animal {

void rolar() {
 System.out.println("Dog rolando...");
}

// Dog sobrescreveu o método comer
void comer() {
 System.out.println("Dog comendo...");
}
}

Um método foi sobrescrito, mas está sendo chamado através de uma referência polimórfica (do superclasse) para apontar para o objeto do subtipo. Como vimos o compilador vai pensar que você está chamando a versão da superclasse. Mas a JVM em tempo de execução vai chamar a versão da subclasse. A versão da superclasse declarou uma exceção, mas a do subtipo não. O código não compila devido a exceção declarada. Mesmo que a versão usada em tempo de execução seja a do subtipo.

Métodos Sobrecarregados

Metodos sobrecarregados tem uma diferença básica dos métodos sobrescritos: Sua lista de argumentos DEVE ser alterada. Por este motivo, os métodos sobrecarregados permitem que se utilize o mesmo nome do método numa mesma classe ou subclasse e isto pode ser usado para dar mais opções a quem for chamar o método. Por exemplo, podemos ter um método que receba uma variável int e outro que receba double, e a pessoa que for chamar não precisará se preocupar com fazer nenhuma conversão.
Diferente dos métodos sobrescritos, os sobrecarregados podem mudar o tipo de retorno e o modificador de acesso sem restrições. Eles também podem lançar exceções verificadas novas ou mais abrangentes.
Os métodos sobrecarregados podem ser declarados na mesma classe ou na subclasse. Saber diferenciar métodos sobrecarregados dos sobrescritos é importante pra não cair em pegadinhas como:
public class Animal {

public void comer(int x, String s) {
 System.out.println("Animal comendo...");
}
}

class Dog extends Animal {

protected void comer(int x, float s) {
 System.out.println("Dog comendo...");
}
}

O código acima pode parecer violar uma das regras de sobrescrita (modificador mais restritivo) porém ele está é sobrecarregando pois houve mudança na lista de argumentos.
Uma questão importante em métodos sorbrecarregados é: Qual método será chamado? Isso vai depender exclusivamente dos argumentos passados. Por exemplo, no caso anterior se você chamar:
new Dog().comer(2, "a");

Vai retornar Animal comendo.... Mas e se usarmos argumentos de objetos ao invés de tipos primitivos?
public class Animal {

void teste(Animal a) {
 System.out.println("Animal");
}


public static void main(String[] args) {
 new Dog().teste(new Dog()); //Chamou passando um Dog
 new Dog().teste(new Animal()); //Chamou passando um Animal
}
}

class Dog extends Animal {

void teste(Dog d) {
 System.out.println("Dog");
}
}

A saída será normal, executou o método de acordo com o tipo passado:
Dog
Animal


Mas e se a referência usada fosse polimórfica:
Animal a = new Dog();
a.teste(a);

Qual versão será executada? Você poderia pensar que seria a versão de Dog, porque estamos passando uma referência ao objeto Dog. Mas não é, a saída seria Animal pois mesmo sabendo que em tempo de execução o objeto utilizado será um Dog e não um Animal, quando o método é sobrecarregado isto não é decidido em tempo de execução. Então lembre-se: Quando o método é sobrecarregado, apenas o tipo da referência importa para saber qual método será chamado.
Resumindo, o método sobrescrito a ser chamado é definido em tempo de execução e o sobrecarregado no tempo de compilação.
Então pra fechar o tópico vou dar mais uma dica: Fique atento aos métodos que parecem ser sobrescritos mas estão sendo sobrecarregados:
public class Animal {

void teste() { }

}

class Dog extends Animal {

void teste(String s) {}

}

A classe Dog tem dois métodos: A versão teste sem argumentos que herdou de Animal e a versão sobrecarregada. Um código que faz referência a Animal pode chamar somente teste sem argumentos mas uma referência a Dog pode chamar qualquer um.

Dever de casa
Dado:
class Plant{

String getName() { return "plant"; }
Plant getType() { return this; }

}

class Flower extends Plant{

 //Insira o código aqui

}

class Tulip extends Flower { }

Quais instruções, inseridas na linha comentada, irão compilar? (Marque todas corretas)
A. Flower getType() { return this; }
B. String getType() { return "this"; }
C. Plant getType() { return this; }
D. Tulip getType() { return new Tulip(); }