Alura > Cursos de Programação > Cursos de Java > Conteúdos de Java > Primeiras aulas do curso Praticando Java: herança, polimorfismo e interfaces

Praticando Java: herança, polimorfismo e interfaces

Herança, interface e polimorfismo - Apresentação

Olá! Meu nome é Jacqueline Oliveira, sou engenheira de software e instrutora aqui na Alura. Quero dar as boas-vindas a este curso sobre herança, polimorfismo e interfaces.

Audiodescrição: Jacqueline se descreve como uma mulher de pele branca, com cabelos longos e reflexos louros. Veste uma blusa rosa, e a parede ao fundo está iluminada em azul.

Neste curso, abordaremos os pilares de herança e polimorfismo e sua importância na programação orientada a objetos. Discutiremos como utilizar as classes abstratas e o papel desse tipo de classe no contexto de herança e polimorfismo. Exploraremos o polimorfismo de sobrecarga e de sobrescrita. Por fim, analisaremos a utilização, definição e os pontos positivos do uso de interfaces.

Este é um conteúdo extremamente relevante para quem está praticando programação orientada a objetos. Nos encontraremos em breve para iniciar essa prática!

Herança, interface e polimorfismo - Herança

Vamos iniciar nossos estudos e práticas abordando o conceito de herança!

Introduzindo o conceito de herança

A herança permite que uma classe filha aproveite atributos e métodos de uma classe pai. Teremos uma classe que conterá informações generalistas sobre uma classe similar, como, por exemplo, funcionários. Funcionários possuem informações em comum, como salário e nome, e podemos ter classes com especializações adicionais, como gerente e desenvolvedor.

No contexto de uma empresa que desenvolve software, essas classes filhas herdarão todos os atributos e métodos da classe funcionário e terão suas especializações.

Por exemplo, um gerente terá um bônus que nem todo funcionário possui. Já um desenvolvedor pode ter uma linguagem de programação, formação, pós-graduação ou cursos extras que, talvez, não sejam relevantes para um gerente. Assim, podemos criar uma classe generalista e suas subclasses especializadas.

Criando a classe base Funcionario

Vamos aplicar isso na prática. No IntelliJ, temos uma classe Funcionario para representar o exemplo mencionado, contendo apenas nome e salário como atributos. Há, ainda, um método construtor que recebe nome e salário para criar uma nova instância de Funcionario.

Além disso, existem dois métodos: exibirInformacoes, que exibe as informações de nome e salário, e reajustarSalario, que ajusta o salário com base em um percentual fornecido.

package br.com.alura;

public class Funcionario {
    private String nome;
    private double salario;

    public Funcionario(String nome, double salario) {
        this.nome = nome;
        this.salario = salario;
    }

    public void exibirInformacoes() {
        System.out.printf("\nFuncionario %s - Salário: %.2f",
                nome, salario);
    }

    public void reajustarSalario(double percentual) {
        salario += salario * (percentual / 100);
        System.out.printf("\nNovo salario de %s é %.2f ", nome, salario);
    }
}

Especializando a classe Gerente

Nosso objetivo é criar especializações de Funcionario. Queremos uma especialização para gerente e outra para desenvolvedor, mas não faria sentido criar uma nova classe repetindo nome, salário e todos os métodos novamente. A herança serve justamente para evitar essa duplicação de código. Assim, conseguimos organizar nossa aplicação sem replicar código desnecessariamente.

Vamos criar uma classe Gerente que herdará as características de Funcionario. Para isso, no nosso pacote br.com.alura, utilizaremos o atalho "Alt + Insert" para criar uma nova classe Java chamada Gerente.

package br.com.alura;

public class Gerente {
}

Agora, precisamos indicar que Gerente é um Funcionario. O termo "é um" é crucial para definir uma herança. Para afirmar que uma herança é bem-sucedida e aplicada corretamente, precisamos dizer que uma classe é um tipo da outra. Assim, um gerente é um funcionário, o que valida a herança.

Para que Gerente herde de Funcionario, usamos a palavra reservada extends.

public class Gerente extends Funcionario {
}

Ao fazer isso, o IntelliJ sublinha essa linha em vermelho, pois, ao afirmar que Gerente estende Funcionario, e considerando que Funcionario possui um construtor que exige nome e salário para criar uma instância, precisamos passar as mesmas informações ao criar um Gerente.

O IntelliJ sugere a opção "Create constructor matching super", que cria um construtor compatível com a superclasse. Ao clicar nessa opção, ele cria um construtor para Gerente, onde precisamos passar nome e salário.

