Este arquivo é um registro de notas e informações legais do Nicolau em cima dos estudos dele em Java. Aqui eu vou listar conceitos, dicas, e tudo mais que eu achar relevante para o meu aprendizado e pra quem olhar. É a minha primeira experiência com Java, então tudo que eu aprender vai estar aqui, cru e sem censura.
Detalhe, tudo oq eu to vendo é em Java 26, a versão mais recente até o momento. 27/03/2026
Eu estou usando para aprender os recursos de Java da w3schools e o roadmap de Java proposto pela roadmap.sh. Eu vou seguir os tópicos e anotar tudo que eu achar importante, desde sintaxe básica até conceitos mais avançados
| Recurso | Link |
|---|---|
| w3schools | https://www.w3schools.com/java/default.asp |
| roadmap.sh | https://roadmap.sh/java |
Vou atuaizando isso conforme eu vou estudando.
Eu trabalhei muito com python, e o Java é bem diferente. Ele é mais verboso, o que significa que eu preciso escrever mais código para fazer as mesmas coisas que eu fazia em Python. Mas isso também tem suas vantagens, como uma estrutura mais clara e uma melhor organização do código. Além disso, diferente do Python, o Java é uma linguagem compilada, o que pode resultar em um desempenho melhor em algumas situações.
- Introdução
- Fundamentos
- Controle de Fluxo
- Classes
- Métodos
- Input de Dados
- Organização de Projeto
- Tratamento de Erros
| # | Tópico | Pasta |
|---|---|---|
| 1 | Introdução | Introdução/ |
| 2 | Fundamentos | Fundamentos/ |
| 3 | Controle de Fluxo | Controle de fluxo/ |
| 4 | Classes | Classes/ |
| 5 | Métodos e Funções | Métodos e funções/ |
| 6 | Input de Dados | Input/ |
| 7 | Organização de Projeto | Organização de projeto/ |
| 8 | Tratamento de Erros | Tratamento de erros/ |
📁 Exemplos deste tópico:
Introdução/
Vamos supor que você já tenha instalado o Java Development Kit (JDK) e configurado as variáveis de ambiente corretamente. E além disso, você e suas super habilidades de programação fizeram um hello world. Para compilar um arquivo Java, nesse caso, seu hello world, você pode usar o comando javac seguido do nome do arquivo. Por exemplo:
javac MeuPrograma.javaIsso vai gerar um arquivo MeuPrograma.class, que é o bytecode que a máquina virtual Java (JVM) pode executar. Para rodar o programa, você usa o comando java seguido do nome da classe (sem a extensão .class):
java MeuProgramaE pronto, rodou seu Hello World
O que acabou de acontecer é que o comando javac compilou seu código Java em bytecode, que é um formato intermediário que a JVM pode entender. O comando java então executa esse bytecode na JVM, permitindo que seu programa seja executado em qualquer plataforma que tenha uma JVM instalada, o que é uma das grandes vantagens do Java: a portabilidade.
flowchart LR
A["📄 MeuPrograma.java"] -->|javac| B["⚙️ MeuPrograma.class\n(bytecode)"]
B -->|java| C["🖥️ JVM executa\no programa"]
📁 Exemplo completo:
Introdução/Main.java
Vamos usar esse código de exemplo:
public class Main {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}A classe Main é a definição de uma classe em Java. Em Java, tudo precisa estar dentro de uma classe, mesmo o código que é executado. O nome da classe deve ser o mesmo do arquivo, ou seja, se a classe se chama Main, o arquivo deve ser Main.java. A palavra-chave public indica que a classe é pública e pode ser acessada por outras classes. A palavra-chave class é usada para definir uma nova classe.
flowchart TD
subgraph Arquivo ["Main.java"]
CL["public class Main"] --> MT["public static void main(String[] args)"]
MT --> ST["System.out.println(...)"]
end
style CL fill:#4a9eff,color:#fff
style MT fill:#f5a623,color:#fff
style ST fill:#7ed321,color:#fff
Estou utilizando um método main que é o ponto de entrada do programa. Ele é obrigatório em qualquer aplicação Java, e é onde o programa começa a ser executado. O método main tem uma assinatura específica: ele deve ser public, static, e retornar void. Ele também aceita um array de strings como argumento, que pode ser usado para passar parâmetros para o programa. As {} conhecidas como chaves indicam o início e o fim de um bloco de código.
Anatomia do
public static void main(String[] args):
Parte Significado publicAcessível de qualquer lugar staticPertence à classe, não precisa de objeto voidNão retorna nada mainNome obrigatório do ponto de entrada String[] argsParâmetros passados pelo terminal
O System.out.println é um método que imprime uma linha de texto no console. Ele é parte da classe System, que é uma classe utilitária fornecida pelo Java para realizar operações de entrada e saída, entre outras coisas. O out, gíria para "output", é um objeto do tipo PrintStream que representa a saída padrão (normalmente o console), e o println, gíria para "print line", é um método desse objeto que imprime o texto seguido de uma nova linha. Ou seja, várias etapas para recriar a função print do Python, mas é assim que o Java funciona, ele é mais detalhado e explícito.
flowchart LR
SYS["System"] --> OUT["out\n(PrintStream)"]
OUT --> PRINTLN["println(texto)\n→ imprime + pula linha"]
Comparação rápida com Python:
Python Java print("Hello")System.out.println("Hello");Sem ponto e vírgula Precisa de ;no finalSem classe obrigatória Tudo dentro de uma classe python arquivo.pyjavac Arquivo.java+java Arquivo
Um programa de computador é simplesmente uma lista de "instruções" que são "executadas" pela máquina. Em linguagem de programação, essas "instruções" são chamadas de "statements". Um statement é uma linha de código que realiza uma ação específica. Por exemplo, em Java, a linha System.out.println("Hello, World!"); é um statement que imprime "Hello, World!" no console.
flowchart TD
S1["Statement 1: int x = 5;"] --> S2["Statement 2: int y = 10;"]
S2 --> S3["Statement 3: System.out.println(x + y);"]
S3 --> FIM["✅ Programa termina"]
Um detalhe importante, os statements em Java terminam com um ponto e vírgula (;). Isso é diferente de algumas outras linguagens, como Python, onde os statements não precisam de um terminador específico. O ponto e vírgula é usado para indicar o final de um statement, permitindo que o compilador saiba onde uma instrução termina e a próxima começa. Se você esquecer de colocar o ponto e vírgula no final de um statement, o compilador vai gerar um erro, porque ele não vai conseguir entender onde o statement termina.
Regra de ouro:
Linguagem humana: "Olá mundo." Java: System.out.println("Olá mundo"); ↑ ponto e vírgula = ponto final da frase
Imagine que um statement como uma frase na linguagem humana. Mas ao invés de utilizar "." para terminar a frase, o Java utiliza ";". Então, cada vez que você escreve um statement, é como se estivesse escrevendo uma frase, e o ponto e vírgula é o sinal de pontuação que indica o fim dessa frase. Sem ele, o Java não consegue entender onde a frase termina, e isso causa um erro de sintaxe.
📁 Exemplos deste tópico:
Fundamentos/
Agora que já sabemos compilar e rodar um programa Java, é hora de aprender os blocos fundamentais da linguagem: variáveis, tipos de dados, operadores, strings, a classe Math, booleanos e arrays. Esses conceitos são a base de tudo que vem depois.
flowchart LR
A["Variáveis"] --> B["Tipos de Dados"]
B --> C["Operadores"]
C --> D["Strings"]
C --> E["Math"]
C --> F["Booleanos"]
B --> G["Arrays"]
G --> L["Lists"]
L --> LL["LinkedList"]
LL --> H["Imports"]
Em Java, toda variável precisa ter um tipo declarado. Isso é bem diferente de Python, onde o tipo é inferido automaticamente. A sintaxe é:
tipo nomeDaVariavel = valor;
📁 Arquivo:
Fundamentos/Variaveis.java
// String: armazena texto (sempre entre aspas duplas)
String nome = "Nicolau";
// int: armazena números inteiros
int idade = 22;
// double: armazena números decimais (ponto flutuante)
double altura = 1.75;
// char: armazena UM único caractere (sempre entre aspas simples)
char inicial = 'N';
// boolean: armazena verdadeiro ou falso
boolean estudante = true;Variáveis podem ser reatribuídas (mudar de valor):
idade = 23; // Ok, mudou o valor
// Declarar sem inicializar e atribuir depois
String cidade;
cidade = "São Paulo";
// Declarar múltiplas variáveis do mesmo tipo
int x = 5, y = 10, z = 15;Regra: variáveis declaradas com
finalviram constantes e não podem ser alteradas:final double PI = 3.14159; PI = 3.14; // ❌ ERRO! Não pode reatribuir constante
Java tem dois grupos de tipos: primitivos (valores diretos na memória) e referência (endereços de objetos como String, arrays, etc.).
📁 Arquivo:
Fundamentos/Tipos_de_dados.java
| Tipo | Tamanho | Faixa de valores | Exemplo |
|---|---|---|---|
byte |
1 byte | -128 a 127 | byte b = 127; |
short |
2 bytes | -32.768 a 32.767 | short s = 32000; |
int |
4 bytes | -2 bi a 2 bi | int i = 2000000000; |
long |
8 bytes | número gigante | long l = 9000000000L; |
float |
4 bytes | ~7 dígitos decimais | float f = 3.14f; |
double |
8 bytes | ~15 dígitos decimais | double d = 3.14159; |
char |
2 bytes | 1 caractere Unicode | char c = 'A'; |
boolean |
1 bit | true ou false | boolean b = true; |
Detalhe:
Stringnão é primitivo — é um objeto (tipo referência).
flowchart LR
BYTE["byte"] --> SHORT["short"] --> INT["int"] --> LONG["long"] --> FLOAT["float"] --> DOUBLE["double"]
style BYTE fill:#4CAF50,color:#fff
style DOUBLE fill:#F44336,color:#fff
| Direção | Nome | O que faz | Exemplo |
|---|---|---|---|
| → Widening | Automático | Tipo menor → tipo maior | double d = meuInt; |
| ← Narrowing | Manual | Tipo maior → tipo menor | int i = (int) meuDouble; |
// Widening (automático): int → double
int numInteiro = 100;
double numDecimal = numInteiro; // 100.0
// Narrowing (manual): double → int
double preco = 9.99;
int precoInteiro = (int) preco; // 9 (perde a parte decimal!)Os operadores são os símbolos que usamos para fazer operações com variáveis e valores. Java tem 4 grupos principais:
📁 Arquivo:
Fundamentos/Operadores.java
| Operador | Nome | Exemplo | Resultado |
|---|---|---|---|
+ |
Soma | 10 + 3 |
13 |
- |
Subtração | 10 - 3 |
7 |
* |
Multiplicação | 10 * 3 |
30 |
/ |
Divisão | 10 / 3 |
3 (inteira!) |
% |
Módulo (resto) | 10 % 3 |
1 |
++ |
Incremento | x++ |
x + 1 |
-- |
Decremento | x-- |
x - 1 |
| Operador | Exemplo | Equivale a |
|---|---|---|
= |
x = 5 |
x = 5 |
+= |
x += 3 |
x = x + 3 |
-= |
x -= 3 |
x = x - 3 |
*= |
x *= 3 |
x = x * 3 |
/= |
x /= 3 |
x = x / 3 |
%= |
x %= 3 |
x = x % 3 |
| Operador | Nome | Exemplo | Resultado |
|---|---|---|---|
== |
Igual | 10 == 20 |
false |
!= |
Diferente | 10 != 20 |
true |
> |
Maior | 10 > 20 |
false |
< |
Menor | 10 < 20 |
true |
>= |
Maior ou igual | 10 >= 20 |
false |
<= |
Menor ou igual | 10 <= 20 |
true |
| Operador | Nome | Descrição | Exemplo |
|---|---|---|---|
&& |
AND | Ambos true? | true && false → false |
|| |
OR | Pelo menos um true? | true || false → true |
! |
NOT | Inverte o valor | !true → false |
String é um dos tipos mais usados em Java. Apesar de parecer um tipo primitivo, ela é na verdade um objeto, e por isso vem com vários métodos prontos pra manipular texto.
📁 Arquivo:
Fundamentos/Strings_em_java.java
String saudacao = "Olá, Mundo!";| Método | O que faz | Exemplo | Resultado |
|---|---|---|---|
length() |
Tamanho da string | "Olá".length() |
3 |
toUpperCase() |
Tudo maiúsculo | "olá".toUpperCase() |
"OLÁ" |
toLowerCase() |
Tudo minúsculo | "OLÁ".toLowerCase() |
"olá" |
indexOf() |
Posição de um trecho | "Olá Mundo".indexOf("Mundo") |
4 |
contains() |
Contém o trecho? | "Java".contains("av") |
true |
charAt() |
Caractere na posição | "Java".charAt(0) |
'J' |
substring() |
Extrai trecho | "Java".substring(1, 3) |
"av" |
replace() |
Substitui trecho | "Olá Mundo".replace("Mundo", "Java") |
"Olá Java" |
trim() |
Remove espaços das pontas | " oi ".trim() |
"oi" |
split() |
Quebra em array | "a,b,c".split(",") |
["a","b","c"] |
String nome = "Nicolau";
int idade = 22;
// Jeito 1: operador +
System.out.println(nome + " tem " + idade + " anos");
// Jeito 2: concat()
System.out.println(nome.concat(" é estudante"));Importante: nunca use
==para comparar Strings! Useequals()ouequalsIgnoreCase().
String a = "Java";
String b = "java";
a.equals(b); // false (case sensitive)
a.equalsIgnoreCase(b); // trueA classe Math já vem pronta no Java e não precisa importar nada. Todos os métodos são static, então chamamos direto com Math.metodo().
📁 Arquivo:
Fundamentos/Math_em_java.java
| Método | O que faz | Exemplo | Resultado |
|---|---|---|---|
Math.max(a, b) |
Maior entre dois | Math.max(10, 20) |
20 |
Math.min(a, b) |
Menor entre dois | Math.min(10, 20) |
10 |
Math.abs(x) |
Valor absoluto | Math.abs(-15) |
15 |
Math.round(x) |
Arredonda | Math.round(4.7) |
5 |
Math.ceil(x) |
Arredonda pra cima | Math.ceil(4.1) |
5.0 |
Math.floor(x) |
Arredonda pra baixo | Math.floor(4.9) |
4.0 |
Math.pow(a, b) |
Potência (a^b) | Math.pow(2, 3) |
8.0 |
Math.sqrt(x) |
Raiz quadrada | Math.sqrt(64) |
8.0 |
Math.random() |
Aleatório [0, 1) | Math.random() |
0.xxx |
Math.PI // 3.141592653589793
Math.E // 2.718281828459045// Aleatório entre 0 e 100
int aleatorio = (int)(Math.random() * 101);
// Aleatório entre min e max (ex: 1 a 10)
int min = 1, max = 10;
int entre = (int)(Math.random() * (max - min + 1)) + min;O tipo boolean só tem dois valores possíveis: true ou false. É a base de todas as decisões no código.
📁 Arquivo:
Fundamentos/Booleanos.java
boolean javaEhLegal = true;
boolean pythonEhIgual = false;Qualquer expressão de comparação retorna um boolean:
int idade = 22;
System.out.println(idade > 18); // true
System.out.println(idade == 30); // falseBooleanos são fundamentais pra usar em condições (if, while, etc.):
boolean maiorDeIdade = idade >= 18;
if (maiorDeIdade) {
System.out.println("Pode entrar!");
}Array é uma estrutura que armazena múltiplos valores do mesmo tipo em posições numeradas (índices começam em 0).
📁 Arquivo:
Fundamentos/Arrays_em_java.java
flowchart LR
subgraph "String[] carros"
I0["[0] Fusca"] --- I1["[1] Civic"] --- I2["[2] Corolla"] --- I3["[3] Gol"]
end
// Jeito 1: com valores
String[] carros = {"Fusca", "Civic", "Corolla", "Gol"};
// Jeito 2: declarar tamanho e preencher depois
int[] notas = new int[4];
notas[0] = 85;
notas[1] = 92;System.out.println(carros[0]); // Fusca
System.out.println(carros.length); // 4
carros[0] = "Kombi"; // Trocou Fusca por Kombi// For normal (quando precisa do índice)
for (int i = 0; i < carros.length; i++) {
System.out.println("Posição " + i + ": " + carros[i]);
}
// For-each (mais limpo quando não precisa do índice)
for (String carro : carros) {
System.out.println("Carro: " + carro);
}int[][] matriz = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
for (int i = 0; i < matriz.length; i++) {
for (int j = 0; j < matriz[i].length; j++) {
System.out.print(matriz[i][j] + " ");
}
System.out.println();
}import java.util.Arrays;
int[] numeros = {5, 2, 8, 1, 9, 3};
Arrays.sort(numeros); // Ordena: [1, 2, 3, 5, 8, 9]
System.out.println(Arrays.toString(numeros)); // Imprime bonito
int indice = Arrays.binarySearch(numeros, 5); // Busca (precisa estar ordenado)Arrays são ótimos, mas têm um problema: o tamanho é fixo. Se você criou um array de 4 posições, ele vai ter 4 posições pra sempre. E se você precisar adicionar mais um item? Criar um array novo, copiar tudo... é trabalhoso. Pra resolver isso, o Java tem as Lists, estruturas de dados dinâmicas que crescem e diminuem conforme a necessidade.
A implementação mais comum é o ArrayList, que fica no pacote java.util.
📁 Arquivo:
Fundamentos/Lists_em_java.java
flowchart LR
subgraph "ArrayList<String> frutas"
I0["[0] Maçã"] --- I1["[1] Banana"] --- I2["[2] Laranja"] --- I3["[3] ..."]
end
ADD["add()"] -->|cresce| I3
import java.util.ArrayList;
import java.util.List;
// Jeito 1: ArrayList direto
ArrayList<String> frutas = new ArrayList<>();
frutas.add("Maçã");
frutas.add("Banana");
frutas.add("Laranja");
// Jeito 2: usando a interface List (boa prática)
List<Integer> numeros = new ArrayList<>();
numeros.add(10);
numeros.add(20);Detalhe importante: Lists não aceitam tipos primitivos (
int,double, etc.). Usamos as Wrapper Classes:
Primitivo Wrapper intIntegerdoubleDoublebooleanBooleancharCharacterO Java faz a conversão automaticamente (autoboxing/unboxing):
List<Integer> nums = new ArrayList<>(); nums.add(42); // autoboxing: int → Integer int valor = nums.get(0); // unboxing: Integer → int
| Método | O que faz | Exemplo | Resultado |
|---|---|---|---|
add(item) |
Adiciona no final | frutas.add("Uva") |
[..., Uva] |
add(pos, item) |
Adiciona na posição | frutas.add(1, "Manga") |
Insere na pos 1 |
get(pos) |
Acessa pelo índice | frutas.get(0) |
"Maçã" |
set(pos, item) |
Substitui na posição | frutas.set(0, "Pera") |
Troca o primeiro |
remove(item) |
Remove pelo valor | frutas.remove("Banana") |
Remove Banana |
remove(pos) |
Remove pelo índice | frutas.remove(0) |
Remove o primeiro |
size() |
Tamanho da lista | frutas.size() |
3 |
contains(item) |
Contém o item? | frutas.contains("Uva") |
true / false |
indexOf(item) |
Posição do item | frutas.indexOf("Uva") |
2 (ou -1) |
isEmpty() |
Está vazia? | frutas.isEmpty() |
true / false |
clear() |
Remove tudo | frutas.clear() |
[] |
// For normal (quando precisa do índice)
for (int i = 0; i < frutas.size(); i++) {
System.out.println("Posição " + i + ": " + frutas.get(i));
}
// For-each (mais limpo)
for (String fruta : frutas) {
System.out.println("Fruta: " + fruta);
}import java.util.Collections;
List<Integer> nums = new ArrayList<>();
// ... adicionar valores ...
Collections.sort(nums); // Ordena
Collections.reverse(nums); // Inverte
Collections.min(nums); // Menor valor
Collections.max(nums); // Maior valor
Array List (ArrayList) Tamanho Fixo (definido na criação) Dinâmico (cresce/diminui) Sintaxe String[] arr = new String[3];List<String> list = new ArrayList<>();Acessar arr[0]list.get(0)Tamanho arr.lengthlist.size()Adicionar Não dá (tamanho fixo) list.add("item")Remover Não dá (tamanho fixo) list.remove("item")Tipos primitivos ✅ Aceita ( int[])❌ Só Wrapper ( Integer)Performance Mais rápido Um pouco mais lento
Quando usar cada um?
- Array: quando o tamanho é conhecido e fixo (ex: dias da semana, meses do ano)
- List: quando o tamanho pode variar (ex: lista de usuários, carrinho de compras)
A LinkedList é outra implementação da interface List, mas funciona de um jeito totalmente diferente do ArrayList. Enquanto o ArrayList usa um array interno (acesso rápido por índice), a LinkedList usa nós encadeados — cada elemento aponta para o próximo e o anterior, como uma corrente.
A grande vantagem: inserir e remover elementos nas pontas (início/fim) é muito rápido. A desvantagem: acessar um elemento pelo índice é lento, porque precisa percorrer a corrente nó por nó.
Além de ser uma List, a LinkedList também implementa a interface Deque (fila de duas pontas), o que permite usá-la como fila (FIFO) ou pilha (LIFO).
📁 Arquivo:
Fundamentos/LinkedList_em_java.java
flowchart LR
subgraph "LinkedList (nós encadeados)"
N1["Nó 1\nTomar café"] -->|próximo| N2["Nó 2\nEstudar Java"]
N2 -->|próximo| N3["Nó 3\nRevisar código"]
N3 -->|próximo| N4["null"]
end
import java.util.LinkedList;
LinkedList<String> tarefas = new LinkedList<>();
tarefas.add("Estudar Java");
tarefas.add("Fazer exercícios");
tarefas.add("Revisar código");Estes métodos não existem no ArrayList:
| Método | O que faz | Exemplo |
|---|---|---|
addFirst(item) |
Adiciona no início | tarefas.addFirst("Café") |
addLast(item) |
Adiciona no final | tarefas.addLast("Dormir") |
getFirst() |
Acessa o primeiro | tarefas.getFirst() |
getLast() |
Acessa o último | tarefas.getLast() |
removeFirst() |
Remove o primeiro | tarefas.removeFirst() |
removeLast() |
Remove o último | tarefas.removeLast() |
FIFO = First In, First Out (primeiro a entrar, primeiro a sair). Como uma fila de banco.
LinkedList<String> fila = new LinkedList<>();
fila.offer("Cliente 1"); // Entra no final
fila.offer("Cliente 2");
fila.peek(); // Olha o primeiro SEM remover → "Cliente 1"
String atendido = fila.poll(); // Remove e retorna o primeiro → "Cliente 1"flowchart LR
subgraph "Fila (FIFO)"
direction LR
ENTRA["offer()"] --> F1["Cliente 1"] --> F2["Cliente 2"] --> F3["Cliente 3"]
F1 --> SAI["poll()"]
end
LIFO = Last In, First Out (último a entrar, primeiro a sair). Como uma pilha de pratos.
LinkedList<String> pilha = new LinkedList<>();
pilha.push("Página 1"); // Empilha (adiciona no topo)
pilha.push("Página 2");
pilha.push("Página 3");
pilha.peek(); // Olha o topo → "Página 3"
String removida = pilha.pop(); // Desempilha → "Página 3"flowchart TD
subgraph "Pilha (LIFO)"
TOPO["push() / pop()"] --> P3["Página 3 ← topo"]
P3 --> P2["Página 2"]
P2 --> P1["Página 1"]
end
ArrayList LinkedList Estrutura interna Array redimensionável Nós encadeados (duplamente) Acesso por índice ( get)⚡ Rápido (O(1)) 🐢 Lento (O(n)) Inserir/remover no meio 🐢 Lento (move elementos) ⚡ Rápido (ajusta ponteiros) Inserir/remover nas pontas 🐢 No início é lento ⚡ Rápido (O(1)) Memória Menos (só os dados) Mais (dados + ponteiros) Usar como Fila/Pilha ❌ Não é ideal ✅ Perfeito
Quando usar cada um?
- ArrayList: maioria dos casos. Acesso rápido, iteração eficiente. Use quando o principal é ler dados.
- LinkedList: quando o principal é inserir/remover frequentemente, especialmente nas pontas. Ou quando precisa de fila/pilha.
Nota: "LinkedArray" não existe em Java. O que existe é
ArrayList(array + list) eLinkedList(lista encadeada). São as duas implementações mais comuns da interfaceList.
Quando a gente usou Arrays.sort() ali em cima, teve que colocar import java.util.Arrays; no topo do arquivo. Isso é porque nem tudo em Java vem "de graça" — a maioria das classes precisa ser importada antes de usar.
📁 Arquivo:
Fundamentos/Imports.java
O Java organiza suas classes em pacotes (packages). Um pacote é basicamente uma pasta que agrupa classes relacionadas. Pra usar uma classe que não tá no seu pacote atual, você precisa dizer pro Java onde encontrar ela com a palavra-chave import.
flowchart TD
JAVA["Java"] --> LANG["java.lang\n(automático)"]
JAVA --> UTIL["java.util"]
JAVA --> IO["java.io"]
JAVA --> TIME["java.time"]
LANG --> STRING["String"]
LANG --> MATH["Math"]
LANG --> INTEGER["Integer"]
UTIL --> SCANNER["Scanner"]
UTIL --> ARRAYS["Arrays"]
UTIL --> ARRAYLIST["ArrayList"]
IO --> FILE["File"]
IO --> BUFFERED["BufferedReader"]
TIME --> LOCAL["LocalDate"]
A sintaxe é simples — o import fica antes da classe e depois do package (se tiver):
import java.util.Scanner; // Importa só o Scanner
import java.util.Arrays; // Importa só o Arrays
import java.time.LocalDate; // Importa só o LocalDateSe você vai usar várias classes do mesmo pacote, pode importar todas de uma vez com o *:
import java.util.*; // Importa TUDO de java.util (Scanner, Arrays, ArrayList, etc.)Classe específica vs Wildcard:
Forma Exemplo Quando usar Específica import java.util.Scanner;Usa 1-2 classes do pacote (mais claro) Wildcard import java.util.*;Usa muitas classes do mesmo pacote
O pacote java.lang é o único que não precisa de import. Ele é importado automaticamente em todo programa Java. Por isso que a gente usa String, Math, System, Integer, etc., sem importar nada:
// Tudo isso funciona SEM import:
String nome = "Nicolau"; // java.lang.String
int numero = Integer.parseInt("42"); // java.lang.Integer
double raiz = Math.sqrt(64); // java.lang.Math
System.out.println("Oi"); // java.lang.System| Pacote | O que tem | Exemplo de classe |
|---|---|---|
java.lang |
Básico (auto-importado) | String, Math, System, Integer |
java.util |
Utilitários e coleções | Scanner, Arrays, ArrayList, HashMap |
java.io |
Entrada/saída de arquivos | File, BufferedReader, FileWriter |
java.time |
Data e hora | LocalDate, LocalTime, LocalDateTime |
java.math |
Precisão numérica | BigDecimal, BigInteger |
Comparação com Python:
Python Java import mathimport java.lang.Math;(desnecessário, já auto-importa)from os import pathimport java.io.File;import *(evitar)import java.util.*;(aceitável)
📁 Exemplos deste tópico:
Controle de fluxo/
Controle de fluxo é o que faz o programa tomar decisões e repetir ações. Sem isso, o código seria só uma sequência reta de instruções.
flowchart TD
CF["Controle de Fluxo"] --> COND["Condicionais"]
CF --> LOOP["Loops"]
CF --> CTRL["Controle de Loop"]
COND --> IF["if / else"]
COND --> SW["switch"]
LOOP --> WH["while"]
LOOP --> FR["for"]
CTRL --> BK["break"]
CTRL --> CT["continue"]
A estrutura if executa um bloco de código se a condição for true. O else trata o caso contrário.
📁 Arquivo:
Controle de fluxo/If_else.java
flowchart TD
START["Início"] --> COND{"condição?"}
COND -->|true| IF["bloco if"]
COND -->|false| ELSE["bloco else"]
IF --> FIM["Fim"]
ELSE --> FIM
int hora = 14;
if (hora < 12) {
System.out.println("Bom dia!");
}int idade = 17;
if (idade >= 18) {
System.out.println("Maior de idade");
} else {
System.out.println("Menor de idade");
}flowchart TD
N["nota = 75"] --> A{">= 90?"}
A -->|Sim| RA["Conceito A"]
A -->|Não| B{">= 80?"}
B -->|Sim| RB["Conceito B"]
B -->|Não| C{">= 70?"}
C -->|Sim| RC["Conceito C ✅"]
C -->|Não| D{">= 60?"}
D -->|Sim| RD["Conceito D"]
D -->|Não| RE["Reprovado"]
int nota = 75;
if (nota >= 90) {
System.out.println("Conceito A");
} else if (nota >= 80) {
System.out.println("Conceito B");
} else if (nota >= 70) {
System.out.println("Conceito C");
} else if (nota >= 60) {
System.out.println("Conceito D");
} else {
System.out.println("Reprovado");
}Um if-else em uma única linha:
// Sintaxe: variavel = (condição) ? valorSeTrue : valorSeFalse;
String resultado = (idade >= 18) ? "Maior" : "Menor";O switch é uma alternativa ao if-else quando você compara uma variável contra vários valores possíveis. Mais limpo e legível.
📁 Arquivo:
Controle de fluxo/Switch_case.java
flowchart TD
VAR["diaSemana = 3"] --> C1{"case 1?"}
C1 -->|Não| C2{"case 2?"}
C2 -->|Não| C3{"case 3?"}
C3 -->|✅ Sim| R3["Quarta-feira → break"]
C3 -->|Não| C4{"case 4?"}
C4 -->|Não| DEF["default"]
int diaSemana = 3;
switch (diaSemana) {
case 1:
System.out.println("Segunda-feira");
break;
case 2:
System.out.println("Terça-feira");
break;
case 3:
System.out.println("Quarta-feira");
break;
// ... mais cases
default:
System.out.println("Dia inválido");
break;
}Importante: sem o
break, o Java executa todos os cases abaixo do que bateu (isso se chama fall-through).
O switch aceita: int, byte, short, char, String e enum.
O while repete um bloco enquanto a condição for true. Cuidado: se a condição nunca virar false, vira loop infinito!
📁 Arquivo:
Controle de fluxo/While_loop.java
flowchart TD
START["i = 1"] --> COND{"i <= 5?"}
COND -->|true| EXEC["println(i)\ni++"]
EXEC --> COND
COND -->|false| FIM["Saiu do loop"]
int i = 1;
while (i <= 5) {
System.out.println("Contando: " + i);
i++; // Sem isso → loop infinito!
}A diferença do do-while é que ele executa o bloco pelo menos uma vez, porque a condição é verificada depois da execução.
int x = 100;
do {
System.out.println("Executou pelo menos uma vez! x = " + x);
} while (x < 5); // false desde o início, mas rodou 1 vezWhile vs Do-While:
Tipo Verifica quando? Executa mínimo whileAntes de executar 0 vezes do-whileDepois de executar 1 vez
O for é usado quando você sabe quantas vezes quer repetir. A sintaxe já inclui inicialização, condição e incremento tudo numa linha.
📁 Arquivo:
Controle de fluxo/For_loop.java
for (início; condição; incremento) { }
↓ ↓ ↓
int i=0 i < 5 i++
// For básico
for (int i = 1; i <= 5; i++) {
System.out.println("Volta " + i);
}
// Contagem regressiva
for (int i = 10; i >= 0; i--) {
System.out.print(i + " ");
}Mais limpo pra percorrer arrays quando não precisamos do índice:
String[] frutas = {"Maçã", "Banana", "Laranja", "Manga"};
for (String fruta : frutas) {
System.out.println("Fruta: " + fruta);
}For vs For-Each:
Tipo Quando usar Sintaxe forPrecisa do índice ou controle fino for (int i = 0; i < n; i++)for-eachSó quer percorrer tudo for (Tipo item : array)
Loop dentro de loop — útil pra matrizes e padrões:
// Tabuada do 3
for (int i = 1; i <= 10; i++) {
System.out.println(3 + " x " + i + " = " + (3 * i));
}Dois comandos pra controlar o fluxo de dentro de um loop:
break: para o loop inteiro e sai delecontinue: pula a iteração atual e vai pra próxima
📁 Arquivo:
Controle de fluxo/Break_continue.java
flowchart LR
subgraph "break"
B1["iteração"] --> B2{"condição?"} -->|true| B3["⛔ SAIR do loop"]
end
subgraph "continue"
C1["iteração"] --> C2{"condição?"} -->|true| C3["⏭️ PRÓXIMA iteração"]
end
for (int i = 1; i <= 10; i++) {
if (i == 5) {
System.out.println("Encontrei o 5! Parando.");
break; // Sai do loop completamente
}
System.out.println("Número: " + i);
}
// Saída: 1, 2, 3, 4, "Encontrei o 5!"// Imprime só os ímpares (pula os pares)
for (int i = 1; i <= 10; i++) {
if (i % 2 == 0) {
continue; // Pula pra próxima iteração
}
System.out.println("Ímpar: " + i);
}
// Saída: 1, 3, 5, 7, 9int soma = 0, num = 1;
while (true) { // Loop "infinito" controlado pelo break
soma += num;
if (soma > 20) {
System.out.println("Soma passou de 20! Soma = " + soma);
break;
}
num++;
}Classes e objetos é o conceito fundamental da programação orientada a objetos (POO ou OOP), que é um paradigma de programação amplamente utilizado em Java. Uma classe é como um molde ou uma planta para criar objetos. Ela define as propriedades (variáveis) e comportamentos (métodos) que os objetos criados a partir dela terão.
Um jeito de se pensar é que uma classe é um template para criar objetos.
flowchart LR
subgraph Classe ["🏭 Classe = Molde"]
P["Propriedades\n(modelo, fabricante, km)"]
M["Métodos\n(exibirInfo, buzinar)"]
end
Classe -->|new| O1["🚗 Objeto 1\nFusca"]
Classe -->|new| O2["🚗 Objeto 2\nCivic"]
Classe -->|new| O3["🚗 Objeto 3\nCorolla"]
Os conceitos de OOP que vou cobrir aqui seguem essa relação:
flowchart TD
CL["Classes & Objetos"] --> AT["Atributos"]
CL --> CO["Construtores"]
CL --> MA["Modificadores de Acesso"]
MA --> EN["Encapsulamento"]
CL --> HE["Herança"]
HE --> PO["Polimorfismo"]
EN --> AB["Abstração"]
HE --> AB
📁 Exemplos deste tópico:
Classes/
Vamos supor que eu tenha que projetar um sistema que coordena informações sobre carros. Como modelo, fabricante, kilometragem, etc. Para agilizar esse processo, eu posso criar uma classe Carro que define as propriedades e comportamentos comuns a todos os carros. Por exemplo:
public class Carro {
String modelo;
String fabricante;
int kilometragem;
void exibirInformacoes() {
System.out.println("Modelo: " + modelo);
System.out.println("Fabricante: " + fabricante);
System.out.println("Kilometragem: " + kilometragem);
}
}No exemplo acima, a classe Carro tem três propriedades: modelo, fabricante, e kilometragem. Ela também tem um método chamado exibirInformacoes(), que imprime as informações do carro no console. Com essa classe, eu posso criar objetos específicos de carros, como um Carro chamado "Fusca" da fabricante "Volkswagen" com uma kilometragem de 100.000 km.
📁 Exemplo básico:
Classes/Basicos_da_classe.java
Atributos (ou variáveis de instância) são as propriedades que definem o estado de um objeto. Quando você cria uma classe, os atributos são as variáveis declaradas dentro dela. Cada objeto criado a partir da classe tem sua própria cópia dos atributos, ou seja, modificar o atributo de um objeto não afeta os outros.
Pra acessar ou modificar um atributo, você usa o operador ponto (.). Por exemplo: carro1.modelo = "Fusca" define o modelo do carro1, e carro1.modelo acessa esse valor. Simples assim.
O importante é entender que os objetos são independentes. Se eu mudar a kilometragem do carro2, a do carro1 continua a mesma. Cada um vive na sua bolha.
📁 Arquivo:
Classes/Atributos.java
public class Atributos {
String modelo;
String fabricante;
int kilometragem;
public static void main(String[] args) {
Atributos carro1 = new Atributos();
carro1.modelo = "Fusca";
carro1.fabricante = "Volkswagen";
carro1.kilometragem = 100000;
Atributos carro2 = new Atributos();
carro2.modelo = "Civic";
carro2.fabricante = "Honda";
carro2.kilometragem = 50000;
System.out.println("Modelo: " + carro1.modelo); // Fusca
System.out.println("Modelo: " + carro2.modelo); // Civic
// Modificando carro2 não afeta carro1
carro2.kilometragem = 55000;
System.out.println(carro1.kilometragem); // 100000 (intacto)
System.out.println(carro2.kilometragem); // 55000
}
}O construtor é um método especial que é chamado automaticamente quando você cria um objeto com new. O nome do construtor deve ser exatamente igual ao nome da classe, e ele não tem tipo de retorno (nem void).
A grande vantagem do construtor é que ao invés de criar o objeto e depois definir cada atributo um por um, você já passa tudo de uma vez na hora de criar. Por exemplo:
Construtores carro1 = new Construtores("Fusca", "Volkswagen", 100000);Dentro do construtor, usamos a palavra-chave this para diferenciar o atributo da classe do parâmetro que estamos recebendo. O this.modelo se refere ao atributo do objeto, enquanto modelo (sem o this) se refere ao parâmetro do construtor.
📁 Arquivo:
Classes/Construtores.java
public class Construtores {
String modelo;
String fabricante;
int kilometragem;
// Construtor: mesmo nome da classe, sem tipo de retorno
Construtores(String modelo, String fabricante, int kilometragem) {
this.modelo = modelo; // this.modelo = atributo do objeto
this.fabricante = fabricante; // modelo (sem this) = parâmetro recebido
this.kilometragem = kilometragem;
}
void exibirInformacoes() {
System.out.println("Modelo: " + modelo);
System.out.println("Fabricante: " + fabricante);
System.out.println("Km: " + kilometragem);
}
public static void main(String[] args) {
// Tudo de uma vez, sem precisar definir atributo por atributo
Construtores carro1 = new Construtores("Fusca", "Volkswagen", 100000);
Construtores carro2 = new Construtores("Civic", "Honda", 50000);
carro1.exibirInformacoes();
carro2.exibirInformacoes();
}
}Modificadores de acesso controlam quem pode ver e usar os atributos e métodos de uma classe. Os principais são:
public: acessível de qualquer lugar, por qualquer classe.private: acessível somente dentro da própria classe. Ninguém de fora consegue acessar diretamente.protected: acessível dentro do mesmo pacote e por subclasses (via herança).- default (sem modificador): acessível apenas dentro do mesmo pacote.
Além dos modificadores de acesso, existem outros modificadores importantes:
static: o atributo/método pertence à classe, não ao objeto. Todos os objetos compartilham o mesmo valor. Você acessa direto pela classe:MinhaClasse.meuAtributo.final: o valor é constante e não pode ser alterado depois de definido. Tentou mudar? O compilador reclama.
Na prática, a regra é: atributos private + métodos public (getters/setters) para acessar. Isso é a base do encapsulamento.
📁 Arquivo:
Classes/Modificadores_de_acesso.java
| Modificador | Mesma classe | Mesmo pacote | Subclasse | Qualquer lugar |
|---|---|---|---|---|
public |
✅ | ✅ | ✅ | ✅ |
protected |
✅ | ✅ | ✅ | ❌ |
| default | ✅ | ✅ | ❌ | ❌ |
private |
✅ | ❌ | ❌ | ❌ |
public class Modificadores_de_acesso {
public String nome = "Nicolau"; // Qualquer classe acessa
private int idade = 22; // Só essa classe acessa
protected String cidade = "São Paulo"; // Mesmo pacote + subclasses
String pais = "Brasil"; // Mesmo pacote (default)
static String especie = "Humano"; // Pertence à classe, não ao objeto
final String tipo = "Estudante"; // Constante, não pode mudar
// Getter: jeito público de acessar um atributo privado
public int getIdade() {
return idade;
}
public static void main(String[] args) {
Modificadores_de_acesso pessoa = new Modificadores_de_acesso();
System.out.println(pessoa.nome); // OK, é public
// System.out.println(pessoa.idade); // ERRO! é private
System.out.println(pessoa.getIdade()); // OK, acessa via getter
System.out.println(Modificadores_de_acesso.especie); // static: acessa pela classe
// pessoa.tipo = "Professor"; // ERRO! final não pode ser reatribuído
}
}Encapsulamento é um dos pilares da programação orientada a objetos. A ideia é simples: esconder os dados internos da classe e controlar o acesso a eles através de métodos.
Na prática, isso significa:
- Atributos são
private(ninguém mexe direto) - Métodos
get(getters) para ler os valores - Métodos
set(setters) para alterar os valores
A grande sacada é que nos setters você pode colocar validações. Por exemplo, impedir que alguém defina uma idade negativa ou um salário abaixo de zero. Em vez de confiar em quem usa a classe, você controla tudo dentro dela. É como ter uma porta com tranca: o dado está lá dentro, mas só entra quem segue as regras.
flowchart LR
EXT["🌍 Código externo"] -->|"setIdade(25)"| SET["✅ Setter\n(valida)"]
SET --> PRIV["🔒 private idade"]
PRIV --> GET["📖 Getter"]
GET -->|"getIdade()"| EXT
EXT -.-x|"objeto.idade = -5"| PRIV
📁 Arquivo:
Classes/Encapsulamento.java
public class Encapsulamento {
private String nome;
private int idade;
Encapsulamento(String nome, int idade) {
this.nome = nome;
setIdade(idade); // Já valida na criação
}
public String getNome() { return nome; }
public void setNome(String nome) { this.nome = nome; }
public int getIdade() { return idade; }
// Setter com validação: impede valores absurdos
public void setIdade(int idade) {
if (idade > 0 && idade < 150) {
this.idade = idade;
} else {
System.out.println("Idade inválida: " + idade);
}
}
public static void main(String[] args) {
Encapsulamento pessoa = new Encapsulamento("Nicolau", 22);
System.out.println(pessoa.getNome()); // Nicolau
System.out.println(pessoa.getIdade()); // 22
pessoa.setIdade(-5); // "Idade inválida: -5"
System.out.println(pessoa.getIdade()); // 22 (não mudou!)
}
}Herança é o mecanismo que permite criar uma nova classe com base em uma classe existente, herdando todos os seus atributos e métodos. A classe que é herdada é chamada de superclasse (ou classe pai), e a nova classe é chamada de subclasse (ou classe filha).
A palavra-chave extends é usada para indicar a herança:
class Carro extends Veiculo {
// Carro herda tudo de Veiculo e pode adicionar coisas próprias
}Dentro do construtor da subclasse, usamos super() para chamar o construtor da classe pai e inicializar os atributos herdados. A subclasse pode usar os métodos da classe pai diretamente, como se fossem dela.
A vantagem é evitar repetição de código. Se Carro e Moto têm atributos em comum (marca, ano), coloca tudo em Veiculo e ambos herdam. Aí cada um adiciona só o que é específico dele.
classDiagram
class Veiculo {
String marca
int ano
buzinar()
exibirInfo()
}
class Carro {
int portas
}
class Moto {
boolean temBau
}
Veiculo <|-- Carro : extends
Veiculo <|-- Moto : extends
📁 Arquivo:
Classes/Heranca.java
class Veiculo {
String marca;
int ano;
Veiculo(String marca, int ano) {
this.marca = marca;
this.ano = ano;
}
void buzinar() {
System.out.println("BEEP BEEP!");
}
}
class Carro extends Veiculo {
int portas;
Carro(String marca, int ano, int portas) {
super(marca, ano); // Chama o construtor da classe pai
this.portas = portas;
}
}
class Moto extends Veiculo {
boolean temBau;
Moto(String marca, int ano, boolean temBau) {
super(marca, ano);
this.temBau = temBau;
}
}Detalhe importante: em Java, uma classe só pode herdar de uma superclasse. Herança múltipla (herdar de duas classes ao mesmo tempo) não é permitida, mas isso se resolve com interfaces.
Polimorfismo significa "muitas formas". É a capacidade de um mesmo método se comportar de maneiras diferentes dependendo de qual classe o implementa. Está diretamente ligado à herança.
Na prática: a classe pai define um método, e cada subclasse sobrescreve (@Override) esse método com sua própria versão. O Java sabe qual versão chamar com base no tipo real do objeto.
classDiagram
class Animal {
String nome
fazerSom() "faz algum som..."
}
class Cachorro {
fazerSom() "Au au!"
}
class Gato {
fazerSom() "Miau!"
}
class Pato {
fazerSom() "Quack!"
}
Animal <|-- Cachorro
Animal <|-- Gato
Animal <|-- Pato
📁 Arquivo:
Classes/Polimorfismo.java
Animal animal1 = new Cachorro("Rex");
animal1.fazerSom(); // Chama a versão de Cachorro: "Au au!"Mesmo que a variável seja do tipo Animal, o objeto é um Cachorro, e o Java é inteligente o suficiente pra chamar o método correto. Isso é o polimorfismo em tempo de execução.
Isso é muito útil porque permite tratar objetos diferentes de forma uniforme. Você pode ter um array de Animal[] com cachorro, gato e pato dentro, e chamar fazerSom() em todos sem saber o tipo específico de cada um.
class Animal {
String nome;
Animal(String nome) { this.nome = nome; }
void fazerSom() {
System.out.println(nome + " faz algum som...");
}
}
class Cachorro extends Animal {
Cachorro(String nome) { super(nome); }
@Override
void fazerSom() {
System.out.println(nome + " faz: Au au!");
}
}
class Gato extends Animal {
Gato(String nome) { super(nome); }
@Override
void fazerSom() {
System.out.println(nome + " faz: Miau!");
}
}
// A mágica: variável do tipo Animal, objeto do tipo específico
Animal[] animais = { new Cachorro("Rex"), new Gato("Mimi") };
for (Animal a : animais) {
a.fazerSom(); // Java chama a versão correta de cada um
}
// Rex faz: Au au!
// Mimi faz: Miau!Abstração é esconder a complexidade e mostrar só o que é essencial. Em Java, usamos classes abstratas e interfaces pra isso.
classDiagram
class Forma {
<<abstract>>
String cor
calcularArea()* double
exibir() void
}
class Desenhavel {
<<interface>>
desenhar() void
}
class Redimensionavel {
<<interface>>
redimensionar(fator) void
}
class Circulo {
double raio
calcularArea() double
}
class Retangulo {
double largura
double altura
calcularArea() double
}
class Quadrado {
double lado
calcularArea() double
desenhar() void
redimensionar(fator) void
}
Forma <|-- Circulo
Forma <|-- Retangulo
Forma <|-- Quadrado
Desenhavel <|.. Quadrado : implements
Redimensionavel <|.. Quadrado : implements
📁 Arquivo:
Classes/Abstracao.java
Classe abstrata:
- Não pode ser instanciada diretamente (não dá pra fazer
new Forma()) - Pode ter métodos abstratos (só a assinatura, sem corpo) que as subclasses são obrigadas a implementar
- Pode ter métodos concretos (com corpo) que são herdados normalmente
abstract class Forma {
abstract double calcularArea(); // Subclasses TÊM QUE implementar
void exibir() { ... } // Método concreto, herdado normalmente
}Interface:
- É como um "contrato" que a classe se compromete a cumprir
- Todos os métodos são abstratos por padrão
- Uma classe pode implementar múltiplas interfaces (resolve o problema da herança múltipla!)
- Usa a palavra-chave
implements
class Quadrado extends Forma implements Desenhavel, Redimensionavel {
// Tem que implementar calcularArea(), desenhar() e redimensionar()
}A diferença entre classe abstrata e interface: a classe abstrata pode ter atributos e métodos completos, enquanto a interface é puramente um contrato de "quais métodos você precisa ter". Use classe abstrata quando as subclasses compartilham código em comum, e interface quando classes totalmente diferentes precisam seguir o mesmo contrato.
Resumo rápido: Classe Abstrata vs Interface
Classe Abstrata Interface Instanciar? ❌ Não ❌ Não Métodos concretos? ✅ Sim ❌ Não (só abstratos) Atributos? ✅ Sim ❌ Só constantes Herança múltipla? ❌ Só uma classe ✅ Várias interfaces Palavra-chave extendsimplements
Até agora, a gente viu métodos mais como "funções soltas" na seção de Métodos. Mas dentro de uma classe, métodos têm papéis mais específicos. Aqui o foco é: como métodos funcionam dentro de uma classe, incluindo a diferença entre métodos de instância e métodos static, sobrecarga (overloading), o uso do this, e métodos privados.
flowchart TD
MC["Métodos de Classe"] --> MI["Métodos de instância\n(precisam de objeto)"]
MC --> MS["Métodos static\n(pertencem à classe)"]
MC --> SO["Sobrecarga\n(mesmo nome, params diferentes)"]
MC --> MP["Métodos privados\n(uso interno)"]
MI --> THIS["this: referência\nao próprio objeto"]
📁 Arquivo:
Classes/Metodos_de_classe.java
Métodos de instância vs static:
Métodos de instância precisam de um objeto pra serem chamados. Eles acessam os atributos do objeto normalmente.
Métodos static pertencem à classe, não ao objeto. São chamados pela classe diretamente e não podem acessar atributos de instância.
class Personagem {
String nome;
private static int totalPersonagens = 0;
Personagem(String nome) {
this.nome = nome;
totalPersonagens++;
}
// Método de instância: precisa de objeto
void apresentar() {
System.out.println("Eu sou " + nome);
}
// Método static: pertence à classe
static int getTotal() {
return totalPersonagens;
// nome não funciona aqui! static não acessa instância
}
}
// Uso:
Personagem p = new Personagem("Arthas");
p.apresentar(); // Método de instância: pelo objeto
Personagem.getTotal(); // Método static: pela classe
Método de Instância Método staticPrecisa de objeto? ✅ Sim ❌ Não Acessa atributos? ✅ Sim ❌ Só static Como chamar? objeto.metodo()Classe.metodo()Exemplo getNome(),atacar()main(),Math.sqrt()
O this dentro de métodos:
A palavra-chave this é uma referência ao próprio objeto. Serve pra duas coisas principais:
- Diferenciar atributo de parâmetro (quando têm o mesmo nome)
- Chamar outros métodos da mesma instância
void apresentar() {
// this.getNome() e getNome() são a mesma coisa aqui dentro
System.out.println("Eu sou " + this.getNome());
}Sobrecarga de métodos (Method Overloading):
Sobrecarga é ter vários métodos com o mesmo nome, mas com parâmetros diferentes. O Java decide qual chamar com base nos argumentos.
void curar() {
// Cura padrão
}
void curar(int bonus) {
// Cura com bônus
}
void curar(int bonus, String fonte) {
// Cura com bônus e origem
}
// O Java escolhe sozinho:
personagem.curar(); // chama curar()
personagem.curar(3); // chama curar(int)
personagem.curar(2, "Poção"); // chama curar(int, String)
⚠️ Sobrecarga ≠ Sobrescrita:
Sobrecarga (Overloading) Sobrescrita (Override) Onde? Mesma classe Subclasse Nome? Mesmo Mesmo Parâmetros? Diferentes Iguais Anotação? Nenhuma @Override
Métodos privados:
Métodos private só podem ser chamados dentro da própria classe. São usados como métodos auxiliares internos.
private int calcularDano() {
return nivel * 10;
}
void atacar() {
// calcularDano() é usado internamente, ninguém de fora precisa saber dele
System.out.println(nome + " ataca com força " + calcularDano() + "!");
}
// De fora:
personagem.atacar(); // OK
personagem.calcularDano(); // ERRO! privateUm método é um bloco de codigo que roda apenas quando é chamado. Você pode passar dados, conhecidos como parâmetros, para um método. Um método pode retornar dados como resultado.
Além disso, métodos são conhecidos por fazer ações específicas, então eles são conhecidos também como funções.
Por que eu deveria utilizar métodos? Para reutilizar código, definir ele uma vez e usar quantas vezes precisar. Além de ser um ótimo e simples jeito de organizar o seu código em "código limpo"
flowchart LR
MAIN["main()"] -->|chama| M1["metodo1()"]
MAIN -->|chama| M2["metodo2()"]
MAIN -->|chama| M3["metodo3()"]
M1 -->|retorna| MAIN
M2 -->|retorna| MAIN
M3 -->|retorna| MAIN
📁 Exemplos deste tópico:
Métodos e funções/
Um método é criado dentro de uma classe. Ele tem um nome, pode ter parâmetros, e pode retornar um valor. A sintaxe básica para criar um método em Java é a seguinte:
public class MinhaClasse {
static void meuMetodo() {
// código do método
}
}MeuMetodoé o nome do método. Você pode escolher qualquer nome que siga as regras de nomenclatura do Java.staticé um modificador que indica que o método pertence à classe, e não a uma instância específica da classe. Isso significa que você pode chamar o método sem criar um objeto da classe.voidé o tipo de retorno do método, indicando que ele não retorna nenhum valor. Se o método retornar um valor, você deve especificar o tipo de retorno, comoint,String, etc. Vou falar mais sobre isso em outro diretório.
Anatomia de um método:
static void meuMetodo () │ │ │ │ │ │ │ └─ parâmetros (vazio nesse caso) │ │ └───────── nome do método │ └──────────────── tipo de retorno (void = nada) └───────────────────── modificador (pertence à classe)
Olha o exemplo abaixo:
📁 Arquivo:
Métodos e funções/Metodo_simples.java
public class Main {
static void myMethod() {
System.out.println("I just got executed!"); // Cria um método simples que imprime um texto
}
public static void main(String[] args) {
myMethod(); // Chama o método myMethod para executar o código dentro dele
}
}Na primeira parte, nós criamos um método chamado myMethod que simplesmente imprime uma mensagem no console. Ele é um método estático, o que significa que podemos chamá-lo diretamente sem precisar criar uma instância da classe Main.
Logo abaixo, no método main, que é o ponto de entrada do programa, nós chamamos myMethod(). Isso faz com que o código dentro de myMethod seja executado, e a mensagem "I just got executed!" seja impressa no console.
Lembrando que métodos podem ser chamados diversas vezes caso necessário
public class Main {
static void myMethod() {
System.out.println("I just got executed!");
}
public static void main(String[] args) {
myMethod();
myMethod();
myMethod();
}
}Informações podem ser passadas para métodos através de parâmetros. Parãmetros vão funcionar como váriaveis dentro do método. Parâmetros são especificados dentro dos parênteses na definição do método. Você pode ter quantos parâmetros quiser, contanto que as separe com uma vírgula. Por exemplo:
public class Main {
static void myMethod(String fname) {
System.out.println(fname + " Refsnes");
}
public static void main(String[] args) {
myMethod("Liam");
myMethod("Jenny");
myMethod("Anja");
}
}No método acima, criamos uma váriavel string chamada fname como parâmetro do método myMethod. Quando chamamos myMethod, passamos um argumento, que é o valor que queremos que o parâmetro receba. No exemplo, passamos "Liam", "Jenny", e "Anja" como argumentos para myMethod. O método então imprime o nome seguido de "Refsnes" no console.
Quando um parâmetro é passando para um método, ele é chamado de argumento. No exemplo acima, "Liam", "Jenny", e "Anja" são argumentos que estão sendo passados para o método myMethod.
Parâmetro vs Argumento:
Conceito O que é Onde aparece Parâmetro A variável na definição do método static void myMethod(String fname)Argumento O valor passado na chamada myMethod("Liam")
flowchart LR
CALL["myMethod(#quot;Liam#quot;)"] -->|Liam → fname| METHOD["static void myMethod(String fname)"]
METHOD --> PRINT["println(fname + #quot;Refsnes#quot;)"]
PRINT --> OUTPUT["Liam Refsnes"]
Como citado anteriormente, você pode ter quantos parâmetros quiser em um método, contanto que os separe com vírgula. Por exemplo:
📁 Arquivo:
Métodos e funções/Multiplos_parametros.java
public class Main {
static void myMethod(String fname, int age) {
System.out.println(fname + " is " + age);
}
public static void main(String[] args) {
myMethod("Liam", 5);
myMethod("Jenny", 8);
myMethod("Anja", 31);
}
}Separamos fnamee age com vírgulas, agora a função recebe 2 parâmetros, o que aumenta por consequência a quantidade de argumentos
📁 Arquivo:
Métodos e funções/Metodos_com_decisoes.java
flowchart TD
START["checkAge(age)"] --> IF{"age < 18?"}
IF -->|✅ Sim| DENIED["Access denied"]
IF -->|❌ Não| GRANTED["Access granted"]
public class Metodos_com_decisoes {
static void checkAge(int age) {
if (age < 18) {
System.out.println("Access denied - You are not old enough!");
} else {
System.out.println("Access granted - You are old enough!");
}
}
public static void main(String[] args) {
checkAge(20);
}
}Acima temos um método chamado checkAge que recebe um parâmetro inteiro age. Dentro do método, utilizamos uma estrutura de decisão if-else para verificar se a idade é menor que 18. Se for, o programa imprime "Access denied - You are not old enough!" no console. Caso contrário, ele imprime "Access granted - You are old enough!".
Não tem muito o que detalhar deste código, é só mais um exemplo de como métodos podem ser utilizados para organizar o código e realizar ações específicas, nesse caso, verificar a idade e imprimir uma mensagem apropriada.
Métodos não precisam retornar um valor, como vimos no exemplo do checkAge, mas eles também podem retornar um valor. Para isso, você precisa especificar o tipo de retorno do método e usar a palavra-chave return para retornar o valor desejado. Por exemplo:
flowchart LR
CALL["myMethod(3)"] --> CALC["return 5 + 3"]
CALC --> RESULT["8"]
RESULT --> PRINT["println(8)"]
voidvs tipo de retorno:
Tipo de retorno O que faz Exemplo voidNão retorna nada, só executa static void checkAge(int age)intRetorna um número inteiro static int myMethod(int x)StringRetorna um texto static String getName()booleanRetorna verdadeiro/falso static boolean isAdult(int age)doubleRetorna um decimal static double calcMedia(int a, int b)
📁 Arquivo:
Métodos e funções/Return.java
public class Main {
static int myMethod(int x) {
return 5 + x;
}
public static void main(String[] args) {
System.out.println(myMethod(3));
}
}No exemplo acima, o método myMethod tem um tipo de retorno int, o que significa que ele retorna um valor inteiro. Dentro do método, usamos a palavra-chave return para retornar o resultado da expressão 5 + x. Quando chamamos myMethod(3), ele retorna 8, que é então impresso no console.
📁 Exemplos deste tópico:
Input/
Até agora, todos os nossos programas só imprimiam coisas no console. Mas e quando a gente quer receber informação do usuário ou ler/escrever arquivos? O Java tem duas grandes ferramentas pra isso:
Scanner— pra ler dados digitados pelo teclado (input do usuário)- I/O Streams — pra ler e escrever dados em arquivos (entrada e saída de dados)
flowchart TD
IO["Input / Output em Java"] --> SC["Scanner"]
IO --> STREAMS["I/O Streams"]
SC --> TECLADO["⌨️ Lê do teclado"]
STREAMS --> IS["InputStream\n(lê bytes)"]
STREAMS --> OS["OutputStream\n(escreve bytes)"]
STREAMS --> RW["Reader / Writer\n(lê/escreve texto)"]
Pra usar o Scanner, primeiro você precisa importar ele, porque ele não faz parte do pacote padrão. A importação fica no topo do arquivo, antes da classe:
import java.util.Scanner;Depois, você cria um objeto Scanner que lê do System.in (a entrada padrão, ou seja, o teclado):
Scanner scanner = new Scanner(System.in);Agora é só usar os métodos do scanner pra ler diferentes tipos de dados:
📁 Arquivo:
Input/Input_de_dados.java
import java.util.Scanner;
public class Input_de_dados {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("Digite seu nome: ");
String nome = scanner.nextLine();
System.out.println("Olá, " + nome + "!");
System.out.print("Digite sua idade: ");
int idade = scanner.nextInt();
System.out.println("Você tem " + idade + " anos.");
scanner.close();
}
}Detalhe: usamos
System.out.print()(sem oln) pra que o cursor fique na mesma linha, esperando o usuário digitar.
Cada tipo de dado tem seu próprio método de leitura no Scanner:
| Método | Tipo que lê | Exemplo |
|---|---|---|
nextLine() |
String (linha inteira) |
scanner.nextLine() |
next() |
String (uma palavra só) |
scanner.next() |
nextInt() |
int |
scanner.nextInt() |
nextDouble() |
double |
scanner.nextDouble() |
nextFloat() |
float |
scanner.nextFloat() |
nextLong() |
long |
scanner.nextLong() |
nextBoolean() |
boolean |
scanner.nextBoolean() |
nextByte() |
byte |
scanner.nextByte() |
nextShort() |
short |
scanner.nextShort() |
next()vsnextLine():
Método O que lê Exemplo de input: "João Silva"next()Até o primeiro espaço "João"nextLine()A linha inteira "João Silva"
Depois de usar o Scanner, é boa prática fechar ele pra liberar recursos:
scanner.close();Esse é um dos bugs mais comuns pra quem tá começando com Java. Quando você usa nextInt(), nextDouble(), ou qualquer outro método que lê um tipo primitivo, ele não consome a quebra de linha (\n) que o usuário digitou ao apertar Enter. Aí, quando você chama nextLine() logo depois, ele lê essa quebra de linha vazia em vez do texto que você queria.
flowchart TD
INPUT["Usuário digita: 22 ⏎"] --> NI["nextInt() lê: 22"]
NI --> BUFFER["Buffer ainda tem: \\n"]
BUFFER --> NL["nextLine() lê: \\n (vazio!)"]
NL --> SKIP["❌ Pulou a leitura!"]
O problema:
System.out.print("Idade: ");
int idade = scanner.nextInt(); // Lê 22, mas o \n fica no buffer
System.out.print("Cidade: ");
String cidade = scanner.nextLine(); // Lê o \n vazio! Não espera digitarA solução: adicionar um scanner.nextLine() extra pra consumir o \n pendente:
System.out.print("Idade: ");
int idade = scanner.nextInt();
scanner.nextLine(); // ← Limpa o \n do buffer
System.out.print("Cidade: ");
String cidade = scanner.nextLine(); // Agora funciona!Regra prática: sempre que usar
nextInt(),nextDouble(), etc., e precisar de umnextLine()depois, coloque umscanner.nextLine()extra entre eles pra limpar o buffer.
📁 Exemplos deste tópico:
Organização de projeto/
Até agora, nos exemplos de herança e polimorfismo, a gente jogou várias classes no mesmo arquivo. Isso funciona, mas conforme o projeto cresce, vira uma bagunça impossível de manter. Organização é o que separa um script de estudo de um projeto real.
flowchart LR
ANTES["📄 Tudo_junto.java\n(6 classes empilhadas)"] -->|refatora| DEPOIS["📁 Projeto organizado\n(1 classe por arquivo)"]
DEPOIS --> MOD["📁 modelo/\nAnimal, Cachorro, Gato..."]
DEPOIS --> SRV["📁 servico/\nAnimalServico"]
DEPOIS --> MAIN["📄 Main.java"]
Em Java, só pode ter UMA classe pública por arquivo, e o nome do arquivo tem que ser igual ao da classe pública. Classes sem public podem ficar no mesmo arquivo (como fizemos em Heranca.java e Polimorfismo.java), mas isso não escala.
📁 Exemplo do problema:
Organização de projeto/Tudo_junto.java
No Tudo_junto.java, temos 6 classes (Animal, Cachorro, Gato, Passaro, AnimalServico e Tudo_junto) todas empilhadas num arquivo só. Funciona? Funciona. Mas imagina isso com 20, 50, 100 classes... é como guardar todas as roupas num saco só.
Packages são o jeito do Java de organizar classes em grupos lógicos, como pastas no seu computador. Na prática, um pacote corresponde a uma pasta no sistema de arquivos.
Pra declarar que uma classe pertence a um pacote, usa a palavra-chave package na primeira linha do arquivo:
package com_organizacao.modelo;
public class Animal {
// ...
}Isso significa que o arquivo Animal.java mora na pasta com_organizacao/modelo/.
Quando uma classe precisa usar outra que está em um pacote diferente, ela precisa importar:
package com_organizacao;
import com_organizacao.modelo.Cachorro; // Importa uma classe específica
import com_organizacao.modelo.Gato;
import com_organizacao.servico.AnimalServico;
public class Main {
public static void main(String[] args) {
AnimalServico servico = new AnimalServico(10);
servico.adicionar(new Cachorro("Rex", "Pastor Alemão"));
servico.adicionar(new Gato("Mimi", true));
servico.listarTodos();
}
}Detalhe importante: quando você organiza em pacotes, as classes precisam ser
publicpra serem acessíveis de outros pacotes. Lembra dos modificadores de acesso? É aqui que eles brilham.
flowchart TD
MAIN["Main.java\npackage com_organizacao"] -->|import| CACH["Cachorro.java\npackage com_organizacao.modelo"]
MAIN -->|import| GATO["Gato.java\npackage com_organizacao.modelo"]
MAIN -->|import| SRV["AnimalServico.java\npackage com_organizacao.servico"]
SRV -->|import| ANIMAL["Animal.java\npackage com_organizacao.modelo"]
O exemplo organizado fica assim:
Organização de projeto/
├── Tudo_junto.java ← O problema (tudo junto)
└── com_organizacao/ ← O projeto organizado
├── Main.java ← Ponto de entrada
├── modelo/ ← Pacote das classes de dados
│ ├── Animal.java ← Classe base
│ ├── Cachorro.java ← Subclasse
│ ├── Gato.java ← Subclasse
│ └── Passaro.java ← Subclasse
└── servico/ ← Pacote da lógica de negócio
└── AnimalServico.java ← Gerencia os animais
📁 Exemplo organizado:
Organização de projeto/com_organizacao/
Cada classe tem seu próprio arquivo, cada pacote tem sua própria pasta. O Main.java ficou pequeno e limpo — ele só importa o que precisa, cria os objetos e chama os métodos. Cada classe cuida do seu próprio código.
Comparação: antes vs depois
Antes (tudo junto) Depois (organizado) 6 classes num arquivo 1 classe por arquivo Sem publicnas classesTodas public(acessíveis entre pacotes)Sem packageCada arquivo declara seu pacote Sem importImports explícitos Difícil de encontrar algo Estrutura de pastas clara
Quando você usa pacotes, a compilação e execução mudam um pouco. Você precisa compilar a partir da pasta pai dos pacotes e referenciar a classe pelo nome completo:
# A partir da pasta "Organização de projeto/":
javac com_organizacao/Main.java com_organizacao/modelo/*.java com_organizacao/servico/*.java
# Pra rodar, usa o nome completo do pacote:
java com_organizacao.MainO javac compila todos os arquivos necessários, e o java executa usando o nome completo da classe (pacote.Classe).
flowchart LR
A["javac com_organizacao/...\n(compila tudo)"] --> B["⚙️ Gera .class\nem cada pasta"]
B --> C["java com_organizacao.Main\n(executa pelo nome completo)"]
O Java tem convenções fortes pra nomear pacotes e organizar projetos:
| Elemento | Convenção | Exemplo |
|---|---|---|
| Pacote | tudo minúsculo, sem espaços | modelo, servico, com.empresa.projeto |
| Classe | PascalCase | Animal, AnimalServico |
| Arquivo | Mesmo nome da classe pública | Animal.java, AnimalServico.java |
| Pasta | Mesmo nome do pacote | modelo/, servico/ |
Em projetos do mundo real, pacotes costumam seguir o domínio reverso da empresa: com.google.maps, br.com.empresa.sistema. Nos nossos exemplos, mantemos simples com com_organizacao.modelo e com_organizacao.servico.
Regra prática: se você tem mais de 3 classes num arquivo, é hora de separar. Se você tem mais de 5 arquivos soltos, é hora de criar pacotes.
📁 Exemplos deste tópico:
Tratamento de erros/
Até agora, se qualquer coisa dava errado no código (dividir por zero, acessar um índice que não existe, etc.), o programa simplesmente quebrava e parava de rodar. No mundo real, isso não pode acontecer — imagina um app de banco que crasha porque alguém digitou uma letra no campo de valor? O tratamento de erros (ou exception handling) é o mecanismo do Java pra capturar esses erros, tratar eles, e deixar o programa continuar rodando.
flowchart TD
TE["Tratamento de Erros"] --> TC["try-catch"]
TE --> FIN["finally"]
TE --> TT["throw / throws"]
TE --> CUSTOM["Exceções customizadas"]
TC --> MULTI["Múltiplos catch"]
TT --> CHECKED["Checked Exceptions"]
TT --> UNCHECKED["Unchecked Exceptions"]
O try-catch é a base de tudo. Você coloca o código que pode dar erro dentro do try, e o tratamento no catch. Se der erro, o Java pula pro catch em vez de crashar.
📁 Arquivo:
Tratamento de erros/Try_catch.java
flowchart TD
TRY["try { código arriscado }"] --> ERR{"Deu erro?"}
ERR -->|Sim| CATCH["catch: trata o erro"]
ERR -->|Não| CONT["Continua normal"]
CATCH --> CONT
try {
int resultado = 10 / 0; // ArithmeticException!
} catch (ArithmeticException e) {
System.out.println("Erro: não pode dividir por zero!");
System.out.println("Mensagem: " + e.getMessage());
}
System.out.println("Programa continua rodando!"); // Não crashouSem o try-catch, a divisão por zero derrubaria o programa inteiro. Com ele, o erro é capturado, tratado, e o código continua executando normalmente.
Cada tipo de erro pode ser tratado de forma diferente. Os catches são verificados em ordem, de cima pra baixo. Coloque os mais específicos primeiro e o mais genérico (Exception) por último:
try {
int[] numeros = {1, 2, 3};
System.out.println(numeros[10]); // Erro!
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Índice fora dos limites!");
} catch (ArithmeticException e) {
System.out.println("Problema matemático!");
} catch (Exception e) {
// Pega QUALQUER erro que não foi capturado acima
System.out.println("Erro inesperado: " + e.getMessage());
}flowchart TD
ERR["Exceção lançada"] --> C1{"ArrayIndex\nOutOfBounds?"}
C1 -->|Sim| T1["Trata índice"]
C1 -->|Não| C2{"Arithmetic\nException?"}
C2 -->|Sim| T2["Trata matemático"]
C2 -->|Não| C3{"Exception\n(genérica)?"}
C3 -->|Sim| T3["Trata genérico"]
| Exceção | Quando acontece | Exemplo |
|---|---|---|
ArithmeticException |
Divisão por zero | 10 / 0 |
ArrayIndexOutOfBoundsException |
Índice fora do array | arr[10] (array de 3) |
NullPointerException |
Acessar algo que é null |
null.length() |
NumberFormatException |
Converter texto inválido | Integer.parseInt("abc") |
StringIndexOutOfBoundsException |
Posição inválida na string | "Oi".charAt(10) |
IllegalArgumentException |
Argumento inválido para método | Valor negativo onde não pode |
ClassCastException |
Cast inválido entre tipos | (String) objetoInteiro |
O bloco finally SEMPRE executa, aconteça o que acontecer — deu erro ou não. É o lugar perfeito pra liberar recursos (fechar arquivos, conexões de banco, etc.).
try {
int resultado = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("Erro capturado!");
} finally {
System.out.println("Isso SEMPRE executa!"); // Sempre aparece
}flowchart TD
TRY["try"] --> ERR{"Erro?"}
ERR -->|Sim| CATCH["catch"]
ERR -->|Não| OK["✅ Sucesso"]
CATCH --> FIN["finally\n(SEMPRE executa)"]
OK --> FIN
FIN --> CONT["Continua o programa"]
Quando usar finally?
- Fechar arquivos, streams, conexões
- Liberar recursos que foram abertos no
try- Qualquer "limpeza" que precisa acontecer independente do resultado
Até agora, os erros aconteciam sozinhos (divisão por zero, índice inválido). Mas e quando você quer lançar um erro propositalmente? É aí que entram throw e throws.
📁 Arquivo:
Tratamento de erros/Throw_throws.java
throw: lança uma exceção manualmente, dentro do métodothrows: declara na assinatura do método que ele pode lançar uma exceção
flowchart LR
subgraph "throw (dentro do método)"
CODE["if (idade < 0)"] --> THROW["throw new\nIllegalArgumentException()"]
end
subgraph "throws (na assinatura)"
SIG["void lerArquivo()\nthrows IOException"] --> AVISA["Avisa: pode dar erro!\nQuem chamar TRATA."]
end
static void verificarIdade(int idade) {
if (idade < 0) {
throw new IllegalArgumentException("Idade não pode ser negativa!");
}
if (idade < 18) {
throw new ArithmeticException("Menor de idade!");
}
System.out.println("Acesso permitido!");
}
// Uso:
try {
verificarIdade(-5); // Lança IllegalArgumentException
} catch (IllegalArgumentException e) {
System.out.println("Erro: " + e.getMessage());
}Quando um método declara throws, quem chamar é obrigado a tratar com try-catch:
// O "throws" avisa: esse método pode lançar IOException
static void lerArquivo(String caminho) throws java.io.IOException {
if (caminho == null || caminho.isEmpty()) {
throw new java.io.IOException("Caminho inválido!");
}
System.out.println("Lendo arquivo: " + caminho);
}
// Quem chama PRECISA tratar:
try {
lerArquivo("dados.txt");
} catch (java.io.IOException e) {
System.out.println("Erro: " + e.getMessage());
}Throw vs Throws:
throwthrowsO que faz Lança a exceção Declara que pode lançar Onde usa Dentro do método Na assinatura do método Sintaxe throw new Exception()void metodo() throws ExceptionQuem trata? Quem chamou (ou o próprio método) Quem chamou é obrigado a tratar
As exceções padrão do Java cobrem muitos casos, mas às vezes você precisa de algo mais específico pro seu domínio. Aí você cria suas próprias exceções!
Basta criar uma classe que extends Exception (checked) ou extends RuntimeException (unchecked):
// Exceção checked: OBRIGA quem chama a tratar
class SaldoInsuficienteException extends Exception {
private double saldoAtual;
private double valorSolicitado;
SaldoInsuficienteException(double saldoAtual, double valorSolicitado) {
super("Saldo insuficiente! Saldo: R$" + saldoAtual +
" | Solicitado: R$" + valorSolicitado);
this.saldoAtual = saldoAtual;
this.valorSolicitado = valorSolicitado;
}
}
// Exceção unchecked: NÃO obriga (mas é boa prática tratar)
class IdadeInvalidaException extends RuntimeException {
IdadeInvalidaException(int idade) {
super("Idade inválida: " + idade);
}
}Usando:
class ContaBancaria {
private double saldo;
void sacar(double valor) throws SaldoInsuficienteException {
if (valor > saldo) {
throw new SaldoInsuficienteException(saldo, valor);
}
saldo -= valor;
}
}
// Uso:
try {
conta.sacar(5000); // Saldo insuficiente!
} catch (SaldoInsuficienteException e) {
System.out.println("Erro: " + e.getMessage());
}Quando criar exceções customizadas?
- Quando as exceções padrão não expressam bem o problema
- Quando você quer carregar informações extras no erro (ex: saldo atual, valor solicitado)
- Em projetos maiores, pra manter a consistência dos erros do sistema
Essa é uma distinção importante em Java que não existe em muitas outras linguagens:
flowchart TD
THROWABLE["Throwable"] --> ERROR["Error\n(problemas graves da JVM)"]
THROWABLE --> EXCEPTION["Exception"]
EXCEPTION --> CHECKED["Checked Exceptions\n(extends Exception)"]
EXCEPTION --> UNCHECKED["Unchecked Exceptions\n(extends RuntimeException)"]
CHECKED --> IO["IOException"]
CHECKED --> SQL["SQLException"]
CHECKED --> CUSTOM1["SaldoInsuficienteException"]
UNCHECKED --> NPE["NullPointerException"]
UNCHECKED --> IAE["IllegalArgumentException"]
UNCHECKED --> AIOOB["ArrayIndexOutOfBoundsException"]
Checked Unchecked Herda de ExceptionRuntimeExceptionCompilador obriga tratar? ✅ Sim ❌ Não Quando acontece? Problemas previsíveis (I/O, rede) Erros de programação (null, índice) throwsna assinatura?✅ Obrigatório ❌ Opcional Exemplo IOException,SQLExceptionNullPointerException,ArithmeticException
Comparação com Python:
Python Java try: ... except:try { ... } catch { ... }raise ValueError()throw new IllegalArgumentException()finally:finally { }Sem checked exceptions Tem checked e unchecked class MeuErro(Exception):class MeuErro extends Exception { }