Aula 01 - Revisão de Análise e Programação Orientada a Objetos


Revisão de APO1

Objetivo: Relembrar fundamentos de Java, OOP, coleções, tratamento de erros e UML básico.

Sintaxe Java e Estruturas de Controle

Conteúdo:

1. Sintaxe Básica do Java

1.1 Tipos Primitivos e Variáveis

No Java, os tipos primitivos representam valores simples, não objetos, e são otimizados para performance. Entre eles temos:

Ao declarar uma variável, fazemos:

int idade = 25;
double salario = 2500.75;
boolean ativo = true;

1.2 Operadores

Exemplo de operador ternário:

int x = (5 > 3) ? 10 : 0; // x será 10

1.3 Estruturas de Controle


2. OOP Básico (Classes, Objetos, Encapsulamento)

Conteúdo:

2.1 Classes e Objetos

Exemplo:

public class Produto {
    // Atributos (propriedades)
    private String nome;
    private double preco;

    // Construtor
    public Produto(String nome, double preco) {
        this.nome = nome;
        this.preco = preco;
    }

    // Métodos de acesso (getters e setters)
    public String getNome() { return nome; }
    public void setNome(String nome) { this.nome = nome; }

    public double getPreco() { return preco; }
    public void setPreco(double preco) { this.preco = preco; }

    // Método de negócio
    public double calcularDesconto(double percentual) {
        return preco - (preco * percentual);
    }
}

2.2 Encapsulamento

O encapsulamento é o princípio de proteger os dados internos de um objeto, expondo somente o necessário por meio de métodos públicos. Isso aumenta a segurança e a clareza do código. Em Java, utilizamos modificadores de acesso:

2.3 Construtores

São métodos especiais que inicializam um objeto no momento em que é criado. Possuem o mesmo nome da classe e não retornam valor.

No exemplo acima, o construtor recebe como parâmetros String nome e double preco e inicializa as variáveis de instância (atributos) com os valores desses parâmetros. A palavra de this distingue variáveis de instância de parâmetros locais.

2.4 Herança

Permite criar classes especializadas a partir de classes existentes. Usamos a palavra-chave extends em Java.

public class Carro extends Veiculo {
    // Carro herda atributos e métodos de Veiculo
}

Entretanto, a composição (ter objetos como membros) é geralmente preferida em vez da herança (estender classes) devido a:

2.4.1. Flexibilidade e Baixo Acoplamento

Exemplo com Herança (Problema):
class Veiculo {
    void ligarMotor() { /* ... */ }
}

class Carro extends Veiculo { 
    // Herda tudo de Veiculo, mesmo que não precise.
}

// E se surgir um CarroElétrico que não usa motor a combustão?
class CarroEletrico extends Veiculo {
    // Sobrescrever ligarMotor()? O motor é elétrico!
}
Exemplo com Composição (Solução):
interface Motor {
    void ligar();
}

class MotorCombustão implements Motor {
    public void ligar() { /* ... */ }
}

class MotorElétrico implements Motor {
    public void ligar() { /* ... */ }
}

class Carro {
    private Motor motor;

    public Carro(Motor motor) {
        this.motor = motor; // Composição: Carro "tem um" Motor
    }

    void ligar() {
        motor.ligar(); // Comportamento definido pelo motor escolhido
    }
}

// Uso:
Carro carroGasolina = new Carro(new MotorCombustão());
Carro carroEletrico = new Carro(new MotorEletrico()); // Flexibilidade!

2.4.2 Evita Hierarquias Complexas

Herança múltipla não é permitida em Java, e hierarquias profundas tornam o código difícil de manter.

Exemplo Ruim (Herança):
abstract class Personagem {
    abstract void mover();
}

class HeroiVoador extends Personagem {
    void mover() { /* Voar */ }
}

class HeroiNadador extends Personagem {
    void mover() { /* Nadar */ }
}

// E se um herói precisar voar E nadar? Teríamos que criar:
class HeroiVoadorNadador extends Personagem {}
/* Duplicação de código! */
Solução com Composição

Encapsule habilidades como componentes independentes:

interface HabilidadeMovimento {
    void mover();
}

class Voar implements HabilidadeMovimento {
    public void mover() { System.out.println("Voando!"); }
}

class Nadar implements HabilidadeMovimento {
    public void mover() { System.out.println("Nadando!"); }
}

class Personagem {
    private HabilidadeMovimento habilidade;

    public Personagem(HabilidadeMovimento habilidade) {
        this.habilidade = habilidade; 
        // Composição: Personagem "tem uma" habilidade
    }

    void mover() {
        habilidade.mover();
    }

