Java¶
Java é uma linguagem orientada a objetos, fortemente tipada, e compilada para um formato intermediário (bytecode) que roda sobre uma máquina virtual. É essa combinação — compilar para bytecode e rodar em uma VM — que dá a Java a fama de "escreva uma vez, rode em qualquer lugar".
Esta página cobre o que é específico da linguagem Java (sintaxe, compilação, JVM). Os conceitos de orientação a objetos em si (classes, herança, polimorfismo, encapsulamento) são agnósticos de linguagem e vivem em Orientação a Objetos.
História e versionamento¶
Java nasceu em 1991 dentro da Sun Microsystems, sob o nome Oak, com foco inicial em navegadores web (as antigas applets). O nome e o lançamento oficial como Java vieram em 1995. Hoje é usado por mais de 9 milhões de desenvolvedores e roda em bilhões de dispositivos.
O maior diferencial histórico da linguagem é o lema "Escreva uma vez, rode em qualquer lugar" (write once, run anywhere) — possível porque o compilador gera bytecode independente de plataforma, e cada sistema operacional roda sua própria JVM:
graph LR
Codigo["Código Java"] --> BC["bytecode (.class)"]
BC --> JL[JVM Linux]
BC --> JW[JVM Windows]
BC --> JM["JVM Mac OS"]
JL --> RL[Roda no Linux]
JW --> RW[Roda no Windows]
JM --> RM[Roda no Mac OS]
Outro traço característico da linguagem é a retrocompatibilidade: código escrito para versões antigas (a partir de 1.0, em 1995) geralmente continua compilando e rodando em versões novas, sem reescrita — mantida por um processo de padronização formal, o Java Community Process (JCP).
Alguns marcos da linha do tempo que valem ser lembrados (o nome de marketing da versão não bate sempre com o número técnico — ex.: 1.5 é comercialmente "Java 5"; desde então, as versões passaram a ser conhecidas só pelo número):
- Java 1 (1995) — primeira versão estável, ainda com bugs e performance ruim.
- Java 1.2 / "Java 2" (1998) — introdução do compilador JIT (Just In Time) e da API de Collections. Por marketing, a linguagem passou a se chamar "Java 2" nessa época.
- Java 1.3 (2000) — nova JVM padrão (HotSpot), que combina interpretação com compilação JIT: identifica os trechos de código mais executados em tempo de execução (hotspots) e só esses são compilados para código nativo — equilibra o custo de compilar com o ganho de performance.
- Java 1.4 (2002) — expressões regulares, processamento de XML, logging.
- Java 5 / 1.5 (2004) — Generics, autoboxing, anotações (metadata), enhanced-for. Um dos marcos mais importantes da linguagem.
- Java 1.6 (2006) — a linguagem se torna open source (licença GPL).
- Java 7 (2011) — primeira versão após a aquisição da Sun pela Oracle; multicatch, diamond operator, try-with-resources, nova API de I/O.
- Java 8 (2014) — expressões lambda, Streams, default methods em interfaces, nova API de datas (inspirada na biblioteca Joda Time).
Definição: JIT (Just In Time)
Estratégia do compilador da JVM de compilar bytecode para código nativo da máquina durante a execução do programa, em vez de só interpretá-lo — um dos principais fatores que tornaram a performance do Java competitiva com linguagens compiladas direto para código nativo.
Compilando e executando um programa¶
Um arquivo Java começa como código-fonte (.java) e passa por dois passos até rodar:
class MeuPrimeiroPrograma {
public static void main(String[] args) {
System.out.println("O primeiro de muitos!");
}
}
graph LR
A["Código-fonte (.java)"] -->|javac| B["Bytecode (.class)"]
B -->|java| C["JVM executa"]
Definição: JVM (Java Virtual Machine)
Programa que interpreta e executa o bytecode gerado pelo compilador Java. É a JVM
que torna Java portável: o mesmo .class roda em qualquer sistema operacional que
tenha uma JVM instalada — o código-fonte não é compilado direto para instruções de
uma máquina específica.
Definição: Bytecode
Formato intermediário gerado pelo compilador (javac), armazenado em arquivos
.class. Não é código de máquina nem é o código-fonte — é o que a JVM sabe
interpretar.
Na prática, pela linha de comando:
javac MeuPrimeiroPrograma.java # gera MeuPrimeiroPrograma.class (bytecode)
java MeuPrimeiroPrograma # a JVM executa o bytecode
Repare que javac recebe o nome do arquivo (com extensão), enquanto java recebe o
nome da classe (sem extensão) — é a classe que a JVM procura para executar, não o
arquivo em si. Em uma IDE (Eclipse, IntelliJ, NetBeans), esses dois passos costumam
acontecer automaticamente a cada vez que você salva o código.
É possível passar argumentos de linha de comando para o programa; eles chegam pelo
parâmetro args do main (ver seção seguinte):
Para instalar Java, existem dois pacotes de distribuição:
Definição: JDK x JRE
JRE (Java Runtime Environment) traz só o necessário para rodar um programa
Java já compilado (basicamente a JVM + bibliotecas padrão). JDK (Java
Development Kit) inclui o JRE e as ferramentas para desenvolver — o
compilador javac, entre outras. Para programar em Java, precisa do JDK; para só
rodar um .jar de terceiros, o JRE bastaria.
Classpath: como javac/java encontram as classes¶
O classpath é o conjunto de diretórios, .jars e .zips onde javac e java
procuram as classes referenciadas por um programa. Por padrão, é só o diretório atual
(.). Duas formas de configurá-lo:
- Variável de ambiente
CLASSPATH— configura globalmente no sistema operacional. Evite: vale para qualquer programa Java rodado na máquina, o que costuma causar mais confusão do que ajuda. - Opção
-cp(ou-classpath) nos comandosjavac/java— a forma recomendada, pois vale só para aquela execução: Para incluir mais de um caminho, use o separador do seu sistema operacional:;(ponto e vírgula) no Windows,:(dois pontos) no Linux/Mac/Unix.
Empacotando em um JAR¶
Um JAR (.jar) é só um .zip com classes Java compiladas dentro — usado para
distribuir bibliotecas ou aplicações inteiras:
jar -cf biblioteca.jar meupacote/ # cria o jar a partir de uma pasta
java -cp biblioteca.jar MinhaClasse # usa o jar via classpath
Se o JAR representa uma aplicação executável (não só uma biblioteca), ele pode declarar
qual classe rodar através do arquivo META-INF/MANIFEST.MF (criado automaticamente
dentro do jar), com a linha Main-Class: pacote.NomeDaClasse — precisa terminar com
uma linha em branco, senão o manifest não é reconhecido. Com isso, roda-se o jar
direto, sem precisar informar a classe:
Convenções da linguagem¶
- Nome do arquivo = nome da classe. Um arquivo
MeuPrimeiroPrograma.javadeve conter uma classeMeuPrimeiroPrograma. Isso evita ambiguidade na hora de compilar/rodar. - Java é case sensitive:
Systemesystemsão coisas diferentes para o compilador. Um erro comum de quem está começando é digitar um identificador com a capitalização errada e receber um erro de compilação por isso. - Ponto e vírgula (
;) termina cada instrução; chaves ({}) delimitam o escopo de uma classe, método ou bloco.
Definição: CamelCase
Convenção de nomenclatura onde não se usa espaço ou _ entre palavras — cada nova
palavra começa com maiúscula (meuPrimeiroPrograma). Em Java, nomes de classe usam a
primeira letra também maiúscula (PascalCase, um caso particular de CamelCase);
métodos e variáveis começam com minúscula.
Java aceita três formas de comentário — não fazem parte do código, podem aparecer em qualquer lugar do arquivo:
// comentário de uma linha
/*
* comentário de várias linhas
*/
/**
* Javadoc: gera documentação HTML a partir do código.
* Começa com /** (duas asteriscos).
*/
O método main¶
Quando você manda a JVM executar uma classe (java NomeDaClasse), ela procura dentro
dessa classe por um método com esta assinatura exata:
Uma aplicação Java, em geral, tem um único main — é o ponto de partida de tudo. O
parâmetro args é um array de String, e é por ele que argumentos passados na linha de
comando chegam ao programa (args[0], args[1], ...).
Se você já pensou em como responder "o que acontece quando eu rodo um programa Java",
essa é a resposta curta: a JVM sobe, carrega a classe indicada, localiza o main e
começa a executar a partir dele.
Para a JVM aceitar um método como ponto de entrada, quatro regras precisam ser respeitadas — o resto (nome do parâmetro, ordem dos modificadores) é flexível:
public static void main(String[] args) {} // forma mais comum
static public void main(String[] args) {} // ordem dos modificadores não importa
public static void main(String... args) {} // varargs também é aceito
public static void main(String args[]) {} // colchete depois do nome também é aceito
- deve ser
public; - deve ser
static(a JVM chama sem ter uma instância da classe ainda); - não deve ter retorno (
void); - deve se chamar
maine receber um array (ou varargs) deString.
Uma classe sem main compila normalmente, só não pode ser usada como ponto de partida da
aplicação pela linha de comando (java NomeDaClasse).
Varargs¶
Desde o Java 5, varargs (Tipo... nome) permite que um método receba uma
quantidade variável de argumentos do mesmo tipo — por baixo dos panos, nums é
tratado como um array dentro do método:
public int soma(int... nums) {
int total = 0;
for (int n : nums) {
total += n;
}
return total;
}
soma(); // 0 — nenhum argumento, cria um array vazio
soma(1); // 1
soma(1, 2, 3); // 6
soma(new int[]{1, 2, 3}); // também aceita um array pronto
Regras que valem a pena lembrar:
- Varargs deve ser sempre o último parâmetro do método, e só pode existir um por assinatura — necessário para o compilador não ter ambiguidade na hora de decidir onde cada argumento começa.
- Se existir uma sobrecarga com o tipo exato (sem varargs), ela tem prioridade sobre a versão varargs.
- Chamar um método que espera um array passando os valores separados (como se fosse varargs) é erro de compilação — varargs aceita chamada "no estilo array", mas o inverso não é verdadeiro.
Escopo de variáveis¶
Escopo é a região do código onde uma variável pode ser usada. Java tem três níveis:
- Variável local — declarada dentro de um método, construtor ou bloco (
if,for, ...). Existe só entre o ponto onde foi declarada e o fim daquele bloco. - Variável de instância (ou de objeto) — declarada na classe, fora de qualquer método. Existe enquanto o objeto existir, e cada instância tem sua própria cópia (ver Orientação a Objetos).
- Variável estática (ou de classe) — declarada com
static. Compartilhada por todas as instâncias da classe (uma única cópia, não uma por objeto), existe enquanto a classe estiver carregada.
class Contador {
static int totalDeInstancias = 0; // uma só cópia, compartilhada
int idPessoal; // uma cópia por instância
Contador() {
totalDeInstancias++;
idPessoal = totalDeInstancias;
}
}
Cuidado com for: a variável declarada na inicialização do loop (for (int i = 0; ...))
só existe dentro do loop — usá-la depois do loop fechar é erro de compilação.
Definição: Shadowing
Quando uma variável local ou parâmetro tem o mesmo nome de uma variável de
instância ou estática — permitido em Java, mas ambíguo de resolver: dentro do
escopo onde há shadowing, o nome sozinho sempre resolve para a variável de menor
escopo (a local). Para acessar a de instância/estática nesse caso, use this.nome
(instância) ou NomeDaClasse.nome (estática):
static: campos e métodos de classe¶
Um membro static (atributo ou método) pertence à classe, não a cada instância —
por isso o acesso natural é pelo nome da classe, sem precisar de new:
public class Car {
public static int totalCars;
}
Car.totalCars = 5; // acessado pela classe, não por uma instância
Um método static também é acessível através de uma instância (carro.getTotalCars()
compila), mas isso é estilo ruim — sugere, incorretamente, que o método depende
daquele objeto específico. Prefira sempre acessar por Car.getTotalCars().
Definição: Dentro de um contexto static, não existe this
Um método static não pode acessar atributos ou métodos de instância
diretamente — faria sentido para qual objeto? Não há nenhum this implícito num
contexto estático:
public class Car {
static int totalCars;
private int weight;
public static int getWeight() {
return weight; // erro de compilação — weight é de instância
}
}
static da própria classe.
Membros estáticos podem referenciar uns aos outros livremente, inclusive membros declarados mais abaixo no arquivo (diferente de variável local, onde a ordem de declaração importa rigorosamente):
static int idade = grabAge(); // ok: chama um método static declarado depois
static int grabAge() {
return 18;
}
Mas cuidado: se um membro estático tenta usar o valor de outro que ainda não foi inicializado (a inicialização acontece em ordem, de cima para baixo, na primeira vez que a classe é carregada), o resultado é o valor default do tipo, não um erro:
static int b = getMethod(); // 0 — "a" ainda não foi inicializado nesse ponto
static int getMethod() {
return a;
}
static int a = 15; // inicializado DEPOIS de "b" já ter sido calculado
Além de atributos e métodos static, uma classe pode ter um bloco estático
(static { ... }) — código que roda uma única vez, no momento em que a classe é
carregada pela JVM (antes de qualquer instância ser criada, ou de qualquer membro
estático ser acessado pela primeira vez). Útil para inicializações mais elaboradas do
que cabe numa atribuição direta:
class Config {
static Map<String, String> valores;
static {
valores = new HashMap<>();
valores.put("versao", "1.0");
}
}
Se algo dentro de um bloco estático (ou na inicialização de uma variável static)
lançar uma exceção, a JVM embrulha esse erro num ExceptionInInitializerError (ver
Boas Práticas).
Definição: Métodos static não são polimórficos
Um dos pontos mais cobrados sobre static: diferente de métodos de instância
(resolvidos em tempo de execução, pelo tipo real do objeto — ver
Polimorfismo), um método static é resolvido
em tempo de compilação (binding estático), pelo tipo da variável/referência
usada para chamá-lo — não pelo tipo real do objeto:
class A {
static void method() { System.out.println("a"); }
}
class B extends A {
static void method() { System.out.println("b"); }
}
A a = new A();
a.method(); // a
B b = new B();
b.method(); // b
A a2 = b; // a2 é do tipo A, mesmo apontando para um objeto B
a2.method(); // a — decidido pelo TIPO DA VARIÁVEL (A), não pelo objeto real (B)
static herdado (nem o contrário) — isso é erro de
compilação, já que um não pode sobrescrever o outro (são mecanismos diferentes:
overriding, resolvido em runtime, exige que ambos sejam de instância).
Variáveis e tipos primitivos¶
Declarar uma variável em Java exige dizer o tipo dela — Java é uma linguagem fortemente tipada, o compilador não deixa passar um valor incompatível com o tipo declarado:
double livroJava8 = 59.90;
double livroTDD = 59.90;
double soma = livroJava8 + livroTDD;
System.out.println("O total em estoque é " + soma);
Os oito tipos primitivos da linguagem — não é possível criar um tipo primitivo novo, essa lista é fechada:
| Tipo | Tamanho | Intervalo | Uso |
|---|---|---|---|
boolean |
1 bit | true/false |
lógico |
byte |
1 byte | -128 a 127 | inteiro pequeno |
short |
2 bytes | -32.768 a 32.767 | inteiro |
char |
2 bytes | 0 a 65.535 | um caractere |
int |
4 bytes | ≈ -2,1 a 2,1 bilhões | inteiro (o mais comum) |
float |
4 bytes | ponto flutuante (≈7 dígitos de precisão) | ponto flutuante |
long |
8 bytes | ≈ ±9,2 quintilhões | inteiro grande |
double |
8 bytes | ponto flutuante (≈15 dígitos de precisão) | ponto flutuante (o mais comum) |
Na prática, dificilmente alguém escolhe short para economizar espaço — na maioria dos
casos usa-se int para inteiros e double para ponto flutuante, e só se pensa nos tipos
menores em cenários bem específicos de otimização.
Definição: char é o único tipo sem sinal
Todo tipo numérico em Java pode ser negativo, exceto char — que só representa
valores de 0 a 65.535 (é, por baixo dos panos, um código de caractere Unicode).
Apesar de ter o mesmo tamanho de um short (2 bytes), um char não consegue guardar
todos os valores que um short guarda, porque um short também cobre negativos.
Um número inteiro aceita mais de uma base além da decimal — útil reconhecer na leitura de código:
int decimal = 267;
int octal = 0761; // começa com 0 — só algarismos de 0 a 7
int hexadecimal = 0xAB34; // começa com 0x — algarismos 0-9 e A-F
int binario = 0b100001011; // começa com 0b — só 0 e 1
Inicialização de variáveis¶
Toda variável precisa estar inicializada (explícita ou implicitamente) antes do primeiro uso — mas a regra muda dependendo de onde ela é declarada:
- Variável local: a inicialização é obrigatória e explícita. Usar uma variável local antes de atribuir um valor a ela é erro de compilação — mesmo que, olhando o fluxo do código, pareça óbvio que ela sempre teria um valor até aquele ponto (o compilador não avalia todos os caminhos possíveis de execução):
- Variável de instância ou estática: recebe inicialização implícita com um valor default do tipo, mesmo sem você atribuir nada:
| Categoria | Valor default |
|---|---|
Numéricos inteiros (byte, short, int, long) |
0 |
Numéricos de ponto flutuante (float, double) |
0.0 |
boolean |
false |
char |
vazio (equivalente a 0) |
| Referências (qualquer objeto) | null |
Arrays seguem a mesma regra dos campos: cada posição já nasce com o valor default do tipo do array, mesmo sem inicialização explícita.
Literais¶
Um literal é um valor escrito diretamente no código-fonte (10, "texto", true).
Por padrão, um número inteiro literal é int e um número com casa decimal é double;
para forçar outro tipo, usa-se um sufixo (maiúsculo ou minúsculo):
long l = 7378212378912L; // L força long (sem o sufixo, esse literal não cabe em int)
float f = 10.5F; // F força float (sem o sufixo, seria double)
double d = 10.5D; // D é opcional para double, mas válido
Também é possível usar notação científica e, desde o Java 7, _ (underline) para
separar dígitos e facilitar a leitura de números grandes — o compilador ignora os _:
O _ só pode ficar entre dígitos — nunca no início/fim do número, nem colado a um
sufixo (L, F, D) ou ao indicador de base (0x, 0b) ou ao ponto decimal:
int v1 = 1_000_000; // ok
int v2 = 0x1_0AF; // ok (entre dígitos hexadecimais)
int erro1 = _1000; // erro — começa com _
int erro2 = 1000_; // erro — termina com _
double erro3 = 1_.5; // erro — _ colado ao ponto
Um literal char fica entre aspas simples — pode ser o caractere em si, seu código
numérico Unicode, ou uma sequência de escape \u (útil para digitar um caractere que não
está disponível no seu teclado):
char a = 'A';
char b = 65; // mesmo valor de 'A' — 65 é o código Unicode de 'A'
char c = 'Ω'; // letra grega Ômega, via escape Unicode
Identificadores e palavras reservadas¶
Identificadores são os nomes que o programador escolhe (para variáveis, métodos,
classes, ...); palavras reservadas (ou palavras-chave) são termos predefinidos da
linguagem que não podem ser usados como identificador, porque já definem comandos —
if, for, class, static, abstract, assert, break, entre outras.
Regras para um identificador válido:
- Não pode ser igual a uma palavra reservada (
false,trueenull, embora sejam tecnicamente literais e não palavras-chave, também não podem ser usados). - Pode conter letras (inclusive Unicode), números,
$e_. - Não pode começar com um número.
- É case sensitive:
idadeeIdadesão identificadores diferentes.
int aName; // ok
int _num; // ok
int $ab_c; // ok
int x_y; // ok
int false; // inválido — palavra reservada
int 4num; // inválido — começa com número
int x-y; // inválido — hífen não é permitido
Um detalhe importante: ao atribuir uma variável de tipo primitivo a outra
(outraVariavel = variavel;), o valor é copiado. As duas variáveis passam a existir
de forma independente — mudar uma não afeta a outra. Isso é diferente do que acontece com
objetos (ver Orientação a Objetos), onde o que é
copiado é a referência.
Definição: Casting
Conversão explícita de um valor de um tipo para outro, quando o compilador não faz
essa conversão sozinho. Java converte um int para double automaticamente (não há
perda de informação), mas o caminho contrário — double para int — precisa de
casting explícito, porque pode perder precisão:
double livroJava8 = 59.90;
int numeroInteiro = (int) livroJava8; // numeroInteiro vale 59 — perdeu o .90
byte → short → int → long → float → double (e, à parte,
char → int). Indo da esquerda para a direita nessa cadeia, é widening automático;
da direita para a esquerda, sempre precisa de casting explícito ((tipo) valor),
truncando o valor se ele não couber — casting entre ponto flutuante e inteiro
descarta as casas decimais, sem arredondar.
Operadores¶
Atribuição e compatibilidade de tipos¶
O operador de atribuição (=) exige que o valor à direita seja compatível com o
tipo da variável à esquerda. Um tipo menos abrangente é sempre compatível com um mais
abrangente (widening implícito — não precisa de casting):
int a = 10;
long b = a; // ok — int cabe num long
float c = 10f;
double d = c; // ok — float cabe num double
O caminho contrário (mais abrangente para menos abrangente) exigiria casting explícito
(ver a definição de Casting, mais abaixo nesta página) — com uma exceção: ao atribuir um
literal a uma
variável byte, short ou char, o compilador aceita sem casting, desde que o valor
literal caiba no tipo (isso é avaliado em tempo de compilação, não é uma conversão em
tempo de execução):
byte b1 = 10; // ok, 10 cabe em byte
byte b2 = 200; // erro de compilação — 200 estoura o intervalo de byte (-128 a 127)
char c1 = 10; // ok
char c2 = -3; // erro de compilação — char não pode ser negativo
Operadores aritméticos¶
Os operadores +, -, *, / e % (resto da divisão — só faz sentido para
inteiros) funcionam como esperado, com uma regra de promoção de tipo importante: o
resultado de uma operação aritmética é sempre, no mínimo, int — mesmo somando dois
byte ou dois short:
byte b = 1;
short s = 2;
int i = b + s; // ok — resultado mínimo é int
byte b2 = b + s; // erro de compilação — não cabe em byte sem casting explícito
Definição: ArithmeticException x Infinity/NaN
Dividir (ou tirar o resto de) um valor inteiro por zero lança
ArithmeticException. Já a divisão de ponto flutuante (float/double) por
zero não lança exceção — o resultado é Infinity (positivo ou negativo, conforme o
sinal) ou NaN (Not a Number, ex.: 0.0/0.0, ou infinito menos infinito):
Operadores de comparação¶
== e != comparam igualdade/diferença; >, <, >=, <= comparam ordem — só
fazem sentido entre valores numéricos (char incluso, já que é numérico por baixo dos
panos). O resultado de qualquer comparação é sempre boolean.
Regras que geram erro de compilação por comparar tipos incompatíveis:
booleansó aceita==/!=(não existe "verdadeiro > falso").- Não é possível comparar tipos sem relação nenhuma entre si (ex.:
Stringcomint).
System.out.println(true < false); // erro de compilação
System.out.println("Ana" > "Bia"); // erro de compilação — use compareTo
System.out.println('a' > 1); // ok — char é numérico
Definição: Cuidado — = (atribuição) x == (comparação)
Um erro sutil e fácil de digitar sem querer: if (a = 5) atribui 5 a a (e,
se a for boolean, ainda compila) em vez de comparar. Para tipos não-boolean,
isso é erro de compilação (a atribuição não retorna boolean) — mas vale o
hábito de sempre conferir qual dos dois você quis usar.
Comparações de ponto flutuante merecem cautela: erros de arredondamento podem fazer
1 == (100.0 / 100) ser false em certos casos, por imprecisão binária do float/
double.
Operadores lógicos¶
Diferente de &&/||/! (vistos acima), Java também tem versões sem
curto-circuito dos operadores E/OU: & (e), | (ou) e ^ (ou exclusivo — xor):
System.out.println(1 == 1 & 1 > 2); // false
System.out.println(1 == 1 | 2 > 1); // true
System.out.println(1 == 1 ^ 2 > 1); // false (os dois lados são true)
Definição: Curto-circuito (&&/||) x sem curto-circuito (&/|)
Com && e ||, se o resultado já pode ser determinado só pelo primeiro operando
(false && ... ou true || ...), o segundo nem é avaliado. Com & e |,
ambos os lados são sempre avaliados, mesmo quando o resultado já está decidido.
Isso importa na prática quando o segundo operando tem efeito colateral (um
incremento, uma chamada de método): o resultado pode mudar dependendo de qual
operador você usa.
Incremento e decremento¶
++/-- somam ou subtraem 1, e existem em duas formas — a posição do operador muda
quando o valor é aplicado:
int i = 10;
System.out.println(i++); // 10 — pós-incremento: usa o valor, DEPOIS incrementa
System.out.println(i); // 11
int j = 10;
System.out.println(++j); // 11 — pré-incremento: incrementa, DEPOIS usa o valor
Operadores de atribuição composta¶
+=, -=, *=, /=, %= combinam uma operação com atribuição numa única instrução.
Uma pegadinha clássica de prova: atribuição composta faz um casting implícito,
mesmo quando a atribuição direta equivalente não compilaria:
byte b = 3;
b += 4; // compila — equivalente a b = (byte) (b + 4)
b = b + 4; // NÃO compila — b + 4 é int, não cabe em byte sem casting explícito
Operador ternário¶
Forma compacta de um if/else que sempre retorna um valor (diferente de um if
comum, que só executa ações):
A estrutura é condição ? valorSeVerdadeiro : valorSeFalso, e pode ser encadeado, mas
aninhar demais prejudica a legibilidade — prefira um if/else if quando tiver mais de
duas ou três alternativas.
Precedência¶
Não é preciso decorar a precedência de todos os operadores — a ordem geral, do que executa primeiro para o que executa por último:
- pré-incremento/decremento (
++x,--x) - multiplicação, divisão, resto (
*,/,%) - soma, subtração (
+,-) - shifts (
<<,>>,>>>) - pós-incremento/decremento (
x++,x--)
Na dúvida sobre a ordem de avaliação de uma expressão complexa, parênteses explícitos deixam a intenção clara e não custam nada em performance.
Condicionais e loopings¶
Java segue a sintaxe C-like comum a várias linguagens para controle de fluxo. A condição
de um if precisa sempre ser uma expressão boolean (diferente de linguagens onde
um número serve como condição) — inclusive um erro clássico de digitação, usar =
(atribuição) em vez de == (comparação), só compila se a variável atribuída for
boolean, e o resultado costuma surpreender:
boolean a = true;
if (a = false) { // atribui false a "a" — não compara nada
System.out.println("não executa");
}
// a agora vale false
if (soma < 150) {
System.out.println("Seu estoque está muito baixo!");
} else if (soma >= 2000) {
System.out.println("Seu estoque está muito alto!");
} else {
System.out.println("Seu estoque está bom");
}
Definição: Não existe elseif em Java
Diferente de outras linguagens, Java não tem uma palavra-chave elseif. O que
parece uma cadeia if / elseif / elseif / else é, na verdade, um if dentro do
else anterior, repetido — funciona por indentação visual, não por sintaxe própria:
if (condicao1) {
// ...
} else if (condicao2) { // na verdade: else { if (condicao2) { ... } }
// ...
} else {
// ...
}
if/else é tão usado em prova:
a indentação não determina a que if um else pertence — o else sempre se
liga ao if aberto mais próximo (e sem chaves) acima dele, esteja ele indentado de
forma enganosa ou não.
Definição: Unreachable code e missing return
O compilador Java analisa estaticamente se todo caminho possível de um método com
retorno realmente retorna um valor (ou lança uma exceção) — se existir algum
caminho que "cai fora" do método sem retornar, é erro de compilação
(missing return statement), mesmo que na prática aquele caminho nunca aconteça em
tempo de execução:
return, dentro do mesmo bloco) é erro de
compilação por unreachable code — mas if (false) { ... } não é considerado
unreachable (o compilador não analisa o valor de condições, só o fluxo de
return/throw), então esse caso específico compila normalmente.
switch¶
Alternativa ao if/else quando o mesmo valor precisa ser comparado contra vários casos
possíveis:
int option = 1;
switch (option) {
case 1:
System.out.println("primeira opção");
break;
case 2:
System.out.println("segunda opção");
break;
default:
System.out.println("nenhuma das opções");
break;
}
O argumento do switch precisa ser de um tipo específico — não é qualquer tipo que
serve:
- Qualquer primitivo menor que
int(ou o próprioint):byte,short,char. - O
wrappercorrespondente (Integer,Character, ...). String.enum.- Não compila com
long,float,doubleouboolean.
Cada valor de case precisa ser compatível com o tipo do switch, e precisa ser uma
constante em tempo de compilação: um literal, ou uma variável final inicializada
na própria declaração (não numa linha separada) — nunca uma variável comum, nem
null explícito.
final int FIVE = 5; // ok como valor de case
final int TEN;
TEN = 10; // inicializada depois — NÃO serve como case (compile error)
switch (v) {
case FIVE: // ok — constante
case 10: // ok — literal
case TEN: // erro de compilação — não inicializada na declaração
}
default cobre o caso em que nenhum case bate — e, detalhe pouco intuitivo,
pode aparecer em qualquer posição dentro do switch, não só no final.
Definição: Fall-through — por que todo switch precisa de break
Ao contrário do if/else, um switch não para automaticamente ao encontrar um
case que bate — ele continua executando todos os cases (e o default) abaixo
dali, na ordem em que aparecem no código, até encontrar um break ou chegar ao fim
do bloco. Isso vale mesmo que o case que bateu esteja fisicamente depois do
default no código:
int v = 1;
switch (v) {
case 1: // bate aqui
case 2:
case 3:
System.out.println("1, 2 ou 3"); // executa (fall-through sem break)
}
break é uma fonte clássica de bug — a regra prática é: todo case
termina com break (ou return), a menos que o fall-through seja
intencional (dois cases que devem ter exatamente o mesmo comportamento).
while¶
Repete o corpo enquanto a condição for verdadeira — testada antes de cada execução (inclusive a primeira: se a condição já começar falsa, o corpo nunca roda):
Definição: Looping infinito
Bug comum: esquecer de atualizar a variável usada na condição, fazendo a condição
permanecer sempre verdadeira e o looping nunca terminar. Isso também tem uma
consequência no compilador: se ele consegue provar que a condição é sempre
verdadeira (while (true), ou uma variável final com valor true), qualquer
código logo depois do loop é considerado unreachable (erro de compilação) — a
menos que exista um break alcançável dentro do loop. Já while (false) (ou
qualquer condição que o compilador resolva como constante falsa em compilação) torna
o corpo do loop unreachable — diferente de if (false), que compila
normalmente mesmo com o corpo nunca executando (ver a definição de unreachable
code, mais acima).
for¶
Reúne três partes numa linha só, separadas por ;: inicialização (roda uma vez, no
início), condição (testada a cada volta) e atualização (roda ao final de cada volta):
Detalhes que valem a pena conhecer:
- Todas as três partes são opcionais.
for (;;) {}é um loop infinito válido — sem inicialização, condição nem atualização, a condição assumetruepor padrão. - A inicialização pode declarar várias variáveis do mesmo tipo, separadas por
vírgula (
for (int i = 0, j = 10; ...)) — mas não de tipos diferentes na mesma inicialização (nesse caso, declare-as antes dofor). - A atualização também aceita múltiplas instruções separadas por vírgula
(
for (int i = 0, j = 10; i < j; i++, j--)), e não precisa ser só incremento — qualquer instrução válida serve ali.
Enhanced for (for-each)¶
Percorre todos os elementos de um array ou Collection, sem índice nem controle manual
de condição/atualização:
Duas limitações importantes, que às vezes forçam a volta ao for tradicional:
- Não é possível modificar a coleção/array através da variável do loop — reatribuir
a variável (
num = 0;) não altera o elemento original, só a cópia local. - Não há contador/índice disponível — se você precisa saber "em qual posição estou"
ou percorrer duas coleções ao mesmo tempo, sincronizadas pelo índice, use o
fortradicional.
do/while¶
Variação do while em que a condição é testada depois do corpo — garantindo que o
corpo execute pelo menos uma vez, mesmo que a condição já comece falsa:
Detalhe sintático fácil de esquecer: a linha do while termina com ; — sem ele, é
erro de compilação.
Comparando os tipos de looping¶
| Situação | Melhor opção |
|---|---|
| Já sei quantas vezes repetir | for |
| Só quero ler todos os elementos de uma coleção/array | enhanced for |
| Preciso percorrer duas coleções ao mesmo tempo, ou remover elementos durante a iteração | for tradicional |
| Não sei quantas vezes, mas sei a condição de parada | while |
| O corpo precisa rodar pelo menos uma vez, mesmo que a condição comece falsa | do/while |
break e continue¶
Dentro de qualquer tipo de looping, continue pula direto para a próxima iteração
(no for, isso significa ir para a atualização antes de reavaliar a condição) e
break interrompe o looping por completo — ambos normalmente avaliados dentro de um
if que decide quando agir.
Definição: Labeled loops (rótulos em laços)
Em loops aninhados, break/continue sem rótulo afetam sempre o loop mais
interno. Para controlar um loop mais externo de dentro de um loop interno, é
preciso rotular o loop externo (um identificador seguido de :, antes do for/
while) e referenciar esse rótulo no break/continue:
externo: for (int i = 1; i < 10; i++) {
for (int j = 1; j < 10; j++) {
if (i * j == 25) {
break externo; // quebra o for externo, não só o interno
}
}
}
break/continue só podem referenciar um rótulo que esteja em um
for, while, do/while (continue) ou também switch (só break — switch
não aceita continue rotulado, já que não é um loop). Dois rótulos podem ter o
mesmo nome, desde que não estejam aninhados um dentro do escopo do outro.
Pegadinha clássica: um break sem rótulo dentro de um switch que está dentro
de um for quebra só o switch, não o for — para quebrar o loop de fora de
dentro do switch, é obrigatório usar um break rotulado apontando para o loop.
Pacotes¶
Java organiza classes em pacotes (package) — o equivalente a pastas, tanto
conceitualmente quanto fisicamente: um pacote br.com.casadocodigo.livraria corresponde
literalmente à pasta br/com/casadocodigo/livraria dentro de src.
package br.com.casadocodigo.livraria.testes;
import br.com.casadocodigo.livraria.Autor;
public class CadastroDeLivros {
// ...
}
Um arquivo .java segue sempre a mesma ordem: package primeiro, depois os imports
necessários, depois a declaração da classe. Convenção de nomenclatura: tudo minúsculo,
começando pelo domínio invertido (com.suaempresa...).
Pacotes existem para dar contexto e evitar ambiguidade — é assim que Java diferencia uma
classe Date sua de java.util.Date, por exemplo. Quando duas classes de pacotes
diferentes têm o mesmo nome, sem import você precisa usar o nome completo
(fully qualified name — pacote.NomeDaClasse) para desambiguar:
Um import evita ter que repetir o nome completo toda vez que a classe é usada no
arquivo. É possível importar um pacote inteiro com import pacote.*;, mas a prática mais
comum é importar cada classe explicitamente — deixa mais claro, para quem lê, exatamente
de onde cada classe vem.
Classes que não declaram nenhum package ficam no pacote default (sem nome) — o que
não é uma boa prática para projetos reais, já que dificulta a organização e aumenta o
risco de colisão de nomes conforme o projeto cresce. Mais um motivo para evitar: classes
do pacote default não podem ser importadas por classes de nenhum outro pacote.
Duas classes de pacotes diferentes com o mesmo nome só podem coexistir num arquivo se uma delas for referenciada pelo fully qualified name — importar as duas por nome simples é erro de compilação:
import java.util.Date;
import java.sql.Date; // erro de compilação: Date já foi importado
class Test {
Date d1; // java.util.Date
java.sql.Date d2; // precisa do nome completo
}
Quando existe um import específico e um import com * (wildcard) para o mesmo nome,
o específico sempre vence — não é erro de compilação:
import java.util.*;
import java.sql.Date; // vence sobre java.util.Date
class Test {
Date d; // java.sql.Date
}
Por padrão, todas as classes do pacote java.lang (String, Object, System, ...) já
vêm importadas automaticamente — escrever import java.lang.String; é permitido, mas
redundante.
Um detalhe que pega muita gente desprevenida: o pacote a.b e o pacote a são
pacotes diferentes, mesmo um sendo "subpacote" do outro na estrutura de pastas —
import a.*; importa as classes soltas em a, mas não importa nada de a.b. Para
importar tudo, incluindo subpacotes, é preciso um import explícito por subpacote (não
existe um "wildcard recursivo").
import static¶
Desde o Java 5, import static importa membros estáticos (atributos e métodos) de
uma classe, permitindo usá-los sem prefixar com o nome dela:
import static java.lang.Math.PI;
import static java.lang.Math.sqrt;
class Circulo {
double raio(double area) {
return sqrt(area / PI); // sem "Math." na frente
}
}
Também aceita * para importar todos os membros estáticos de uma classe
(import static java.lang.Math.*;).
Uma classe/interface pública por arquivo¶
Um arquivo .java pode conter mais de uma classe/interface, seguindo três regras:
- Pode existir no máximo uma classe/interface
publicpor arquivo. - Se existir uma classe/interface
public, o nome do arquivo precisa ser exatamente igual ao nome dela. - Se nenhuma for
public, o arquivo pode ter qualquer nome.
// Pessoa.java — obrigatório esse nome, por causa da classe pública
public class Pessoa { }
class Endereco { } // outra classe, mesmo arquivo, sem problema
interface Contato { } // e uma interface também
A classe Object¶
Toda classe em Java, mesmo sem declarar extends nenhum, herda implicitamente de
Object — direta ou indiretamente. É esse ancestral comum que garante que toda classe já
nasce com um punhado de métodos básicos, incluindo três que praticamente todo dia a dia
Java acaba sobrescrevendo: toString, equals e hashCode.
Definição: toString
Método que define como um objeto vira texto — é o que roda, por baixo dos panos,
quando você faz System.out.println(objeto) ou "" + objeto. Sem sobrescrever,
o padrão de Object imprime algo pouco útil (pacote.Classe@hashcode):
Definição: equals
Método que define igualdade de conteúdo entre dois objetos — diferente de ==,
que compara referência (ver Orientação a Objetos).
Sem sobrescrever, o equals herdado de Object só faz o mesmo que ==.
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Autor)) return false;
Autor outro = (Autor) obj;
return this.nome.equals(outro.nome);
}
instanceof para checar o tipo com segurança antes de fazer o
casting — evita um ClassCastException caso obj seja de um tipo totalmente
diferente. hashCode deve sempre ser sobrescrito junto com equals (dois objetos
considerados iguais por equals precisam ter o mesmo hashCode) — mais sobre isso
quando falarmos de Map em Collections.
Ciclo de vida de um objeto e Garbage Collector¶
Um objeto passa por três momentos: criação (new), um período em que está
acessível, e o momento em que se torna inacessível.
- Acessível: existe pelo menos um caminho (direto ou indireto) de alguma variável em uso até aquele objeto.
- Inacessível: não existe mais nenhum caminho até ele — seja porque a variável que o
referenciava recebeu outro valor (
null, ou outro objeto), seja porque o escopo dessa variável terminou (ela era local a um método/bloco que já encerrou).
Person p = new Person(); // criado e acessível
p = null; // o objeto Person original agora está inacessível
Definição: Garbage Collector
Mecanismo da JVM que libera automaticamente a memória de objetos inacessíveis — diferente de linguagens como C/C++, onde liberar memória é responsabilidade manual do programador. Pontos importantes (e comuns em prova/entrevista):
- Um objeto inacessível é chamado de elegível para o garbage collector — não significa que ele já foi coletado, só que já pode ser.
- O garbage collector roda em segundo plano, em um momento não determinístico — não é possível prever (nem garantir) exatamente quando um objeto elegível será efetivamente coletado.
- Uma referência indireta ainda conta como caminho de acesso: se um objeto
Areferencia um objetoB, eAainda é acessível a partir do código em execução,Btambém é — mesmo sem nenhuma variável apontando paraBdiretamente.
Wrappers e autoboxing¶
Tipos primitivos (int, double, boolean, ...) não são objetos — não herdam de
Object. Para tratar um valor primitivo como referência (por exemplo, para guardá-lo
numa estrutura que só aceita objetos), cada tipo primitivo tem uma classe wrapper
correspondente:
| Primitivo | Wrapper |
|---|---|
boolean |
Boolean |
byte |
Byte |
short |
Short |
char |
Character |
int |
Integer |
float |
Float |
long |
Long |
double |
Double |
Integer numero = new Integer(10); // "embrulha" o int
int valor = numero.intValue(); // "desembrulha" de volta
Definição: Autoboxing
Desde o Java 5, o compilador converte automaticamente entre um primitivo e seu
wrapper (Integer numero = 10; funciona direto, sem new Integer(10) explícito).
É açúcar sintático — por baixo dos panos, o wrapper continua sendo criado.
Definição: Cache de Integer (e outros wrappers) — a pegadinha do ==
Como wrapper é objeto, comparar dois wrappers com == deveria sempre comparar
referência (ver Orientação a Objetos) — mas
para economizar memória, a JVM mantém em cache (e reutiliza) instâncias de
valores pequenos e comuns: todo Boolean e Byte, Short/Integer/Long entre
-128 e 127, e Character para os códigos ASCII. Isso faz == "funcionar por
acidente" para valores dentro dessa faixa — e falhar fora dela:
Integer i1 = 123;
Integer i2 = 123;
System.out.println(i1 == i2); // true — 123 está no cache
Integer i3 = 1234;
Integer i4 = 1234;
System.out.println(i3 == i4); // false — 1234 é grande demais pro cache, objetos diferentes
== — use sempre .equals(...),
que compara conteúdo de verdade, independente de estar ou não no cache.
Definição: NullPointerException ao unboxing de null
Um wrapper pode assumir null (afinal, é um objeto) — mas qualquer operação que
force o unboxing dele para o primitivo correspondente (uma conta aritmética, por
exemplo) lança NullPointerException se o valor for null:
As classes wrapper também trazem métodos estáticos utilitários, especialmente para
conversão de/para String:
Passar um texto que não representa o tipo esperado (Integer.parseInt("ABC")) lança
NumberFormatException — uma unchecked exception (ver
Boas Práticas).
Cheat-sheet: convertendo entre primitivo, wrapper e String¶
| Direção | Como |
|---|---|
| primitivo → wrapper | new Integer(10) (evitar) ou autoboxing direto (Integer i = 10;) |
| wrapper → primitivo | numero.intValue(), numero.doubleValue(), ... (um método xxxValue() por tipo) |
String → primitivo |
Integer.parseInt(s), Double.parseDouble(s), ... (lança NumberFormatException se inválido) |
String → wrapper |
Integer.valueOf(s) (mesmo risco de NumberFormatException) |
primitivo/wrapper → String |
String.valueOf(valor) ou valor.toString() |
Números inteiros (parseInt, parseLong, ...) aceitam uma base opcional como
segundo argumento, para interpretar o texto em binário, octal ou hexadecimal:
E o caminho inverso, formatando um número numa base específica:
String binario = Integer.toBinaryString(8); // "1000"
String hexa = Long.toHexString(11); // "b"
String octal = Integer.toOctalString(22); // "26"
Character e Boolean são os únicos wrappers com um construtor de um argumento só
(char/boolean, sem versão numérica) — Boolean aceita String de forma
case-insensitive ("TrUe" vira true; qualquer outro texto vira false, sem lançar
exceção).
O pacote java.lang¶
java.lang é o único pacote Java disponível automaticamente em qualquer arquivo, sem
precisar de import — é onde vivem Object, String, System, as classes wrapper, e
utilitários como Math (Math.round, Math.max, Math.sqrt, ...) e Random.
String é imutável¶
String guarda referência a um objeto (não é tipo primitivo), mas tem duas
características próprias que a diferenciam de qualquer outra classe:
- String pool: por otimização, a JVM mantém um cache de strings literais. Duas
variáveis com o mesmo literal (
"java") apontam para o mesmo objeto no pool — por isso"java" == "java"dátrue, uma exceção prática à regra geral de que==entre objetos compara referência. - Imutabilidade: nenhum método de
String(replace,toUpperCase,substring, ...) altera a string original — todos retornam uma string nova. Um erro comum de quem está começando é chamartexto.replace(...)sem reatribuir o retorno, e se surpreender quetexto"não mudou": ele realmente não muda, é imutável.
Definição: Regras finas do String pool
Vale a pena ter clareza sobre quando exatamente uma String vai para o pool —
é um dos tópicos mais cobrados em prova de certificação Java:
- Só vão para o pool strings criadas por literal.
new String("x")cria um objeto novo, fora do pool, mesmo que"x"já exista lá. - Concatenar dois literais também conta como literal, se o compilador consegue resolver o valor em tempo de compilação (constant folding) — o resultado vai para o pool:
- Se qualquer lado da concatenação for uma variável (não um literal puro), o resultado é montado em tempo de execução — um objeto novo, fora do pool:
- Um método de
Stringque devolve o mesmo conteúdo que já tinha (ex.:toUpperCase()numa string já maiúscula) não é obrigado a criar um objeto novo — pode devolver a própria referência. Isso não é uma garantia geral de que "métodos de String nunca criam objeto novo": depende de o conteúdo resultante ser idêntico ao original.
O método equals¶
Comparar duas referências com == sempre funciona, mas compara identidade
(mesmo objeto), não conteúdo. Para comparar conteúdo, sobrescreva equals (herdado de
Object, ver mais abaixo) — como String e Integer já fazem.
Definição: Cuidado ao sobrescrever equals — overload x override
Um erro clássico: declarar equals recebendo o tipo da própria classe em vez de
Object. Isso não sobrescreve (override) o equals de Object — cria uma
sobrecarga (overload) nova, e o equals(Object) original continua ativo (e
é o que qualquer código genérico, como uma List, vai efetivamente chamar):
class Client {
String name;
// ERRADO: overload, não override — parâmetro deveria ser Object
public boolean equals(Client outro) {
return this.name.equals(outro.name);
}
}
Object e usa instanceof + casting para checar o tipo
com segurança (ver o exemplo na definição de equals, mais acima nesta página).
Adicionar @Override no método é o jeito mais simples de pegar esse erro cedo — o
compilador acusa quando a assinatura não bate com nenhum método da superclasse.
Concatenar null com uma String não lança exceção — o null é convertido para o texto
"null":
Principais métodos de String¶
| Método | Faz |
|---|---|
length() |
tamanho da string (atributo em array, mas método em String) |
isEmpty() |
true se o tamanho for 0 |
toUpperCase() / toLowerCase() |
devolve uma nova string em maiúsculas/minúsculas |
trim() |
devolve uma nova string sem espaços em branco no começo/fim |
charAt(indice) |
o caractere numa posição (índice começa em 0) |
substring(inicio, fim) |
recorte da string — ver detalhe abaixo |
concat(String) |
concatena ao final (equivalente ao +) |
replace(char, char) |
substitui todas as ocorrências de um caractere por outro |
contains(String) |
true se contém a sequência |
startsWith(String) / endsWith(String) |
true se começa/termina com a sequência |
indexOf(String) / lastIndexOf(String) |
índice da primeira/última ocorrência (-1 se não achar) |
equals(String) / equalsIgnoreCase(String) |
igualdade de conteúdo (exata ou ignorando maiúsculas) |
compareTo(String) |
ordem lexicográfica: negativo (vem antes), positivo (vem depois), 0 (igual) — ver Comparable |
Definição: substring — início inclusivo, fim exclusivo
substring(inicio, fim) inclui o caractere do índice inicio, mas não
inclui o caractere do índice fim — um erro clássico de off-by-one se você
esperar o comportamento contrário:
String text = "Java";
text.substring(1, 4); // "ava" — pega os índices 1, 2 e 3 (não o 4)
text.substring(5); // StringIndexOutOfBoundsException — índice além do tamanho
StringIndexOutOfBoundsException, não
ArrayIndexOutOfBoundsException — são exceções diferentes, mesmo representando o
mesmo tipo de erro (índice inválido) em estruturas diferentes.
StringBuilder e StringBuffer: strings mutáveis¶
Como String é imutável, concatenar muito (ex.: dentro de um loop) cria um objeto novo a
cada += — desperdício de memória e performance. Para isso, Java tem duas classes de
string mutável, que alteram o próprio objeto em vez de criar um novo a cada operação:
StringBuilder sb = new StringBuilder();
sb.append("Caelum");
sb.append(" - ");
sb.append("Alura");
System.out.println(sb); // Caelum - Alura (mesmo objeto sb, alterado 3 vezes)
StringBuilder e StringBuffer têm exatamente a mesma interface (mesmos métodos); a
diferença é que StringBuffer é thread-safe (mais lento) e StringBuilder não é
(mais rápido) — na dúvida, entre os dois, prefira StringBuilder.
Principais métodos, todos encadeáveis (retornam o próprio objeto):
| Método | Faz |
|---|---|
append(valor) |
adiciona ao final |
insert(indice, valor) |
insere numa posição específica |
delete(inicio, fim) |
remove um trecho |
reverse() |
inverte o conteúdo |
toString() |
converte para uma String (imutável) comum |
StringBuilder sb = new StringBuilder("Caelum - Ensino e Inovação");
sb.delete(9, 18); // remove " Ensino e"
System.out.println(sb); // Caelum - Inovação
Definição: Concatenação de String usa StringBuilder por baixo dos panos
O operador + não existe de verdade para String — é açúcar sintático. O
compilador troca toda concatenação ("a" + "b" + variavel) por chamadas de
StringBuilder (new StringBuilder().append("a").append("b").append(variavel)).
Isso já acontece automaticamente para uma linha só; só vale a pena declarar seu
próprio StringBuilder explicitamente quando a concatenação acontece repetida
ao longo de várias linhas/iterações (como dentro de um loop).
Saída formatada: printf/String.format¶
System.out.print/println só concatenam o que você já monta manualmente. Para
formatação mais elaborada (casas decimais, largura mínima, separador de milhar), Java 5
trouxe printf (e String.format, que faz a mesma formatação, devolvendo uma String
em vez de imprimir direto):
O primeiro argumento é o texto com marcadores %; os demais são os valores a inserir,
na ordem. Cada marcador segue %[index$][flags][width][.precision]type, e só o % e o
type são obrigatórios:
| Elemento | Faz |
|---|---|
type (b, c, d, f, s, n) |
tipo do valor: boolean, char, inteiro, decimal, String, quebra de linha |
index$ |
qual argumento usar (fora de ordem, ou repetido) — ex.: %2$s %1$s |
width |
largura mínima (completa com espaço à esquerda por padrão) |
flags |
+ (sinal sempre), ( (negativos entre parênteses), - (alinha à esquerda), 0 (completa com zero), , (separador de milhar) |
.precision |
casas decimais, só para f |
System.out.printf("%d%n", 42); // 42
System.out.printf("%5d%n", 22); // " 22" (largura mínima 5)
System.out.printf("%-5s|%n", "foo"); // "foo |" (alinhado à esquerda)
System.out.printf("%,f%n", 1234.56); // 1,234.560000 (separador de milhar)
System.out.printf("%.2f%n", 22.5); // 22.50 (2 casas decimais)
O separador de milhar/decimal (,) depende do Locale — em outro local (pt, BR),
o separador de milhar vira . e o decimal vira ,, seguindo a convenção regional.
java.time (Java 8): datas e horas¶
Antes do Java 8, manipular datas em Java era sofrido — java.util.Date e
java.util.Calendar têm uma API confusa e, pior, mutável. O pacote java.time
substitui as duas por classes imutáveis (como String) e bem mais previsíveis:
| Classe | Representa |
|---|---|
LocalDate |
uma data, sem hora (yyyy-MM-dd) |
LocalTime |
uma hora, sem data (HH:mm:ss.zzz) |
LocalDateTime |
data + hora, sem fuso |
MonthDay |
dia e mês, sem ano (ex.: aniversário recorrente) |
YearMonth |
mês e ano, sem dia |
Period |
intervalo de tempo em anos/meses/dias |
Duration |
intervalo de tempo em horas/minutos/segundos, entre dois Instant |
DateTimeFormatter |
formata e faz parse de datas/horas |
LocalDate hoje = LocalDate.now();
LocalDate natal = LocalDate.of(2024, 12, 25); // ano, mês, dia
LocalDate natal2 = LocalDate.of(2024, Month.DECEMBER, 25); // aceita o enum Month também
Definição: Convenção de nomes dos métodos de java.time
Os mesmos prefixos aparecem em (quase) toda a API, o que ajuda bastante a adivinhar o comportamento de um método que você nunca usou:
get...— obtém um valor (getDayOfMonth(),getYear()).is...— pergunta verdadeiro/falso (isAfter,isBefore,isEqual,isSupported).with...— como um setter, mas devolve um objeto novo com o valor alterado (as classes são imutáveis — nada é alterado in place).plus.../minus...— soma/subtrai uma unidade, devolvendo objeto novo.to...— converte para outro tipo (toLocalDate(),toInstant()).at...— combina com outro objeto (atTime(...),atDate(...)).
LocalDate d = LocalDate.of(2013, 9, 7);
d = d.withMonth(12).plusYears(2); // encadeável — cada chamada devolve um objeto novo
System.out.println(d); // 2015-12-07 — o "d" original de 2013 nunca foi alterado
Para somar/subtrair, existem duas formas — uma com enum fixo (Month,
DayOfWeek), outra com uma unidade genérica (ChronoUnit, quando você quer
parametrizar a unidade em vez de escrever um método por unidade):
d.plusMonths(3); // fixo, direto
d.plus(3, ChronoUnit.WEEKS); // genérico — a unidade é um parâmetro
Cuidado: nem toda classe suporta toda unidade — LocalDate não tem hora, então
d.plus(3, ChronoUnit.HOURS) lança UnsupportedTemporalTypeException; e MonthDay não
tem nenhum método plus/minus, porque um "dia e mês" sozinho não é uma quantidade que
faça sentido somar. Para verificar antes de tentar, existe isSupported(...).
Para calcular a diferença entre duas datas, duas opções, dependendo da unidade:
Period idade = Period.between(nascimento, hoje); // quebrado em anos/meses/dias
System.out.println(idade.getYears() + " anos, " + idade.getMonths() + " meses");
long diasTotais = ChronoUnit.DAYS.between(nascimento, hoje); // um número só, numa unidade
Duration é o equivalente a Period, mas para granularidade mais fina (segundos,
baseado em Instant, não em datas de calendário):
Instant t1 = Instant.now();
Instant t2 = t1.plus(Duration.ofSeconds(10));
long segundos = Duration.between(t1, t2).getSeconds(); // 10
Formatação e parse, com DateTimeFormatter (mesmo padrão de símbolos do antigo
SimpleDateFormat):
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
String texto = formatter.format(LocalDate.of(1986, 4, 23)); // "23/04/1986"
LocalDate data = LocalDate.parse("23/04/1986", formatter); // caminho inverso
Um formato ou caractere não reconhecido, ao fazer o parse, lança
DateTimeParseException.
Compatibilidade com a API antiga: Date/Calendar ganharam métodos para converter
para/de java.time, usando Instant como intermediário:
Date legado = new Date();
Instant instant = legado.toInstant();
LocalDateTime moderno = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
Expressões lambda e Streams (Java 8)¶
Antes do Java 8, passar um "comportamento" como argumento (ex.: um critério de comparação) exigia criar uma classe (nomeada, ou uma classe anônima) que implementasse a interface esperada:
Collections.sort(livros, new Comparator<Livro>() {
@Override
public int compare(Livro l1, Livro l2) {
return l1.getNome().compareTo(l2.getNome());
}
});
Quando essa interface é uma interface funcional
(um único método abstrato — Comparator é uma delas), uma expressão lambda substitui
a classe anônima inteira por uma sintaxe muito mais enxuta:
Collections.sort(livros, (Livro l1, Livro l2) -> l1.getNome().compareTo(l2.getNome()));
// o compilador infere os tipos dos parâmetros a partir do Comparator esperado:
Collections.sort(livros, (l1, l2) -> l1.getNome().compareTo(l2.getNome()));
Definição: Expressão lambda
Forma compacta de escrever a implementação de uma interface funcional:
(parâmetros) -> expressão-ou-bloco. Sem nome de classe, sem @Override, sem
declarar tipo dos parâmetros (o compilador infere a partir do contexto). É, por
baixo dos panos, uma instância daquela interface — não um conceito novo de linguagem.
Quando o corpo do lambda só delega para um método já existente, um method reference é ainda mais direto:
Regras de sintaxe do lambda¶
A forma completa é (parâmetros) -> { corpo }, mas várias partes podem ser omitidas —
o que explica por que lambdas aparecem em tantos estilos diferentes:
- Sem parâmetro: precisa dos parênteses vazios —
() -> System.out.println("oi"). - Um parâmetro só: os parênteses são opcionais —
p -> p.getAge() >= 18. - Mais de um parâmetro: precisa dos parênteses, separados por vírgula —
(a, b) -> a + b. - Tipo dos parâmetros: opcional, o compilador infere pelo contexto — mas se você declarar o tipo de um parâmetro, precisa declarar de todos (não dá pra misturar).
- Corpo com uma instrução só que já é o retorno: pode omitir as chaves e a
palavra
returnjuntos —p -> p.getAge() >= 18(sem{ return ...; }). - Corpo com mais de uma instrução: as chaves são obrigatórias, e aí
returnvolta a ser necessário como em qualquer método.
Predicate<T>: um lambda pronto para "isso é verdade?"¶
Antes de escrever sua própria interface funcional para representar "um critério que
retorna boolean", vale conhecer java.util.function.Predicate<T> — já pronta na API,
com um único método test(T t):
Predicate é só uma das interfaces funcionais prontas da API (Function<T,R>,
Consumer<T>, Supplier<T>, entre outras) — todas seguem o mesmo espírito: evitar que
cada código precise declarar sua própria interface de um método só.
Captura de variáveis: effectively final¶
Um lambda pode acessar livremente atributos (de instância ou estáticos) da classe onde
foi criado — sem restrição. Já para variáveis locais e parâmetros do método onde o
lambda vive, a regra é mais rígida: só pode acessar as que forem final ou
effectively final (nunca reatribuídas depois de inicializadas, mesmo sem a
palavra-chave final escrita):
void metodo() {
int contador = 0; // effectively final (nunca muda depois)
int total = 10;
total = total + 5; // reatribuída — deixa de ser effectively final
Runnable r = () -> {
System.out.println(contador); // ok
System.out.println(total); // erro de compilação — total não é (effectively) final
};
}
A justificativa é o próprio ciclo de vida: o lambda pode ser executado (por outra thread, por exemplo) bem depois que o método que o criou já retornou — nesse ponto, a variável local já não existe mais na pilha. O lambda recebe uma cópia congelada do valor, e permitir reatribuição criaria a ilusão enganosa de que as duas ficam sincronizadas, quando não ficam.
Outra consequência do lambda compartilhar o escopo do método (diferente de uma classe anônima, que tem escopo próprio): o nome de um parâmetro do lambda não pode conflitar com uma variável já visível ali — declarar de novo o mesmo nome é erro de compilação, exatamente como declarar duas variáveis locais de mesmo nome no mesmo escopo.
Stream: operações declarativas sobre coleções¶
Stream é uma API (também Java 8) para processar coleções de forma declarativa —
descrevendo o quê fazer, em vez de escrever o for manual de como fazer:
livros.stream()
.filter(l -> l.getNome().contains("Java"))
.forEach(l -> System.out.println(l.getNome()));
Pontos importantes sobre Stream:
- Não modifica a coleção original —
filterdevolve um novo Stream; a lista original (livros) continua intacta. Isso evita efeitos colaterais inesperados. - Encadeia operações (
filter, depoisforEach, e várias outras da API) de forma fluente, sem precisar de listas intermediárias para guardar resultados parciais. stream()eforEachtambém são default methods — deCollectioneIterablerespectivamente — o que permite adicioná-los a toda a API de Collections já existente sem quebrar compatibilidade com código anterior ao Java 8 (ver Orientação a Objetos, seção Interface).
Guia rápido de sintaxe: o que consultar no dia a dia¶
Complemento de consulta com a sintaxe mais usada, em Java moderno (as formas com setas no
switch, record e Stream.toList() exigem JDKs recentes — confira a versão). Os
conceitos de base (tipos, operadores, laços, métodos, String) estão nas seções acima;
aqui entram os pontos que ainda não apareciam.
Entrada de dados com Scanner¶
import java.util.Scanner;
Scanner sc = new Scanner(System.in);
System.out.print("Idade: ");
int idade = sc.nextInt();
sc.nextLine(); // consome a quebra de linha que sobrou
System.out.print("Nome completo: ");
String nome = sc.nextLine();
sc.close();
nextInt/nextDouble leem o valor, mas deixam a quebra de linha no buffer; sem o
nextLine() extra, o próximo nextLine() devolve uma linha vazia. Feche o Scanner só
quando não houver mais leituras (fechar também fecha o System.in).
Arrays e matrizes¶
int[] notas = new int[4]; // tamanho fixo; índices de 0 a length - 1
int[] valores = {4, 7, 2, 9};
int tamanho = valores.length; // length é atributo, sem parênteses
int[][] matriz = { {1, 2, 3}, {4, 5, 6} };
int valor = matriz[1][2]; // linha 1, coluna 2 -> 6
for (int i = 0; i < matriz.length; i++) {
for (int j = 0; j < matriz[i].length; j++) { // length da LINHA no laço interno
System.out.print(matriz[i][j] + " ");
}
}
A classe utilitária java.util.Arrays cobre o resto:
| Método | Função |
|---|---|
Arrays.sort(a) |
Ordena o array (in-place) |
Arrays.binarySearch(a, x) |
Busca binária — exige array ordenado |
Arrays.copyOf(a, n) |
Copia aumentando ou reduzindo o tamanho |
Arrays.toString(a) / deepToString(m) |
Texto legível do array / da matriz |
Acessar índice < 0 ou >= length lança ArrayIndexOutOfBoundsException. Matrizes são
arrays de arrays — as linhas podem ter tamanhos diferentes. No for-each, alterar a
variável do laço não substitui o elemento primitivo no array.
switch como expressão (Java 14+)¶
A forma com setas não precisa de break, não "cai" para o caso seguinte e produz um
valor:
int dia = 2;
String tipo = switch (dia) {
case 1, 7 -> "fim de semana"; // vários rótulos no mesmo caso
case 2, 3, 4, 5, 6 -> "útil";
default -> "inválido";
}; // termina com ;
String faixa = switch (dia) {
case 1 -> "domingo";
default -> {
String x = "outro";
yield x; // bloco: entrega o valor com yield
}
};
No switch clássico (case 1: ... break;), esquecer o break faz a execução continuar
nos casos seguintes (fall-through).
enum e record¶
enum Prioridade {
BAIXA(1), MEDIA(2), ALTA(3);
private final int peso;
Prioridade(int peso) { this.peso = peso; }
public int getPeso() { return peso; }
}
Prioridade p = Prioridade.ALTA;
for (Prioridade item : Prioridade.values()) System.out.println(item);
Prioridade.valueOf("MEDIA"); // converte o nome exato
Definição: enum
Conjunto fechado e controlado de constantes (instâncias), que pode ter atributos,
construtor e métodos. Prefira enum a Strings soltas para estados e categorias
fixas.
record Ponto(int x, int y) {
Ponto { // construtor compacto: validação
if (x < 0 || y < 0) throw new IllegalArgumentException();
}
int soma() { return x + y; }
}
Ponto p = new Ponto(2, 3);
System.out.println(p.x()); // acessor é x(), não getX()
Definição: record (Java 16+)
Classe imutável de dados: o compilador gera construtor, acessores, equals,
hashCode e toString a partir dos componentes (que são final). Ideal para DTOs
e valores; não substitui toda classe de domínio.
Coleções: API de consulta¶
List<String> lista = new ArrayList<>(); // sequência, aceita repetidos
Set<String> unicos = new HashSet<>(); // valores únicos
Map<String, Integer> mapa = new HashMap<>(); // chave -> valor
Queue<String> fila = new ArrayDeque<>(); // FIFO
Declare pela interface (List, não ArrayList, no tipo da variável) para poder
trocar a implementação. Estruturas por trás de cada uma em
Estrutura de Dados.
| Estrutura | Principais operações | Detalhes |
|---|---|---|
| List | add, add(i, x), get, set, remove, contains, size |
Em List<Integer>, remove(1) remove pelo índice; use remove(Integer.valueOf(1)) para remover o valor |
| Set | add, contains, remove |
HashSet sem ordem; LinkedHashSet preserva a inserção; TreeSet mantém ordem natural; exige equals/hashCode coerentes |
| Map | put, get, getOrDefault, containsKey, containsValue, entrySet, remove |
put insere ou substitui; chaves únicas |
| Queue | offer, peek, poll |
peek consulta sem remover; poll remove e devolve null se vazia |
| Deque | addFirst, addLast, removeFirst, removeLast |
Duas pontas; ArrayDeque não aceita null e substitui Stack |
Generics: parametrizam classes e métodos; o compilador impede misturas incompatíveis.
Não aceitam primitivos (use Integer, Double...).
class Caixa<T> {
private T valor;
void guardar(T valor) { this.valor = valor; }
T obter() { return valor; }
}
static <T> T primeiro(List<T> itens) { return itens.get(0); } // método genérico
Exceções: sintaxe¶
try {
int n = Integer.parseInt("abc");
} catch (NumberFormatException e) { // capture tipos específicos
System.out.println("Número inválido");
} finally {
System.out.println("Roda sempre"); // opcional
}
try { /* ... */ } catch (IOException | SecurityException e) { /* multicatch */ }
class SaldoInsuficienteException extends RuntimeException {
SaldoInsuficienteException(String msg) { super(msg); }
}
void sacar(double valor) {
if (valor > saldo) throw new SaldoInsuficienteException("Saldo insuficiente");
}
void ler() throws IOException { } // declara que pode propagar
throw lança uma instância; throws faz parte da assinatura. Nunca deixe um catch
vazio — trate, registre ou propague; e não use exceção para controlar fluxo normal.
Estratégia de tratamento em
Boas Práticas.
Arquivos: Path e Files¶
import java.nio.file.*;
Path p = Path.of("dados", "nomes.txt");
Files.createDirectories(p.getParent());
Files.writeString(p, "Ana\nBia\n");
String conteudo = Files.readString(p);
List<String> linhas = Files.readAllLines(p); // carrega tudo na memória
boolean existe = Files.exists(p);
try (BufferedReader leitor = Files.newBufferedReader(p)) { // fecha sozinho
String linha;
while ((linha = leitor.readLine()) != null) System.out.println(linha);
} catch (IOException e) {
System.out.println("Falha na leitura");
}
Os métodos de Files lançam IOException. readAllLines não serve para arquivos
grandes (use leitura em fluxo). Com try-with-resources, vários recursos podem ser
declarados separados por ;.
Programação funcional: referências, Collectors e Optional¶
Complementa Expressões lambda e Streams.
nomes.forEach(System.out::println); // referência de método
List<String> maiusculas = nomes.stream().map(String::toUpperCase).toList();
// formas: Classe::estático objeto::instância Classe::new
Map<String, List<Produto>> porCategoria =
produtos.stream().collect(Collectors.groupingBy(Produto::categoria));
Map<String, Long> quantidade = produtos.stream()
.collect(Collectors.groupingBy(Produto::categoria, Collectors.counting()));
String nomesJuntos = produtos.stream().map(Produto::nome)
.collect(Collectors.joining(", "));
Definição: Optional
Contêiner que torna explícita a possível ausência de um valor e oferece operações
para tratar cada caso, em vez de devolver null.
Optional<Usuario> encontrado = buscarPorId(10);
String nome = encontrado.map(Usuario::getNome).orElse("Desconhecido");
encontrado.ifPresent(System.out::println);
Usuario u = encontrado.orElseThrow(() -> new IllegalStateException("Ausente"));
Optional.of exige valor não nulo; ofNullable aceita null. É mais indicado como
retorno de método; evite-o indiscriminadamente em campos e parâmetros.
Anotações comuns¶
Anotações descrevem elementos do código (metadados para compilador e ferramentas); não são chamadas de método.
| Anotação | Efeito |
|---|---|
@Override |
O compilador verifica que o método de fato sobrescreve outro |
@Deprecated |
Marca API antiga |
@SuppressWarnings("unchecked") |
Silencia um aviso — só depois de entender a causa |
@FunctionalInterface |
Valida que a interface tem um único método abstrato |
Checklist de depuração antes de executar¶
- Arquivo e classe pública têm o mesmo nome.
- Chaves, parênteses e aspas estão fechados; instruções terminam com
;. - Maiúsculas/minúsculas respeitadas (Java diferencia).
- Não há divisão inteira onde se esperava resultado decimal (
7 / 2é3). Strings são comparadas comequals, não==.- Índices ficam entre
0etamanho - 1. - Laços alteram a variável usada na condição.
- Métodos retornam valor em todos os caminhos.
null, entradas inválidas e coleções vazias foram considerados.
Se o erro persistir, reduza o programa até a menor parte que ainda falha e leia a primeira mensagem do compilador antes das demais.