Aula 01 - Revisão sobre Java e POO

O objetivo principal desta disciplina é o desenvolvimento de APIs e microsserviços com Java e Spring Boot. Contudo, a construção de software depende diretamente do domínio de seus fundamentos. É por isso que nesse primeiro momento faremos uma ampla revisão dos pilares da programação orientada a objetos com Java.

Nesta revisão estratégica de APOO1 e APOO2, não vamos apenas relembrar a sintaxe, mas entender o "porquê" por trás de cada conceito. Por que preferir composição à herança em certos cenários? Como as coleções do Java Framework otimizam nosso trabalho? De que forma o tratamento de exceções torna nossas aplicações mais confiáveis?

Este material foi desenhado para ser um guia prático e consolidar esta base essencial, que será a chave para desenvolvermos, juntos, soluções de software cada vez mais sofisticadas.

ATENÇÃO!

Essa é a orientação mais importante que vou passar para vocês: não utilize ferramentas de IA para solucionar os exercícios. O objetivo agora é que vocês se dediquem a entender os fundamentos, enfrentando os desafios e raciocinando sobre os problemas por conta própria.. Esse ainda não é o momento de usar essas ferramentas e esse esforço é necessário para que, no futuro, vocês possam utilizá-las da forma correta: como aliadas que aumentam a produtividade, e não como um recurso que gera dependência. IAs servem para acelerar o trabalho, não se deixem tornar desenvolvedores que dependem dela para fazer o básico.

Entendido? Então vamos lá!


PARTE I - Fundamentos da Linguagem Java

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 Operadores de Atribuição:

int saldo = 100;
saldo += 50; // Equivalente a: saldo = saldo + 100; (saldo agora é 150)
saldo -= 20; // Equivalente a: saldo = saldo - 20; (saldo agora é 130)

int x = (5 > 3) ? 10 : 0; // Exemplo de ternário, que também faz uma atribuição

1.3. Estruturas de Controle

Estruturas de controle direcionam o fluxo de execução do programa.

if (idade >= 18) {
    System.out.println("Maior de idade");
} else {
    System.out.println("Menor de idade");
}
for (int i = 0; i < 5; i++) {
    System.out.println("O valor de i é: " + i);
}
List<String> nomes = Arrays.asList("Ana", "João", "Carlos");

for (String nome : nomes) {
    System.out.println("Olá, " + nome);
}
int contador = 0;
while (contador < 3) {
    System.out.println("Contador: " + contador);
    contador++;
}

1.4. 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.

1.5. A Classe String

Diferente dos tipos vistos na seção 1.1, String não é um tipo primitivo, e sim uma classe. Isso significa que toda variável do tipo String é um objeto, com seus próprios métodos.

String nome = "Maria Silva"; // 'nome' é um objeto da classe String

int quantidadeDeLetras = nome.length(); // Chamando um método do objeto
String nomeMaiusculo = nome.toUpperCase(); // Criando um novo objeto String

System.out.println(nomeMaiusculo); // Imprime "MARIA SILVA"

1.6. Constantes com a Palavra-chave final

Para declarar uma variável cujo valor não pode ser alterado após a inicialização (uma constante), usamos a palavra-chave final.

final double PI = 3.14159;
// A linha abaixo causaria um erro de compilação, pois PI é uma constante.
// PI = 3.14; 

1.7. Conversão de Tipos (Type Casting)

Converter um valor de um tipo para outro é uma operação comum.

1.8. Entrada de Dados pelo Usuário (Scanner)

Para criar programas interativos, usamos a classe Scanner para ler dados digitados pelo usuário.

import java.util.Scanner; // 1. Precisa importar a classe

public class InteracaoUsuario {
    public static void main(String[] args) {
        // 2. Cria o objeto Scanner para ler da entrada do sistema
        Scanner leitor = new Scanner(System.in);

        System.out.print("Digite seu nome: ");
        String nome = leitor.nextLine(); // Lê uma linha de texto

        System.out.print("Digite sua idade: ");
        int idade = leitor.nextInt(); // Lê um número inteiro

        System.out.printf("Olá, %s! Você tem %d anos.%n", nome, idade);

        // 3. É uma boa prática fechar o leitor quando não for mais usar
        leitor.close(); 
    }
}