    // Permite mudar a habilidade em tempo de execução!
    void setHabilidade(HabilidadeMovimento novaHabilidade) {
        this.habilidade = novaHabilidade;
    }
}

// Uso dinâmico:
Personagem heroi = new Personagem(new Voar());
heroi.mover(); // Voando!

heroi.setHabilidade(new Nadar());
heroi.mover(); // Nadando!`

2.4.3. Reutilização de Código sem Acoplamento

Na composição, você reutiliza comportamentos sem herdar métodos desnecessários.

Exemplo com Herança
class ListaPersonalizada extends ArrayList<String> {
    // Herda todos os métodos de ArrayList, mesmo os indesejados.
}
Exemplo com Composição
class ListaPersonalizada {
    private List<String> lista = new ArrayList<>();

    // Expõe apenas os métodos necessários:
    public void addElemento(String elemento) {
        lista.add(elemento);
    }

    public int tamanho() {
        return lista.size();
    }
}

O código exemplo acima é redundante, pois é meramente um wrapper para a classe ArrayList, mas demonstra o ponto principal.

2.4.4. Princípio SOLID: Aberto/Fechado e Inversão de Dependência

A composição permite estender comportamentos sem modificar a classe original, seguindo o princípio "Prefira interfaces a implementações".

Exemplo com Interfaces:
interface Notificador {
    void enviar(String mensagem);
}

class EmailNotificador implements Notificador {
    public void enviar(String mensagem) { /* ... */ }
}

class SMSNotificador implements Notificador {
    public void enviar(String mensagem) { /* ... */ }
}

class Sistema {
    private Notificador notificador;

    public Sistema(Notificador notificador) { 
        // Injeção de dependência
        this.notificador = notificador;
    }

    void alertar() {
        notificador.enviar("Alerta!"); 
        // Troque o notificador sem alterar Sistema
    }
}

2.4.5 Quando Usar Herança?

A herança é útil quando:

A composição oferece:

Use herança para modelar relações hierárquicas genuínas e imutáveis. Para tudo mais, prefira composição! 🛠️

2.5. Polimorfismo

Significa “muitas formas”. Em OOP, há dois principais tipos:

2.6. Interfaces e Classes Abstratas

2.7 Exemplos Práticos

Classe ContaBancaria

Modelando uma conta bancária com encapsulamento e validação de saldo.

public class ContaBancaria {
    private String titular;
    private double saldo;

    public ContaBancaria(String titular, double saldoInicial) {
        this.titular = titular;
        this.saldo = saldoInicial;
    }

    public String getTitular() { return titular; }

    public double getSaldo() { return saldo; }

    public void depositar(double valor) {
        if (valor > 0) {
            saldo += valor;
        } else {
            System.out.println("Valor inválido para depósito.");
        }
    }

    public boolean sacar(double valor) {
        if (valor > 0 && saldo >= valor) {
            saldo -= valor;
            return true;
        }
        System.out.println("Saldo insuficiente ou valor inválido.");
        return false;
    }
}

Uso da ContaBancaria

public class BancoTeste {
    public static void main(String[] args) {
        ContaBancaria conta = new ContaBancaria("Ana Silva", 1000.0);
        conta.depositar(500);
        conta.sacar(300);
        System.out.printf("Saldo final: R$%.2f%n", conta.getSaldo());
    }
}

Uso de Herança: ContaPoupanca

Criando uma conta poupança que herda de ContaBancaria.

public class ContaPoupanca extends ContaBancaria {
    private double taxaJuros;

    public ContaPoupanca(String titular, double saldoInicial, double taxaJuros) {
        super(titular, saldoInicial);
        this.taxaJuros = taxaJuros;
    }

    public void aplicarJuros() {
        depositar(getSaldo() * taxaJuros);
    }
}

Uso de Composição: Cliente e ContaBancaria

Evitando herança desnecessária com um relacionamento "tem-um".

public class Cliente {
    private String nome;
    private ContaBancaria conta;

    public Cliente(String nome, ContaBancaria conta) {
        this.nome = nome;
        this.conta = conta;
    }

    public void exibirSaldo() {
        System.out.printf("Cliente: %s - Saldo: R$%.2f%n", nome, conta.getSaldo());
    }
}

Polimorfismo: Interface Notificacao

interface Notificacao {
    void enviar(String mensagem);
}

class EmailNotificacao implements Notificacao {
    public void enviar(String mensagem) {
        System.out.println("Email enviado: " + mensagem);
    }
}

class SMSNotificacao implements Notificacao {
    public void enviar(String mensagem) {
        System.out.println("SMS enviado: " + mensagem);
    }
}