public class Gerente extends Funcionario {
    public Gerente(String nome, double salario) {
        super(nome, salario);
    }
}

Definindo atributos exclusivos para o gerente

Na classe Gerente, incluiremos o atributo adicional bonus.

public class Gerente extends Funcionario {
    private double bonus;

    public Gerente(String nome, double salario) {
        super(nome, salario);
    }
}

Então, temos algumas alternativas para definir esse bônus. Uma opção seria passá-lo diretamente no construtor, adicionando mais um parâmetro. Outra possibilidade é criar os métodos assessores, conhecidos como get e set.

Neste exemplo específico, optaremos por usar os métodos get e set, em vez de incluir o bônus no construtor. Para facilitar, utilizaremos a própria IDE na criação desses métodos. Basta pressionar "Alt + Insert" dentro da classe, selecionar a opção "Getter and setter" e escolher a propriedade do bônus.

public class Gerente extends Funcionario {
  private double bonus;
​
  public Gerente(String nome, double salario) {
    super(nome, salario);
    }

  public double getBonus() {
    return bonus;
  }

  public void setBonus(double bonus) {
    this.bonus = bonus;
  }
}

Pronto, os métodos get e set já foram gerados automaticamente.

Criando uma instância da classe Gerente

Com essas informações, vamos entender como criar uma nova instância de Gerente. Na classe principal, o procedimento será muito semelhante ao que já fizemos com funcionários em cursos anteriores. Primeiro, declaramos o tipo, que é Gerente, definimos o nome da variável, que também será gerente, e a instanciamos com new Gerente.

Na criação da instância, precisamos passar o nome, como "Mario", e o salário, como 15000, por exemplo.

public class Principal {
    public static void main(String[] args) {
        Gerente gerente = new Gerente("Mario", 15000);
    }
}

Reutilizando métodos herdados

Feito isso, ao chamar algo como gerente., o sistema já me permite acessar o método exibirInformações. Mas repare: na classe Gerente, não temos esse método exibirInformações. Isso acontece porque ele está sendo herdado diretamente da classe Funcionário.

public class Principal {
    public static void main(String[] args) {
        Gerente gerente = new Gerente("Mario", 15000);
        gerente.exibirInformações();
    }
}

Então, se executaramos a aplicação agora, obteremos o seguinte:

Funcionario Mario - Salário: 15000,00

Podemos aplicar o mesmo procedimento para reajuste.

public class Principal {
    public static void main(String[] args) {
        Gerente gerente = new Gerente("Mario", 15000);
        gerente.exibirInformações();
        gerente.reajustarSalario(2);
    }
}

Ao executar, obtemos:

Funcionario Mario - Salário: 15000,00

Novo salario de Mario é 15300,00

Podemos adicionar mais funcionalidades. Por exemplo, podemos definir um bônus específico para o Gerente.

public class Principal {
    public static void main(String[] args) {
        Gerente gerente = new Gerente("Mario", 15000);
        gerente.exibirInformações();
        gerente.reajustarSalario(2);
        gerente.setBonus(1000);
    }
}

A partir daí, poderíamos ter métodos específicos para lidar com esses bônus. Por exemplo, no método exibirInformações, poderíamos mostrar não apenas o salário, mas também o valor do bônus. Mas vamos deixar isso para um pouco mais adiante, quando falarmos sobre polimorfismo e abordarmos os conceitos de sobrecarga e sobrescrita.

Especializando a classe Desenvolvedor

Herdamos para o Gerente as informações de salário, nome e os métodos já existentes. Vamos criar mais uma classe para ilustrar o uso da herança: a classe Desenvolvedor.

package br.com.alura;

public class Desenvolvedor {
}

A classe Desenvolvedor também estenderá Funcionario.

public class Desenvolvedor extends Funcionario {
}

Adicionando atributos exclusivos ao desenvolvedor

Para o Desenvolvedor, adicionaremos um atributo stack, que representa a linguagem de programação ou a área de atuação, como back-end ou front-end.

public class Desenvolvedor extends Funcionario {
    private String stack;
}

Após criar esse atributo, para que possamos montar o construtor sem que a IDE apresente erros, vamos criá-lo manualmente, incluindo o campo stack. Para isso, basta pressionar "Alt + Insert" nessa classe, selecionar a opção "Constructor" e incluir o campo stack.