PARTE II - O Paradigma da Programação Orientada a Objetos (POO)

A Programação Orientada a Objetos (POO) é um paradigma que estrutura o software em torno de "objetos" em vez de funções e lógica. Vamos explorar seus pilares fundamentais.

2.1. Classes, Objetos e Construtores

Exemplo Simples: A classe Produto define o que todo produto no nosso sistema terá: um nome e um preço. O construtor garante que todo produto seja criado com esses valores.

public class Produto {
    // Atributos (estado do objeto)
    private String nome;
    private double preco;

    // Construtor: inicializa o objeto quando ele é criado
    public Produto(String nome, double preco) {
        this.nome = nome;
        this.preco = preco;
    }

    // Métodos (comportamento do objeto)
    public String getNome() { return nome; }
    public double getPreco() { return preco; }

    public double calcularDesconto(double percentual) {
        return this.preco * (1 - percentual / 100);
    }
}

2.2. Encapsulamento e Modificadores de Acesso

O encapsulamento é o princípio de proteger os dados internos de um objeto de acessos indevidos. Em Java, isso é feito declarando os atributos como private e fornecendo métodos públicos (getters e setters) para acessá-los de forma controlada.

Exemplo Prático: Na classe SalariedEmployee, o weeklySalary é privado. O método setWeeklySalary valida o valor antes de atribuí-lo, garantindo que o salário nunca seja negativo.

public class SalariedEmployee extends Employee {
    private double weeklySalary; // Atributo privado e protegido

    // Método público para obter o valor (Getter)
    public double getWeeklySalary() {
        return weeklySalary;
    }

    // Método público para alterar o valor com validação (Setter)
    public void setWeeklySalary(double weeklySalary) {
        if (weeklySalary < 0.0) {
            throw new IllegalArgumentException("Weekly salary must be >= 0.0");
        }
        this.weeklySalary = weeklySalary;
    }
    //...
}

2.3. Herança e a Palavra-chave super

A herança permite que uma classe (subclasse) herde atributos e métodos de outra (superclasse), promovendo o reuso de código através de uma relação "é-um". A subclasse pode adicionar novos comportamentos ou modificar os herdados.

Exemplo Prático: No nosso sistema de pagamentos, BasePlusCommissionEmployee é um CommissionEmployee que também tem um salário base. Ele herda tudo de CommissionEmployee e apenas adiciona o que lhe é específico.

// Superclasse
public class CommissionEmployee {
    // Atributos como firstName, lastName, grossSales, etc.
    public CommissionEmployee(String firstName, ..., double commissionRate) {
        // ... lógica do construtor
    }
    public double earnings() {
        return getCommissionRate() * getGrossSales();
    }
    // ...
}

// Subclasse
public class BasePlusCommissionEmployee extends CommissionEmployee {
    private double baseSalary;

    public BasePlusCommissionEmployee(String firstName, ..., double baseSalary) {
        // 1. Chama o construtor da superclasse para inicializar os atributos herdados
        super(firstName, ..., commissionRate);
        // 2. Inicializa seu próprio atributo
        this.baseSalary = baseSalary;
    }

    // Sobrescreve o método earnings para adicionar sua própria lógica
    @Override
    public double earnings() {
        // 3. Reutiliza o método da superclasse e adiciona o salário base
        return getBaseSalary() + super.earnings();
    }
    // ...
}

2.4. Polimorfismo e a Palavra-chave this

Polimorfismo (do grego, "muitas formas") é a capacidade de um objeto ser referenciado de múltiplas maneiras. Em termos práticos, permite que tratemos objetos de subclasses diferentes de forma uniforme, através da referência da superclasse.

Exemplo Prático: Podemos ter um array do tipo Employee (a superclasse) que armazena objetos de vários tipos de funcionários (SalariedEmployee, HourlyEmployee, etc.). Ao iterar e chamar o método getPaymentAmount(), o Java, através da ligação dinâmica, executa a versão correta do método para cada objeto específico.

// A interface Payable define o contrato
public interface Payable {
    double getPaymentAmount();
}

// Employee implementa o contrato
public abstract class Employee implements Payable {
    // ...
}

// As subclasses concretas fornecem a implementação
public class SalariedEmployee extends Employee {
    private double weeklySalary;
    // ...
    @Override
    public double getPaymentAmount() { return this.weeklySalary; } // 'this' é opcional aqui
}

