













Estude fácil! Tem muito documento disponível na Docsity
Ganhe pontos ajudando outros esrudantes ou compre um plano Premium
Prepare-se para as provas
Estude fácil! Tem muito documento disponível na Docsity
Prepare-se para as provas com trabalhos de outros alunos como você, aqui na Docsity
Encontra documentos específicos para os exames da tua universidade
Prepare-se com as videoaulas e exercícios resolvidos criados a partir da grade da sua Universidade
Responda perguntas de provas passadas e avalie sua preparação.
Ganhe pontos para baixar
Ganhe pontos ajudando outros esrudantes ou compre um plano Premium
Neste documento, aprenda sobre classes abstratas, interfaces e exceções em java. Saiba como solucionar problemas de herança, implementar interfaces e lidar com diferentes tipos de exceções. Encontre exemplos práticos e compreenda como essas conceitos ajudam a escrever código mais limpo e legível.
Tipologia: Notas de estudo
1 / 21
Esta página não é visível na pré-visualização
Não perca as partes importantes!














Módulo
Classes Abstratas, Interfaces e Exceções
Estamos alcançando o fim da nossa trilha – que é apenas o início do maravilhoso mundo que Java nos oferece – e este momento é bastante adeqüado para introduzirmos os conceitos de classes Abstratas, Interfaces e Exceções. Classes Abstratas e Interfaces são estruturas muito importantes quando programamos Orientado a Objetos com Java e, visto que já conhecemos os demais fundamentos da linguagem (reuso, herança, polimorfismo, encapsulamento, métodos, etc.), está na hora de aplicarmos estes recursos existentes na linguagem. Uma das diferenças mais evidentes entre um bom e um mal programa é o tratamento de exceções, isto é, a capacidade que o seu programa tem de responder a situações inesperadas. Quando o tratamento de exceções é feito adeqüadamente o seu programa fica mais robusto e ao mesmo tempo a usabilidade – usabilidade nada mais é do que a facilidade que um usuário tem em utilizar o programa – aumenta sensivelmente. Porém antes de iniciarmos o novo conteúdo vamos conhecer uma importante característica do construtor com parâmetros levando em conta o uso de herança.
Nós já vimos que uma classe herda de sua classe pai as suas características porém a subclasse não herda o construtor , isto é, o construtor é individual para cada classe. Sempre que o construtor de uma classe é invocado ele invoca , quando não explicitado, implicitamente o construtor da superclasse. Desta forma o construtor da
classe Object sempre é invocado, pois todas as classes estendem dela. Observe o código abaixo referente a classe Veiculo: public class Veiculo { private String marca; private Double capacidadeTanqueCombustivel; public Veiculo(String marca) { this .marca = marca; } //Demais membros... Quando o compilador gerar o bytecode o que nós teremos é o seguinte: public class Veiculo { private String marca; private Double capacidadeTanqueCombustivel; public Veiculo(String marca) { super (); this .marca = marca; } //Demais membros... Observe que a declaração super () indica que o construtor default – construtor public e sem parâmetros – da superclasse está sendo invocado. Isto acontece de forma automática, nós não precisamos nos preocupar. O construtor default é adicionado pelo compilador quando não há construtor declarado explicitamente, isto é, se o programador não declarar um construtor o compilador irá adicionar, implicitamente, um público e sem parâmetros. No entanto se for adicionado explicitamente um construtor o compilador não irá adicionar implicitamente o construtor default, o que significa que se nós criarmos um construtor com parâmetros e estendermos a classe, obrigatoriamente teremos que, explicitamente, invocar o construtor da superclasse passando os parâmetros. Para ficar mais claro vamos estender a classe Veiculo que possui um construtor com parâmetros: public class Jipe extends Veiculo{ public Jipe() { } //Demais membros... O código acima demonstrado não compila. Imagine o compilador tentando
construtor que recebe o mesmo parâmetro da superclasse e outro que recebe dois parâmetros (marca e modelo), ou seja, poderíamos ter N-construtores, um para cada situação.
Até este momento todas as classes que havíamos desenvolvido eram classes concretas , isto é todas as nossas classes podem originar objetos. Classes concretas são estruturas definidas e prontas para instanciarem objetos. Uma classe abstrata se comporta de forma diferente de uma classe concreta. Uma classe abstrata nunca pode ser instanciada de forma direta, seu maior propósito, a razão da sua existência, é ser estendida. Para que serve esta classe então? Imagine a seguinte situação, nós temos uma hierarquia de classe conforme abaixo: Todos os animais (cachorro, cavalo e preguiça) estendem da classe Animal, no entanto, no nosso, programa a classe Animal por si só não representa nenhum tipo de animal. Isto é, esta classe representa apenas um animal genérico e ela existe apenas para ser estendida dando origem a um animal de fato. Nesta situação é mais adeqüado que seja feita a mudança na classe Animal a fim de torná-la abstrata e inviabilizar a sua instanciação por qualquer parte do programa. Isto é realizado adicionando-se o modificador abstract conforme abaixo: public abstract class Animal { //corpo da classe } Observe que agora a classe Animal não pode ser instanciada indevidamente em nenhum trecho do nosso programa, o exemplo abaixo irá produzir um erro de
compilação: Animal animal = new Animal(); Desta forma nós temos certeza que durante a execução do programa nunca haverá um objeto da classe Animal e sim, sempre, de uma de suas subclasses. Uma classe abstrata pode possuir métodos concretos – um método concreto é todo aquele possui corpo – veja o exemplo abaixo: public abstract class Animal { private String nome; //getters e setters... public void emitirSom() { System. out .println("Animal emitindo som"); } } Perceba que o método emitirSom() na verdade deverá ser sobreescrito por cada uma das subclasses da classe Animal a fim de que forneçam um comportamento adeqüado pois, cada tipo de animal emite um som diferente dos demais. Porém, da forma como foi feito, esta condição não é garantida pois, podemos ter uma subclasse da classe Animal que não sobreescreve o método emitir som, no entanto existem formas de forçarmos esta condição. Ao modificarmos o método com abstract impedimos que alguma das subclasses não implementem o método. Segue exemplo abaixo: public abstract class Animal { private String nome; //getters e setters... public abstract void emitirSom(); } Agora todas as subclasses da classe Animal irão obrigatoriamente ter que fornecer uma implementação para o método emitirSom(). Além da palavra reservada abstract devemos encerrar o método sem as chaves para que seja um método abstrato válido. Uma classe abstrata pode conter apenas métodos abstratos, concretos ou ambos. No entanto a presença de um método abstrato torna, obrigatoriamente, a classe abstrata também.
Método concreto Método abstrato
tipo e nós devemos utilizar interfaces quando existem determinados comportamentos que sejam similares entre hierarquias de classes diferentes. Veja o método emitirSom() da classe abstrata Animal, agora suponha que no mesmo programa eu tenha uma classe Bola, que por sinal também emite som. Em um determinado momento do programa eu desejo invocar o método emitirSom para alguns objetos conforme a situação abaixo: public void limpar(Object obj) { //Código para limpar uma bola ou um animal... if (obj instanceof Bola) { ((Bola)obj).emitirSom(); } else if (obj instanceof Animal) { ((Animal)obj).emitirSom(); } } Neste programa nós temos um método que efetua a limpeza de qualquer objeto, seja ele um animal ou uma bola, porém caso o objeto seja de um dos dois tipos, Bola ou Animal, estes deverão emitir seus sons característicos. Esta situação não pode ser resolvida através do uso de herança já que a classe Bola e a classe Animal não estão na mesma hierarquia de classes – na verdade até poderia ser resolvida com herança e sobreescrita, mas havemos de convir que colocar a classe Bola e a classe Animal na mesma hierarquia é um erro de projeto – logo, a solução seria a criação da interface EmiteSom conforme abaixo: public abstract interface EmiteSom { public abstract void emitirSom(); } Observe que o uso do abstract é opcional tanto na classe quanto no método pois toda interface é obrigatoriamente abstrata e seus métodos também o são. A classe Animal e a classe Bola ficariam da seguinte forma: public class Bola implements EmiteSom{ public void emitirSom() { System. out .println("Boing, boing..."); } }
Implementação do método abstrato
public abstract class Animal implements EmiteSom{ private String nome; //getters e setters... } Perceba o uso da palavra reservada implements que informa ao compilador que esta classe está implementando uma interface. A classe Animal não fornece uma implementação para o método emitirSom(), ao contrário da classe Bola, isto acontece porque a classe Animal é do tipo abstract. Quando uma classe abstrata implementa uma interface ela pode ou não fornecer a implementação dos métodos oriundos da interface. Caso a classe abstrata não forneça a implementação destes métodos (situação existente na classe Animal), obrigatoriamente cada subclasse concreta da classe abstrata irá ter que fornecer a implementação. Por exemplo, a classe Cachorro fica da seguinte forma: public class Cachorro extends Animal { public void emitirSom() { System. out .println("au! au!"); } } Então, podemos interpretar a situação da seguinte forma: “A classe Animal implementa a interface EmiteSom, mas deixou a responsabilidade pela implementação do método para a classe concreta Cachorro”. E agora o nosso mesmo método limpar() utilizando a interface: public void limpar(Object obj) { //Código para limpar uma bola ou um animal... if (obj instanceof EmiteSom) { ((EmiteSom)obj).emitirSom(); } } Perceba que independente do tipo de objeto que vier como parâmetro, desde que ele seja do tipo EmiteSom, eu sei que irá possuir a implementação do método emitirSom() e que portanto o código será executado sem problemas, isto também nos permite acabar com aquelas cadeias indesejáveis de if's deixando o código mais legível.
Cadê a implementação? Implementação do método
A exceção foi do tipo java.Lang.ArrayIndexOutOfBoundsException que significa a tentativa de acesso a uma posição fora dos limites do array, a seguir ele informa que posição foi, neste caso o índice 0 (zero). Embaixo da definição da exceção nós temos a stacktrace que contém a seqüência da pilha de execução. Pela stacktrace é possível perceber quais caminhos a execução percorreu até alcançar a exceção. A primeira linha da stacktrace contém a definição da exceção, a segunda linha contém o último trecho de código executado com o número da linha no código fonte, neste caso foi a invocação do método imprimirPosicao0() da classe RecebeArray , que no código fonte (arquivo RecebeArray.java) refere-se a linha 10. A fim de construirmos programas mais robustos, poderíamos tratar situações como esta. Para isto iremos utilizar o bloco try/catch : try { //Código perigoso } catch (Exception e) { //tratamento da exceção } O bloco try/catch define dois trechos de código, um sujeito a erros (try) e outro que responsável pelo tratamento do erro caso aconteça (catch). Vamos modificar agora a nossa classe RecebeArray e adicionar este bloco ao método imprimirPosicao0(): public class RecebeArray { public RecebeArray(String[] array) { imprimirPosicao0(array); } public void imprimirPosicao0(String[] array) { try { System. out .println(array[0]); } catch (ArrayIndexOutOfBoundsException e) { System. out .println("Erro: Array vazio, execute o programa novamente" + " passando ao menos um parâmetro."); }
} } E agora ao executarmos novamente este mesmo programa o usuário irá receber uma mensagem mais significativa, observe a execução abaixo: Pronto! Agora ao executar o programa sem parâmetros o usuário saberá o que fazer. Porém o mais interessante é o seguinte, uma vez que a exceção foi tratada, o programa continua o fluxo de execução e termina normalmente. Devemos observar o seguinte, esta não é a única exceção que pode acontecer neste método, vamos modificar o método main() de forma que ele encaminhe como parâmetro um valor nulo ( null ). public class modulo10Main { public static void main(String[] args) { new RecebeArray( null ); System. out .println("Termino do programa!"); } } Repare que agora a nova instância da classe RecebeArray está recebendo como parâmetro uma referência nula, isto ocasionará uma exceção do tipo java.lang.NullPointerException. Vejamos o resultado da execução deste código: Ok! O programa encerrou de forma inesperada – a mensagem “Termino do
Todas as exceções em Java derivam da classe Throwable conforme hierarquia a seguir: Observe que, conforme dito em módulos anteriores, todas as classes em Java estendem de alguma forma – direta ou indiretamente – da classe Object. Nesta imagem estão representadas as três modalidades de exceções existentes na linguagem Java: Unchecked Exception , Checked Exception e Error. Error : Hierarquia em laranja na imagem, representam situações incomuns, que não são causadas pelo programa, indicam situações que não acontecem usualmente durante a execução de um programa (Ex: Estouro da pilha de execução – StackOverflowError); Checked Exception : Hierarquia em cinza, representam situações que, geralmente, não são erros de programação e sim indisponibilidade de recursos ou condição necessária para a correta execução inexistente (Ex: Em aplicações distribuídas existe dependência externa de rede de
comunicação – NoRouteToHostException). Unchecked Exception (RuntimeException) : Hierarquia em turquesa (azul escuro), representam situações que, geralmente, identificam erros de programação (programa não é robusto) ou mesmo situações incomuns/difíceis de tratar (Ex: Acessar índice inválido em um array – ArrayIndexOutOfBoundsException); As checked exceptions são tratadas obrigatoriamente, isto é, o compilador só compila a classe se houver um tratamento (bloco try/catch) para aquele tipo de exceção. Segue quadro com relação de algumas das mais comuns exceções:
ArrayIndexOutOfBounds Exception Tentativa de acesso a posição inexistente no array. ClassCastException Tentativa de efetuar um cast em uma referência que não é classe ou subclasse do tipo desejado. IllegalArgumentException Argumento formatado de forma diferente do esperado pelo método. IllegalStateException O estado do ambiente não permite a execução da operação desejada. NullPointerException Acesso a objeto que é referenciado por uma variável cujo valor é null. NumberFormatException Tentativa de converter uma String inválida em número. StackOverflowError Normalmente acontece quando existe uma chamada recursiva muito profunda. NoClassDefFoundError Indica que a JVM não conseguiu localizar uma classe necessária a execução.
Possivelmente em algum trecho do nosso programa, se uma determinada condição acontecer, nós queiramos lançar uma exceção – por incrível que possa parecer – para informar que a situação esperada não aconteceu.
A linha em destaque significa que estou invocando o construtor da superclasse (Exception) e enviando o parâmetro mensagem para ela. O novo método abastecer fica da seguinte forma: public void abastecer(Double litros) { if (litros < 0) { throw new QuantidadeLitrosException("Valor indevido. Era esperado um valor maior que 0: "+litros); } else { tanque += litros; } } A execução deste método com um valor negativo resulta em: Exception in thread "main" QuantidadeLitrosException: Valor indevido. Era esperado um valor maior que 0: -1. at Veiculo.abastecer(Veiculo.java:10) at modulo10Main.main(modulo10Main.java:6) Na prática, são poucos os motivos que nos levam a criar as nossas próprias exceções, normalmente as exceções existentes nas bibliotecas Java são suficientes para a maioria das ocorrências.
Aprenda com quem também está aprendendo, veja e compartilhe as suas respostas no nosso Fórum: Exercícios – Módulo 08 – Classes Abstratas, Interfaces e Exceções