Uso da Interface Notificacao

public class SistemaNotificacao {
    public static void main(String[] args) {
        Notificacao notificacao = new EmailNotificacao();
        notificacao.enviar("Seu saldo foi atualizado.");

        notificacao = new SMSNotificacao();
        notificacao.enviar("Pagamento recebido.");
    }
}

2.8. Boas Práticas

Exemplo de violação desse princípio

// Código duplicado para calcular imposto em dois lugares diferentes  
double calcularImpostoPedido(Pedido pedido) {  
    return pedido.getTotal() * 0.10;  
}  

void gerarNotaFiscal(Pedido pedido) {  
    double imposto = pedido.getTotal() * 0.10; // Cálculo repetido  
    // ...  
}  

Solução (extrair para um único método)

double calcularImposto(double valor) {  
    return valor * 0.10;  
}  

// Reutilização em múltiplos lugares:  
void gerarNotaFiscal(Pedido pedido) {  
    double imposto = calcularImposto(pedido.getTotal());  
    // ...  
}  

double totalComImposto(Pedido pedido) {  
    return pedido.getTotal() + calcularImposto(pedido.getTotal());  
}  

Exemplo de violação

void sacar(double valor) {  
    saldo -= valor; 
    // Problema: valor pode ser negativo ou maior que o saldo  
}  

Solução

void sacar(double valor) {  
    if (valor <= 0) {  
        throw new IllegalArgumentException("Valor deve ser positivo.");  
    }  
    if (valor > saldo) {  
        throw new SaldoInsuficienteException("Saldo indisponível.");  
    }  
    saldo -= valor;  
}  

Para garantir que este princípio está sendo seguido, tenha em mente


3. Coleções, Generics, Reflexão e Exceções

3.1 Arrays e Coleções

Arrays

Os arrays em Java são estruturas de dados de tamanho fixo, utilizados para armazenar elementos do mesmo tipo. Eles são declarados da seguinte forma:

int[] numeros = new int[5]; // Array de inteiros com 5 posições
String[] nomes = {"João", "Maria", "Pedro"}; // Array inicializado diretamente

Os arrays têm a vantagem de serem eficientes em termos de memória e acessíveis via índices, mas sua desvantagem é o tamanho fixo, que não pode ser alterado após a criação.

Collections Framework

O framework de coleções do Java (“Java Collections Framework” - JCF) fornece estruturas de dados prontas, dinâmicas e mais flexíveis do que arrays tradicionais. Algumas das principais interfaces e classes incluem:

List

A interface List representa uma coleção ordenada de elementos, permitindo duplicatas. As principais implementações são:

Exemplo:

List<String> nomes = new ArrayList<>();
nomes.add("Ana");
nomes.add("Carlos");
nomes.add("Beatriz");

System.out.println(nomes.get(1)); // Exibe "Carlos"

Set

A interface Set representa uma coleção de elementos únicos (sem duplicatas). As principais implementações são:

Exemplo:

Set<Integer> numeros = new HashSet<>();
numeros.add(5);
numeros.add(10);
numeros.add(5); // Ignorado pois já existe

System.out.println(numeros); // Exibe [5, 10]

Map

A interface Map armazena pares (chave, valor). As principais implementações são:

Exemplo:

Map<String, Integer> idades = new HashMap<>();
idades.put("Ana", 25);
idades.put("Carlos", 30);

System.out.println(idades.get("Ana")); // Exibe 25

Queue e Deque

Exemplo:

Queue<String> fila = new LinkedList<>();
fila.add("Ana");
fila.add("Carlos");
System.out.println(fila.poll()); // Remove e retorna "Ana"

Comparativo das Principais Classes de Coleção

Estrutura Permite Duplicatas? Mantém Ordem? Melhor Uso
ArrayList Sim Sim Lista dinâmica com acesso rápido por índice
LinkedList Sim Sim Inserções e remoções frequentes
HashSet Não Não Conjunto de elementos únicos sem ordem
TreeSet Não Sim (ordenado) Conjunto de elementos ordenados
HashMap Não (chaves) Não Estrutura rápida para pares chave-valor
TreeMap Não (chaves) Sim (ordenado) Mapeamento chave-valor ordenado

O Java Collections Framework fornece estruturas robustas e eficientes para manipular dados de maneira flexível, sendo uma alternativa muito superior aos arrays quando é necessário lidar com coleções dinâmicas.

3.2 Generics

Os Generics permitem definir tipos parametrizados, aumentando a segurança de tipos em tempo de compilação e evitando erros de conversão.

Conceito de Generics

