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:
- byte (8 bits, -128 a 127)
- short (16 bits, -32768 a 32767)
- int (32 bits, -2.147.483.648 a 2.147.483.647)
- long (64 bits)
- float (32 bits, ponto flutuante)
- double (64 bits, ponto flutuante, maior precisão)
- char (16 bits, representa um caractere Unicode)
- boolean (pode ser
true
oufalse
)
Ao declarar uma variável, fazemos:
int idade = 25;
double salario = 2500.75;
boolean ativo = true;
1.2. Operadores
- Aritméticos:
+
,-
,*
,/
,%
- De Atribuição:
=
(Atribuição simples): Atribui o valor da direita à variável da esquerda.+=
,-=
,*=
,/=
,%=
(Atribuição composta): Realiza uma operação aritmética e atribui o resultado à variável. Por exemplo,x += y
é uma forma abreviada dex = x + y
.
- Relacionais:
==
,!=
,>
,>=
,<
,<=
- Lógicos:
&&
(E),||
(OU),!
(NÃO) - Unários:
++
(incremento),--
(decremento) - Ternário:
condicao ? valorSeVerdadeiro : valorSeFalso
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.
- Decisão (
if-else
,switch
):
if (idade >= 18) {
System.out.println("Maior de idade");
} else {
System.out.println("Menor de idade");
}
Repetição (
for
,for-each
,while
,do-while
):for
tradicional: Ideal quando você precisa de acesso ao índice ou tem um controle mais complexo sobre a iteração.
for (int i = 0; i < 5; i++) {
System.out.println("O valor de i é: " + i);
}
for-each
(Enhanced for loop): A forma mais simples e segura de percorrer todos os elementos de um array ou coleção, sem se preocupar com índices.
List<String> nomes = Arrays.asList("Ana", "João", "Carlos");
for (String nome : nomes) {
System.out.println("Olá, " + nome);
}
while
edo-while
: Executam um bloco de código enquanto uma condição for verdadeira. A diferença é que odo-while
garante que o bloco seja executado pelo menos uma vez.
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.
Conversão Implícita (Alargamento): Ocorre automaticamente quando não há risco de perda de dados (de um tipo menor para um maior).
int meuInt = 100; double meuDouble = meuInt; // Conversão automática para 100.0
Conversão Explícita (Estreitamento / Casting): Deve ser feita manualmente quando há risco de perda de informação.
double precoProduto = 19.99; int precoInteiro = (int) precoProduto; // Forçamos a conversão para int // O valor de precoInteiro será 19 (a parte decimal é perdida)
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
- Classe: É o nosso molde ou planta. Ela define um conjunto de atributos (características ou dados) e métodos (ações ou comportamentos) que um tipo de objeto terá.
- Objeto: É a instância concreta de uma classe, criada em memória durante a execução do programa. Cada objeto tem seu próprio estado (valores dos seus atributos).
- Construtor: Um método especial, com o mesmo nome da classe, responsável por inicializar um objeto no momento de sua criação (
new
).
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.
public
: Acessível de qualquer lugar.private
: Acessível apenas de dentro da própria classe.protected
: Acessível pela própria classe, por classes no mesmo pacote e por subclasses.- (default): Acessível apenas por classes no mesmo pacote.
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.
extends
: Palavra-chave usada para definir a herança.super
: Palavra-chave usada para se referir à superclasse, seja para chamar seu construtor (super(...)
) ou seus métodos (super.metodo()
).
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.
@Override
: Anotação que indica que um método está sobrescrevendo um método da superclasse.this
: Palavra-chave que se refere à instância atual do objeto. É usada para desambiguar variáveis de instância de parâmetros locais ou para chamar outro construtor da mesma classe (this(...)
).
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:
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.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
ouInvoice
). 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.
Classe Abstrata: Usada para criar uma classe base que compartilha código comum (atributos e métodos concretos) com múltiplas subclasses. Uma classe só pode herdar de uma classe abstrata. Use quando as subclasses compartilham uma forte relação "é-um" e código.
- Exemplo:
Employee
é uma classe abstrata porque todos os funcionários têmfirstName
elastName
, mas o cálculo degetPaymentAmount()
é específico para cada tipo.
- Exemplo:
Interface: Define um contrato puro de comportamentos (métodos) que uma classe deve implementar. Uma classe pode implementar múltiplas interfaces. Use para definir uma capacidade ou "papel" que classes não relacionadas podem desempenhar.
- Exemplo:
Payable
é uma interface porque tanto umEmployee
quanto umaInvoice
podem ser "pagáveis", mas não compartilham nenhuma outra característica em comum.
- Exemplo:
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 a relação "é-um" é genuína e imutável (
SalariedEmployee
sempre será umEmployee
). - Quando a superclasse foi projetada para ser estendida e é estável.
Quando preferir Composição?
- Para reutilizar código de classes não relacionadas.
- Quando você quer poder alterar o comportamento em tempo de execução.
- Para criar designs mais flexíveis e com menor acoplamento, favorecendo a injeção de dependências.
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
Exceções Checadas (Checked Exceptions): São exceções que o compilador obriga o programador a tratar (
try-catch
) ou declarar (throws
). Geralmente representam condições externas recuperáveis (ex:IOException
,SQLException
).try { FileReader file = new FileReader("arquivo_inexistente.txt"); } catch (FileNotFoundException e) { System.out.println("Arquivo não pôde ser encontrado."); }
Exceções Não Checadas (Unchecked Exceptions): São exceções que ocorrem em tempo de execução, geralmente devido a erros de programação, e não precisam ser obrigatoriamente tratadas (ex:
NullPointerException
,ArrayIndexOutOfBoundsException
).String texto = null; // A linha abaixo lançará uma NullPointerException em tempo de execução // System.out.println(texto.length());
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
- Segurança de tipos: Detecta erros de tipo em tempo de compilação.
- Reutilização de código: Permite criar componentes genéricos que funcionam com qualquer tipo.
- Legibilidade: O código se torna mais claro, pois as intenções de tipo são explícitas.
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:
List
: Para sequências ordenadas de elementos.Set
: Para conjuntos de elementos únicos.Map
: Para associações de chave-valor.
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
- Característica Principal: Uma lista redimensionável que se destaca no acesso rápido a elementos por sua posição.
- Estrutura Interna: Utiliza um array (
[]
) por baixo dos panos. Quando o array enche, um novo array maior é alocado e os elementos são copiados, um processo que pode ser custoso. - Performance:
- Acesso por índice (
get(i)
): Excelente, tempo constante - $O(1)$. - Adicionar/Remover no final: Rápido, tempo constante amortizado - $O(1)$.
- Adicionar/Remover no meio ou início: Lento, pois exige o deslocamento de todos os elementos subsequentes - tempo linear - $O(n)$.
- Acesso por índice (
- Caso de Uso Ideal: Quando a principal operação é a leitura de dados por índice ou a iteração sobre a lista. É a
List
de propósito geral mais comum.
LinkedList
- Característica Principal: Uma lista otimizada para operações de inserção e remoção rápidas em qualquer ponto da lista.
- Estrutura Interna: É uma lista duplamente encadeada, onde cada elemento (nó) armazena o valor e ponteiros para o elemento anterior e o próximo.
- Performance:
- Adicionar/Remover no início ou fim: Excelente, tempo constante - $O(1)$.
- Acesso por índice (
get(i)
): Lento, pois precisa percorrer a lista desde o início ou o fim até encontrar a posição - tempo linear - $O(n)$. - Inserção/Remoção no meio (se você já tem a referência): Rápido, tempo constante - $O(1)$.
- Caso de Uso Ideal: Quando a aplicação realiza um grande número de inserções e remoções no início ou no meio da lista, como em uma fila de processamento ou ao construir uma estrutura de dados complexa.
Vector
- Característica Principal: Uma versão legada e sincronizada (
thread-safe
) doArrayList
. - Performance: Similar ao
ArrayList
, mas com uma sobrecarga de performance devido à sincronização em todos os seus métodos públicos. - Caso de Uso Ideal: Raramente é usado em código novo. Para programação concorrente, prefira usar um
ArrayList
com sincronização explícita ou as collections do pacotejava.util.concurrent
.
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
- Característica Principal: Armazenar elementos únicos com a máxima velocidade, sem se preocupar com a ordem.
- Estrutura Interna: Utiliza uma tabela de dispersão (
HashMap
por baixo dos panos). A posição de cada elemento é determinada pelo seuhashCode()
. - Performance: Excelente para as operações principais (
add
,remove
,contains
), que geralmente são executadas em tempo constante - $O(1)$. A performance de iteração não é previsível. - Caso de Uso Ideal: Verificar rapidamente se um item existe em um grande conjunto de dados, ou simplesmente para garantir a unicidade dos elementos.
LinkedHashSet
- Característica Principal: Um
HashSet
que lembra a ordem em que os elementos foram inseridos. - Estrutura Interna: Combina uma tabela de dispersão com uma lista duplamente encadeada.
- Performance: Quase tão rápida quanto o
HashSet
(operações em $O(1)$), mas com uma pequena sobrecarga para manter a ordem da lista. - Caso de Uso Ideal: Quando você precisa da velocidade e unicidade de um
HashSet
, mas também da capacidade de iterar sobre os elementos na ordem original de inserção.
TreeSet
- Característica Principal: Armazena elementos únicos e os mantém perpetuamente em ordem crescente.
- Estrutura Interna: Utiliza uma árvore Rubro-Negra (
Red-Black Tree
). - Performance: Boa, mas mais lenta que
HashSet
. As operações (add
,remove
,contains
) são executadas em tempo de logaritmo - $O(\log n)$. - Ordenação: Os elementos devem implementar a interface
Comparable
(ordem natural) ou umComparator
deve ser fornecido no construtor doTreeSet
. - Caso de Uso Ideal: Quando você precisa manter uma coleção de itens únicos sempre ordenada, como um ranking de pontuações ou uma lista de nomes em ordem alfabética.
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.)
HashMap
: Máxima velocidade ($O(1)$) paraput
,get
,remove
. A ordem não é garantida. Caso de uso mais comum para mapas.LinkedHashMap
: Velocidade deHashMap
com ordem de iteração previsível (ordem de inserção). Ideal para caches ou dados que precisam ser processados na ordem em que chegaram.TreeMap
: Chaves mantidas em ordem crescente ($O(\log n)$). Ideal para dicionários ou dados que precisam ser recuperados em um intervalo ordenado.
4.4 Interfaces Queue
e Deque
: Filas e Pilhas
Queue
(Fila)
- Característica Principal: Representa uma estrutura FIFO (First-In, First-Out), onde o primeiro elemento a entrar é o primeiro a sair, como uma fila de banco.
- Implementações Comuns:
LinkedList
ePriorityQueue
(uma fila especial que ordena os elementos com base em sua prioridade). - Caso de Uso Ideal: Gerenciar tarefas em uma fila de processamento, algoritmos de busca em largura (BFS), ou qualquer cenário que exija processamento ordenado por chegada.
Deque
(Fila de Duas Pontas)
- Característica Principal: Uma "double-ended queue" que permite adicionar e remover elementos tanto do início quanto do fim.
- Uso como Pilha (Stack): Um
Deque
é a estrutura recomendada atualmente para implementar uma pilha LIFO (Last-In, First-Out).- Use
push(e)
para adicionar ao início. - Use
pop()
para remover do início.
- Use
- Implementação Recomendada:
ArrayDeque
. É mais eficiente e moderno que a antiga classeStack
. - Caso de Uso Ideal: Implementar a funcionalidade de "desfazer" (undo), analisar expressões matemáticas (parsing) ou em algoritmos de busca em profundidade (DFS).
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.
- Complexidade de Tempo: Mede o tempo de execução.
- Complexidade de Espaço: Mede a memória adicional necessária.
Guia Rápido da Notação Big O:
- $O(1)$ (Tempo Constante): Excelente. A operação leva o mesmo tempo, não importa o tamanho da coleção. É o "santo graal" da performance.
- $O(\log n)$ (Tempo Logarítmico): Ótimo. O tempo de execução cresce muito lentamente. Dobrar o número de elementos não dobra o tempo.
- $O(n)$ (Tempo Linear): Razoável. O tempo de execução cresce em proporção direta ao número de elementos. Percorrer uma lista inteira é um exemplo clássico.
- $O(n^2)$ (Tempo Quadrático): Ruim. O tempo de execução cresce exponencialmente. Deve ser evitado para grandes coleções (ex: laços aninhados que percorrem a mesma coleçã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.
- Observação para
ArrayList
: UmArrayList
tem umacapacidade
interna que pode ser maior que seutamanho
real. Ele cresce em saltos para otimizar o custo de adicionar novos elementos. Isso significa que, por um tempo, ele pode ocupar um pouco mais de memória do que o estritamente necessário para os elementos que contém.
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:
- Visualizar: Dar uma forma concreta às ideias abstratas do sistema.
- Especificar: Descrever o sistema de forma precisa, sem ambiguidades.
- Documentar: Criar um registro da arquitetura e das decisões de design.
- Comunicar: Servir como uma ponte de comunicação clara entre analistas, desenvolvedores, testadores e até mesmo clientes.
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:
- Ator (Actor): Representa um papel desempenhado por um usuário, outro sistema ou até mesmo o tempo, que interage com o sistema. É desenhado como um "boneco palito".
- Exemplo:
Cliente
,Administrador
,Sistema de Pagamento Externo
.
- Exemplo:
- Caso de Uso (Use Case): Representa uma funcionalidade ou um objetivo que um ator deseja alcançar com o sistema. É desenhado como uma elipse.
- Exemplo:
Realizar Login
,Cadastrar Produto
,Gerar Relatório de Vendas
.
- Exemplo:
- Fronteira do Sistema (System Boundary): Uma caixa que delimita o escopo do sistema, separando os casos de uso (dentro) dos atores (fora).
Relações Comuns:
- Associação: Uma linha contínua ligando um Ator a um Caso de Uso, indicando que o ator participa daquela funcionalidade.
<<include>>
(Inclusão): Uma seta pontilhada que indica que um caso de uso obrigatoriamente inclui a funcionalidade de outro. É usado para reutilizar comportamento comum.- Exemplo: Os casos de uso
Consultar Saldo
eRealizar Transferência
ambos incluem o caso de usoValidar Credenciais
.
- Exemplo: Os casos de uso
<<extend>>
(Extensão): Uma seta pontilhada que indica um comportamento opcional ou alternativo que pode estender um caso de uso base, sob certas condições.- Exemplo: O caso de uso
Realizar Empréstimo de Livro
pode ser estendido pelo casoCalcular Multa por Atraso
, mas apenas se o usuário tiver livros atrasados.
- Exemplo: O caso de uso
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:
- Nome da Classe: No topo.
- Atributos (Campos): No meio.
- Métodos (Operações): Na base.
Notação de Visibilidade:
+
:public
-
:private
#
:protected
+---------------------------+
| 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:
Associação: Uma linha contínua que representa uma relação estrutural entre classes (um objeto "conhece" ou "usa" o outro). Pode ter multiplicidade, que indica quantos objetos estão envolvidos.
1
: Exatamente um.*
: Zero ou mais.1..*
: Um ou mais.0..1
: Zero ou um.
+-----------+ 1 1..* +-----------+ | Professor |<>----------| Turma | +-----------+ +-----------+ (Um Professor ensina em uma ou mais Turmas)
Agregação: Um tipo especial de associação (relação "tem-um") onde as classes têm um ciclo de vida independente. É representada por um losango vazio.
- Exemplo: Um
Time
de futebol temJogadores
. Se o time for desfeito, os jogadores continuam a existir.
+------+ <>----* +---------+ | Time | | Jogador | +------+ +---------+
- Exemplo: Um
Composição: Uma forma forte de agregação ("parte-de") onde o ciclo de vida das partes depende do todo. Se o todo é destruído, as partes também são. É representada por um losango preenchido.
- Exemplo: Uma
NotaFiscal
é composta porItensDaNota
. Se a nota fiscal for excluída, seus itens não fazem mais sentido e são excluídos também.
+------------+ <*>----1..* +------------+ | NotaFiscal | | ItemDaNota | +------------+ +------------+
- Exemplo: Uma
Generalização (Herança): Representa a relação "é-um" (
extends
em Java). É representada por uma seta com uma ponta de triângulo vazia apontando para a superclasse.+---------------+ | ContaBancaria | +---------------+ ^ | ---------'--------- | |
+---------------+ +---------------+ | ContaCorrente | | ContaPoupanca | +---------------+ +---------------+ ```
Realização (Implementação): Representa a relação entre uma classe e uma
interface
que ela implementa. É representada por uma linha pontilhada com uma ponta de triângulo vazia.+-------------+ | Payable | (<<interface>>) +-------------+ ^ | (linha pontilhada) +-------------+ | Invoice | +-------------+
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)
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 estruturaif-else
, informe se o aluno foi "Aprovado" (média >= 7), "Recuperação" (média >= 5 e < 7) ou "Reprovado" (média < 5).Tabuada com
for
: Peça ao usuário um número inteiro. Use um laçofor
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", ...).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.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)
Classe
Carro
: Crie uma classeCarro
com os seguintes atributos privados:marca
(String),modelo
(String) eano
(int). Implemente um construtor para inicializar esses atributos e métodos públicosgetters
para cada um deles. Adicione um métodoexibirInfo()
que imprime os detalhes do carro.Classe
Circulo
com Encapsulamento: Crie uma classeCirculo
com um atributo privadoraio
(double). Crie um construtor e os métodosgetRaio
esetRaio
. NosetRaio
, adicione uma validação para garantir que o raio nunca seja um valor negativo ou zero (lance umaIllegalArgumentException
se a condição não for atendida). Crie também um métodocalcularArea()
que retorna a área do círculo (pi * raioˆ2).Herança de
Veiculo
: Crie uma classeVeiculo
com atributosmarca
emodelo
. Em seguida, crie duas subclasses:Carro
(que adicionanumeroDePortas
) eMoto
(que adicionacilindradas
). Sobrescreva o métodotoString()
em todas as classes para exibir suas informações de forma completa.Exceção Personalizada
SaldoInsuficienteException
: Reutilizando a ideia daContaBancaria
da aula, crie sua própria exceção checadaSaldoInsuficienteException
. Modifique o métodosacar
para que, em vez de retornarfalse
, ele lance essa exceção quando o saldo for insuficiente. Crie uma classe de teste para tratar essa exceção com um blocotry-catch
.
Bloco 3: API de Collections - List
(Parte IV)
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 umArrayList
.Ordenando Números: Crie um
ArrayList
deInteger
. Adicione 10 números inteiros aleatórios ou definidos por você. Utilize a classeCollections
e seu métodosort()
para ordenar a lista em ordem crescente e, em seguida, exiba o resultado.Manipulando o Início e o Fim (
LinkedList
): Crie umaLinkedList
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.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étodocontains()
. Se estiver, informe o índice da sua primeira ocorrência usando o métodoindexOf()
.
Bloco 4: API de Collections - Set
(Parte IV)
Removendo Duplicatas: Crie um
ArrayList
deInteger
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 umHashSet
).Unicidade de E-mails (
HashSet
): Crie umHashSet
para armazenar endereços de e-mail (Strings). Tente adicionar alguns e-mails, incluindo um que seja duplicado. Imprima o tamanho doSet
para confirmar que o e-mail duplicado não foi adicionado.Ordem de Inserção (
LinkedHashSet
): Crie umLinkedHashSet
e adicione os nomes dos dias da semana fora de ordem (ex: "Quarta", "Segunda", "Sexta"). Itere sobre oSet
e imprima os elementos para verificar que eles são exibidos na ordem exata em que foram inseridos.Nomes em Ordem Alfabética (
TreeSet
): Crie umTreeSet
de Strings e adicione 5 nomes de pessoas fora da ordem alfabética. Itere sobre oSet
e observe que os nomes são impressos em ordem alfabética natural.Objetos Personalizados em um
TreeSet
: Crie uma classeProduto
comnome
(String) epreco
(double). Faça com que a classeProduto
implemente a interfaceComparable
para que os produtos sejam ordenados pelo preço (do menor para o maior). Crie umTreeSet<Produto>
e adicione alguns produtos para testar a ordenação.
Bloco 5: API de Collections - Map
(Parte IV)
Dicionário Simples (
HashMap
): Crie umHashMap
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.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.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).Mantendo a Ordem de Cadastro (
LinkedHashMap
): Crie umLinkedHashMap
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.Listagem Ordenada (
TreeMap
): Crie umTreeMap
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 oTreeMap
os exibe em ordem alfabética pelo nome.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)
Fila de Impressão (
Queue
): Simule uma fila de impressão. Crie umaQueue
(usandoLinkedList
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.Pilha de Livros (
Deque
como Stack): Use umArrayDeque
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, usepeek
para "espiar" o livro que está no topo da pilha sem removê-lo.
Bloco 7: Exercícios Integrados
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 classeProduto
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.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.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.Histórico de Navegação: Use uma
LinkedList
para simular o histórico de um navegador. Crie métodosvisitar(String url)
,voltar()
eavancar()
. O métodovoltar
deve navegar para a URL anterior no histórico, e oavancar
para a próxima, gerenciando o índice da página atual.Agrupando Alunos por Nota: Tendo uma
List<Aluno>
(ondeAluno
temnome
enota
), crie umMap<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.
- 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.
Objetivo: Crie uma classe
AnalisadorDeClasse
com um método estáticopublic static void inspecionar(Object obj)
. Este método deve receber qualquer objeto Java e imprimir no console:- O nome completo da classe do objeto.
- O nome de todos os seus atributos (campos), incluindo os privados.
- O nome de todos os seus métodos, incluindo os privados.
Dicas:
- Use
obj.getClass()
para obter o objetoClass
. - Use
getDeclaredFields()
para obter os atributos. - Use
getDeclaredMethods()
para obter os métodos.
- Use
Classe para Teste:
class Produto { private int codigo; public String nome; protected double preco; public Produto(int codigo, String nome, double preco) { this.codigo = codigo; this.nome = nome; this.preco = preco; } private double calcularImposto() { return preco * 0.1; } } // No seu método main: // Produto p = new Produto(101, "Notebook Gamer", 8500.0); // AnalisadorDeClasse.inspecionar(p);
- 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.
Objetivo: Crie uma classe
Configuracao
com um atributoprivate String urlConexao = "localhost:5432";
. Em outra classe, crie um métodomain
que, sem usar getters ou setters, utilize reflection para alterar o valor deste atributo privado para"db.producao.com:5432"
. Ao final, imprima o valor para confirmar a alteração.Dicas:
- Crie uma instância de
Configuracao
. - Obtenha o
Field
(campo) correspondente aurlConexao
usandogetDeclaredField("urlConexao")
. - Torne o campo acessível com
field.setAccessible(true)
. - Altere seu valor usando
field.set(objetoInstanciado, "novoValor")
. - Para verificar, use
field.get(objetoInstanciado)
para ler o novo valor e imprimi-lo.
- Crie uma instância de
- 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.
Objetivo: Desenvolver um pequeno executor de testes que executa métodos marcados com uma anotação personalizada.
Passos:
- Crie uma anotação:
import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.annotation.ElementType; @Retention(RetentionPolicy.RUNTIME) // Essencial para que a anotação esteja disponível via reflection @Target(ElementType.METHOD) // A anotação só pode ser aplicada a métodos public @interface Teste { }
- Crie uma classe com métodos de "teste":
public class MinhaClasseDeTeste { @Teste public void testeSoma() { System.out.println("Executando testeSoma: SUCESSO"); } public void metodoComum() { System.out.println("Este não é um teste."); } @Teste public void testeLogin() { System.out.println("Executando testeLogin: SUCESSO"); } }
- Crie a classe
ExecutorDeTestes
:- Ela deve ter um método
public static void executarTestes(Object obj)
. - Dentro deste método, use reflection para obter todos os métodos da classe do objeto recebido.
- Itere sobre os métodos e verifique, para cada um, se ele possui a anotação
@Teste
usandomethod.isAnnotationPresent(Teste.class)
. - Se um método tiver a anotação, invoque-o dinamicamente usando
method.invoke(obj)
.
- Ela deve ter um método
- Crie uma anotação:
Bloco 9: VcRiquinho e Lanchonete Quase Três Lanches
- Os exercícios VcRiquinho e Lanchonete Quase Três Lanches estão disponíveis no Moodle. Leia o enunciado e elabore as tarefas pedidas.