Repare que, inicialmente, o construtor apresenta apenas esse atributo. Ao confirmar com "OK", a IDE gera automaticamente o construtor da classe Desenvolvedor, incluindo os parâmetros nome, salário e stack.

Para os campos nome e salário, o construtor chama a superclasse com o super. Já para o stack, que é uma característica exclusiva da classe Desenvolvedor, o valor é atribuído diretamente com this.stack = stack.

public class Desenvolvedor extends Funcionario {
    private String stack;
    
    public Desenvolvedor(String nome, double salario, String stack) {
        super(nome, salario);
        this.stack = stack;
    }
}

Instanciando e utilizando a classe Desenvolvedor

Agora, vamos até a nossa classe principal para criar também um desenvolvedor.

Primeiro, declaramos a variável desenvolvedor e a inicializamos com uma nova instância da classe Desenvolvedor, passando três parâmetros: o nome "Carla", um salário de "12000" e a stack que será "Backend Java".

public class Principal {
    public static void main(String[] args) {
        Gerente gerente = new Gerente("Mario", 15000);
        gerente.exibirInformações();
        gerente.reajustarSalario(2);
        gerente.setBonus(1000);
        
        Desenvolvedor desenvolvedor = new Desenvolvedor("Carla", 12000, "Backend Java");

    }
}

Ao chamar desenvolvedor.exibirInformacoes, ele exibirá também as informações de Carla.

public class Principal {
    public static void main(String[] args) {
        Gerente gerente = new Gerente("Mario", 15000);
        gerente.exibirInformações();
        gerente.reajustarSalario(2);
        gerente.setBonus(1000);
        
        Desenvolvedor desenvolvedor = new Desenvolvedor("Carla", 12000, "Backend Java");
        desenvolvedor.exibirInformacoes();
    }
}

Funcionario Mario - Salário: 15000,00

Novo salario de Mario é 15300,00

Funcionario Carla - Salário: 12000,00

Considerações finais

Ambas as classes, Gerente e Desenvolvedor, aproveitaram códigos já existentes para Funcionarios. Esses códigos são gerais e aplicáveis a qualquer funcionário, permitindo exibir informações, realizar reajustes e manter atributos como nome e salário.

Essa é a ideia central da herança: aproveitar código sem reescrever, evitando erros e inconsistências entre classes que representam funcionários.

A herança é um tema abrangente, e na sequência, abordaremos classes abstratas, polimorfismo e outros conceitos relacionados à herança. Esperamos que tenha gostado. Pratique criando as classes junto conosco, observe os métodos e atributos utilizados. Até o próximo vídeo!

Herança, interface e polimorfismo - Polimorfismo

Vamos nos aprofundar em mais um pilar da orientação a objetos: o polimorfismo. O polimorfismo permite que o mesmo método tenha comportamentos diferentes dependendo de qual objeto o está chamando.

Perceberemos isso na prática ao ter métodos com o mesmo nome, por exemplo, e ao chamá-los, o método que será chamado dependerá do tipo de instância que foi criada.

Implementando o polimorfismo

Voltando ao IntelliJ, na classe Principal, alteraremos a definição e a criação do gerente e do desenvolvedor. Em vez de declarar Gerente gerente, vamos declarar Funcionario gerente, porque ele é um funcionário, é um gerente, e estamos instanciando-o com new Gerente.

Principal.java:

public static void main(String[] args) {
    Funcionario gerente = new Gerente("Mario", 15000);
    
    // Código omitido
    
}

Observaremos que o setBonus() dará erro, pois declaramos como sendo Funcionario, e nessa classe não temos o método setBonus().

Para o desenvolvedor, será a mesma coisa. Vamos apagar a definição Desenvolvedor e colocaremos Funcionario. Assim, Funcionario gerente recebe new Gerente e Funcionario desenvolvedor recebe um new Desenvolvedor.

public static void main(String[] args) {
    
    // Código omitido
    
    Funcionario desenvolvedor = new Desenvolvedor("Carla", 12000, "Backend Java");

}

Ajustando o método setBonus()

No momento em que definimos de forma genérica que esse gerente é um Funcionario, não conseguimos fazer essa atribuição para o setBonus() sem fazer um typecast. Precisamos fazer um cast, adicionando um (Gerente) para gerente, permitindo que ele entenda que deve utilizar o setBonus().

public static void main(String[] args) {
    
    // Código omitido
    
    ((Gerente) gerente).setBonus(1000);

}

Com esse typecast, estamos forçando-o a entender que essa pessoa colaboradora é um gerente — ou seja, que esse gerente é da classe Funcionario, mas que sua subclasse é Gerente.

