Herança
Como já vimos, em programação orientada a objetos, o código é escrito em classes que são instanciadas como objetos. Às vezes, é interessante reusar métodos ou classes, ou ainda construir código que estenda um código existente. Uma das formas de estender o funcionamento de uma classe é através de herança.
Herança é um tipo de relacionamento entre classes que permite que código seja reusado. Nesse relacionamento, é criado uma hierarquia entre classes, em que uma classe herda de outra. Existem dois tipos de classes nesse relacionamento: a superclasse, classe mãe ou base e a subclasse ou classe filha, que herda os atributos e métodos da classe base. Ao herdar as propriedades e métodos da classe base, a classe filha também pode estendê-los.
Em Java, herança é transitiva — se Faca
estende Arma
e Arma
estende Item
, então Arma
também estende Item
. Item
será a superclasse para Arma
e Faca
. Herança é amplamente usada em aplicações Java. Por exemplo, para criar uma exceção específica para determinados problemas, é preciso estender a classe Exception
, como é o caso de NullPointerException
. De forma similar, toda classe criada em Java implicitamente estende a classe Object
Observe, no Código 1 a seguir, as classes Item
, Arma
e Escudo
. Observe a palavra especial extends
na definição das classes Arma
e Escudo
. A declaração Arma extends Item
significa que a classe Arma
herda da classe Item
. Dessa forma, a classe Item
será a classe base e as classes Arma
e Escudo
serão as classes filhas nesse relacionamento. A classe Item
compartilhará com as classes Arma
e Escudo
os seus métodos e atributos.
// Início - Classe Item
public class Item {
private String nome;
private int pontos;
public Item(String nome, int pontos) {
this.nome = nome;
this.pontos = pontos;
}
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
public int getPontos() {
return pontos;
}
public void setPontos(int pontos) {
this.pontos = pontos;
}
}
// Fim - Classe Item
// Início - Classe Arma
public class Arma extends Item {
public Arma(String nome, int dano) {
super(nome, dano);
}
public int getDano() {
return getPontos();
}
public void setDano(int dano) {
setPontos(dano);
}
}
// Fim - Classe Arma
// Início - Classe Escudo
public class Escudo extends Item {
public Escudo(String nome, int dano) {
super(nome, dano);
}
public int getProtecao() {
return getPontos();
}
public void setProtecao(int protecao) {
setPontos(protecao);
}
}
// Fim - Classe Escudo
Os métodos getNome
, setNome
, getPontos
e setPontos
estão disponíveis nas classes Arma
e Escudo
, mesmo sem estas implementarem os métodos, porque Arma
e Escudo
herdam de Veiculo
.
Perceba como os métodos getPontos
e setPontos
foram reaproveitados na implementação dos métodos getDano
, setDano
, getProtecao
e setProtecao
.
No código a seguir, é demonstrado o uso das classes Arma
e Escudo
. O método getNome
está presente nos dois objetos, mas em cada objeto, os métodos getPontos
e setPontos
foram usados de forma diferente.
public class Main {
public static void main(String[] args) {
Arma faca = new Arma("Faca", 20);
Escudo escudo = new Escudo("Escudo de madeira", 40);
System.out.printf("%s possui %d pontos de dano%n", faca.getNome(), faca.getDano());
System.out.printf("%s possui %d pontos de defesa%n", escudo.getNome(), escudo.getDefesa());
}
}
A saída do código anterior é a seguinte.
Faca possui 20 pontos de dano
Escudo de madeira possui 40 pontos de defesa
O maior benefício do uso de herança é o reúso de código, porque subclasses herdam as variáveis e métodos da superclasse. Entretanto, membros privados da superclasse não são diretamente acessíveis para a subclasse (veja mais sobre visibilidade).
Os construtores da superclasse não são herdados pela subclasse. Se a superclasse não possui um construtor padrão (sem argumentos), então a subclasse também precisa ter um construtor explícito definido. Caso contrário, uma exceção será lançada em tempo de compilação. No caso de a superclasse não ter um construtor padrão, o construtor da superclasse deve ser obrigatoriamente chamado usando super()
, da forma que foi feito em Arma
e Escudo
. Esse deve ser o primeiro comando no construtor da subclasse.
Apesar de a herança em Java ser transitiva, uma subclasse somente pode estender uma classe.
Podemos criar uma instância de uma subclasse e, então, atribui-la a uma variável do tipo da superclasse. Isso é chamado de upcasting. Um exemplo de upcasting é mostrado a seguir.
Arma faca = new Arma("Faca", 20);
Item item = faca; // upcasting - até aqui tudo bem, Arma é um Item
Downcasting, é quando uma instância de uma superclasse é atribuída a uma variável do tipo da subclasse. Nesse caso, precisamos explicitamente fazer o cast para a subclasse. Um exemplo é dado no código a seguir.
Arma faca = new Arma("Faca", 20);
Item item = faca;
Arma faca1 = (Arma) item; // casting explícito - funciona porque item é do tipo Arma
Devido ao casting explícito, o compilador não irá reclamar se fizermos o casting errado. Nesse caso, uma ClassCastException
será lançada em tempo de execução. A seguir está um exemplo de código em que isso acontece.
Arma faca = new Arma("Faca", 20);
Item item = faca;
Escudo escudo = (Escudo) item; // ClassCastException em tempo de execução
Item item = new Item("Item", 0);
Escudo escudo = (Escudo) item; // ClassCastException porque o tipo de item é Item em tempo de execução
Podemos sobrescrever métodos da superclasse na subclasse. Entretanto, devemos sempre anotar métodos sobrescritos com a anotação @Override
. Assim, o compilador saberá que estamos sobrescrevendo um método e se algo mudar no método da superclasse, veremos o problema em tempo de compilação e não em tempo de execução.
Além dos construtores, também podemos chamar os métodos da superclasse ou acessar variáveis da superclasse usando a palavra super
. Isso é útil quando temos a mesma variável ou método na subclasse, mas queremos acessar a variável ou método da superclasse.
A instrução instanceof
é usada para verificar herança entre objetos. Veja os exemplos do código a seguir.
Arma faca = new Arma("Faca", 20);
Escudo escudo = new Escudo("Escudo de madeira", 40);
Item i = faca;
System.out.println(faca instanceof Arma); // imprime true
System.out.println(i instanceof Escudo); // imprime false, i é do tipo Arma em tempo de execução
System.out.println(faca instanceof Item); // imprime true, faca também é um Item
System.out.println(i instanceof Arma); // imprime true, i é do tipo Arma em tempo de execução
Classes marcadas com final
não podem ser estendidas em Java. Se a superclasse não vai ser usada no código, isto é, se ela é só uma base para manter o código reusável, o melhor é mantê-la como abstrata para evitar instanciações desnecessárias e restringir a criação de instâncias da classe base.
Classes abstratas
Classes abstratas não podem ser instanciadas e são tipicamente usadas para prover um código base que as subclasses podem estender e implementar os métodos abstratos e sobrescrever ou usar os métodos implementados na classe abstrata. A palavra abstract
é usada para criar classes e métodos abstratos.
Métodos abstratos são métodos sem implementação na superclasse, que devem possuir obrigatoriamente uma implementação na subclasse. Métodos concretos possuem implementação completa e provêm comportamento padrão que pode ser sobrescrito por subclasses, caso necessário. O exemplo a seguir inclui uma classe abstrata Forma
e uma classe concreta Circulo
que herda de Forma
.
// Início - Classe Forma
public abstract class Forma {
// Método abstrato (sem implementação)
abstract double calcularArea();
abstract String getNome();
// Método concreto
public void mostrar() {
System.out.printf("Este é um %s", getNome());
}
}
// Fim - Classe Forma
// Início - Classe Circulo
public class Circulo extends Forma {
private double raio;
public Circulo(double raio) {
this.raio = raio;
}
@Override
double calcularArea() {
return Math.PI * raio * raio;
}
@Override
String getNome() {
return "Círculo";
}
}
// Fim - Classe Circulo
Nesse exemplo, Forma
é uma classe abstrata que define um método abstrato calcularArea()
e um método concreto mostrar()
. Subclasses de Forma
(por exemplo, Circulo
) devem prover implementações para calcularArea()
e também herdam o método mostrar()
.