Partilhar: Classroom Teams WhatsApp
aulify · Sebenta
UC · Unidade de Competência · UC00619

Java (UC00619)

Linguagem OOP industrial e multiplataforma
50h · 4.5 pontos crédito Curso: T. Desenv. Software ↗ Referencial oficial SNQ
Índice

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:

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

Onde se usa Java

Quando NÃO usar Java

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

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:

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:

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

  1. Sequência: os passos executam-se por ordem.
  2. Seleção: um passo depende de uma condição (se ... então ... senão).
  3. 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

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:

4. Ambiente de desenvolvimento

Instalar o JDK

JDK = Java Development Kit (compilador + ferramentas). JRE = Java Runtime Environment (só corre, não desenvolve).

Recomendado:

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

Depurar com o IDE

O depurador deixa executar o programa passo a passo:

  1. Clica na margem esquerda de uma linha para pôr um breakpoint (ponto de paragem).
  2. Executa em modo Debug: o programa pára nessa linha.
  3. Step Over avança uma linha; Step Into entra no método chamado; Step Out sai do método atual.
  4. Observa as variáveis e as watches (expressões que escolhes acompanhar).
  5. 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.

  1. Procura na API a classe java.util.ArrayList. Em Constructor Summary há três construtores: ArrayList(), ArrayList(int initialCapacity) e ArrayList(Collection<? extends E> c). O último cria uma lista com cópia dos elementos de outra coleção.
  2. Em Method Summary procura removeIf. A assinatura é boolean removeIf(Predicate<? super E> filter). Lê-se: recebe um critério (um Predicate, uma função que devolve true ou false para cada elemento), remove os elementos para os quais o critério dá true e devolve true se removeu algum.
  3. 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:

Vantagens:

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:

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

  1. Reproduzir o erro de forma fiável.
  2. Isolar: reduzir ao caso mais pequeno que falha.
  3. Formular uma hipótese e confirmá-la com o depurador ou com um teste.
  4. Corrigir a causa, não o sintoma.
  5. 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

Princípios

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)

// 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:

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

Glossário

Recursos

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.