Skip to content

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

java MeuPrimeiroPrograma Java Rodrigo

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 comandos javac/java — a forma recomendada, pois vale só para aquela execução:
    javac -cp /caminho/para/lib.jar MeuArquivo.java
    java -cp /caminho/para/lib.jar MeuArquivo
    
    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:

java -jar aplicacao.jar

Convenções da linguagem

  • Nome do arquivo = nome da classe. Um arquivo MeuPrimeiroPrograma.java deve conter uma classe MeuPrimeiroPrograma. Isso evita ambiguidade na hora de compilar/rodar.
  • Java é case sensitive: System e system sã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:

public static void main(String[] args) {
    // ponto de entrada do programa
}

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 main e receber um array (ou varargs) de String.

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

class X {
    int a = 100;
    void method() {
        int a = 200; // shadowing
        System.out.println(a);      // 200 (a local)
        System.out.println(this.a); // 100 (a de instância)
    }
}
Duas variáveis não podem ter o mesmo nome no mesmo escopo (isso é erro de compilação) — shadowing só é permitido entre escopos diferentes.

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
    }
}
O caminho inverso funciona sem problema: um método de instância pode acessar livremente membros 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)
Além disso, uma classe não pode ter um método de instância com a mesma assinatura de um método 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):
    void method() {
        int idade;
        System.out.println(idade); // erro de compilação — não inicializada
    }
    
  • 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 _:

double d = 3.1E2;        // 310.0
int milhao = 1_000_000;  // == 1000000

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, true e null, 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: idade e Idade sã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
Um jeito de decorar quando precisa de casting: os tipos numéricos formam uma cadeia de "cabe dentro de" — 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):

int i = 200;
System.out.println(i / 0);    // ArithmeticException
System.out.println(i / 0.0);  // Infinity — não lança exceção

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.

if (v1 > v2 && v2 < v3) {
    // v1 é > que v2 e v2 é < que v3
}

Regras que geram erro de compilação por comparar tipos incompatíveis:

  • boolean só aceita ==/!= (não existe "verdadeiro > falso").
  • Não é possível comparar tipos sem relação nenhuma entre si (ex.: String com int).
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.

int i = 10;
System.out.println(i == 2 & i++ == 0); // false, mas i vira 11 (& sempre avalia)
int j = 10;
System.out.println(j == 2 && j++ == 0); // false, e j continua 10 (&& não avaliou)

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

String mensagem = (i % 2 == 0) ? "par" : "ímpar";

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:

  1. pré-incremento/decremento (++x, --x)
  2. multiplicação, divisão, resto (*, /, %)
  3. soma, subtração (+, -)
  4. shifts (<<, >>, >>>)
  5. 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 {
    // ...
}
Isso também explica por que código mal indentado com 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:

int method(int x) {
    if (x > 200) {
        return 5;
    }
    // erro: e se x <= 200? falta um retorno aqui
}
Na direção oposta, código que o compilador consegue provar que nunca roda (ex.: qualquer instrução logo após um 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óprio int): byte, short, char.
  • O wrapper correspondente (Integer, Character, ...).
  • String.
  • enum.
  • Não compila com long, float, double ou boolean.

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)
}
Esquecer o 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):

int i = 1;
while (i < 10) {
    System.out.println(i);
    i++;
}

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

for (int i = 0; i < 35; i++) {
    soma += 59.90;
}

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 assume true por 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 do for).
  • 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:

int[] numbers = {1, 2, 3, 4, 5};
for (int num : numbers) {
    System.out.println(num);
}

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 for tradicional.

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:

int i = 1;
do {
    System.out.println(i);
    i++;
} while (i < 10);

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
        }
    }
}
Regras importantes: um rótulo pode vir antes de qualquer instrução (não só loops), mas 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:

java.util.Date data = new java.util.Date();

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 public por 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):

@Override
public String toString() {
    return "Nome: " + nome + ", Valor: " + valor;
}

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);
}
Repare no uso de 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 A referencia um objeto B, e A ainda é acessível a partir do código em execução, B também é — mesmo sem nenhuma variável apontando para B diretamente.

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
A lição prática: nunca compare wrappers com == — 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:

Integer a = null;
int b = 44;
System.out.println(a + b); // NullPointerException — unboxing de a (null)

As classes wrapper também trazem métodos estáticos utilitários, especialmente para conversão de/para String:

int numero = Integer.parseInt("10");
String texto = String.valueOf(10);

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:

int hex = Integer.parseInt("FF", 16);   // 255
int bin = Integer.parseInt("11", 2);    // 3

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 é chamar texto.replace(...) sem reatribuir o retorno, e se surpreender que texto "não mudou": ele realmente não muda, é imutável.
    String java = "java";
    java.replace("j", "J"); // não faz nada com "java" — o retorno foi descartado
    java = java.replace("j", "J"); // agora sim: java vale "Java"
    

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:
    String a = "a" + "b";
    System.out.println("ab" == a); // true — "a" + "b" virou literal em compilação
    
  • 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:
    String a = "a";
    String ab = a + "b"; // não é constant folding — a é variável
    System.out.println("ab" == ab); // false
    
  • Um método de String que 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);
    }
}
A versão correta recebe 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":

String nulo = null;
System.out.println("valor: " + nulo); // valor: 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
Repare no nome da exceção: 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):

System.out.printf("Hello %s, have a nice day!%n", "Mario");
// Hello Mario, have a nice day!

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:

Collections.sort(livros, Comparator.comparing(Livro::getNome));

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 return juntos — p -> p.getAge() >= 18 (sem { return ...; }).
  • Corpo com mais de uma instrução: as chaves são obrigatórias, e aí return volta 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<Person> maiorDeIdade = p -> p.getAge() >= 18;
maiorDeIdade.test(pessoa); // true ou false

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 — filter devolve um novo Stream; a lista original (livros) continua intacta. Isso evita efeitos colaterais inesperados.
  • Encadeia operações (filter, depois forEach, e várias outras da API) de forma fluente, sem precisar de listas intermediárias para guardar resultados parciais.
  • stream() e forEach também são default methods — de Collection e Iterable respectivamente — 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
for (var e : estoque.entrySet()) {
    System.out.println(e.getKey() + " = " + e.getValue());
}

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

  1. Arquivo e classe pública têm o mesmo nome.
  2. Chaves, parênteses e aspas estão fechados; instruções terminam com ;.
  3. Maiúsculas/minúsculas respeitadas (Java diferencia).
  4. Não há divisão inteira onde se esperava resultado decimal (7 / 2 é 3).
  5. Strings são comparadas com equals, não ==.
  6. Índices ficam entre 0 e tamanho - 1.
  7. Laços alteram a variável usada na condição.
  8. Métodos retornam valor em todos os caminhos.
  9. 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.