public class Invoice implements Payable {
    private int quantity;
    private double pricePerItem;
    // ...
    @Override
    public double getPaymentAmount() { return this.quantity * this.pricePerItem; }
}

// --- Polimorfismo em Ação ---
public class TestePagamentos {
    public static void main(String[] args) {
        // Array do tipo da INTERFACE pode conter qualquer objeto que a implemente
        Payable[] objetosPagaveis = new Payable[2];
        objetosPagaveis[0] = new SalariedEmployee("João", "Silva", "111", 1200.0);
        objetosPagaveis[1] = new Invoice("01234", "Peça de computador", 2, 350.0);

        System.out.println("Processando pagamentos de forma polimórfica:");

        for (Payable pagavel : objetosPagaveis) {
            // Não importa se 'pagavel' é um Employee ou um Invoice,
            // ele responderá à chamada getPaymentAmount() da sua própria maneira.
            System.out.printf("Pagamento devido: $%,.2f%n", pagavel.getPaymentAmount());
        }
    }
}

Algo importante a se citar é que o comportamento polimórfico acima é viabilizado pelo mecanismo de ligação dinâmica (dynamic binding), como mencionado anteriormente. Esse processo ocorre em duas etapas:

  1. Em tempo de compilação: O compilador valida a chamada pagavel.getPaymentAmount() apenas com base no tipo da referência (Payable), garantindo que o método existe no contrato da interface. Nesta fase, a implementação específica a ser executada ainda é desconhecida.

  2. Em tempo de execução: A JVM (Java Virtual Machine) identifica a classe real do objeto ao qual a referência pagavel aponta a cada iteração (SalariedEmployee ou Invoice). Somente nesse momento a JVM "liga" a chamada do método à sua implementação (@Override) correspondente, encontrada na classe do objeto real.

Dessa forma, a mesma linha de código no laço for invoca diferentes blocos de código, o que torna o sistema extensível e flexível.

2.5. Classes Abstratas e Interfaces

Tanto classes abstratas quanto interfaces são usadas para definir contratos e alcançar o polimorfismo, mas elas têm propósitos diferentes.

Característica Classe Abstrata Interface
Herança Uma classe pode herdar de apenas UMA classe abstrata. Uma classe pode implementar MÚLTIPLAS interfaces.
Atributos Pode ter atributos de instância (não static). Não pode ter atributos de instância (apenas constantes static final).
Métodos Concretos Pode ter métodos com implementação. Pode ter métodos default e static (desde o Java 8).
Propósito Principal Compartilhar código e identidade comum (relação "é-um"). Definir um contrato de comportamento (relação "é capaz de").

2.6. Composição sobre Herança: Um Princípio de Design

Como vimos, a herança cria um forte acoplamento. Muitas vezes, um design mais flexível é alcançado através da composição, onde uma classe contém uma instância de outra classe (relação "tem-um").

Quando usar Herança?

Quando preferir Composição?

Exemplo de Design Flexível com Composição: Um Personagem que pode ter diferentes HabilidadeMovimento. Em vez de criar HeroiVoador e HeroiNadador, o Personagem tem uma HabilidadeMovimento que pode ser trocada.

interface HabilidadeMovimento {
    void mover();
}
class Voar implements HabilidadeMovimento { /*...*/ }
class Nadar implements HabilidadeMovimento { /*...*/ }

class Personagem {
    private HabilidadeMovimento habilidade; // Composição

    public Personagem(HabilidadeMovimento habilidadeInicial) {
        this.habilidade = habilidadeInicial;
    }