Sobrescrevendo métodos

Até aqui, não falamos de polimorfismo, apenas que ambos são declarados como pessoas colaboradoras.

Suponha que queiramos exibir as informações do gerente de forma diferente, porque o gerente tem bônus. Na classe Gerente, precisamos sobrescrever o método exibirInformacoes(). O polimorfismo de sobrescrita é exatamente isso: colocamos um método com a mesma definição do método da superclasse, mas alteramos seu comportamento.

Por exemplo, abaixo do método setonus(), ao digitar exibirInformacoes, a IDE já gera esse método usando a anotação @Override.

Gerente.java:

@Override
public void exibirInformacoes() {
    super.exibirInformacoes();
}

A IDE cria um método com o mesmo nome, public void exibirInformacoes, anotado com @Override, para deixar claro que estamos sobrescrevendo o método original da classe Funcionario. Dentro desse método, a IDE sugere que chamemos super.exibirInformacoes(), para exibir as informações da mesma forma que é feito no Funcionario.

Não é isso que queremos. Portanto, apagaremos o super.exibirInformacoes() e faremos outro System.out.printf(), no qual colocaremos, por exemplo, "Gerente: %s - Salário: %.2f - Bônus: %.2f", passando os três atributos: nome, salario e bonus.

@Override
public void exibirInformacoes() {
    System.out.printf("Gerente: %s - salário %.2f - bônus: %.2f",
                    nome, salario, bonus);
}

Ajustando a visibilidade dos atributos

Ao começar a digitar nome, perceberemos que ele não será encontrado. Isso ocorre porque nome não está definido na classe gerente, mas na classe funcionário. Para referenciar nome, salario e bonus, não usaremos os métodos acessores get() — precisaremos chamar super.nome, e assim por diante.

@Override
public void exibirInformacoes() {
    System.out.printf("Gerente: %s - salário %.2f - bônus: %.2f",
                    super.nome);
}

Entretanto, isso não funciona porque usamos um atributo privado. Quando o atributo é privado, ele só pode ser utilizado pela classe original, que é Funcionario. Se quisermos dar acesso também às subclasses, precisamos acessar a classe Funcionario e trocar a visibilidade desses atributos para protected.

Funcionario.java:

protected String nome;
protected double salario;

Voltando à classe Gerente, conseguiremos usar nome, salario e bonus.

Gerente.java:

@Override
public void exibirInformacoes() {
    System.out.printf("Gerente: %s - salário %.2f - bônus: %.2f",
                    nome, salario, bonus);
}

Resumindo, para permitir que a classe e suas subclasses tenham acesso às informações sem os métodos acessores, usamos o padrão de visibilidade protected no encapsulamento.

Exibindo informações na classe Principal

Na classe Principal, vamos exibir os dados do gerente. Já temos gerente.exibirInformacoes(). Quando o usarmos, perceberemos que ele não chamará mais o exibirInformacoes() da classe funcionário, que mostra apenas a pessoa colaboradora e o salário: ele chamará o método exibirInformacoes() da classe Gerente, que mostra gerente, salário e bônus.

Esse é um exemplo de polimorfismo de sobrescrita, onde sobrescrevemos o comportamento do método exibirInformacoes.

Vamos executar a classe principal para observar isso.

Na exibição de informações, foi mostrado o gerente Mário, com salário e o bônus zero.

Gerente: Mario - salário 15000,00 - bônus: 0,00

O bônus está zerado porque foi configurado após a exibição das informações. Se tivéssemos configurado antes, ele mostraria o bônus corretamente.

O importante é perceber que chamamos o método da classe correta, que é Gerente.

Sobrescrevendo na classe Desenvolvedor

Vamos sobrescrever também esse método para a pessoa desenvolvedora, para que possamos ver a diferença entre eles. Na classe Desenvolvedor, após o restante do código, adicionaremos a opção de exibir informações.

Foi gerado um @Override, e faremos um printf(), colocando apenas as informações básicas: uma quebra de linha, um "Desenvolvedor: %s - salário %0.2f - Stack: %s" e os atributos nome, salario e stack.

Podemos reparar que, após usar o protected na classe Funcionario, todas as subclasses — incluindo Desenvolvedor — possuem acesso aos atributos.

Desenvolvedor.java:

@Override
public void exibirInformacoes() {
    System.out.printf("\nDesenvolvedor: %s salário %.2f - Stack: %s",
                nome, salario, stack);
}