Generics permitem que classes, interfaces e métodos sejam parametrizados por tipos, tornando-os mais reutilizáveis e seguros. Antes da introdução dos Generics no Java 5, coleções e outras classes utilizavam objetos genéricos (Object), exigindo conversões explícitas e permitindo a inserção de elementos de tipos incompatíveis.

Com Generics, podemos definir um tipo parametrizado, garantindo que somente elementos do tipo especificado sejam armazenados.

Exemplo com List

List<String> nomes = new ArrayList<>();
nomes.add("Ana");
// nomes.add(10); // Erro de compilação

Benefícios dos Generics

Os Generics são amplamente utilizados no Java, especialmente no framework de coleções, proporcionando flexibilidade e segurança de tipos nas implementações.

3.3 Reflexão

A Reflexão é um mecanismo do Java que permite inspecionar e manipular classes, métodos e atributos em tempo de execução. Ela é amplamente utilizada em frameworks para injeção de dependências, serialização de objetos e mapeamento de entidades.

Como Funciona a Reflexão

A reflexão permite obter informações de uma classe e modificar seu comportamento dinamicamente. Isso é feito com a API java.lang.reflect.

Exemplo de Inspeção de Classe

import java.lang.reflect.Method;

class Exemplo {
    public void metodoPublico() {
        System.out.println("Método Público");
    }
}

public class ReflexaoExemplo {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> classe = Class.forName("Exemplo");
        Method[] metodos = classe.getDeclaredMethods();
        
        for (Method metodo : metodos) {
            System.out.println("Método encontrado: " + metodo.getName());
        }
    }
}

Neste exemplo, usamos a reflexão para listar os métodos declarados da classe Exemplo.

Modificação de Atributos Privados

A reflexão também pode ser usada para acessar e modificar atributos privados:

import java.lang.reflect.Field;

class Pessoa {
    private String nome = "João";
}

public class ReflexaoAtributo {
    public static void main(String[] args) throws Exception {
        Pessoa p = new Pessoa();
        Class<?> classe = p.getClass();
        Field campo = classe.getDeclaredField("nome");
        campo.setAccessible(true);
        campo.set(p, "Maria");
        System.out.println("Novo nome: " + campo.get(p));
    }
}

Aplicabilidade da Reflexão

A reflexão é utilizada em diversas situações no desenvolvimento de software, incluindo:

Embora poderosa, a reflexão deve ser usada com cautela, pois pode impactar a performance da aplicação e quebrar o encapsulamento dos objetos.

3.4 Tratamento de Exceções

O tratamento de exceções em Java é essencial para lidar com erros que ocorrem durante a execução do programa. As exceções podem ser tratadas utilizando os blocos try, catch e finally.

Exemplo de Tratamento de Exceção

try {
    int resultado = 10 / 0;
} catch (ArithmeticException e) {
    System.out.println("Erro de divisão por zero");
} finally {
    System.out.println("Este bloco sempre será executado");
}

Tipos de Exceções

As exceções em Java são divididas em dois tipos principais:

Exceções Checadas (Checked Exceptions)

import java.io.*;

public class ExemploChecked {
    public static void main(String[] args) {
        try {
            FileReader file = new FileReader("arquivo.txt");
        } catch (FileNotFoundException e) {
            System.out.println("Arquivo não encontrado");
        }
    }
}

Exceções Não Checadas (Unchecked Exceptions)

public class ExemploUnchecked {
    public static void main(String[] args) {
        String texto = null;
        System.out.println(texto.length()); // Lança NullPointerException
    }
}

Criando Exceções Personalizadas

É possível definir exceções personalizadas criando classes que herdam de Exception (checada) ou RuntimeException (não checada):

class MinhaExcecao extends Exception {
    public MinhaExcecao(String mensagem) {
        super(mensagem);
    }
}

public class TesteExcecaoPersonalizada {
    public static void main(String[] args) {
        try {
            throw new MinhaExcecao("Erro personalizado");
        } catch (MinhaExcecao e) {
            System.out.println("Capturado: " + e.getMessage());
        }
    }
}

Considerações sobre Tratamento de Exceções


4. Introdução à UML (Unified Modeling Language)

A UML é um conjunto de diagramas para modelar sistemas orientados a objetos. Ajuda a visualizar, especificar e documentar a arquitetura de um software.

4.1 Diagrama de Caso de Uso

4.2 Diagrama de Classes


5. Exercícios de Fixação de Conteúdo

Os exercícios de fixação de conteúdo estão disponibilizados por meio da plataforma Moodle. Entregue-os dentro do prazo estabelecido e, em caso de dúvidas, não hesite entrar em contato!

Bons estudos!