    public void setHabilidade(HabilidadeMovimento novaHabilidade) {
        this.habilidade = novaHabilidade; // Comportamento pode ser alterado
    }

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

PARTE III - Tópicos Essenciais do Ecossistema Java

Com os fundamentos da POO estabelecidos, vamos agora explorar recursos cruciais da plataforma Java que nos permitem escrever código mais seguro, robusto e flexível.

3.1. Tratamento de Exceções

O tratamento de exceções é o mecanismo do Java para lidar com erros que ocorrem durante a execução do programa de forma controlada, evitando que a aplicação pare abruptamente. As exceções são tratadas utilizando os blocos try, catch e finally.

Exemplo de Tratamento de Exceção

try {
    // Bloco de código onde um erro pode ocorrer
    int resultado = 10 / 0;
} catch (ArithmeticException e) {
    // Bloco executado se a exceção do tipo especificado ocorrer
    System.out.println("Erro: Tentativa de divisão por zero.");
} finally {
    // Bloco opcional que SEMPRE será executado, com ou sem exceção
    System.out.println("Finalizando a operação.");
}

Tipos de Exceções

Criando Exceções Personalizadas

Podemos criar nossas próprias exceções para representar erros específicos do nosso sistema, herdando de Exception (checada) ou RuntimeException (não checada).

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

3.2. Generics

Os Generics permitem definir tipos parametrizados para classes, interfaces e métodos. O principal benefício é aumentar a segurança de tipos em tempo de compilação, eliminando a necessidade de casts e evitando erros em tempo de execução.

Conceito de Generics

Antes dos Generics (Java 5), coleções armazenavam Object, o que permitia adicionar qualquer tipo de dado a uma mesma lista, gerando potenciais ClassCastException em tempo de execução. Com Generics, especificamos o tipo de dado que a coleção irá armazenar.

Exemplo com List

// O uso de <String> garante que esta lista só aceitará Strings
List<String> nomes = new ArrayList<>();
nomes.add("Ana");
nomes.add("Carlos");

// A linha abaixo causaria um ERRO DE COMPILAÇÃO, garantindo a segurança de tipos.
// nomes.add(10); 

String primeiroNome = nomes.get(0); // Não é necessário fazer cast: (String) nomes.get(0)

Benefícios dos Generics

3.3. Reflexão (Reflection)

A Reflexão é um mecanismo avançado da API do Java que permite a um programa inspecionar e manipular suas próprias estruturas (classes, métodos, atributos) em tempo de execução.

É uma ferramenta poderosa, sendo a base para o funcionamento de muitos frameworks modernos como Spring (injeção de dependências) e Hibernate/JPA (mapeamento objeto-relacional).

Exemplo de Inspeção de Classe

Podemos, por exemplo, listar todos os métodos de uma classe dinamicamente.

Class<?> classe = String.class; // Obtém a representação da classe String
Method[] metodos = classe.getDeclaredMethods();

System.out.println("Métodos da classe String:");
for (Method metodo : metodos) {
    System.out.println("- " + metodo.getName());
}

Exemplo de Modificação de Atributos Privados

A reflexão pode até mesmo quebrar o encapsulamento para acessar e modificar atributos privados (algo que deve ser feito com extremo cuidado, geralmente em testes ou frameworks).

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

//...
Pessoa p = new Pessoa();
Field campo = p.getClass().getDeclaredField("nome");
campo.setAccessible(true); // Permite o acesso ao campo privado
campo.set(p, "Maria"); // Altera o valor do atributo 'nome' no objeto 'p'
System.out.println(campo.get(p)); // Imprime "Maria"

PARTE IV - Explorando a API de Collections do Java

Enquanto arrays são úteis para armazenar sequências de tamanho fixo, a maioria das aplicações precisa gerenciar grupos de objetos de forma dinâmica. O Java Collections Framework (JCF) resolve esse problema, oferecendo um conjunto robusto e eficiente de estruturas de dados prontas para uso. A escolha da Collection correta é uma decisão de design crucial que impacta diretamente a performance e a clareza do seu código.

As três interfaces principais que você mais usará são:

Vamos analisar as implementações mais comuns de cada uma.

4.1. A Interface List: Sequências Ordenadas

Promessa: Uma List garante que os elementos serão mantidos na ordem em que foram inseridos e permite elementos duplicados. O acesso é feito por um índice numérico, assim como nos arrays.

ArrayList

LinkedList

Vector

4.2. A Interface Set: Conjuntos de Elementos Únicos

Promessa: Um Set garante que não haverá elementos duplicados. A tentativa de adicionar um elemento que já existe (verificado pelos métodos hashCode() e equals()) é simplesmente ignorada.

HashSet

LinkedHashSet

TreeSet

4.3. A Interface Map: Associações Chave-Valor

Promessa: Um Map armazena pares de chave-valor. Cada chave é única e mapeia para um único valor. É a estrutura de dados ideal para buscas rápidas baseadas em um identificador único.

(As implementações HashMap, LinkedHashMap e TreeMap seguem exatamente a mesma lógica de suas contrapartes Set em termos de estrutura interna, performance e ordenação, mas aplicadas às chaves do mapa.)

4.4 Interfaces Queue e Deque: Filas e Pilhas

Queue (Fila)

Deque (Fila de Duas Pontas)

4.5. Tabela Resumo: Quando Usar Cada Collection

Estrutura Preciso de Duplicatas? Preciso de Ordem? Qual é meu foco?
ArrayList Sim Sim, de inserção Acesso rápido por índice e iteração simples.
LinkedList Sim Sim, de inserção Muitas inserções/remoções no início/fim da lista.
HashSet Não Não Máxima velocidade para verificar se um item existe.
LinkedHashSet Não Sim, de inserção Velocidade de HashSet com ordem de iteração previsível.
TreeSet Não Sim, ordenada Manter os itens sempre ordenados.
HashMap Não (chaves) Não Acesso ultra-rápido a um valor através de uma chave.
LinkedHashMap Não (chaves) Sim, de inserção Acesso rápido de HashMap com ordem de iteração previsível.
TreeMap Não (chaves) Sim, ordenada Manter as chaves sempre ordenadas.
ArrayDeque Sim Sim Implementar uma Fila (FIFO) ou uma Pilha (LIFO) de forma eficiente.

4.6. Análise de Performance: Complexidade de Tempo e Espaço

Para tomar decisões de design informadas, é importante entender a complexidade computacional das operações em cada Collection. Usamos a notação Big O para descrever como a performance de um algoritmo escala conforme o número de elementos (n) na coleção aumenta.

Guia Rápido da Notação Big O:

4.7. Tabela Comparativa de Complexidade de Tempo (Big O)

A tabela a seguir apresenta a complexidade de tempo para as operações mais comuns nas principais implementações.

Estrutura add / put remove get / contains Iteração (next)
ArrayList $O(1)$ (Amortizado) [^1] $O(n)$ [^2] $O(1)$ $O(1)$
LinkedList $O(1)$ [^3] $O(1)$ [^3] $O(n)$ $O(1)$
HashSet $O(1)$ (Médio) [^4] $O(1)$ (Médio) [^4] $O(1)$ (Médio) [^4] $O(1)$ (Médio) [^4]
LinkedHashSet $O(1)$ (Médio) [^4] $O(1)$ (Médio) [^4] $O(1)$ (Médio) [^4] $O(1)$
TreeSet $O(\log n)$ $O(\log n)$ $O(\log n)$ $O(\log n)$
HashMap $O(1)$ (Médio) [^4] $O(1)$ (Médio) [^4] $O(1)$ (Médio) [^4] $O(1)$ (Médio) [^4]
LinkedHashMap $O(1)$ (Médio) [^4] $O(1)$ (Médio) [^4] $O(1)$ (Médio) [^4] $O(1)$
TreeMap $O(\log n)$ $O(\log n)$ $O(\log n)$ $O(\log n)$
ArrayDeque $O(1)$ (Amortizado) [^1] $O(1)$ (Amortizado) [^1] Não aplicável $O(1)$

Legenda e Observações Importantes:

[^1]: $O(1)$ Amortizado: A operação é geralmente muito rápida ($O(1)$), mas ocasionalmente pode ser lenta ($O(n)$). No ArrayList e ArrayDeque, isso acontece quando a capacidade interna do array se esgota e é preciso alocar um novo array maior e copiar todos os elementos. Na média, ao longo de muitas operações, o custo se "amortiza" para $O(1)$.

[^2]: $O(n)$ em ArrayList.remove: Este custo se refere a remover um elemento pelo índice no meio da lista. Remover o último elemento é $O(1)$.

[^3]: $O(1)$ em LinkedList.add/remove: Este custo se refere a adicionar ou remover elementos no início ou no fim da lista. Inserir ou remover no meio é $O(n)$, pois primeiro é preciso navegar até a posição desejada.

[^4]: $O(1)$ Médio em Estruturas Hash: HashSet e HashMap (e suas variantes Linked) oferecem performance de tempo constante no cenário médio. No entanto, no pior caso, que ocorre quando há muitas colisões de hash (objetos diferentes gerando o mesmo hashCode), a performance pode degradar para $O(n)$. Isso é raro se os métodos hashCode() e equals() forem bem implementados.

Complexidade de Espaço

Para todas as coleções mencionadas, a complexidade de espaço é $O(n)$. Isso significa que a memória utilizada cresce linearmente com o número de elementos (n) armazenados na coleção.


PARTE V - Modelagem com UML

5.1 O que é e por que usar a UML?

A UML (Unified Modeling Language) não é apenas um "conjunto de diagramas", mas sim uma linguagem de modelagem visual padronizada para a engenharia de software. Pense nela como a planta de uma casa: antes de construir, você desenha a planta para planejar, comunicar ideias e garantir que todos os envolvidos (engenheiros, eletricistas, proprietário) entendam o projeto da mesma forma.

Na engenharia de software, a UML serve para:

Vamos focar nos dois diagramas mais fundamentais para o início de um projeto.

1. Diagrama de Caso de Uso (Use Case Diagram)

Este diagrama responde à pergunta: "O que o sistema faz do ponto de vista do usuário?". Ele descreve as funcionalidades principais do sistema e como os usuários (ou outros sistemas) interagem com ele.

Componentes Principais:

Relações Comuns:

2. Diagrama de Classes (Class Diagram)

Este diagrama responde à pergunta: "Qual é a estrutura estática do meu sistema?". Ele é o mapa dos "tijolos" de um sistema orientado a objetos: as classes, seus atributos, métodos e como elas se relacionam umas com as outras.

A Caixa da Classe

Uma classe é representada por um retângulo dividido em três partes:

  1. Nome da Classe: No topo.
  2. Atributos (Campos): No meio.
  3. Métodos (Operações): Na base.

Notação de Visibilidade:

+---------------------------+
|         ContaBancaria     |  <-- Nome da Classe
+---------------------------+
| - titular: String         |  <-- Atributos com visibilidade
| - saldo: double           |
+---------------------------+
| + depositar(valor: double): void |  <-- Métodos com visibilidade e parâmetros
| + sacar(valor: double): boolean  |
| + getSaldo(): double      |
+---------------------------+

Relacionamentos Fundamentais:

+---------------+ +---------------+ | ContaCorrente | | ContaPoupanca | +---------------+ +---------------+ ```


Exercícios ⚒️

Os exercícios abaixo ajudarão a fixar os conceitos abordados. Elabore-os individualmente.

Bloco 1: Fundamentos e Estruturas de Controle (Parte I)

  1. Calculadora de Média: Escreva um programa que utiliza a classe Scanner para ler 3 notas de um aluno. Calcule e exiba a média aritmética das notas. Em seguida, usando uma estrutura if-else, informe se o aluno foi "Aprovado" (média >= 7), "Recuperação" (média >= 5 e < 7) ou "Reprovado" (média < 5).

  2. Tabuada com for: Peça ao usuário um número inteiro. Use um laço for tradicional para calcular e exibir a tabuada de multiplicação desse número, do 1 ao 10. (Ex: "5 x 1 = 5", "5 x 2 = 10", ...).

  3. Adivinhe o Número: Gere um número aleatório entre 1 e 100. Peça ao usuário para adivinhar o número. Use um laço while para continuar pedindo um número até que o usuário acerte. A cada tentativa, dê uma dica se o palpite foi "muito alto" ou "muito baixo". No final, informe o número de tentativas.

  4. Soma de Ímpares em um Array: Crie um array de inteiros com números pré-definidos. Utilize um laço for-each para percorrer o array e somar todos os números que forem ímpares. Exiba o resultado final.

Bloco 2: Programação Orientada a Objetos (Parte II)

  1. Classe Carro: Crie uma classe Carro com os seguintes atributos privados: marca (String), modelo (String) e ano (int). Implemente um construtor para inicializar esses atributos e métodos públicos getters para cada um deles. Adicione um método exibirInfo() que imprime os detalhes do carro.

  2. Classe Circulo com Encapsulamento: Crie uma classe Circulo com um atributo privado raio (double). Crie um construtor e os métodos getRaio e setRaio. No setRaio, adicione uma validação para garantir que o raio nunca seja um valor negativo ou zero (lance uma IllegalArgumentException se a condição não for atendida). Crie também um método calcularArea() que retorna a área do círculo (pi * raioˆ2).

  3. Herança de Veiculo: Crie uma classe Veiculo com atributos marca e modelo. Em seguida, crie duas subclasses: Carro (que adiciona numeroDePortas) e Moto (que adiciona cilindradas). Sobrescreva o método toString() em todas as classes para exibir suas informações de forma completa.

  4. Exceção Personalizada SaldoInsuficienteException: Reutilizando a ideia da ContaBancaria da aula, crie sua própria exceção checada SaldoInsuficienteException. Modifique o método sacar para que, em vez de retornar false, ele lance essa exceção quando o saldo for insuficiente. Crie uma classe de teste para tratar essa exceção com um bloco try-catch.

Bloco 3: API de Collections - List (Parte IV)

  1. Lista de Tarefas (ArrayList): Crie um programa que gerencia uma lista de tarefas (Strings). Permita ao usuário: adicionar uma tarefa, remover uma tarefa pelo seu índice e listar todas as tarefas. Use um ArrayList.

  2. Ordenando Números: Crie um ArrayList de Integer. Adicione 10 números inteiros aleatórios ou definidos por você. Utilize a classe Collections e seu método sort() para ordenar a lista em ordem crescente e, em seguida, exiba o resultado.

  3. Manipulando o Início e o Fim (LinkedList): Crie uma LinkedList para simular uma fila de atendimento. Adicione 5 nomes de clientes no final da fila. Em seguida, "atenda" os 2 primeiros clientes (removendo-os do início da lista). Por fim, adicione 2 novos clientes "prioritários" no início da fila. Exiba a ordem final da fila.

  4. Busca por Elemento: Crie um ArrayList de Strings com nomes de cidades. Peça ao usuário para digitar o nome de uma cidade. Verifique se a cidade está presente na lista usando o método contains(). Se estiver, informe o índice da sua primeira ocorrência usando o método indexOf().

Bloco 4: API de Collections - Set (Parte IV)

  1. Removendo Duplicatas: Crie um ArrayList de Integer que contenha números duplicados. Escreva um código que receba esta lista e retorne uma nova coleção sem os elementos duplicados. (Dica: a forma mais fácil é usar um HashSet).

  2. Unicidade de E-mails (HashSet): Crie um HashSet para armazenar endereços de e-mail (Strings). Tente adicionar alguns e-mails, incluindo um que seja duplicado. Imprima o tamanho do Set para confirmar que o e-mail duplicado não foi adicionado.

  3. Ordem de Inserção (LinkedHashSet): Crie um LinkedHashSet e adicione os nomes dos dias da semana fora de ordem (ex: "Quarta", "Segunda", "Sexta"). Itere sobre o Set e imprima os elementos para verificar que eles são exibidos na ordem exata em que foram inseridos.

  4. Nomes em Ordem Alfabética (TreeSet): Crie um TreeSet de Strings e adicione 5 nomes de pessoas fora da ordem alfabética. Itere sobre o Set e observe que os nomes são impressos em ordem alfabética natural.

  5. Objetos Personalizados em um TreeSet: Crie uma classe Produto com nome (String) e preco (double). Faça com que a classe Produto implemente a interface Comparable para que os produtos sejam ordenados pelo preço (do menor para o maior). Crie um TreeSet<Produto> e adicione alguns produtos para testar a ordenação.

Bloco 5: API de Collections - Map (Parte IV)

  1. Dicionário Simples (HashMap): Crie um HashMap para funcionar como um dicionário de tradução simples (Português -> Inglês). Adicione 5 palavras e suas traduções. Peça ao usuário uma palavra em português e, se ela existir no mapa, exiba sua tradução.

  2. Contador de Frequência de Palavras: Crie uma String contendo um parágrafo de texto. Use um HashMap<String, Integer> para contar a frequência de cada palavra no texto. Ao final, itere sobre o mapa e exiba cada palavra e sua contagem.

  3. Agenda de Contatos: Use um HashMap para criar uma agenda onde a chave é o nome do contato (String) e o valor é o número de telefone (String). Permita ao usuário: adicionar um novo contato, buscar um telefone pelo nome e listar todos os contatos (nome e telefone).

  4. Mantendo a Ordem de Cadastro (LinkedHashMap): Crie um LinkedHashMap para armazenar produtos e seus respectivos códigos (ex: Integer como chave, String como valor). Adicione 5 produtos. Itere sobre o mapa e mostre que a ordem de exibição é a mesma da ordem de inserção.

  5. Listagem Ordenada (TreeMap): Crie um TreeMap para armazenar as notas de alunos em uma prova, onde a chave é o nome do aluno (String) e o valor é a nota (Double). Adicione 5 alunos fora de ordem alfabética. Ao listar os alunos e suas notas, observe que o TreeMap os exibe em ordem alfabética pelo nome.

  6. Verificando a Existência de Chave e Valor: Usando o HashMap do exercício da agenda, escreva um código que verifique se um determinado nome (containsKey) e se um determinado telefone (containsValue) já existem na agenda.

Bloco 6: API de Collections - Queue e Deque (Parte IV)

  1. Fila de Impressão (Queue): Simule uma fila de impressão. Crie uma Queue (usando LinkedList como implementação) e adicione 5 documentos (Strings com nomes como "Documento1.pdf", "Foto.png", etc.). Em seguida, processe a fila, "imprimindo" (removendo) cada documento e exibindo seu nome na ordem em que entraram.

  2. Pilha de Livros (Deque como Stack): Use um ArrayDeque para simular uma pilha de livros. Permita ao usuário "empilhar" 3 livros (push). Depois, "desempilhe" um livro (pop) e veja qual foi removido (o último que entrou). Por fim, use peek para "espiar" o livro que está no topo da pilha sem removê-lo.

Bloco 7: Exercícios Integrados

  1. Catálogo de Produtos por Categoria: Crie uma estrutura de dados para um catálogo de produtos. Use um Map<String, List<Produto>>, onde a chave é o nome da categoria (ex: "Eletrônicos") e o valor é uma lista de objetos da classe Produto pertencentes àquela categoria. Popule a estrutura com alguns dados e depois escreva um código para listar todos os produtos de uma categoria específica.

  2. Sorteio de Ganhadores Únicos: Crie uma lista (ArrayList) com nomes de participantes, permitindo que alguns nomes se repitam. Escreva um método que realize um sorteio: ele deve primeiro garantir que cada participante seja considerado apenas uma vez (mesmo que seu nome apareça várias vezes) e depois sortear aleatoriamente 3 nomes únicos para serem os ganhadores.

  3. Invertendo uma Frase: Peça ao usuário uma frase. Use um Deque (como uma pilha) para armazenar cada palavra da frase. Em seguida, desempilhe as palavras uma a uma para formar e exibir a frase na ordem inversa.

  4. Histórico de Navegação: Use uma LinkedList para simular o histórico de um navegador. Crie métodos visitar(String url), voltar() e avancar(). O método voltar deve navegar para a URL anterior no histórico, e o avancar para a próxima, gerenciando o índice da página atual.

  5. Agrupando Alunos por Nota: Tendo uma List<Aluno> (onde Aluno tem nome e nota), crie um Map<String, List<Aluno>> que agrupe os alunos por faixa de nota: "Aprovados" (nota >= 7), "Recuperação" (nota >= 5 e < 7) e "Reprovados" (nota < 5).

Bloco 8: Desafios - Reflection

Os exercícios a seguir têm como objetivo praticar o uso da API de Reflexão do Java para inspecionar e manipular objetos dinamicamente.

  1. Inspetor de Classe com Reflection

O primeiro exemplo da aula mostra como listar os métodos de uma classe. Vamos expandir essa ideia para criar um inspetor universal.

  1. Modificador de Atributos Privados

A aula demonstra como a reflexão pode quebrar o encapsulamento para modificar atributos privados, uma técnica essencial para frameworks de injeção de dependência e ORM.

  1. Framework de Testes Simulado com Anotações

Este exercício simula como frameworks (JUnit, TestNG) usam reflection para encontrar e executar métodos de teste automaticamente.

Bloco 9: VcRiquinho e Lanchonete Quase Três Lanches

  1. Os exercícios VcRiquinho e Lanchonete Quase Três Lanches estão disponíveis no Moodle. Leia o enunciado e elabore as tarefas pedidas.

Todos os exercícios deverão ser entregues no Moodle!

Bom trabalho! ⚒️