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:

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:

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:

  1. A classe Invoice, que representa faturas de fornecedores.
  2. 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?


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?

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:

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.