Voltando à classe Principal, no método main, moveremos o setBonus() para antes da exibição, para visualizar melhor.

Principal.java:

((Gerente) gerente).setBonus(1000);
gerente.exibirInformacoes();
gerente.reajustarSalario(2);

Configuramos o bônus, exibimos as informações do gerente, reajustamos o salário e, em seguida, exibimos as informações da pessoa desenvolvedora. Podemos executar a aplicação e visualizar as definições separadas:

Gerente: Mario - salário 15000,00 - bônus: 1000,00

Novo salario de Mario é 15300,00

Desenvolvedor: Carla salário 12000,00 - Stack: Backend Java

o gerente Mário é mostrado com salário antigo e o novo. Abaixo, a desenvolvedora Carla aparece com seu salário.

Conclusão sobre polimorfismo de sobrescrita

Aplicamos o polimorfismo por sobrescrita, conhecido como Override, pois os métodos têm o mesmo nome e assinatura, mas em tempo de execução é analisado o tipo de pessoa colaboradora. Ambas são declaradas como Funcionario, mas o Java determina se a instância é de Gerente ou de Desenvolvedor, chamando o método exibirInformacoes() correspondente.

Conhecendo o polimorfismo de sobrecarga

Agora, vamos entender o polimorfismo de sobrecarga. Nele, temos métodos com o mesmo nome, mas assinaturas diferentes, para que o Java saiba qual método chamar.

Suponhamos que no reajustarSalario() queiramos dois modelos: um em que passamos o percentual, aplicado sobre o salário, e outro em que não passamos nada, acrescentando sempre 500 reais ao salário.

Na classe Funcionario, abaixo do primeiro reajustarSalario(), criaremos um novo método com o mesmo nome. Ao invés de passar um valor percentual como parâmetro entre parênteses, manteremos o método apenas com o nome.

Esse método vai acrescentar um valor fixo ao salário. Colocaremos salario += 500, ou seja, somar 500 reais ao valor atual do salário.

Depois, copiaremos a linha que imprime no terminal para mostrar qual é o novo salário após esse reajuste. Mudaremos a frase impressa para "Salário com dissídio" para justificar o valor fixo de 500 reais.

Funcionario.java:

public void reajustarSalario(){
    salario += 500;
    System.out.printf("\nSalário com dissídio de %s é %.2f ", nome, salario);
}

Se pedirmos para reajustar o salário passando o percentual, o primeiro método será chamado. Caso contrário, o método sem parâmetros será chamado.

Na classe Principal, no método main(), o gerente já é reajustado com percentual de 2. Para a desenvolvedora Carla, que começa com salário de 12 mil, faremos um reajuste sem passar parâmetros.

Abaixo da instância de Desenvolvedor, chamaremos o método reajustarSalario() no objeto desenvolvedor, sem passar nenhum parâmetro. Ao exibir as informações na linha seguinte, o salário dela deverá ser 12.500.

Principal.java:

Funcionario desenvolvedor = new Desenvolvedor("Carla", 12000, "Backend Java");
desenvolvedor.reajustarSalario();
desenvolvedor.exibirInformacoes();

Ao executar a aplicação, observaremos que o gerente Mário, com salário inicial de 15 mil, foi reajustado para 15.300. Para Carla, o método do "Salário com dissídio" foi aplicado, resultando em 12.500, com acréscimo de 500 reais.

Gerente: Mario - salário 15000,00 - bônus: 1000,00

Novo salario de Mario é 15300,00

Salário com dissídio de Carla é 12500,00

Desenvolvedor: Carla salário 12500,00 - Stack: Backend Java

Conclusão e próximos passos

Essas são as formas de polimorfismo que podem ser usadas com a herança: polimorfismo de sobrescrita e de sobrecarga. São muito úteis, pois evitam a criação de inúmeros métodos diferentes para cada situação. Podemos criar o método reajustarSalario() com parâmetros diferenciados.

Ao usar métodos no Java, temos várias opções de assinaturas, o que é uma prática comum e uma forma ampla de utilizar polimorfismo, tanto de sobrescrita quanto de sobrecarga. Esses conceitos são importantes no nosso dia a dia como pessoas desenvolvedoras.

No próximo vídeo, abordaremos classes abstratas e interfaces.

Sobre o curso Praticando Java: herança, polimorfismo e interfaces

O curso Praticando Java: herança, polimorfismo e interfaces possui 44 minutos de vídeos, em um total de 20 atividades. Gostou? Conheça nossos outros cursos de Java em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Java acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas