Java (UC00619)
- Introdução
- 1. Java em contexto
- 2. Pensamento computacional e algoritmos
- 3. Planeamento e metodologias de desenvolvimento
- 4. Ambiente de desenvolvimento
- 5. Projeto Maven
- 6. Sintaxe básica
- 7. Programação orientada por objetos
- O paradigma
- Classe, objeto, atributo, método, mensagem
- Encapsulamento
- Construtores múltiplos (sobrecarga)
- Membros estáticos
- Método toString, equals e hashCode
- Relações entre classes
- Relações entre objetos
- Herança
- Polimorfismo
- Classes abstratas
- Interfaces
- Exemplo resolvido · Polimorfismo com figuras
- Exemplo resolvido · Mini-biblioteca com três classes
- Records (Java 16 ou superior)
- Enumerações
- Classes genéricas
- 8. Strings
- 9. Coleções
- 10. Exceções
- 11. Ficheiros e I/O
- 12. JDBC e bases de dados
- 13. Testes e depuração com JUnit 5
- 14. Boas práticas, normas e documentação
- 15. Próximos passos
- Erros comuns
- Exercícios resolvidos
- Síntese
- Glossário
- Recursos
Há um pacote premium desta unidade: um PowerPoint editável de 100 ou mais diapositivos, um manual em PDF e dois testes com correção. Esta sebenta continua gratuita.
Introdução
Java é uma das linguagens mais usadas no mundo. Criada em 1995 pela Sun Microsystems (hoje Oracle), domina bancos, sistemas empresariais, Android, big data (Hadoop, Spark) e milhares de aplicações que provavelmente usas todos os dias. Aprender Java significa entrar num ecossistema vasto, maduro e bem remunerado.
Esta sebenta cobre Java moderno (JDK 17 e 21, ambas versões de suporte prolongado) com programação orientada por objetos completa, coleções, exceções, ficheiros, JDBC para bases de dados, testes e princípios de boa engenharia. Segue o percurso de um programador a sério: primeiro pensa-se no problema (algoritmos e planeamento), depois prepara-se o ambiente, depois escreve-se, testa-se e corrige-se.
No fim da unidade deves ser capaz de:
- Planear as etapas de desenvolvimento de uma aplicação, do requisito ao código entregue.
- Estruturar programas em classes, interfaces e pacotes, num projeto Maven.
- Testar e depurar programas, escrevendo testes automáticos e corrigindo os erros que eles revelam.
- Interpretar a documentação técnica (API, manuais, tutoriais) e desenhar a interface com o utilizador antes de a programar.
- Documentar o que fizeste num guião técnico ou manual de utilizador, respeitando normas e convenções.
Cada capítulo alterna blocos curtos, que ilustram uma instrução, com exemplos resolvidos completos, que têm uma classe com main e podes copiar para o teu computador e correr. Escrever o código à mão, em vez de o colar, é a diferença entre reconhecer Java e saber Java.
1. Java em contexto
Características-chave
- Compilado para bytecode: corre na JVM (Java Virtual Machine), funciona em qualquer sistema operativo sem recompilar.
- Tipado estaticamente: os tipos são verificados em compilação, o que evita muitos erros em execução.
- Orientado por objetos: quase tudo é classe (exceto os oito tipos primitivos).
- Com recolha automática de memória (garbage collector): não libertas memória à mão.
- Maduro: cerca de 30 anos de evolução e um ecossistema gigantesco.
Onde se usa Java
- Backend empresarial: Spring Boot, Jakarta EE.
- Android: Java e Kotlin, interoperáveis.
- Big Data: Hadoop, Spark, Kafka.
- Sistemas financeiros: bancos, seguradoras, trading.
- IDEs: IntelliJ IDEA, Eclipse e Android Studio são escritos em Java ou Kotlin.
- Ferramentas de build: Maven e Gradle correm na JVM.
Quando NÃO usar Java
- Scripting rápido: Python.
- Interfaces no navegador: JavaScript.
- Sistemas de baixo nível: C ou Rust.
- Protótipos descartáveis: Python ou Node.
Mas para aplicações grandes, com equipas grandes, que vão durar mais de 10 anos, Java continua a ser uma escolha muito sólida.
Do código-fonte à execução
Escreves ficheiros .java (código-fonte). O compilador javac traduz-os para ficheiros .class com bytecode, uma linguagem intermédia que não é de nenhum processador em concreto. A JVM lê o bytecode e executa-o, traduzindo-o para instruções do processador onde está a correr. É por isto que o mesmo .class corre em Windows, Linux e macOS.
| Etapa | Ferramenta | Entrada | Saída |
|---|---|---|---|
| Escrever | Editor ou IDE | Ideias, algoritmo | Ficheiro.java |
| Compilar | javac |
Ficheiro.java |
Ficheiro.class |
| Executar | java (JVM) |
Ficheiro.class |
Resultado no ecrã |
Se o compilador encontra um erro (um ; em falta, um tipo errado), não gera .class. Estes são erros de compilação. Os erros que só aparecem quando o programa já está a correr (dividir por zero, aceder a um índice inexistente) são erros de execução. Os que não rebentam nada mas dão um resultado errado são erros lógicos, os mais difíceis de apanhar.
JDK, JRE e JVM
- JVM: a máquina virtual que executa o bytecode.
- JRE: JVM mais as bibliotecas standard, o necessário para correr aplicações.
- JDK: JRE mais as ferramentas de desenvolvimento (
javac,jar,javadoc).
Quem desenvolve instala o JDK. Quem só usa uma aplicação precisa apenas do ambiente de execução.
Memória: stack e heap
A JVM organiza a memória em duas zonas principais:
- Stack (pilha): guarda as variáveis locais e as chamadas de métodos. Cada chamada cria uma "moldura" que desaparece quando o método termina.
- Heap (monte): guarda os objetos. Uma variável de tipo objeto na stack guarda apenas uma referência (uma seta) para o objeto no heap.
Quando um objeto deixa de ter referências a apontar para ele, o garbage collector pode libertar a memória. Não sabes quando acontece, e não precisas de saber.
int idade = 30; // valor guardado diretamente na stack
String nome = "Ana"; // referência na stack, objeto String no heap
String outro = nome; // duas referências para o MESMO objeto
2. Pensamento computacional e algoritmos
Antes de escrever código, é preciso saber o que se quer resolver. Programar é, em grande parte, pensar. O pensamento computacional é a forma de resolver problemas de modo que um computador (ou uma pessoa) consiga executar a solução.
Os quatro pilares
| Pilar | Pergunta | Exemplo: sistema de notas de uma turma |
|---|---|---|
| Decomposição | Como parto o problema em partes pequenas? | Ler notas, calcular média, decidir aprovação, mostrar relatório |
| Reconhecimento de padrões | Já resolvi algo parecido? | Calcular a média de uma turma é igual a calcular a média de qualquer lista de números |
| Abstração | O que é essencial e o que posso ignorar? | Interessa o número da nota, não a cor da caneta do aluno |
| Algoritmos | Quais os passos, por ordem, que resolvem o problema? | Somar todas as notas e dividir pela quantidade |
O que é um algoritmo
Um algoritmo é uma sequência finita e ordenada de passos que resolve um problema. Tem de ser:
- Não ambíguo: cada passo diz exatamente o que fazer.
- Finito: termina sempre.
- Com entradas e saídas: recebe dados e produz um resultado.
- Eficaz: cada passo é executável.
Um algoritmo pode ser escrito em linguagem natural, em pseudocódigo ou desenhado como fluxograma. Só depois é traduzido para Java.
As três estruturas de qualquer algoritmo
- Sequência: os passos executam-se por ordem.
- Seleção: um passo depende de uma condição (
se ... então ... senão). - Repetição: um conjunto de passos repete-se enquanto uma condição for verdadeira.
Tudo o que vais escrever em Java combina estas três ideias.
Exemplo resolvido · Maior de três números
Problema: dados três números inteiros, indicar o maior.
Pseudocódigo:
INÍCIO
LER a, b, c
maior <- a
SE b > maior ENTÃO maior <- b
SE c > maior ENTÃO maior <- c
ESCREVER maior
FIM
Teste de mesa (executar o algoritmo à mão com a = 4, b = 9, c = 7):
| Passo | a | b | c | maior | Observação |
|---|---|---|---|---|---|
| Ler | 4 | 9 | 7 | (nenhuma) | Entradas lidas |
| maior <- a | 4 | 9 | 7 | 4 | Assume-se que a é o maior |
| b > maior? (9 > 4) | 4 | 9 | 7 | 9 | Verdadeiro, maior passa a 9 |
| c > maior? (7 > 9) | 4 | 9 | 7 | 9 | Falso, maior mantém-se |
| Escrever | 4 | 9 | 7 | 9 | Resultado: 9 |
Tradução para Java:
public class MaiorDeTres {
public static void main(String[] args) {
int a = 4, b = 9, c = 7;
int maior = a;
if (b > maior) {
maior = b;
}
if (c > maior) {
maior = c;
}
System.out.println("Maior: " + maior); // Maior: 9
}
}
Exemplo resolvido · Número primo
Problema: indicar se um número inteiro n >= 2 é primo.
Ideia: um número é primo se nenhum inteiro entre 2 e a sua raiz quadrada o divide exatamente. Basta testar até à raiz porque, se n tem um divisor maior do que a raiz, tem também um menor.
INÍCIO
LER n
primo <- verdadeiro
PARA d DE 2 ATÉ raiz(n)
SE n mod d = 0 ENTÃO primo <- falso
FIM PARA
ESCREVER primo
FIM
public class Primos {
static boolean ehPrimo(int n) {
if (n < 2) {
return false;
}
for (int d = 2; d * d <= n; d++) {
if (n % d == 0) {
return false;
}
}
return true;
}
public static void main(String[] args) {
int[] testes = {2, 9, 13, 25, 29};
for (int t : testes) {
System.out.println(t + " é primo? " + ehPrimo(t));
}
}
}
Saída: 2 é primo? true, 9 é primo? false, 13 é primo? true, 25 é primo? false, 29 é primo? true. A condição d * d <= n evita calcular a raiz quadrada.
Complexidade, em duas palavras
Dois algoritmos corretos podem ter custos muito diferentes. Procurar um valor numa lista não ordenada obriga, no pior caso, a olhar para todos os elementos (cresce linearmente com o tamanho). Procurar num conjunto organizado por chave (HashMap) é, em média, quase instantâneo. Mais à frente vais escolher a estrutura de dados certa com este critério.
3. Planeamento e metodologias de desenvolvimento
Porque se planeia
Escrever código sem planear dá programas que funcionam na primeira demonstração e se tornam impossíveis de alterar na terceira. Planear as etapas reduz o retrabalho e permite estimar prazos.
Fases do desenvolvimento de software
| Fase | Pergunta central | Produto |
|---|---|---|
| Levantamento de requisitos | O que deve o sistema fazer? | Lista de requisitos, histórias de utilizador |
| Análise e desenho | Como vai estar organizado? | Diagrama de classes, esquema da base de dados |
| Implementação | Como se escreve? | Código-fonte versionado |
| Testes | Faz o que foi pedido? | Relatório de testes, defeitos corrigidos |
| Entrega e documentação | Como se instala e usa? | Pacote executável, guião técnico, manual de utilizador |
| Manutenção | Como evolui e se corrige? | Novas versões |
Metodologias
Cascata (waterfall): as fases fazem-se uma a uma, sem voltar atrás. Funciona quando os requisitos são estáveis e conhecidos de antemão (um contrato público com especificações fechadas, por exemplo). Falha quando o cliente só percebe o que quer ao ver o produto.
Metodologias ágeis (Scrum, Kanban): o trabalho divide-se em ciclos curtos (em Scrum, os sprints, tipicamente de uma a quatro semanas). No fim de cada ciclo há uma versão utilizável e o cliente dá feedback. Os requisitos mudam sem drama.
| Aspeto | Cascata | Ágil |
|---|---|---|
| Entregas | Uma, no fim | Várias, incrementais |
| Mudança de requisitos | Cara | Esperada |
| Documentação | Extensa, inicial | Suficiente, contínua |
| Feedback do cliente | No fim | Em cada ciclo |
| Risco | Descoberto tarde | Descoberto cedo |
Nesta unidade vais trabalhar de forma ágil e simplificada: pequenas entregas, testes à medida que avanças, versões guardadas em Git.
Requisitos e histórias de utilizador
Um requisito funcional descreve o que o sistema faz; um requisito não funcional descreve como (desempenho, segurança, usabilidade). Uma história de utilizador escreve-se numa frase: Como [papel], quero [funcionalidade] para [benefício].
Exemplo resolvido · Planear uma aplicação de biblioteca
Enunciado: a biblioteca da escola quer registar livros e empréstimos.
Passo 1, histórias de utilizador
- Como bibliotecário, quero registar um livro novo para o ter no catálogo.
- Como bibliotecário, quero emprestar um livro a um leitor para saber quem o tem.
- Como bibliotecário, quero listar os livros em atraso para os reclamar.
Passo 2, identificar nomes e verbos. Os nomes sugerem classes (Livro, Leitor, Emprestimo). Os verbos sugerem métodos (registar, emprestar, devolver).
Passo 3, decompor em etapas e estimar
| Etapa | Tarefa | Horas estimadas |
|---|---|---|
| 1 | Modelo de dados (classes Livro, Leitor, Emprestimo) |
2 |
| 2 | Regras de negócio (um livro só pode estar emprestado a um leitor) | 2 |
| 3 | Persistência em ficheiro ou base de dados | 3 |
| 4 | Interface de consola | 2 |
| 5 | Testes automáticos | 2 |
| 6 | Documentação e entrega | 1 |
Passo 4, ordenar por dependência e risco. O modelo vem antes das regras; a persistência depende do modelo; os testes acompanham cada etapa e não ficam para o fim.
Diagrama de classes (UML)
O diagrama de classes mostra as classes, os seus atributos, métodos e as relações entre elas. Cada caixa tem três compartimentos: nome, atributos, métodos. Em texto:
+-----------------+ +----------------------+ +-----------------+
| Leitor | | Emprestimo | | Livro |
+-----------------+ +----------------------+ +-----------------+
| - nome: String | 1 * | - data: LocalDate | * 1 | - titulo: String|
| - numero: int |-------| - devolvido: boolean |-------| - isbn: String |
+-----------------+ +----------------------+ +-----------------+
| + pedir(Livro) | | + devolver() | | + getTitulo() |
+-----------------+ +----------------------+ +-----------------+
- indica private, + indica public. Os números nas pontas (1 e *) são a multiplicidade: um leitor pode ter muitos empréstimos; cada empréstimo é de um só leitor.
Desenhar a interface
A interface com o utilizador é tudo o que a pessoa vê e escreve: menus, perguntas, resultados e mensagens de erro. Desenha-se antes de programar, num esboço em texto ou no papel, porque é mais barato corrigir um esboço do que um programa. Três regras ajudam: uma ação por opção de menu; feedback claro depois de cada ação; mensagens de erro que dizem o que correu mal e o que fazer a seguir.
Exemplo resolvido · Desenhar a interface da biblioteca
Passo 1, o esboço do menu principal
=== Biblioteca da Escola ===
1. Registar livro
2. Emprestar livro
3. Devolver livro
4. Listar livros em atraso
0. Sair
Opção: _
Passo 2, o fluxo entre ecrãs. Cada opção leva a um pequeno ecrã de perguntas e regressa sempre ao menu principal:
Opção 2 (Emprestar)
1. Pede o ISBN
ISBN desconhecido -> mensagem de erro e volta a pedir
2. Pede o número do leitor
leitor desconhecido -> mensagem de erro e volta a pedir
3. Livro já emprestado -> mensagem de erro e volta ao menu principal
4. Tudo certo -> confirma o empréstimo e volta ao menu principal
Passo 3, mensagens. Escrevem-se à partida, para serem consistentes:
| Situação | Mensagem |
|---|---|
| Empréstimo feito | Livro "Os Maias" emprestado a Ana. |
| ISBN desconhecido | Não existe nenhum livro com esse ISBN. Tenta outra vez. |
| Leitor desconhecido | Não existe nenhum leitor com esse número. Tenta outra vez. |
| Livro já emprestado | Esse livro já está emprestado. Escolhe outro. |
| Opção inválida | Opção inválida. Escolhe um número de 0 a 4. |
Passo 4, validação. A entrada do utilizador nunca é de confiança: o número do menu lê-se como texto e converte-se com tratamento de erro (capítulo 10), repetindo a pergunta até ser válida.
Normas, regulamentos e licenças
Programar para o mundo real obriga a cumprir regras:
- Convenções de código: o guia de estilo da equipa ou os nomes e formatação habituais de Java (ver capítulo 14).
- RGPD (Regulamento Geral sobre a Proteção de Dados): se a aplicação guarda dados pessoais (nomes, moradas, NIF), é preciso ter fundamento legal, recolher o mínimo necessário e proteger os dados. Uma palavra-passe nunca se guarda em texto simples.
- Licenças de software: cada biblioteca que usas tem uma licença (por exemplo Apache 2.0, MIT, GPL) que define o que podes fazer com ela. Convém conhecê-las antes de distribuir um produto.
- Direitos de autor: não copies código que não podes reutilizar e cita a fonte quando o adaptas.
- Acessibilidade e usabilidade: mesmo numa aplicação de consola, mensagens claras e consistentes.
4. Ambiente de desenvolvimento
Instalar o JDK
JDK = Java Development Kit (compilador + ferramentas). JRE = Java Runtime Environment (só corre, não desenvolve).
Recomendado:
- Adoptium Temurin 21 (LTS), gratuito e de código aberto.
- Amazon Corretto 21, alternativa empresarial gratuita.
Verifica:
java --version
# openjdk 21.0.x ...
javac --version
# javac 21.0.x
Se o terminal responder "comando não encontrado", o JDK não está no PATH (a lista de pastas onde o sistema procura programas). Define também a variável JAVA_HOME com a pasta de instalação do JDK; o Maven usa-a para saber qual Java usar.
Editor de texto ou IDE
Um editor de texto (Bloco de Notas, Notepad++, VS Code sem extensões) edita ficheiros mas não compreende Java. Um IDE (ambiente de desenvolvimento integrado) junta editor, compilador, depurador, gestor de dependências e controlo de versões.
| Funcionalidade | Editor simples | IDE |
|---|---|---|
| Realce de sintaxe | Sim | Sim |
| Sugestões e autocompletar | Não | Sim, conhece as tuas classes |
| Deteção de erros enquanto escreves | Não | Sim, sublinha a vermelho |
| Compilar e executar | Terminal à parte | Botão Run |
| Depurador (breakpoints) | Não | Sim |
| Refatoração (renomear em todo o projeto) | Não | Sim |
| Procura e substituição | Básica | Por símbolo, não por texto |
IDEs usados:
- IntelliJ IDEA, padrão de facto. A JetBrains tem reorganizado as suas edições; confirma no site oficial as condições de utilização gratuita em vigor.
- Eclipse IDE for Java Developers.
- VS Code com a extensão "Extension Pack for Java".
IntelliJ oferece a melhor experiência: importação automática, refatoração poderosa, navegação inteligente. Mesmo assim, convém saber usar um editor simples e o terminal, porque em servidores e em contexto de teste vais encontrar apenas isso.
Funcionalidades de editor que poupam horas: procurar e substituir (com expressões regulares), edição com vários cursores, atalhos para comentar linhas, indentação automática, ir para a definição, formatar documento.
Hello World sem IDE
// HelloWorld.java
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Olá mundo!");
}
}
Compila e corre:
javac HelloWorld.java # gera HelloWorld.class
java HelloWorld # corre (sem .class)
O nome do ficheiro tem de coincidir com o da classe pública (HelloWorld.java para public class HelloWorld), incluindo maiúsculas.
Execução de ficheiro único (Java 11 ou superior)
java HelloWorld.java # compila e corre diretamente
Útil para scripts rápidos.
Anatomia do main
public: o método é visível a todos, incluindo à JVM que o vai chamar.static: pertence à classe, por isso a JVM chama-o sem criar um objeto.void: não devolve valor.String[] args: os argumentos da linha de comandos (java App um doisdáargs[0]igual a"um").
Depurar com o IDE
O depurador deixa executar o programa passo a passo:
- Clica na margem esquerda de uma linha para pôr um breakpoint (ponto de paragem).
- Executa em modo Debug: o programa pára nessa linha.
- Step Over avança uma linha; Step Into entra no método chamado; Step Out sai do método atual.
- Observa as variáveis e as watches (expressões que escolhes acompanhar).
- Descobre a linha em que o valor passa de certo a errado. Aí está o defeito.
Depurar é mais fiável do que espalhar System.out.println por todo o lado, e quase sempre mais rápido.
Exemplo resolvido · Ler um erro do compilador
public class Teste {
public static void main(String[] args) {
int x = "dez";
System.out.println(x)
}
}
O compilador devolve algo como:
Teste.java:4: error: ';' expected
System.out.println(x)
^
1 error
A mensagem traz o ficheiro, a linha e a descrição, e o ^ aponta o sítio. Na linha 4 falta o ;. O código tem ainda outro erro, na linha 3 (atribuímos texto a um int), mas o compilador não o mostra: um erro de sintaxe é detetado logo na leitura do ficheiro e a compilação pára aí. Corrigido o ponto e vírgula, o compilador passa à análise de tipos e só então mostra:
Teste.java:3: error: incompatible types: String cannot be converted to int
int x = "dez";
^
1 error
Por isso se compila de novo depois de cada correção, começando pelo primeiro erro que aparece.
Ler documentação técnica
Quase tudo o que precisas de saber sobre uma classe de Java está na documentação oficial, a API (em Java 21, docs.oracle.com/en/java/javase/21/docs/api). Uma página de classe segue sempre a mesma ordem: o pacote e a hierarquia no topo, uma descrição geral, depois a lista de construtores (Constructor Summary) e de métodos (Method Summary), cada um com a assinatura, a descrição, os parâmetros, o valor devolvido e as exceções que pode lançar. Os tutoriais (por exemplo em dev.java) explicam o porquê e dão exemplos; os manuais das ferramentas (mvn, javac) listam as opções; um README diz como instalar e correr um projeto; um pom.xml alheio mostra de que bibliotecas o projeto depende.
Exemplo resolvido · Aprender removeIf na documentação
Objetivo: remover de uma lista todas as notas negativas (inferiores a 10) sem escrever um ciclo.
- Procura na API a classe
java.util.ArrayList. Em Constructor Summary há três construtores:ArrayList(),ArrayList(int initialCapacity)eArrayList(Collection<? extends E> c). O último cria uma lista com cópia dos elementos de outra coleção. - Em Method Summary procura
removeIf. A assinatura éboolean removeIf(Predicate<? super E> filter). Lê-se: recebe um critério (umPredicate, uma função que devolvetrueoufalsepara cada elemento), remove os elementos para os quais o critério dátruee devolvetruese removeu algum. - Aplica:
import java.util.ArrayList;
import java.util.List;
List<Integer> notas = new ArrayList<>(List.of(8, 15, 6, 12));
boolean mudou = notas.removeIf(n -> n < 10);
System.out.println(notas + " " + mudou);
Resultado: [15, 12] true. Repara que a lista foi criada com new ArrayList<>(List.of(...)), o terceiro construtor: List.of devolve uma lista imutável e removeIf falharia sobre ela com UnsupportedOperationException.
5. Projeto Maven
Para projetos reais, usa Maven (ou Gradle). O Maven trata de três coisas: estrutura do projeto, dependências (bibliotecas de que dependes) e ciclo de build (compilar, testar, empacotar).
pom.xml mínimo
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.exemplo</groupId>
<artifactId>minha-app</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Substitui cada marcador pela versão estável mais recente que
encontrares em central.sonatype.com. Qualquer dependência que
use uma destas propriedades sem o marcador substituído faz o
Maven recusar o pom.xml. -->
<junit.version>VERSAO_ESCOLHIDA</junit.version>
<mysql.version>VERSAO_ESCOLHIDA</mysql.version>
<sqlite.version>VERSAO_ESCOLHIDA</sqlite.version>
</properties>
<dependencies>
<!-- dependências aqui -->
</dependencies>
</project>
As três coordenadas groupId, artifactId e version identificam de forma única o teu projeto (e qualquer biblioteca) no repositório Maven.
Estrutura standard
minha-app/
├── pom.xml
└── src/
├── main/
│ ├── java/
│ │ └── com/exemplo/App.java
│ └── resources/
└── test/
├── java/
│ └── com/exemplo/AppTest.java
└── resources/
O pacote com.exemplo corresponde às pastas com/exemplo. Seguir a estrutura permite que qualquer programador (e qualquer ferramenta) perceba o projeto sem explicações.
Comandos essenciais
mvn compile # compila
mvn test # corre testes
mvn package # cria .jar em target/
mvn clean install # limpa, compila, testa, instala localmente
mvn dependency:tree # mostra dependências
Ciclo de vida
validate, compile, test, package, verify, install, deploy. Pedir uma fase executa todas as anteriores: mvn package compila e corre os testes antes de empacotar. Se um teste falhar, o .jar não é criado, e é isto que protege a equipa de entregar código partido.
Dependências
Para usar uma biblioteca, acrescenta-a ao pom.xml; o Maven descarrega-a (e as dependências dela) automaticamente.
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
A propriedade junit.version define-se no bloco <properties> do pom.xml, com a versão estável mais recente de org.junit.jupiter:junit-jupiter que encontrares no repositório Maven Central (central.sonatype.com). Este código usa a API org.junit.jupiter.api, a do JUnit 5; escolhe uma versão que a mantenha. Não copies números de versão de tutoriais antigos. Sem essa linha o Maven nem chega a compilar: recusa o ficheiro com 'dependencies.dependency.version' ... must be a valid version but is '${junit.version}'. O mesmo vale para ${mysql.version} e ${sqlite.version} nos capítulos 12 e 13 e nas fichas.
O scope test diz que a biblioteca só é necessária para testes e não entra no .jar final.
6. Sintaxe básica
Tipos primitivos
Java tem 8 tipos primitivos (com tamanho fixo):
| Tipo | Tamanho | Intervalo |
|---|---|---|
byte |
8 bits | -128 a 127 |
short |
16 bits | -32 768 a 32 767 |
int |
32 bits | cerca de -2,1 mil milhões a 2,1 mil milhões |
long |
64 bits | muito grande |
float |
32 bits | decimal, cerca de 7 dígitos de precisão |
double |
64 bits | decimal, cerca de 15 dígitos de precisão |
boolean |
(depende da JVM) | true ou false |
char |
16 bits | um carácter Unicode |
int idade = 25;
long populacao = 8_000_000_000L; // L para long
double preco = 9.99;
float taxa = 0.21f; // f para float
boolean ativo = true;
char letra = 'A';
String não é primitivo, é objeto:
String nome = "Ana";
String saudacao = "Olá, " + nome;
int len = nome.length();
String maiusc = nome.toUpperCase();
Variáveis com var
Java 10 ou superior permite inferência de tipo:
var idade = 25; // int
var nome = "Ana"; // String
var lista = new ArrayList<Integer>(); // ArrayList<Integer>
Só funciona para variáveis locais (não em campos de classe nem em parâmetros). O tipo continua a ser estático: o compilador deduz-o, não o deixa mudar.
Constantes
Com final, o valor só se atribui uma vez:
final double IVA = 0.23;
// IVA = 0.20; // erro de compilação
Operadores
// Aritméticos
int soma = 10 + 5; // 15
int sub = 10 - 5; // 5
int mul = 10 * 5; // 50
int div = 10 / 3; // 3 (truncado!)
int mod = 10 % 3; // 1
double real = 10.0 / 3; // 3.333...
// Atribuição composta
int x = 10;
x += 5; // x = x + 5
x *= 2; // x = x * 2
x++; // x = x + 1
// Comparação
boolean eq = (a == b); // CUIDADO: == em objetos compara referências!
boolean neq = (a != b);
// Lógicos
boolean e = (a && b);
boolean ou = (a || b);
boolean nao = !a;
// Strings: usa .equals(), não ==
String s1 = "Ana";
String s2 = "Ana";
boolean igual = s1.equals(s2); // correto
boolean iguais = (s1 == s2); // aqui dá true por causa do pool de strings, mas não é fiável
Os operadores && e || fazem avaliação em curto-circuito: em a != 0 && 10 / a > 1, se a for 0 a segunda parte nem chega a ser calculada, o que evita a divisão por zero.
Conversão de tipos
Converter para um tipo maior é automático (widening). Para um tipo menor, é preciso um cast explícito e pode perder-se informação.
int i = 7;
double d = i; // automático: 7.0
double pi = 3.99;
int truncado = (int) pi; // 3 (não arredonda, corta)
long grande = 3_000_000_000L;
int perdido = (int) grande; // overflow: valor sem sentido
Para converter texto em número:
int n = Integer.parseInt("42");
double v = Double.parseDouble("3.14");
Se o texto não for um número, parseInt lança NumberFormatException.
Leitura do teclado
import java.util.Scanner;
Scanner sc = new Scanner(System.in);
System.out.print("Nome: ");
String nome = sc.nextLine();
System.out.print("Idade: ");
int idade = sc.nextInt();
System.out.println("Olá, " + nome + ", " + idade + " anos");
sc.close();
Estruturas de controlo
// If/else
if (idade >= 18) {
System.out.println("Adulto");
} else if (idade >= 13) {
System.out.println("Adolescente");
} else {
System.out.println("Criança");
}
// Switch tradicional
switch (dia) {
case 1: System.out.println("Segunda"); break;
case 2: System.out.println("Terça"); break;
default: System.out.println("Outro");
}
// Switch expression (Java 14 ou superior), moderno
String tipo = switch (dia) {
case 1, 2, 3, 4, 5 -> "Útil";
case 6, 7 -> "Fim de semana";
default -> "Inválido";
};
No switch tradicional, esquecer o break faz o código "cair" para o caso seguinte. A forma com -> não tem esse problema.
Ciclos
// For tradicional
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
// Foreach (enhanced for)
String[] nomes = {"Ana", "Bruno", "Carla"};
for (String nome : nomes) {
System.out.println(nome);
}
// While
int i = 0;
while (i < 5) {
System.out.println(i);
i++;
}
// Do-while (corre pelo menos 1 vez)
int n;
do {
n = sc.nextInt();
} while (n < 0);
// Break e continue
for (int j = 0; j < 100; j++) {
if (j == 50) break; // sai do ciclo
if (j % 2 == 0) continue; // salta a iteração
System.out.println(j);
}
Regra prática: for quando sabes quantas vezes repetes; while quando repetes até uma condição mudar; do-while quando o corpo tem de correr pelo menos uma vez (pedir um valor e validar).
Arrays
// Declaração + inicialização
int[] numeros = new int[5]; // [0, 0, 0, 0, 0]
int[] valores = {1, 2, 3, 4, 5};
// Acesso e modificação
numeros[0] = 10;
int primeiro = valores[0];
// Tamanho
int n = valores.length;
// Iterar
for (int i = 0; i < valores.length; i++) {
System.out.println(valores[i]);
}
// Multidimensional
int[][] matriz = {
{1, 2, 3},
{4, 5, 6}
};
int valor = matriz[1][2]; // 6
Os índices começam em 0 e o último é length - 1. O tamanho de um array é fixo; se precisas de crescer, usa uma ArrayList (capítulo 9). A classe java.util.Arrays traz utilitários: Arrays.toString(v), Arrays.sort(v), Arrays.fill(v, 0).
Métodos
Um método agrupa instruções sob um nome. Recebe parâmetros e pode devolver um valor.
public class Utilitarios {
static double areaCirculo(double raio) {
return Math.PI * raio * raio;
}
static int somar(int a, int b) {
return a + b;
}
static double somar(double a, double b) { // sobrecarga: mesmo nome, tipos diferentes
return a + b;
}
public static void main(String[] args) {
System.out.println(somar(2, 3)); // 5
System.out.println(somar(2.5, 3.5)); // 6.0
System.out.printf("%.2f%n", areaCirculo(2)); // 12,57 (ou 12.57, conforme a localização)
}
}
Em Java, os argumentos passam por valor. Para tipos primitivos, o método recebe uma cópia. Para objetos, recebe uma cópia da referência: pode alterar o objeto para onde ela aponta, mas não pode fazer a variável do chamador apontar para outro objeto.
Exemplo resolvido · Média e classificação
Problema: ler três notas (0 a 20), calcular a média e classificar: aprovado se for pelo menos 10, com distinção se for pelo menos 16.
import java.util.Scanner;
public class MediaNotas {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
double soma = 0;
for (int i = 1; i <= 3; i++) {
System.out.print("Nota " + i + ": ");
soma += sc.nextDouble();
}
double media = soma / 3;
String estado;
if (media >= 16) {
estado = "Aprovado com distinção";
} else if (media >= 10) {
estado = "Aprovado";
} else {
estado = "Reprovado";
}
System.out.printf("Média: %.1f -> %s%n", media, estado);
sc.close();
}
}
Com as notas 12, 15 e 18: soma 45, média 15,0, estado "Aprovado". O formato %.1f mostra uma casa decimal e %n muda de linha.
7. Programação orientada por objetos
O paradigma
Na programação orientada por objetos (POO ou OOP), o programa é um conjunto de objetos que guardam estado e colaboram trocando mensagens (chamadas de métodos). Em vez de pensar "que passos executo", pensas "que entidades existem e o que cada uma sabe e faz".
As características fundamentais:
| Característica | Ideia | Em Java |
|---|---|---|
| Encapsulamento | Esconder o estado, expor operações | Atributos private, métodos public |
| Partilha de comportamento | Reutilizar código comum | Herança, composição, interfaces |
| Evolução | Estender sem reescrever | Subclasses, novas implementações de interfaces |
| Polimorfismo | Tratar tipos diferentes pela mesma interface | Sobreposição de métodos, interfaces |
Classe, objeto, atributo, método, mensagem
Uma classe é um molde; um objeto é uma instância dessa classe. Os atributos são o estado; os métodos são o comportamento. Chamar p1.apresentar() é enviar a mensagem apresentar ao objeto p1.
public class Pessoa {
// Atributos (estado)
private String nome;
private int idade;
// Construtor
public Pessoa(String nome, int idade) {
this.nome = nome;
this.idade = idade;
}
// Métodos (comportamento)
public void apresentar() {
System.out.println("Olá, sou " + nome + " e tenho " + idade + " anos");
}
// Getters
public String getNome() {
return nome;
}
public int getIdade() {
return idade;
}
// Setters
public void setNome(String nome) {
this.nome = nome;
}
public void setIdade(int idade) {
if (idade < 0) {
throw new IllegalArgumentException("Idade negativa");
}
this.idade = idade;
}
}
Usar:
Pessoa p1 = new Pessoa("Ana", 25);
Pessoa p2 = new Pessoa("Bruno", 30);
p1.apresentar();
System.out.println(p2.getNome());
p1.setIdade(26);
new cria o objeto no heap e chama o construtor; a variável p1 guarda a referência. this é a referência do objeto atual, usada para distinguir o atributo do parâmetro com o mesmo nome.
Encapsulamento
Atributos privados, acesso controlado:
private: só dentro da classe.- (sem modificador): só dentro do mesmo pacote.
protected: o mesmo pacote e as subclasses.public: qualquer lado.
Vantagens:
- Validação nos setters (a idade nunca fica negativa).
- Mudar a implementação interna sem partir quem usa a classe.
- Pensar em interfaces de utilização, não em detalhes.
Construtores múltiplos (sobrecarga)
public class Pessoa {
private String nome;
private int idade;
public Pessoa() {
this("Anónimo", 0);
}
public Pessoa(String nome) {
this(nome, 0);
}
public Pessoa(String nome, int idade) {
this.nome = nome;
this.idade = idade;
}
}
// Uso
Pessoa p1 = new Pessoa();
Pessoa p2 = new Pessoa("Ana");
Pessoa p3 = new Pessoa("Bruno", 30);
this(...) chama outro construtor da mesma classe e deve ser a primeira instrução. Assim a lógica de inicialização fica num só sítio.
Membros estáticos
Um membro static pertence à classe, não a cada objeto. É partilhado por todos.
public class Contador {
private static int total = 0; // um só valor, partilhado
private final int numero; // um por objeto
public Contador() {
total++;
numero = total;
}
public int getNumero() { return numero; }
public static int getTotal() { return total; }
}
Depois de new Contador(); new Contador();, Contador.getTotal() devolve 2. Métodos static não podem usar this nem atributos de instância.
Método toString, equals e hashCode
Toda a classe herda de Object. Três métodos merecem ser sobrepostos nas classes de dados:
import java.util.Objects;
public class Livro {
private final String isbn;
private final String titulo;
public Livro(String isbn, String titulo) {
this.isbn = isbn;
this.titulo = titulo;
}
@Override
public String toString() {
return titulo + " (" + isbn + ")";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Livro)) return false;
Livro outro = (Livro) o;
return isbn.equals(outro.isbn);
}
@Override
public int hashCode() {
return Objects.hash(isbn);
}
}
Sem equals, dois objetos Livro com o mesmo ISBN seriam considerados diferentes. Quem sobrepõe equals tem de sobrepor hashCode de forma coerente; HashMap e HashSet dependem de ambos.
Relações entre classes
| Relação | Significado | Exemplo | Em código |
|---|---|---|---|
| Dependência | Uma classe usa outra de passagem | Relatorio usa Impressora num método |
Parâmetro ou variável local |
| Associação | Uma classe conhece outra | Leitor conhece Emprestimo |
Atributo que referencia o outro |
| Agregação | "tem um", partes podem existir sem o todo | Turma tem Alunos |
Atributo (lista) preenchido de fora |
| Composição | "é composto por", partes morrem com o todo | Casa tem Divisaos |
Atributo criado e gerido dentro da classe |
| Herança | "é um" | Empregado é uma Pessoa |
extends |
| Realização | Implementa um contrato | Carro implementa Veiculo |
implements |
Relações entre objetos
Em execução, os objetos relacionam-se através de referências. Os casos clássicos:
- Um para um: um
Alunotem umCartaoEstudante. - Um para muitos: um
Leitortem uma lista deEmprestimo. - Muitos para muitos: vários
Alunos inscrevem-se em váriasDisciplinas (em geral resolve-se com uma classe intermédia,Inscricao).
import java.util.ArrayList;
import java.util.List;
public class Leitor {
private final String nome;
private final List<Livro> emprestados = new ArrayList<>(); // associação: os livros existem independentemente do leitor
public Leitor(String nome) { this.nome = nome; }
public void pedir(Livro l) { emprestados.add(l); }
public int quantos() { return emprestados.size(); }
}
Herança
public class Empregado extends Pessoa {
private double salario;
private String cargo;
public Empregado(String nome, int idade, double salario, String cargo) {
super(nome, idade); // chama o construtor da Pessoa
this.salario = salario;
this.cargo = cargo;
}
@Override
public void apresentar() {
super.apresentar();
System.out.println("Cargo: " + cargo + ", salário: " + salario + "€");
}
public double getSalarioAnual() {
return salario * 14; // 14 meses em Portugal
}
}
super = referência à classe pai.
@Override = anotação que indica que sobrepões um método (o compilador valida).
A herança é a forma clássica de partilhar comportamento e permite a evolução: acrescentas uma subclasse sem tocar na classe existente. Mas é uma relação forte. Antes de herdar, pergunta: "um Empregado é uma Pessoa?" Se a resposta for "tem uma", usa composição.
Polimorfismo
Pessoa p = new Empregado("Ana", 25, 1000, "Dev");
p.apresentar(); // chama a versão de Empregado (ligação dinâmica)
Uma variável do tipo Pessoa pode referenciar qualquer subclasse. O método chamado é o da classe real do objeto, não o do tipo da variável. Isto permite escrever código que funciona com tipos que ainda nem existem.
Classes abstratas
public abstract class Figura {
public abstract double area(); // sem implementação
public void descrever() {
System.out.println("Área: " + area());
}
}
public class Circulo extends Figura {
private double raio;
public Circulo(double raio) {
this.raio = raio;
}
@Override
public double area() {
return Math.PI * raio * raio;
}
}
Não se pode fazer new Figura(): só serve como base. As subclasses concretas são obrigadas a implementar area().
Interfaces
public interface Veiculo {
void acelerar();
void travar();
// Método default (Java 8 ou superior): opcional sobrescrever
default void buzinar() {
System.out.println("Beep!");
}
// Constante
int VELOCIDADE_MAX = 120;
}
public class Carro implements Veiculo {
@Override
public void acelerar() { /* ... */ }
@Override
public void travar() { /* ... */ }
}
Uma classe implementa múltiplas interfaces mas herda de uma só classe. Uma interface é um contrato: define o que se pode fazer, sem dizer como.
| Classe abstrata | Interface | |
|---|---|---|
| Estado (atributos de instância) | Sim | Não (só constantes) |
| Herança múltipla | Não | Sim, uma classe implementa várias |
| Construtor | Sim | Não |
| Quando usar | Base comum com código partilhado | Capacidade ou contrato transversal a classes sem relação |
Interfaces com um único método abstrato são interfaces funcionais e podem ser escritas como lambdas, como vais ver nos Comparator e nos streams.
Exemplo resolvido · Polimorfismo com figuras
Problema: guardar figuras diferentes numa só lista e calcular a área total.
import java.util.List;
abstract class Figura {
abstract double area();
}
class Circulo extends Figura {
private final double raio;
Circulo(double raio) { this.raio = raio; }
@Override double area() { return Math.PI * raio * raio; }
}
class Retangulo extends Figura {
private final double largura, altura;
Retangulo(double largura, double altura) {
this.largura = largura;
this.altura = altura;
}
@Override double area() { return largura * altura; }
}
public class AreaTotal {
public static void main(String[] args) {
List<Figura> figuras = List.of(new Circulo(1), new Retangulo(2, 3), new Retangulo(1, 1));
double total = 0;
for (Figura f : figuras) {
total += f.area(); // cada objeto responde à sua maneira
}
System.out.printf("Área total: %.2f%n", total);
}
}
Passos: (1) a lista tem o tipo Figura, mas guarda objetos concretos; (2) em cada volta, f.area() executa a versão da classe real; (3) áreas: 3,14 + 6 + 1 = 10,14. Saída: Área total: 10,14 (ou 10.14, conforme a localização do sistema). Para acrescentar um Triangulo, basta escrever a classe: o ciclo não muda.
Exemplo resolvido · Mini-biblioteca com três classes
Problema: modelar livros, leitores e empréstimos (o cenário planeado no capítulo 3) e impedir que um livro seja emprestado duas vezes.
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
class Livro {
private final String titulo;
private boolean emprestado;
Livro(String titulo) { this.titulo = titulo; }
String getTitulo() { return titulo; }
boolean isEmprestado() { return emprestado; }
void setEmprestado(boolean emprestado) { this.emprestado = emprestado; }
}
class Emprestimo {
private final Livro livro;
private final LocalDate data;
Emprestimo(Livro livro, LocalDate data) {
this.livro = livro;
this.data = data;
}
Livro getLivro() { return livro; }
LocalDate getData() { return data; }
}
class Leitor {
private final String nome;
private final List<Emprestimo> emprestimos = new ArrayList<>();
Leitor(String nome) { this.nome = nome; }
String getNome() { return nome; }
int quantos() { return emprestimos.size(); }
void pedir(Livro livro, LocalDate data) {
if (livro.isEmprestado()) {
throw new IllegalStateException("Livro já emprestado: " + livro.getTitulo());
}
livro.setEmprestado(true);
emprestimos.add(new Emprestimo(livro, data));
}
}
public class Biblioteca {
public static void main(String[] args) {
Livro l = new Livro("Java Básico");
Leitor ana = new Leitor("Ana");
Leitor carla = new Leitor("Carla");
ana.pedir(l, LocalDate.of(2026, 10, 1));
try {
carla.pedir(l, LocalDate.of(2026, 10, 2));
} catch (IllegalStateException e) {
System.out.println(e.getMessage());
}
System.out.println(ana.getNome() + " tem " + ana.quantos() + " livro(s)");
}
}
Passos: (1) Leitor associa-se a Emprestimo (um para muitos) e Emprestimo associa-se a Livro; (2) a regra de negócio vive em pedir, não na interface; (3) o segundo pedido lança IllegalStateException, que o main apanha. Saída: Livro já emprestado: Java Básico e Ana tem 1 livro(s).
Records (Java 16 ou superior)
Para classes simples de dados (DTOs):
public record Ponto(double x, double y) { }
// Equivalente a uma classe imutável com:
// - construtor (x, y)
// - acessores x() e y()
// - equals, hashCode, toString automáticos
Ponto p = new Ponto(1, 2);
System.out.println(p.x()); // 1.0
System.out.println(p); // Ponto[x=1.0, y=2.0]
Enumerações
Um enum define um conjunto fechado de valores.
public enum Estado {
DISPONIVEL, EMPRESTADO, PERDIDO
}
Estado e = Estado.DISPONIVEL;
switch (e) {
case DISPONIVEL -> System.out.println("Pode ser emprestado");
case EMPRESTADO -> System.out.println("Aguarda devolução");
case PERDIDO -> System.out.println("Fora do catálogo");
}
É mais seguro do que usar números ou texto soltos ("1" significa o quê?), porque o compilador recusa valores fora do conjunto.
Classes genéricas
Os genéricos permitem escrever classes e métodos que funcionam com vários tipos, mantendo a verificação de tipos em compilação. É o <T> que viste em List<String>.
public class Caixa<T> {
private T conteudo;
public Caixa(T conteudo) { this.conteudo = conteudo; }
public T getConteudo() { return conteudo; }
public void setConteudo(T conteudo) { this.conteudo = conteudo; }
}
public class Teste {
public static void main(String[] args) {
Caixa<String> c1 = new Caixa<>("Olá");
Caixa<Integer> c2 = new Caixa<>(42);
String s = c1.getConteudo(); // sem cast
// Caixa<Integer> errada = new Caixa<>("texto"); // erro de compilação
System.out.println(s + " " + c2.getConteudo());
}
}
Um método genérico com limite de tipo (T extends Comparable<T>) só aceita tipos que sabem comparar-se:
static <T extends Comparable<T>> T maior(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
// maior(3, 7) devolve 7; maior("ana", "bruno") devolve "bruno"
Os genéricos só funcionam com tipos de referência: List<int> não existe; usa List<Integer> (a conversão int para Integer, chamada autoboxing, é automática).
8. Strings
As String são imutáveis: cada operação devolve uma nova String, a original nunca muda.
String s = "Hello World";
// Tamanho
int len = s.length(); // 11
// Substring
String sub = s.substring(0, 5); // "Hello"
// Maiúsculas e minúsculas
String upper = s.toUpperCase(); // "HELLO WORLD"
String lower = s.toLowerCase();
// Substituição
String r = s.replace("World", "Java");
// Split
String csv = "Ana,Bruno,Carla";
String[] partes = csv.split(",");
// Concatenação eficiente
StringBuilder sb = new StringBuilder();
sb.append("Olá ");
sb.append("mundo");
sb.append("!");
String resultado = sb.toString();
// Formatação
String formatted = String.format("%s tem %d anos", "Ana", 25);
// Comparação
boolean igual = s.equals("Hello World");
boolean igualIgnoreCase = s.equalsIgnoreCase("HELLO WORLD");
// Pesquisa
boolean contem = s.contains("World");
boolean comeca = s.startsWith("Hello");
boolean termina = s.endsWith("World");
int pos = s.indexOf("World"); // 6, ou -1 se não existir
String limpa = " texto ".trim(); // "texto"
Dentro de ciclos longos, concatenar com + cria uma String nova a cada volta; StringBuilder evita esse desperdício.
9. Coleções
O pacote java.util tem o Collections Framework: estruturas de dados padronizadas, todas genéricas (List<String>, Map<String, Integer>).
Qual escolher
| Preciso de... | Usa | Notas |
|---|---|---|
| Sequência ordenada, acesso por posição | ArrayList |
Acesso rápido por índice; inserir no meio é lento |
| Inserir e remover nas pontas com frequência | ArrayDeque ou LinkedList |
Fila e pilha |
| Valores sem repetições | HashSet |
Sem ordem garantida |
| Valores sem repetições e ordenados | TreeSet |
Ordem natural ou Comparator |
| Procurar valor por chave | HashMap |
Chaves únicas, sem ordem |
| Chaves ordenadas | TreeMap |
Ordem alfabética ou numérica |
List
Lista ordenada, permite duplicados.
import java.util.*;
List<String> nomes = new ArrayList<>();
nomes.add("Ana");
nomes.add("Bruno");
nomes.add("Carla");
// Acesso
String primeiro = nomes.get(0);
int n = nomes.size();
boolean tem = nomes.contains("Ana");
// Remover
nomes.remove("Bruno");
nomes.remove(0); // por índice
// Iterar
for (String nome : nomes) {
System.out.println(nome);
}
// Lista imutável
List<String> fixos = List.of("a", "b", "c");
Programa sempre contra a interface (List<String> x = new ArrayList<>()), não contra a implementação. Se mais tarde precisares de outra implementação, só mudas uma linha.
Map
Chave -> valor.
Map<String, Integer> idades = new HashMap<>();
idades.put("Ana", 25);
idades.put("Bruno", 30);
// Acesso
Integer idadeAna = idades.get("Ana"); // 25
Integer naoExiste = idades.get("Carla"); // null
Integer comDefault = idades.getOrDefault("Carla", 0); // 0
// Verificar
boolean tem = idades.containsKey("Ana");
// Remover
idades.remove("Ana");
// Iterar
for (Map.Entry<String, Integer> e : idades.entrySet()) {
System.out.println(e.getKey() + " -> " + e.getValue());
}
// Só chaves ou só valores
for (String k : idades.keySet()) { /* ... */ }
for (Integer v : idades.values()) { /* ... */ }
Set
Sem duplicados, sem ordem.
Set<String> unicos = new HashSet<>();
unicos.add("Ana");
unicos.add("Ana"); // ignorado
unicos.add("Bruno");
System.out.println(unicos.size()); // 2
// TreeSet, ordenado
Set<String> ordenados = new TreeSet<>();
ordenados.add("Carla");
ordenados.add("Ana");
ordenados.add("Bruno");
// Ana, Bruno, Carla (ordem alfabética)
Ordenar: Comparable e Comparator
Uma classe é comparável se implementar Comparable<T> (ordem natural). Um Comparator define uma ordem alternativa, sem tocar na classe.
import java.util.*;
record Aluno(String nome, int nota) { }
public class Ordenacao {
public static void main(String[] args) {
List<Aluno> turma = new ArrayList<>(List.of(
new Aluno("Carla", 15),
new Aluno("Ana", 18),
new Aluno("Bruno", 15)));
turma.sort(Comparator.comparing(Aluno::nome));
System.out.println(turma);
turma.sort(Comparator.comparingInt(Aluno::nota).reversed()
.thenComparing(Aluno::nome));
System.out.println(turma);
}
}
A primeira linha mostra [Aluno[nome=Ana, nota=18], Aluno[nome=Bruno, nota=15], Aluno[nome=Carla, nota=15]]; a segunda, por nota decrescente e depois por nome, dá Ana (18), Bruno (15), Carla (15).
Streams API
Programação funcional sobre coleções:
import java.util.stream.*;
List<Integer> numeros = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Filter
List<Integer> pares = numeros.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
// Map
List<Integer> dobrados = numeros.stream()
.map(n -> n * 2)
.collect(Collectors.toList());
// Soma
int soma = numeros.stream()
.mapToInt(Integer::intValue)
.sum();
// Count
long quantos = numeros.stream()
.filter(n -> n > 5)
.count();
// Cadeias
String resultado = List.of("ana", "bruno", "carla").stream()
.map(String::toUpperCase)
.filter(s -> s.length() > 3)
.collect(Collectors.joining(", "));
// "BRUNO, CARLA"
Um stream não altera a coleção original: produz um resultado novo. Só executa quando chega a operação terminal (collect, count, sum, forEach).
Exemplo resolvido · Contar palavras com Map
Problema: contar quantas vezes cada palavra aparece num texto.
import java.util.*;
public class ContaPalavras {
public static void main(String[] args) {
String texto = "java é java e python é python mas java ganha";
Map<String, Integer> contagem = new TreeMap<>();
for (String palavra : texto.split(" ")) {
contagem.merge(palavra, 1, Integer::sum);
}
for (Map.Entry<String, Integer> e : contagem.entrySet()) {
System.out.println(e.getKey() + ": " + e.getValue());
}
}
}
Passos: (1) split(" ") parte o texto em palavras; (2) merge insere a palavra com 1 ou, se já existe, soma 1 ao valor existente; (3) o TreeMap percorre as chaves por ordem alfabética. Saída: e: 1, ganha: 1, java: 3, mas: 1, python: 2, é: 2. (A letra acentuada fica no fim porque a ordenação é por código Unicode.)
10. Exceções
Hierarquia
Throwable
├── Error (não tratar: falhas graves da JVM)
└── Exception
├── RuntimeException (unchecked: tratar é opcional)
│ ├── NullPointerException
│ ├── ArithmeticException
│ └── IndexOutOfBoundsException
└── IOException (checked: tratar é obrigatório)
Uma exceção é um objeto que representa uma situação anómala. Quando é lançada, a execução normal pára e a JVM procura um catch que a trate, subindo pela cadeia de chamadas. Se ninguém a trata, o programa termina com uma mensagem e a stack trace.
Try-catch-finally
try {
int[] arr = {1, 2, 3};
System.out.println(arr[10]); // ArrayIndexOutOfBoundsException
int x = 10 / 0; // ArithmeticException (nunca chega aqui)
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Índice inválido: " + e.getMessage());
} catch (ArithmeticException e) {
System.out.println("Erro aritmético: " + e.getMessage());
} catch (Exception e) {
System.out.println("Outro erro: " + e.getMessage());
} finally {
System.out.println("Sempre executa");
}
Os catch leem-se por ordem: o mais específico primeiro, o mais geral (Exception) no fim. O compilador recusa um catch geral antes de um específico.
Multi-catch
try {
// ...
} catch (IOException | SQLException e) {
// tratar ambos igual
}
Try-with-resources
Para recursos que precisam de ser fechados (ficheiros, ligações):
try (Scanner sc = new Scanner(new File("dados.txt"))) {
while (sc.hasNextLine()) {
System.out.println(sc.nextLine());
}
} // sc.close() automático, mesmo se houver exceção
Throw e throws
public void retirar(double valor) {
if (valor <= 0) {
throw new IllegalArgumentException("Valor deve ser positivo");
}
if (valor > saldo) {
throw new IllegalStateException("Saldo insuficiente");
}
saldo -= valor;
}
throw lança uma exceção; throws na assinatura avisa que o método pode lançar uma exceção checked.
Exceções próprias
public class SaldoInsuficienteException extends Exception {
public SaldoInsuficienteException(String message) {
super(message);
}
}
// Uso (checked: declara-se no throws)
public void sacar(double valor) throws SaldoInsuficienteException {
if (valor > saldo) {
throw new SaldoInsuficienteException(
"Saldo: " + saldo + "€, pedido: " + valor + "€"
);
}
saldo -= valor;
}
Exemplo resolvido · Ler um número com validação
Problema: pedir um inteiro ao utilizador e repetir até ele escrever um valor válido.
import java.util.Scanner;
public class LeInteiro {
static int lerInteiro(Scanner sc, String pergunta) {
while (true) {
System.out.print(pergunta);
String linha = sc.nextLine();
try {
return Integer.parseInt(linha.trim());
} catch (NumberFormatException e) {
System.out.println("'" + linha + "' não é um número inteiro. Tenta de novo.");
}
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int idade = lerInteiro(sc, "Idade: ");
System.out.println("Registado: " + idade);
}
}
Passos: (1) lê-se sempre a linha inteira com nextLine, o que evita o problema do \n que fica no buffer; (2) parseInt lança NumberFormatException se o texto não for número; (3) o catch mostra uma mensagem e o ciclo while (true) repete; (4) o return só é alcançado com valor válido.
11. Ficheiros e I/O
NIO.2 (moderno, Java 7 ou superior)
import java.nio.file.*;
import java.io.IOException;
import java.util.List;
Path path = Path.of("dados.txt");
// Ler tudo
String texto = Files.readString(path);
// Ler linhas
List<String> linhas = Files.readAllLines(path);
// Escrever
Files.writeString(path, "Olá mundo!\n");
Files.write(path, List.of("a", "b", "c"));
// Acrescentar no fim
Files.writeString(path, "Nova linha\n", StandardOpenOption.APPEND);
// Verificar
boolean existe = Files.exists(path);
long tamanho = Files.size(path);
// Criar / apagar
Files.createFile(path);
Files.createDirectories(Path.of("pasta/subpasta"));
Files.delete(path);
// Listar diretório
try (var stream = Files.list(Path.of("."))) {
stream.forEach(System.out::println);
}
Este bloco é um catálogo: cada operação é independente das outras. Não o corras de seguida, de uma ponta à outra: Files.createFile falha com FileAlreadyExistsException se o ficheiro já existir (e já existe depois de writeString), e Files.delete apaga o ficheiro de que as linhas seguintes dependem. Experimenta uma operação de cada vez.
Quase todos estes métodos lançam IOException (checked): têm de estar dentro de um try ou o método tem de declarar throws IOException. Path.of e Files.readString existem desde o Java 11.
Ficheiros grandes: ler linha a linha
try (var stream = Files.lines(Path.of("dados.txt"))) {
stream
.filter(l -> l.contains("erro"))
.forEach(System.out::println);
}
Não carrega tudo em memória, lê linha a linha.
Exemplo resolvido · Guardar e ler um CSV
Problema: guardar uma lista de produtos em produtos.csv (nome;preco) e voltar a lê-la.
import java.io.IOException;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.List;
public class CsvProdutos {
record Produto(String nome, double preco) { }
public static void main(String[] args) throws IOException {
Path ficheiro = Path.of("produtos.csv");
List<String> linhas = List.of("Caneta;1.5", "Caderno;2.25", "Mochila;19.9");
Files.write(ficheiro, linhas);
List<Produto> produtos = new ArrayList<>();
for (String linha : Files.readAllLines(ficheiro)) {
String[] campos = linha.split(";");
produtos.add(new Produto(campos[0], Double.parseDouble(campos[1])));
}
double total = 0;
for (Produto p : produtos) {
total += p.preco();
}
System.out.println(produtos.size() + " produtos, total " + total);
}
}
Saída: 3 produtos, total 23.65. Usa ponto como separador decimal no ficheiro, e ; como separador de campos para não colidir com vírgulas decimais.
12. JDBC e bases de dados
JDBC = Java Database Connectivity. API standard para qualquer sistema de gestão de bases de dados (SGBD).
Adicionar o driver
No pom.xml (exemplo MySQL):
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
</dependency>
A propriedade mysql.version define-se no bloco <properties> com a versão estável mais recente de com.mysql:mysql-connector-j no repositório Maven Central (central.sonatype.com). Não fixes um número copiado de um exemplo.
Para PostgreSQL: org.postgresql:postgresql.
Para SQLite: org.xerial:sqlite-jdbc. O SQLite guarda a base num só ficheiro e não precisa de servidor, o que o torna ideal para aprender e para o projeto final (URL jdbc:sqlite:loja.db).
Ligação
import java.sql.*;
String url = "jdbc:mysql://localhost:3306/loja";
String user = "root";
String password = System.getenv("DB_PASSWORD");
try (Connection conn = DriverManager.getConnection(url, user, password)) {
System.out.println("Ligado!");
}
A palavra-passe não se escreve no código nem se guarda no repositório: lê-se de uma variável de ambiente ou de um ficheiro de configuração fora do controlo de versões.
SELECT
String sql = "SELECT id, nome, preco FROM produtos WHERE preco > ?";
try (Connection conn = DriverManager.getConnection(url, user, password);
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setDouble(1, 10.0);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
int id = rs.getInt("id");
String nome = rs.getString("nome");
double preco = rs.getDouble("preco");
System.out.println(id + " | " + nome + " | " + preco + "€");
}
}
}
O ResultSet funciona como um cursor: next() avança para a linha seguinte e devolve false quando acaba.
INSERT / UPDATE / DELETE
String sql = "INSERT INTO produtos (nome, preco) VALUES (?, ?)";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, "Caneta");
stmt.setDouble(2, 1.50);
int linhas = stmt.executeUpdate();
System.out.println(linhas + " linha(s) inserida(s)");
}
Transações
Quando várias operações têm de acontecer todas ou nenhuma (transferir dinheiro: debitar e creditar), usa uma transação.
conn.setAutoCommit(false);
try {
// várias operações executeUpdate...
conn.commit();
} catch (SQLException e) {
conn.rollback();
throw e;
}
Padrão DAO (Data Access Object)
Separar o acesso à BD do resto do código. Neste capítulo, um Produto tem também o id gerado pela base de dados:
// Produto.java
public record Produto(int id, String nome, double preco) { }
// ProdutoDAO.java
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class ProdutoDAO {
private final String url, user, password;
public ProdutoDAO(String url, String user, String password) {
this.url = url;
this.user = user;
this.password = password;
}
private Connection conectar() throws SQLException {
return DriverManager.getConnection(url, user, password);
}
public List<Produto> listar() throws SQLException {
List<Produto> lista = new ArrayList<>();
String sql = "SELECT id, nome, preco FROM produtos";
try (Connection conn = conectar();
PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
lista.add(new Produto(
rs.getInt("id"),
rs.getString("nome"),
rs.getDouble("preco")
));
}
}
return lista;
}
public void inserir(Produto p) throws SQLException {
String sql = "INSERT INTO produtos (nome, preco) VALUES (?, ?)";
try (Connection conn = conectar();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, p.nome());
stmt.setDouble(2, p.preco());
stmt.executeUpdate();
}
}
}
O resto da aplicação só conhece ProdutoDAO.listar(). Se trocares de MySQL para SQLite, só mexes no DAO.
Exemplo resolvido · SQLite de ponta a ponta
Com a dependência org.xerial:sqlite-jdbc no pom.xml (versão definida numa propriedade sqlite.version, escolhida no repositório Maven), este programa cria a tabela, insere dois produtos e agrega:
import java.sql.*;
public class SqliteDemo {
public static void main(String[] args) throws SQLException {
String url = "jdbc:sqlite:loja.db";
try (Connection conn = DriverManager.getConnection(url);
Statement st = conn.createStatement()) {
st.execute("CREATE TABLE IF NOT EXISTS produtos ("
+ "id INTEGER PRIMARY KEY AUTOINCREMENT, "
+ "nome TEXT NOT NULL, preco REAL NOT NULL)");
try (PreparedStatement ins = conn.prepareStatement(
"INSERT INTO produtos (nome, preco) VALUES (?, ?)")) {
ins.setString(1, "Caneta");
ins.setDouble(2, 1.5);
ins.executeUpdate();
ins.setString(1, "Caderno");
ins.setDouble(2, 2.25);
ins.executeUpdate();
}
try (ResultSet rs = st.executeQuery(
"SELECT COUNT(*) AS n, SUM(preco) AS total FROM produtos")) {
if (rs.next()) {
System.out.println(rs.getInt("n") + " produtos, total " + rs.getDouble("total"));
}
}
}
}
}
Na primeira execução a saída é 2 produtos, total 3.75. O ficheiro loja.db persiste, por isso cada nova execução acrescenta mais dois produtos. Reutiliza-se o mesmo PreparedStatement com valores diferentes: é mais seguro e mais eficiente.
Segurança: sempre PreparedStatement
// NUNCA: vulnerável a SQL injection
String sql = "SELECT * FROM users WHERE name = '" + nome + "'";
Statement st = conn.createStatement();
st.executeQuery(sql);
// SEMPRE
String sql = "SELECT * FROM users WHERE name = ?";
PreparedStatement st = conn.prepareStatement(sql);
st.setString(1, nome);
Se nome for ' OR '1'='1, a primeira versão devolve todos os utilizadores. Na segunda, o texto é tratado como valor e nunca como parte do comando SQL.
13. Testes e depuração com JUnit 5
Um teste automático é código que verifica se outro código faz o que deve. Corre em segundos, sempre que quiseres, e avisa quando uma alteração estraga algo que funcionava.
Dependência
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
A propriedade junit.version é a do capítulo 5. Com Maven 3.9 ou superior, o plugin que corre os testes já suporta JUnit 5 sem configuração adicional.
Classe a testar e classe de teste
// src/main/java/com/exemplo/Calculadora.java
package com.exemplo;
public class Calculadora {
public int somar(int a, int b) { return a + b; }
public int dividir(int a, int b) { return a / b; }
}
// src/test/java/com/exemplo/CalculadoraTest.java
package com.exemplo;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.*;
class CalculadoraTest {
private Calculadora c;
@BeforeEach
void preparar() {
c = new Calculadora(); // objeto novo antes de cada teste
}
@Test
void somaSimples() {
assertEquals(5, c.somar(2, 3));
}
@Test
void divisaoPorZeroLancaExcecao() {
assertThrows(ArithmeticException.class, () -> c.dividir(10, 0));
}
@ParameterizedTest
@CsvSource({"1,1,2", "0,0,0", "-5,5,0", "10,20,30"})
void somaVariosCasos(int a, int b, int esperado) {
assertEquals(esperado, c.somar(a, b));
}
}
Correr: mvn test. As anotações principais: @Test marca um teste; @BeforeEach corre antes de cada um; @ParameterizedTest repete o mesmo teste com vários dados. As afirmações mais usadas: assertEquals(esperado, obtido), assertTrue, assertFalse, assertNull, assertThrows.
Como pensar nos testes
Cada teste segue o padrão Preparar, Agir, Verificar (Arrange, Act, Assert). Testa o caso normal, os casos-limite (zero, vazio, valor máximo) e os casos de erro. Um bom teste tem um nome que explica o que verifica e falha por uma única razão.
Exemplo resolvido · Do teste ao defeito
Uma classe Desconto deve aplicar 10% a compras de 100 euros ou mais:
public class Desconto {
public static double aplicar(double total) {
if (total > 100) { // defeito: devia ser >=
return total * 0.9;
}
return total;
}
}
O teste dos casos-limite revela o problema:
@Test
void exatamente100TemDesconto() {
assertEquals(90.0, Desconto.aplicar(100.0), 0.001);
}
O teste falha com expected: <90.0> but was: <100.0>. O depurador confirma que a condição 100 > 100 é falsa. Corrigindo para >=, o teste passa. O terceiro argumento de assertEquals é a tolerância, necessária para comparar double.
Depurar com método
- Reproduzir o erro de forma fiável.
- Isolar: reduzir ao caso mais pequeno que falha.
- Formular uma hipótese e confirmá-la com o depurador ou com um teste.
- Corrigir a causa, não o sintoma.
- Escrever um teste que teria apanhado o defeito, para que não regresse.
Lê sempre a stack trace de cima para baixo: a primeira linha diz o tipo de exceção e a mensagem; as linhas seguintes indicam onde ocorreu, e a primeira que aponta para código teu é a que interessa.
14. Boas práticas, normas e documentação
Convenções de código
- Classes, records e interfaces:
PascalCase, comoCliente,BancoCentral,Comparable. - Métodos e variáveis:
camelCase, comogetNome(),totalVendas. - Constantes:
UPPER_SNAKE_CASE, comoMAX_TENTATIVAS,PI. - Pacotes: minúsculas separadas por pontos, como
com.empresa.app.servicos. - Booleanos: começam por
isouhas, comoisAtivo,hasPermissao. - Um ficheiro por classe pública; indentação consistente; uma instrução por linha; nomes que dizem o que fazem (
calcularTotalComIva, nãocalc2).
Princípios
- Encapsular: atributos privados, comportamento público.
- Composição antes de herança quando possível.
- Imutabilidade: prefere
finale records. - Responsabilidade única: cada classe faz uma coisa.
- DRY (Don't Repeat Yourself): extrai métodos para evitar repetição.
- YAGNI (You Aren't Gonna Need It): não construas o que ainda ninguém pediu.
Exemplo resolvido · Refatorar código repetido
Antes:
System.out.println("Total sem IVA: " + (qtd * preco));
System.out.println("IVA: " + (qtd * preco * 0.23));
System.out.println("Total com IVA: " + (qtd * preco * 1.23));
Depois:
final double TAXA_IVA = 0.23;
double semIva = qtd * preco;
double iva = semIva * TAXA_IVA;
System.out.println("Total sem IVA: " + semIva);
System.out.println("IVA: " + iva);
System.out.println("Total com IVA: " + (semIva + iva));
A taxa tem nome, o cálculo faz-se uma vez e, se a taxa mudar, alteras uma linha.
Java moderno (vale a pena adotar)
varpara variáveis locais.recordpara DTOs.switchcom expressões (->).- Blocos de texto (
"""). Optional<T>para evitarnull.- Streams para coleções.
// Bloco de texto (Java 15 ou superior)
String json = """
{
"nome": "Ana",
"idade": 25
}
""";
// Optional: evita null
public Optional<Pessoa> procurarPorId(int id) {
// ...
return Optional.ofNullable(pessoa);
}
// Uso
Optional<Pessoa> encontrada = procurarPorId(7);
encontrada.ifPresent(p -> System.out.println(p.getNome()));
String nome = encontrada.map(Pessoa::getNome).orElse("Desconhecido");
Documentar o código: Javadoc
Os comentários /** ... */ antes de classes e métodos geram documentação HTML com a ferramenta javadoc.
/**
* Calcula o preço final com IVA.
*
* @param preco preço sem IVA, em euros (maior ou igual a zero)
* @param taxa taxa de IVA, por exemplo 0.23
* @return preço com IVA
* @throws IllegalArgumentException se o preço for negativo
*/
public static double comIva(double preco, double taxa) {
if (preco < 0) {
throw new IllegalArgumentException("Preço negativo");
}
return preco * (1 + taxa);
}
Os comentários normais (//) explicam o porquê de uma decisão; o código já diz o quê.
Guião técnico e manual de utilizador
Quem recebe a aplicação precisa de documentação. Escreve dois documentos distintos:
| Documento | Para quem | Conteúdo |
|---|---|---|
| Guião técnico | Outros programadores | Requisitos de ambiente (versão do JDK, base de dados), como compilar e executar (mvn package), estrutura de pacotes, diagrama de classes, decisões de desenho, como correr os testes |
| Manual de utilizador | Quem usa a aplicação | Como instalar, descrição de cada menu ou ecrã, exemplos de utilização, mensagens de erro e o que fazer perante elas |
Ambos devem ter um índice, linguagem clara, exemplos e a versão do programa a que se referem. Um ficheiro README.md na raiz do repositório é o mínimo.
Controlo de versões
Guarda o projeto em Git: faz commits pequenos com mensagens que dizem o que mudou e porquê (Adiciona validação de idade no setter), e nunca inclui palavras-passe nem a pasta target/ (usa um .gitignore).
15. Próximos passos
Depois desta base, explora:
- Spring Boot, o framework dominante de backend Java.
- Hibernate / JPA, ORM para bases de dados.
- Lombok, que reduz código repetitivo (getters, builders).
- JUnit + Mockito para testes completos.
- Gradle, alternativa moderna ao Maven.
- Kotlin, linguagem moderna da JVM, interoperável com Java.
- Programação reativa: Reactor, RxJava.
Erros comuns
| Erro | Sintoma | Correção |
|---|---|---|
Comparar String com == |
Condição falha sem razão aparente | Usar equals() ou equalsIgnoreCase() |
nextInt() seguido de nextLine() |
O nextLine() devolve texto vazio |
Sempre que leias texto depois de um número, lê a linha inteira com nextLine() e converte com Integer.parseInt. A causa é a mudança de linha que o nextInt() deixa por ler. |
Divisão inteira (7 / 2) |
Resultado 3 em vez de 3,5 | Tornar um dos operandos double (7 / 2.0) |
Ciclo com i <= v.length |
ArrayIndexOutOfBoundsException |
Usar i < v.length |
Usar um objeto null |
NullPointerException |
Inicializar o objeto, verificar null ou usar Optional |
| Nome de ficheiro diferente da classe pública | class X is public, should be declared in a file named X.java |
Renomear o ficheiro com o nome exato da classe |
switch tradicional sem break |
Executam-se vários casos seguidos | Usar break ou a forma com -> |
catch (Exception e) { } vazio |
Erros desaparecem sem rasto | Tratar, registar ou voltar a lançar a exceção |
| Não fechar ficheiros ou ligações | Recursos esgotados, ficheiros bloqueados | try-with-resources |
Remover de uma lista dentro do for |
ConcurrentModificationException |
removeIf(...) ou um Iterator |
equals sem hashCode |
Objetos "iguais" não se encontram num HashSet |
Sobrepor os dois em conjunto |
| Concatenar texto do utilizador em SQL | SQL injection | PreparedStatement com ? |
| Herdar só para reutilizar código | Hierarquias rígidas e sem sentido | Preferir composição |
| Guardar palavras-passe no código | Fuga de credenciais | Variáveis de ambiente ou ficheiro de configuração fora do Git |
Exercícios resolvidos
Exemplo resolvido · Conta com validação
Enunciado: cria uma classe Conta com saldo privado. Depositar só aceita valores positivos; levantar lança uma exceção própria (checked) se o saldo não chegar.
class SaldoInsuficienteException extends Exception {
public SaldoInsuficienteException(String mensagem) {
super(mensagem);
}
}
class Conta {
private double saldo;
public void depositar(double valor) {
if (valor <= 0) {
throw new IllegalArgumentException("Valor deve ser positivo");
}
saldo += valor;
}
public void levantar(double valor) throws SaldoInsuficienteException {
if (valor > saldo) {
throw new SaldoInsuficienteException("Saldo " + saldo + ", pedido " + valor);
}
saldo -= valor;
}
public double getSaldo() {
return saldo;
}
}
public class Main {
public static void main(String[] args) {
Conta c = new Conta();
c.depositar(100);
try {
c.levantar(30);
c.levantar(200);
} catch (SaldoInsuficienteException e) {
System.out.println("Recusado: " + e.getMessage());
}
System.out.println("Saldo: " + c.getSaldo());
}
}
Resolução: depois do depósito o saldo é 100. O primeiro levantamento deixa 70. O segundo pede 200 e lança a exceção, que o catch apanha. Saída: Recusado: Saldo 70.0, pedido 200.0 e Saldo: 70.0. O saldo nunca é alterado diretamente de fora, e o estado da conta fica consistente depois da falha.
Exemplo resolvido · Filtrar e agregar produtos
Enunciado: dada uma lista de produtos, listar os que custam menos de 2 euros e indicar o mais caro.
import java.util.List;
import java.util.Comparator;
import java.util.stream.Collectors;
public class Catalogo {
record Produto(String nome, double preco) { }
public static void main(String[] args) {
List<Produto> produtos = List.of(
new Produto("Caneta", 1.5),
new Produto("Caderno", 2.25),
new Produto("Mochila", 19.9),
new Produto("Régua", 0.8));
String baratos = produtos.stream()
.filter(p -> p.preco() < 2)
.map(Produto::nome)
.collect(Collectors.joining(", "));
Produto maisCaro = produtos.stream()
.max(Comparator.comparingDouble(Produto::preco))
.get();
System.out.println("Baratos: " + baratos);
System.out.println("Mais caro: " + maisCaro.nome());
}
}
Resolução: o filter deixa passar a Caneta (1,5) e a Régua (0,8); o map extrai os nomes; o joining junta-os. O max com o comparador por preço devolve a Mochila. Saída: Baratos: Caneta, Régua e Mais caro: Mochila. O .get() serve aqui porque a lista não está vazia; num caso real, trata o Optional vazio.
Exemplo resolvido · Palíndromo com teste
Enunciado: escreve um método que indique se um texto é palíndromo, ignorando maiúsculas e espaços, e testa-o.
public class Texto {
public static boolean ehPalindromo(String s) {
String limpo = s.toLowerCase().replace(" ", "");
String invertido = new StringBuilder(limpo).reverse().toString();
return limpo.equals(invertido);
}
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class TextoTest {
@Test void palavraSimples() { assertTrue(Texto.ehPalindromo("Arara")); }
@Test void comEspacos() { assertTrue(Texto.ehPalindromo("Luz azul")); }
@Test void naoPalindromo() { assertFalse(Texto.ehPalindromo("Java")); }
@Test void vazio() { assertTrue(Texto.ehPalindromo("")); }
}
Resolução: em "Luz azul" o texto limpo é luzazul, que invertido dá o mesmo. "Java" dá java contra avaj. O texto vazio é palíndromo por definição, e o teste documenta essa decisão. Se quisesses tratar acentos ou pontuação, escreverias primeiro o teste que falha e só depois alterarias o método.
Síntese
- Java compila para bytecode que corre na JVM em qualquer sistema; o JDK traz as ferramentas de desenvolvimento.
- Antes de programar: decompor o problema, escrever o algoritmo (sequência, seleção, repetição) e planear as etapas, de preferência de forma incremental.
- O ambiente compõe-se de JDK, IDE ou editor, terminal, Maven e Git; o depurador poupa mais tempo do que
println. - A sintaxe assenta em 8 primitivos,
String, operadores,if,switch, ciclos, arrays e métodos; os argumentos passam por valor. - Na POO, classes definem atributos e métodos; o encapsulamento protege o estado; a herança partilha comportamento; o polimorfismo deixa o código ser genérico; as interfaces são contratos; os genéricos dão segurança de tipos.
- Entre classes existem relações de dependência, associação, agregação, composição, herança e realização.
- As coleções (
List,Set,Map) devem escolher-se pelo tipo de acesso; programa contra a interface. - As exceções separam o fluxo normal do tratamento de erros:
try-catch-finally,try-with-resources,throw,throwse exceções próprias. - Ficheiros lêem-se e escrevem-se com
FilesePath; bases de dados acedem-se com JDBC, sempre comPreparedStatement, e o acesso isola-se num DAO. - Testar com JUnit 5 e depurar com método. Cada defeito corrigido ganha um teste.
- Documentar com Javadoc, guião técnico e manual de utilizador, respeitando convenções, licenças e o RGPD.
Glossário
- Algoritmo: sequência finita e ordenada de passos que resolve um problema.
- Bytecode: código intermédio gerado pelo compilador, executado pela JVM.
- Checked Exception: exceção que o compilador obriga a tratar ou declarar com
throws. - Classe: molde que define atributos e métodos de um conjunto de objetos.
- Composição: relação "é composto por", em que as partes não vivem sem o todo.
- DAO: Data Access Object, classe que isola o acesso à base de dados.
- DTO: Data Transfer Object, objeto simples que transporta dados.
- Encapsulamento: esconder o estado interno e expor apenas operações controladas.
- Garbage Collector: mecanismo que liberta a memória de objetos sem referências.
- Generics: tipos parametrizados, como
List<String>. - Heap: zona de memória onde vivem os objetos.
- Herança: mecanismo em que uma classe reutiliza e estende outra.
- IDE: ambiente de desenvolvimento integrado, que junta editor, compilador e depurador.
- Interface: contrato que declara métodos que as classes se comprometem a implementar.
- JDK: Java Development Kit, inclui o JRE e o compilador.
- JVM: Java Virtual Machine, executa bytecode.
- Maven: ferramenta de build e gestão de dependências.
- Polimorfismo: capacidade de tratar objetos de classes diferentes pela mesma interface.
- POJO: Plain Old Java Object, classe simples sem dependências especiais.
- POM: Project Object Model, ficheiro de configuração do Maven.
- Refatoração: melhorar a estrutura do código sem alterar o comportamento.
- Stack: zona de memória das variáveis locais e chamadas de métodos.
- UML: linguagem gráfica para modelar sistemas, incluindo diagramas de classes.
Recursos
- Documentação oficial: docs.oracle.com/en/java.
- Tutoriais: dev.java.
- Baeldung: baeldung.com, com artigos práticos.
- Effective Java (Joshua Bloch), livro essencial.
- JetBrains Academy, cursos gratuitos.
Java é uma linguagem que recompensa o investimento. A sintaxe é mais verbosa que a de Python ou JavaScript, mas o tipo estático apanha erros cedo, o ecossistema é gigantesco e a procura no mercado é consistente. O segredo é escrever muito código: não chega ler.