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:
- Tipos primitivos, operadores (lógicos, relacionais, ternários).
- Estruturas de decisão (
if-else
,switch
), repetição (for
,while
,do-while
). - Boas práticas: legibilidade e eficiência.
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:
+
,-
,*
,/
,%
- Lógicos:
&&
(E),||
(OU),!
(NÃO) - Relacionais:
==
,!=
,>
,>=
,<
,<=
- Unários:
++
(incremento),--
(decremento) - Ternário:
condicao ? valorSeVerdadeiro : valorSeFalso
Exemplo de operador ternário:
int x = (5 > 3) ? 10 : 0; // x será 10
1.3 Estruturas de Controle
- if/else:
if (idade >= 18) { System.out.println("Maior de idade"); } else { System.out.println("Menor de idade"); }
- switch:
switch (diaDaSemana) { case 1: System.out.println("Segunda"); break; case 2: System.out.println("Terça"); break; // ... default: System.out.println("Dia inválido"); break; }
- for (tradicional):
for (int i = 0; i < 10; i++) { System.out.println(i); }
- while / do-while:
int i = 0; while (i < 10) { System.out.println(i); i++; }
2. OOP Básico (Classes, Objetos, Encapsulamento)
Conteúdo:
- Classes vs. objetos: definição e instanciação.
- Modificadores de acesso (
private
,public
,protected
). - Encapsulamento: uso de getters/setters.
2.1 Classes e Objetos
- Classe: Molde que define atributos (dados) e métodos (comportamentos).
- Objeto: Instância de uma classe em tempo de execução.
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:
public
: acessível de qualquer lugar.private
: acessível apenas dentro da própria classe.protected
: acessível na classe, no mesmo pacote ou em subclasses.- (default – sem palavra-chave): acessível no mesmo pacote.
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
}
- Promove o reuso de código.
- É importante verificar se a relação “é-um” (IS-A) realmente faz sentido no modelo, caso contrário não se deve usar herança.
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
- Herança cria uma relação rígida "é-um" entre classes, tornando o código frágil a mudanças na superclasse.
- Composição estabelece uma relação "tem-um", permitindo substituir componentes sem afetar a classe principal.
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:
- Existe uma relação clara "é-um" (ex:
Gato extends Animal
). - Você precisa de polimorfismo verdadeiro.
- A superclasse é estável e não mudará frequentemente.
A composição oferece:
- Menos acoplamento: Mudanças em componentes não afetam a classe principal.
- Maior flexibilidade: Comportamentos podem ser trocados em tempo de execução.
- Melhor teste: Componentes podem ser mockados facilmente.
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:
- Sobrecarga (Overload): mesmo nome de método, mas parâmetros diferentes.
public int soma(int a, int b) { return a + b; } public double soma(double a, double b) { return a + b; }
- Sobreposição (Override): redefinir um método herdado de uma superclasse.
@Override public void acelerar() { System.out.println("O carro está acelerando"); }
2.6. Interfaces e Classes Abstratas
- Interfaces: definem contratos de métodos (sem implementação),
obrigando as
classes que as implementam a fornecer a lógica.
public interface Conectavel { void connect(); void disconnect(); }
- Classes Abstratas: não podem ser instanciadas diretamente e podem conter métodos
abstratos
(sem corpo) ou concretos (com implementação).
public abstract class Funcionario { public abstract void calculaSalario(); public void baterPonto() { System.out.println("Ponto registrado"); } }
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
- DRY (Don't Repeat Yourself): Evitar código duplicado.
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());
}
- Validação em Métodos: Garantir lógica consistente.
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
-
Validação de Parâmetros: Verifique null, intervalos numéricos, ou formatos (ex: e-mail).
-
Uso de Exceções Significativas: Lançe exceções específicas (IllegalArgumentException, InvalidStateException).
-
Uso de Mensagens Claras: Descreva o erro de forma que ajude na depuração (ex: "Valor não pode ser negativo").
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:
ArrayList
- Implementação baseada em array dinâmico. Rápido para acesso direto, mas mais lento para inserções e remoções no meio da lista.LinkedList
- Baseado em uma lista duplamente encadeada. Rápido para inserções e remoções, mas mais lento para acessos aleatórios.Vector
- Similar aoArrayList
, mas sincronizado para acesso concorrente. Geralmente substituído peloArrayList
devido às melhorias no gerenciamento de concorrência do Java.
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:
HashSet
- Baseado em tabela hash, oferece alta performance para buscas, mas não garante ordenação.LinkedHashSet
- Mantém a ordem de inserção dos elementos.TreeSet
- Baseado em umaRed-Black Tree
, garante ordenação natural dos elementos.
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:
HashMap
- Baseado em tabela hash, não garante ordem de inserção.LinkedHashMap
- Mantém a ordem de inserção dos pares.TreeMap
- Ordena os pares com base na chave.
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
Queue
- Representa uma fila (FIFO - First In, First Out). Implementações incluemLinkedList
ePriorityQueue
.Deque
- Representa uma fila de dois extremos (pode adicionar e remover elementos no início e no fim). Implementações incluemArrayDeque
eLinkedList
.
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
- Segurança de tipos: evita erros de tempo de execução ao garantir que apenas elementos do tipo correto sejam adicionados.
- Reutilização de código: classes e métodos podem ser escritos para trabalhar com diferentes tipos sem precisar de conversões.
- Legibilidade e manutenção: código mais claro e menos propenso a erros.
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:
- Frameworks de injeção de dependências (Spring, CDI) - para criar instâncias de classes dinamicamente.
- ORMs (Hibernate, JPA) - para mapear entidades a tabelas do banco de dados.
- Serialização e desserialização - conversão de objetos em formatos como JSON e XML.
- Criação de testes automatizados - para acessar métodos privados e validar comportamentos sem modificar diretamente o código-fonte.
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)
- São aquelas que obrigam o programador a tratá-las em tempo de compilação.
- Exemplo:
IOException
,SQLException
. - Devem ser tratadas explicitamente usando
try-catch
ou propagadas comthrows
.
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)
- São aquelas que ocorrem em tempo de execução e não precisam ser obrigatoriamente tratadas.
- Exemplo:
NullPointerException
,ArrayIndexOutOfBoundsException
.
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
- Evite capturar exceções genéricas como
Exception
, pois isso pode ocultar erros específicos. - O tratamento adequado de exceções melhora a robustez do código e evita falhas inesperadas durante a execução do programa.
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
- Mostra atores (usuários ou sistemas externos) e casos de uso (funcionalidades ou objetivos).
- Relações: include (um caso de uso inclui outro), extend (um caso de uso estende outro).
4.2 Diagrama de Classes
- Mostra classes, seus atributos e métodos, além de
relacionamentos:
- Associação: relação simples (um objeto “conhece” o outro).
- Agregação: relação “tem-um” mas com vida independente.
- Composição: relação “parte-todo” com ciclo de vida dependente (ex.: carro e motor).
- Generalização (herança): seta com linha contínua e triângulo apontando para a superclasse.
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!