Tipos por referência: o que a variável realmente guarda
Em Java, muitos valores não cabem “dentro” da variável como acontece com tipos primitivos. Para objetos (como String, arrays e instâncias de classes), a variável guarda uma referência: um valor que aponta para um objeto existente na memória. A referência permite acessar o objeto (seus campos e métodos), mas não é o objeto em si.
Na prática, isso explica por que duas variáveis podem “apontar” para o mesmo objeto e por que atribuições e parâmetros podem causar efeitos colaterais quando o objeto é mutável.
Analogia útil (sem depender de detalhes internos)
- Primitivo: a variável guarda o valor (ex.:
intguarda 10). - Referência: a variável guarda um “endereço/ponteiro lógico” para um objeto (ex.:
Clienteguarda uma referência para um objetoCliente).
Criação de objetos com new (passo a passo)
Quando você usa new, Java cria um objeto e devolve uma referência para ele. O passo a passo mental é:
- Reservar memória para o novo objeto.
- Inicializar seus campos com valores padrão (ou com o que o construtor definir).
- Executar o construtor.
- Retornar a referência para a variável receber.
class Pessoa { String nome; int idade;}Pessoa p = new Pessoa(); // p recebe uma referência para um novo objeto PessoaApós isso, p não “contém” a pessoa; p aponta para ela.
Diferença prática: primitivos vs referências
Atribuição
Em primitivos, a atribuição copia o valor. Em referências, a atribuição copia a referência (ou seja, passa a apontar para o mesmo objeto).
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
int a = 10;int b = a; // copia o valor 10b = 99; // não afeta aPessoa p1 = new Pessoa();p1.nome = "Ana";Pessoa p2 = p1; // copia a referência (p2 aponta para o mesmo objeto)p2.nome = "Bia"; // altera o mesmo objeto apontado por p1 e p2System.out.println(p1.nome); // BiaIdentidade (mesmo objeto) vs igualdade (mesmo conteúdo)
Para referências, existem duas perguntas diferentes:
- Identidade: são o mesmo objeto? Use
==. - Igualdade: têm o mesmo conteúdo/estado? Use
equals(quando implementado).
Exemplo com String: == vs equals
String é um objeto. Em geral, compare conteúdo com equals.
String s1 = new String("java");String s2 = new String("java");System.out.println(s1 == s2); // false (objetos diferentes)System.out.println(s1.equals(s2)); // true (mesmo conteúdo)Agora um caso comum: literais podem ser reutilizados internamente, então == pode “parecer funcionar” em alguns casos, mas não é uma regra segura para conteúdo.
String a = "java";String b = "java";System.out.println(a == b); // pode ser true (mesma referência reutilizada)System.out.println(a.equals(b)); // trueRegra prática: para String, use equals para comparar texto.
Exemplo com objeto simples: implementando equals
Se você quer igualdade por conteúdo em suas classes, implemente equals (e, em projetos reais, também hashCode).
class Ponto { int x; int y; Ponto(int x, int y) { this.x = x; this.y = y; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Ponto p = (Ponto) o; return x == p.x && y == p.y; }}Ponto p1 = new Ponto(1, 2);Ponto p2 = new Ponto(1, 2);System.out.println(p1 == p2); // false (objetos diferentes)System.out.println(p1.equals(p2)); // true (mesmo conteúdo)Passagem de parâmetros: o que realmente é passado
Em Java, parâmetros são sempre passados por valor. A confusão acontece porque, no caso de objetos, o “valor” passado é a referência. Isso significa:
- O método recebe uma cópia da referência.
- Com essa cópia, ele pode modificar o objeto (se ele for mutável).
- Mas se o método reatribuir o parâmetro para outro objeto, isso não muda a variável original de quem chamou.
Exemplo 1: método altera o objeto (efeito visível fora)
class Conta { double saldo;}static void depositar(Conta c, double valor) { c.saldo += valor;}Conta conta = new Conta();conta.saldo = 100;depositar(conta, 50);System.out.println(conta.saldo); // 150Exemplo 2: método reatribui o parâmetro (não afeta quem chamou)
static void trocarConta(Conta c) { c = new Conta(); c.saldo = 999;}Conta conta = new Conta();conta.saldo = 100;trocarConta(conta);System.out.println(conta.saldo); // 100 (não mudou)O método recebeu uma cópia da referência. Ao fazer c = new Conta(), ele só mudou a cópia local.
null: ausência de objeto
null é um valor especial que significa “nenhum objeto”. Ele só pode ser atribuído a variáveis de referência (não a primitivos).
Pessoa p = null;String nome = null;Se você tentar acessar um membro (campo/método) através de uma referência null, ocorre NullPointerException.
Pessoa p = null;System.out.println(p.nome); // NullPointerExceptionOnde o null aparece com frequência
- Variável declarada mas não inicializada com objeto.
- Retorno de método indicando “não encontrado”.
- Campo opcional que ainda não foi preenchido.
- Elementos de array/coleção não preenchidos (dependendo do caso).
Padrões de checagem para evitar NullPointerException
1) Checagem explícita com if (passo a passo)
Quando um valor pode ser null, valide antes de usar:
- Verifique
obj == null. - Trate o caso (retorne, lance exceção, use padrão).
- Somente então acesse membros.
static int tamanhoDoNome(Pessoa p) { if (p == null || p.nome == null) { return 0; } return p.nome.length();}2) Comparação segura de String
Para evitar NPE ao comparar texto, prefira chamar equals em um literal (que não é null):
String status = null;boolean ativo = "ATIVO".equals(status); // false, sem NPEEvite:
// pode lançar NPE se status for nullboolean ativo = status.equals("ATIVO");3) Falhar rápido com validação
Quando null é inválido para o seu método, é melhor acusar o erro imediatamente:
static void imprimirNome(Pessoa p) { if (p == null) { throw new IllegalArgumentException("pessoa não pode ser null"); } if (p.nome == null) { throw new IllegalArgumentException("nome não pode ser null"); } System.out.println(p.nome);}4) Valor padrão (quando fizer sentido)
Em alguns casos, você pode substituir null por um valor padrão:
static String nomeOuPadrao(Pessoa p) { if (p == null || p.nome == null) return "(sem nome)"; return p.nome;}Noções de memória: referências compartilhadas e efeitos colaterais
Quando duas variáveis guardam a mesma referência, qualquer alteração no objeto por uma delas será vista pela outra. Isso é comum ao:
- Atribuir uma variável de referência para outra.
- Passar um objeto para métodos que modificam seu estado.
- Guardar o mesmo objeto em mais de um lugar (ex.: duas posições de array).
Exemplo: referência compartilhada em dois “donos”
class Endereco { String cidade;}class Cliente { String nome; Endereco endereco;}Endereco e = new Endereco();e.cidade = "Recife";Cliente c1 = new Cliente();c1.nome = "João";c1.endereco = e;Cliente c2 = new Cliente();c2.nome = "Maria";c2.endereco = e; // compartilham o mesmo Enderecoc2.endereco.cidade = "Natal";System.out.println(c1.endereco.cidade); // Natal (efeito colateral)Se a intenção era cada cliente ter seu próprio endereço, você precisa criar outro objeto:
c2.endereco = new Endereco();c2.endereco.cidade = "Natal";Atividade prática: diagnosticar erros comuns com null e referências
Leia cada trecho, responda: (1) o que acontece ao executar? (2) por quê? (3) como corrigir com a menor mudança possível?
Parte A — NPE por falta de inicialização
class Produto { String nome;}Produto p = null;System.out.println(p.nome.toUpperCase());- Dica: identifique exatamente qual acesso dispara a exceção.
Parte B — NPE em comparação de String
String categoria = null;if (categoria.equals("LIVRO")) { System.out.println("É livro");}- Dica: reescreva a comparação para ser null-safe.
Parte C — Igualdade vs identidade
String x = new String("ok");String y = new String("ok");if (x == y) { System.out.println("iguais");} else { System.out.println("diferentes");}- Dica: o que muda se usar
equals?
Parte D — Referência compartilhada (efeito colateral inesperado)
class Carrinho { int itens;}Carrinho a = new Carrinho();a.itens = 1;Carrinho b = a;b.itens++;System.out.println(a.itens);- Dica: como criar um carrinho independente para
b?
Parte E — Parâmetro reatribuído não altera o chamador
class Usuario { String nome;}static void renomear(Usuario u) { u = new Usuario(); u.nome = "Novo";}Usuario u = new Usuario();u.nome = "Antigo";renomear(u);System.out.println(u.nome);- Dica: para mudar o nome do mesmo objeto, o método deve alterar o campo do objeto recebido, não reatribuir o parâmetro.
Checklist de diagnóstico (use em todas as partes)
| Pergunta | O que verificar |
|---|---|
Alguma referência pode ser null? | Variáveis, campos, retornos de método |
Estou usando == para conteúdo? | Trocar por equals quando a intenção for comparar valores |
| Há mais de uma variável apontando para o mesmo objeto? | Atribuições do tipo b = a, campos compartilhados |
| O método reatribui o parâmetro? | Reatribuição não altera a referência do chamador |