Aula 02 - Continuação de revisão de conteúdo
Revisão de APOO1
Objetivo: Reforçar fundamentos de Java e OOP.
Sintaxe Java e Estruturas de Controle
Conteúdo:
- Encapsulamento
- Herança
- Polimorfismo
- Interfaces
- Classes Abstratas
1. Conceitos Fundamentais de Programação Orientada a Objetos em Java
Para essa revisão utilizaremos três exemplos disponibilizados no Moodle (Exemplos de código-fonte: Herança, Polimorfismo e Interface) que demonstram conceitos essenciais da Programação Orientada a Objetos (POO), um paradigma amplamente utilizado no desenvolvimento de software moderno.
Cada um dos conceitos abordados nos exemplos será detalhado abaixo, com explicações e exemplos extraídos dos códigos fornecidos. Os códigos foram retirados do livro Java - Como Programar, 10ª edição, de Paul Deitel e Harvey Deitel.
1. Classes e Objetos
O que são classes?
Uma classe é um modelo para criar objetos. Ela define atributos (dados) e métodos (comportamentos) que os objetos daquela classe terão.
O que são objetos?
Um objeto é uma instância de uma classe. Ele possui um estado (definido pelos atributos da classe) e um comportamento (definido pelos métodos da classe).
Exemplo de Classe e Objeto
No exemplo do sistema de folha de pagamento, temos a classe Employee
:
public abstract class Employee {
private final String firstName;
private final String lastName;
private final String socialSecurityNumber;
}
E no arquivo PayrollSystemTest.java
, criamos objetos dessa classe:
SalariedEmployee salariedEmployee =
new SalariedEmployee("John", "Smith", "111-11-1111", 800.00);
Aqui, salariedEmployee
é um objeto da classe SalariedEmployee
.
2. Encapsulamento
O encapsulamento é um princípio que protege os dados de um objeto, permitindo que sejam acessados apenas por meio de métodos específicos (getters e setters).
Exemplo de Encapsulamento
No código abaixo, os atributos são privados (private
), e só podem ser acessados por métodos
públicos (public
).
private double weeklySalary;
public double getWeeklySalary() {
return weeklySalary;
}
public void setWeeklySalary(double weeklySalary) {
if (weeklySalary < 0.0)
throw new IllegalArgumentException("Weekly salary must be >= 0.0");
this.weeklySalary = weeklySalary;
}
Isso garante que nenhum outro código pode modificar weeklySalary
diretamente, prevenindo valores inválidos.
3. Herança
A herança permite que uma classe herde atributos e métodos de outra classe. Isso promove a reutilização de código e evita redundância. Vejamos o exemplo do sistema de folha de pagamento, disponibilizado abaixo. Leia com atenção as classes abaixo e execute o código em sua máquina.
CommissionEmployee
// Classe que representa um funcionário que recebe comissões.
public class CommissionEmployee
{
private final String firstName;
private final String lastName;
private final String socialSecurityNumber;
private double grossSales; // Vendas brutas semanais
private double commissionRate; // Porcentagem de comissão
// Construtor com cinco argumentos
public CommissionEmployee(String firstName, String lastName,
String socialSecurityNumber, double grossSales,
double commissionRate)
{
if (grossSales < 0.0)
throw new IllegalArgumentException("Gross sales must be >= 0.0");
if (commissionRate <= 0.0 || commissionRate >= 1.0)
throw new IllegalArgumentException("Commission rate must be > 0.0 and < 1.0");
this.firstName = firstName;
this.lastName = lastName;
this.socialSecurityNumber = socialSecurityNumber;
this.grossSales = grossSales;
this.commissionRate = commissionRate;
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public String getSocialSecurityNumber() { return socialSecurityNumber; }
public void setGrossSales(double grossSales)
{
if (grossSales < 0.0)
throw new IllegalArgumentException("Gross sales must be >= 0.0");
this.grossSales = grossSales;
}
public double getGrossSales() { return grossSales; }
public void setCommissionRate(double commissionRate)
{
if (commissionRate <= 0.0 || commissionRate >= 1.0)
throw new IllegalArgumentException("Commission rate must be > 0.0 and < 1.0");
this.commissionRate = commissionRate;
}
public double getCommissionRate() { return commissionRate; }
public double earnings() { return getCommissionRate() * getGrossSales(); }
@Override
public String toString()
{
return String.format("commission employee: %s %s%n" +
"social security number: %s%n" +
"gross sales: %.2f%ncommission rate: %.2f",
getFirstName(), getLastName(),
getSocialSecurityNumber(), getGrossSales(),
getCommissionRate());
}
}
BasePlusCommissionEmployee
// Classe que herda CommissionEmployee e adiciona um salário base.
public class BasePlusCommissionEmployee extends CommissionEmployee
{
private double baseSalary;
public BasePlusCommissionEmployee(String firstName, String lastName,
String socialSecurityNumber, double grossSales,
double commissionRate, double baseSalary)
{
super(firstName, lastName, socialSecurityNumber, grossSales, commissionRate);
if (baseSalary < 0.0)
throw new IllegalArgumentException("Base salary must be >= 0.0");
this.baseSalary = baseSalary;
}
public void setBaseSalary(double baseSalary)
{
if (baseSalary < 0.0)
throw new IllegalArgumentException("Base salary must be >= 0.0");
this.baseSalary = baseSalary;
}
public double getBaseSalary() { return baseSalary; }
@Override
public double earnings() { return getBaseSalary() + super.earnings(); }
@Override
public String toString()
{
return String.format("base-salaried %s%nbase salary: %.2f",
super.toString(), getBaseSalary());
}
}
BasePlusCommissionEmployeeTest
// Programa principal para testar BasePlusCommissionEmployee.
public class BasePlusCommissionEmployeeTest
{
public static void main(String[] args)
{
BasePlusCommissionEmployee employee =
new BasePlusCommissionEmployee("Bob", "Lewis", "333-33-3333", 5000, .04, 300);
System.out.println("Employee information obtained by get methods:");
System.out.println(employee.toString());
employee.setBaseSalary(1000);
System.out.println("\nUpdated employee information:");
System.out.println(employee.toString());
}
}
Explicação do código com Herança
Perceba que o código possui duas classes principais CommissionEmployee
e
BasePlusCommissionEmployee
. A classe BasePlusCommissionEmployee
herda as
funcionalidades de CommissionEmployee
e as sobrecreve, implementando a regra de negócio
adequada para a subclasse. Ou seja, BasePlusCommissionEmployee
é um
CommissionEmployee
com regras específicas.
4. Polimorfismo e Classe Abstrata
O polimorfismo permite que diferentes classes usem o mesmo método de formas diferentes.
Ainda sobre os conceitos de Herança, na extensão do exemplo do sistema de folha de pagamento,
SalariedEmployee
, HourlyEmployee
e CommissionEmployee
herdam de
Employee
:
public class SalariedEmployee extends Employee {
}
Isso significa que SalariedEmployee
automaticamente herda os atributos
firstName
, lastName
e socialSecurityNumber
, definidos em
Employee
.
O método toString()
na superclasse Employee
também pode ser reutilizado pelas
subclasses:
@Override
public String toString() {
return String.format("%s %s%nsocial security number: %s",
getFirstName(), getLastName(), getSocialSecurityNumber());
}
Como resultado, qualquer classe que herde Employee
terá esse método sem
precisar reescrevê-lo.
Assim, no exemplo PayrollSystemTest.java
, um array de Employee[]
contém
diferentes tipos de funcionários, mas todos podem ser tratados genericamente.
Employee[] employees = new Employee[4];
employees[0] = salariedEmployee;
employees[1] = hourlyEmployee;
employees[2] = commissionEmployee;
employees[3] = basePlusCommissionEmployee;
Depois, podemos percorrer esse array e chamar earnings()
sem precisar saber o tipo exato de
funcionário:
for (Employee currentEmployee : employees) {
System.out.printf("earned $%,.2f%n%n", currentEmployee.earnings());
}
Isso é possível porque todas as classes herdam de Employee
e sobrescrevem o
método earnings()
. Isso é chamado de Polimorfismo de tempo de
execução ou Polimorfismo Dinâmico, uma vez que a chamada
para um mesmo método produz resultados distintos dependendo do objeto que é chamado.
O polimorfismo de tempo de execução ocorre quando uma superclasse define um método, e uma subclasse fornece sua própria implementação desse método. Quando um objeto de uma subclasse é referenciado por uma variável da superclasse, a versão do método executada será aquela definida na subclasse e não na superclasse.
Isso é possível graças à sobrescrita de métodos (method overriding), que permite redefinir um método herdado.
No exemplo da folha de pagamento, a classe abstrata Employee
define um método abstrato
earnings()
:
public abstract class Employee {
public abstract double earnings();
}
Todas as classes que herdam Employee
são obrigadas a fornecer uma
implementação para earnings()
, pois Employee
é abstrata.
Sobrescrita em Subclasses
Cada tipo de funcionário tem uma maneira diferente de calcular os ganhos:
Funcionário Assalariado (SalariedEmployee
)
@Override
public double earnings() {
return getWeeklySalary();
}
Aqui, o salário é fixo por semana.
Funcionário Horista (HourlyEmployee
)
@Override
public double earnings() {
if (getHours() <= 40)
return getWage() * getHours();
else
return 40 * getWage() + (getHours() - 40) * getWage() * 1.5;
}
Aqui, o cálculo depende do número de horas trabalhadas, com pagamento extra para horas acima de 40.
Funcionário Comissionado (CommissionEmployee
)
@Override
public double earnings() {
return getCommissionRate() * getGrossSales();
}
Aqui, o pagamento depende das vendas realizadas.
Como o Polimorfismo Funciona no Código
O polimorfismo acontece quando os objetos são manipulados através de uma variável de tipo da
superclasse (Employee
), mas a versão do método executada é aquela
definida na subclasse real do objeto.
Veja o trecho de código no arquivo PayrollSystemTest.java
:
Employee[] employees = new Employee[4];
employees[0] = new SalariedEmployee("John", "Smith", "111-11-1111", 800.00);
employees[1] = new HourlyEmployee("Karen", "Price", "222-22-2222", 16.75, 40);
employees[2] = new CommissionEmployee("Sue", "Jones", "333-33-3333", 10000, .06);
employees[3] = new BasePlusCommissionEmployee("Bob", "Lewis", "444-44-4444", 5000, .04, 300);
Aqui, criamos um array de Employee
, mas ele contém diferentes tipos de
funcionários.
Depois, processamos cada funcionário de forma genérica:
for (Employee currentEmployee : employees) {
System.out.printf("earned $%,.2f%n%n", currentEmployee.earnings());
}
Mesmo que currentEmployee
seja do tipo Employee
, o método
earnings()
chamado é o da subclasse real do objeto.
Isso ocorre porque Java usa ligação dinâmica (dynamic binding), ou seja, só decide qual versão do método executar durante a execução do programa.
Esse uso de polimorfismo torna o código mais flexível, extensível e modular, permitindo
adicionar novos tipos de Employee
sem modificar o código que processa os pagamentos.
Classe Abstrata no Contexto do Exemplo do Sistema de Folha de Pagamento
No exemplo do sistema de folha de pagamento, a classe abstrata
utilizada é, como mencionado, Employee
. Ela serve como uma superclasse
genérica para diferentes tipos de funcionários:
SalariedEmployee
(funcionário assalariado)HourlyEmployee
(funcionário horista)CommissionEmployee
(funcionário comissionado)BasePlusCommissionEmployee
(funcionário com salário base + comissão)
O uso de uma classe abstrata permite definir um modelo comum para todas essas classes, evitando duplicação de código e garantindo uma estrutura uniforme.
O que é uma Classe Abstrata?
Uma classe abstrata em Java é uma classe que não pode ser instanciada diretamente e que pode conter métodos abstratos (métodos sem implementação, que devem ser implementados por suas subclasses).
Ela é usada quando há um comportamento comum entre várias classes, mas a implementação exata desse comportamento depende de cada subclasse.
Definição da Classe Abstrata Employee
No código do exemplo, a classe Employee
é definida assim:
// Classe abstrata Employee
public abstract class Employee {
private final String firstName;
private final String lastName;
private final String socialSecurityNumber;
// Construtor da classe abstrata
public Employee(String firstName, String lastName, String socialSecurityNumber) {
this.firstName = firstName;
this.lastName = lastName;
this.socialSecurityNumber = socialSecurityNumber;
}
// Métodos concretos (com implementação)
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public String getSocialSecurityNumber() { return socialSecurityNumber; }
// Método abstrato - deve ser implementado pelas subclasses
public abstract double earnings();
@Override
public String toString() {
return String.format("%s %s%nsocial security number: %s",
getFirstName(), getLastName(), getSocialSecurityNumber());
}
}
Por que Employee
é Abstrata?
A classe Employee
não representa um funcionário específico, mas sim um
modelo genérico para funcionários. Cada tipo de funcionário calcula seus ganhos
(earnings()
) de uma maneira diferente.
Por isso, o método earnings()
é abstrato:
public abstract double earnings();
Isso significa que todas as subclasses de Employee
DEVEM implementar esse
método, pois cada tipo de funcionário tem uma forma diferente de calcular seu pagamento.
Benefícios do Uso de uma Classe Abstrata
O uso de Employee
como classe abstrata traz várias vantagens:
✅ 1. Garante uma Estrutura Comum
Todas as subclasses devem implementar earnings()
, garantindo que cada tipo
de funcionário tenha um método para calcular seus ganhos.
✅ 2. Facilita o Polimorfismo
Podemos tratar diferentes tipos de funcionários de forma genérica, como no exemplo:
Employee[] employees = new Employee[4];
employees[0] = new SalariedEmployee("John", "Smith", "111-11-1111", 800.00);
employees[1] = new HourlyEmployee("Karen", "Price", "222-22-2222", 16.75, 40);
employees[2] = new CommissionEmployee("Sue", "Jones", "333-33-3333", 10000, .06);
employees[3] = new BasePlusCommissionEmployee("Bob", "Lewis", "444-44-4444", 5000, .04, 300);
Aqui, todos os objetos são tratados como Employee
, mas cada um executa
earnings()
de acordo com sua classe específica (explicação detalhada fornecida acima).
✅ 3. Evita Duplicação de Código
O código comum a todos os funcionários (como firstName
,
lastName
e socialSecurityNumber
) fica centralizado na classe
Employee
, evitando repetição em cada subclasse.
Ou seja...
🔹 Employee
foi definida como uma classe abstrata porque não faz
sentido instanciar diretamente um "funcionário genérico".
🔹 O método
earnings()
foi definido como abstrato para forçar cada subclasse a implementar
seu próprio cálculo de pagamento.
🔹 Isso torna o código mais organizado, modular e
extensível, permitindo adicionar novos tipos de funcionários no futuro sem modificar o
código existente.
5. Interfaces
Uma interface em Java é uma estrutura que define um contrato para classes. Esse contrato estabelece um conjunto de métodos que as classes que implementam essa interface devem fornecer.
Diferente das classes abstratas, uma interface não pode conter atributos de
instância (exceto constantes static final
) e não pode conter
implementações de métodos (até Java 8, que introduziu métodos default
). As
interfaces são frequentemente usadas para garantir que diferentes classes compartilhem um comportamento
comum, independentemente da hierarquia de herança.
Interface Payable
no Código de Exemplo
No sistema de folha de pagamento, foi definida a seguinte interface:
// Interface Payable
public interface Payable {
double getPaymentAmount(); // Método abstrato sem implementação
}
Essa interface estabelece que qualquer classe que a implemente DEVE fornecer um método
getPaymentAmount()
.
A ideia central é permitir que diferentes tipos de entidades (como empregados e faturas) possam ser processadas de forma uniforme, já que todas elas fornecem um valor a ser pago.
Implementações da Interface Payable
A interface Payable
é implementada por duas categorias diferentes de
classes:
- A classe
Invoice
, que representa faturas de fornecedores. - A classe
Employee
e suas subclasses, que representam diferentes tipos de funcionários.
1️⃣ Classe Invoice
Implementando Payable
A classe Invoice
representa uma fatura de um fornecedor. Como uma fatura
não é um funcionário, ela não poderia herdar de Employee
, mas pode
implementar Payable
para ser processada de maneira uniforme com
funcionários.
Invoice
// Classe Invoice que implementa Payable
public class Invoice implements Payable {
private final String partNumber;
private final String partDescription;
private int quantity;
private double pricePerItem;
public Invoice(String partNumber, String partDescription, int quantity, double pricePerItem) {
if (quantity < 0)
throw new IllegalArgumentException("Quantity must be >= 0");
if (pricePerItem < 0.0)
throw new IllegalArgumentException("Price per item must be >= 0");
this.partNumber = partNumber;
this.partDescription = partDescription;
this.quantity = quantity;
this.pricePerItem = pricePerItem;
}
// Implementação do método getPaymentAmount() da interface Payable
@Override
public double getPaymentAmount() {
return getQuantity() * getPricePerItem(); // O valor total da fatura
}
@Override
public String toString() {
return String.format("invoice: %npart number: %s (%s) %nquantity: %d %nprice per item: $%,.2f",
getPartNumber(), getPartDescription(), getQuantity(), getPricePerItem());
}
}
📌 O que acontece aqui?
- A classe
Invoice
implementaPayable
, o que a obriga a fornecer uma implementação paragetPaymentAmount()
. getPaymentAmount()
calcula o valor total da fatura (quantidade * preço por item
).- Isso permite que uma fatura seja tratada de forma semelhante a um empregado dentro do sistema de folha de pagamento.
2️⃣ Classe Employee
e Subclasses Implementando Payable
Como Employee
representa diferentes tipos de funcionários, e cada funcionário tem um
valor a ser pago, Employee
também implementa Payable
:
// Classe abstrata Employee que implementa Payable
public abstract class Employee implements Payable {
private final String firstName;
private final String lastName;
private final String socialSecurityNumber;
public Employee(String firstName, String lastName, String socialSecurityNumber) {
this.firstName = firstName;
this.lastName = lastName;
this.socialSecurityNumber = socialSecurityNumber;
}
@Override
public String toString() {
return String.format("%s %s%nsocial security number: %s",
getFirstName(), getLastName(), getSocialSecurityNumber());
}
}
Subclasses de Employee
Sobrescrevendo getPaymentAmount()
Cada subclasse de Employee
implementa getPaymentAmount()
de acordo com sua
forma específica de cálculo de pagamento.
Funcionário Assalariado (SalariedEmployee
)
public class SalariedEmployee extends Employee {
private double weeklySalary;
public SalariedEmployee(String firstName, String lastName, String socialSecurityNumber, double weeklySalary) {
super(firstName, lastName, socialSecurityNumber);
if (weeklySalary < 0.0)
throw new IllegalArgumentException("Weekly salary must be >= 0.0");
this.weeklySalary = weeklySalary;
}
@Override
public double getPaymentAmount() {
return getWeeklySalary();
}
}
Funcionário Horista (HourlyEmployee
)
public class HourlyEmployee extends Employee {
private double wage;
private double hours;
public HourlyEmployee(String firstName, String lastName, String socialSecurityNumber, double wage, double hours) {
super(firstName, lastName, socialSecurityNumber);
this.wage = wage;
this.hours = hours;
}
@Override
public double getPaymentAmount() {
if (hours <= 40)
return wage * hours;
else
return 40 * wage + (hours - 40) * wage * 1.5;
}
}
O Poder do Polimorfismo com Interfaces
Agora que Invoice
e Employee
implementam Payable
,
podemos armazenar tanto funcionários quanto faturas no mesmo array e processá-los de maneira
uniforme.
Trecho de código do PayrollSystemTest.java
// Criando um array de Payable para armazenar diferentes tipos de objetos
Payable[] payableObjects = new Payable[4];
payableObjects[0] = new Invoice("01234", "seat", 2, 375.00);
payableObjects[1] = new Invoice("56789", "tire", 4, 79.95);
payableObjects[2] = new SalariedEmployee("John", "Smith", "111-11-1111", 800.00);
payableObjects[3] = new SalariedEmployee("Lisa", "Barnes", "888-88-8888", 1200.00);
System.out.println("Invoices and Employees processed polymorphically:");
for (Payable currentPayable : payableObjects) {
System.out.printf("%n%s %n%s: $%,.2f%n",
currentPayable.toString(),
"payment due", currentPayable.getPaymentAmount());
}
📌E aqui, o que acontece?
- Criamos um array de
Payable
contendo faturas (Invoice
) e funcionários (SalariedEmployee
). - Usamos polimorfismo para tratar todos os objetos como
Payable
, independentemente de serem faturas ou funcionários. - Chamamos
getPaymentAmount()
sem nos preocuparmos com o tipo exato do objeto, pois sabemos que todos os objetos no array implementamPayable
.
Ou seja, a interface Payable
foi usada para padronizar o cálculo de valores a serem
pagos a diferentes entidades (Invoice
e Employee
). Isso nos
fornece diversas vantagens:
- Permite tratar faturas e funcionários de forma uniforme, simplificando o código.
- Facilita a expansão do sistema, pois novos tipos de
Payable
podem ser adicionados sem alterar o código existente. - Reduz o acoplamento, pois
Invoice
eEmployee
não precisam estar na mesma hierarquia de classes para serem processados de maneira semelhante.
Esse é um ótimo exemplo de polimorfismo e boas práticas de POO, demonstrando como interfaces tornam um sistema mais flexível, extensível e reutilizável! 🚀
Conclusão
Esses conceitos basilares nos permitem criar aplicações que seguem os bons padrões da programação orientada a objetos, permitindo extensibilidade e reúso de código-fonte.
📌 Lembrem-se: o mais importante é buscarmos alta coesão e baixo acoplamento. Para isso, devemos nos valer das possibilidades que são proporcionadas pelo paradigma da POO.