Java Escopo
Toda variável tem um "território": a parte do código em que ela existe e pode ser usada. Esse território se chama escopo. Fora dele, para o Java, a variável simplesmente não existe, e o resultado é o erro mais comum de todos: cannot find symbol.
- A regra das chaves: uma variável vale do ponto em que é declarada até o
}que fecha o seu bloco. - Os três níveis: variável de bloco, de método e de classe.
- Quanto tempo cada uma "vive" (por que um contador local volta a zero a cada chamada).
- O que é sombreamento: uma variável local com o mesmo nome de uma da classe.
- Os erros reais
cannot find symbol,already definedemight not have been initialized, e como resolvê-los.
A ideia antes do código: os cômodos da escola
Pense na escola da Ana. No mural do corredor há avisos que todo mundo vê, de qualquer sala. Dentro de cada sala, o quadro tem o que o professor escreveu ali; quem está em outra sala não vê. E, dentro da sala, um aluno pode ter um bilhete na carteira, que só existe enquanto ele está sentado ali.
| Na escola | No Java | Quem enxerga |
|---|---|---|
| Mural do corredor | Variável de classe (um campo) | Todos os métodos da classe |
| Quadro de uma sala | Variável de método (local, ou um parâmetro) | Só o método onde foi declarada |
| Bilhete numa carteira | Variável de bloco (dentro de um if, for, { }) | Só aquele bloco |
A regra é sempre a mesma: de dentro você enxerga para fora; de fora você não enxerga para dentro. Quem está numa carteira vê o quadro da sala e o mural do corredor. Quem está no corredor não vê o bilhete de ninguém.
A regra das chaves
Em Java, os "cômodos" são marcados por chaves { }. Uma variável existe do ponto em que é declarada até a chave } que fecha o bloco onde ela foi declarada. Depois disso, ela some.
Variável de bloco
A Ana declarou a mensagem dentro do if e tentou usá-la depois:
public class Main {
public static void main(String[] args) {
double nota = 8.5;
if (nota >= 7) {
String mensagem = "Aprovado!";
System.out.println(mensagem);
}
System.out.println(mensagem);
}
}
$ javac Main.java
Main.java:8: error: cannot find symbol
System.out.println(mensagem);
^
symbol: variable mensagem
location: class Main
1 error
- double nota
- Declarada direto no
main: vale até o}domain(linha 9). Pode ser usada dentro doif, porque oifestá "dentro" domain. - String mensagem
- Declarada dentro das chaves do
if: vale só até o}da linha 7. - linha 6
- Ainda dentro do
if:mensagemexiste. Funciona. - linha 8
- Fora do
if:mensagemjá sumiu. O compilador procura e não encontra:cannot find symbol … variable mensagem.
A correção é declarar a variável fora (antes) do bloco, no escopo em que ela será usada, e só dar o valor lá dentro:
public class Main {
public static void main(String[] args) {
double nota = 8.5;
String mensagem;
if (nota >= 7) {
mensagem = "Aprovado!";
} else {
mensagem = "Reprovado.";
}
System.out.println(mensagem);
}
}
$ javac Main.java && java Main Aprovado!
Repare no else. Sem ele, o compilador reclama de outra coisa: se a nota for menor que 7, mensagem chegaria ao println sem valor nenhum.
$ javac Main.java
Main.java:8: error: variable mensagem might not have been initialized
System.out.println(mensagem);
^
1 error
"A variável mensagem pode não ter sido inicializada". Variáveis locais em Java não têm valor automático: alguém precisa dar um valor antes do primeiro uso, em todos os caminhos. Soluções: o else, ou um valor inicial na declaração (String mensagem = "Reprovado.";).
Missão: o código não compila por causa do escopo. Mova a declaração de mensagem para antes do if (com um valor inicial ou com um else) para o programa imprimir Aprovado! e depois Resultado: Aprovado!.
public class Main {
public static void main(String[] args) {
double nota = 8.5;
if (nota >= 7) {
String mensagem = "Aprovado!";
System.out.println(mensagem);
}
System.out.println("Resultado: " + mensagem);
}
}
public class Main {
public static void main(String[] args) {
double nota = 8.5;
String mensagem = "Reprovado.";
if (nota >= 7) {
mensagem = "Aprovado!";
System.out.println(mensagem);
}
System.out.println("Resultado: " + mensagem);
}
}
A variável do for
A variável declarada nos parênteses do for pertence ao laço. Quando o laço acaba, ela some:
public class Main {
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) {
System.out.println("Volta " + i);
}
System.out.println("Terminei na volta " + i);
}
}
$ javac Main.java
Main.java:6: error: cannot find symbol
System.out.println("Terminei na volta " + i);
^
symbol: variable i
location: class Main
1 error
Se você precisa do valor depois do laço, declare a variável antes: int i; e for (i = 1; i <= 3; i++). Mais sobre isso em for.
A vantagem dessa regra é que você pode reaproveitar nomes em blocos separados (vizinhos), sem conflito:
public class Main {
public static void main(String[] args) {
for (int i = 1; i <= 2; i++) {
System.out.println("Primeiro laço: " + i);
}
for (int i = 10; i <= 11; i++) {
System.out.println("Segundo laço: " + i);
}
{
int x = 1;
System.out.println("Bloco solto: x = " + x);
}
int x = 2;
System.out.println("Depois do bloco: x = " + x);
}
}
$ javac Main.java && java Main Primeiro laço: 1 Primeiro laço: 2 Segundo laço: 10 Segundo laço: 11 Bloco solto: x = 1 Depois do bloco: x = 2
Cada i vive no seu for. O { … } "solto" no meio do main é um bloco válido (raro na prática, mas mostra a regra): o primeiro x some no }, e o nome fica livre para o segundo.
Variável de método
Cada método é uma sala fechada. Uma variável declarada no main não existe dentro de outro método, mesmo que esse método seja chamado pelo main:
public class Main {
static void mostrarAluno() {
System.out.println("Aluno: " + nome);
}
public static void main(String[] args) {
String nome = "Bruno";
mostrarAluno();
}
}
$ javac Main.java
Main.java:3: error: cannot find symbol
System.out.println("Aluno: " + nome);
^
symbol: variable nome
location: class Main
1 error
O nome é do main. O mostrarAluno não enxerga o "quadro" de outra sala. A forma certa de levar o valor até lá é um parâmetro: static void mostrarAluno(String nome) e a chamada mostrarAluno(nome) (veja Parâmetros). Os parâmetros também são variáveis de método: valem no corpo inteiro do método e somem quando ele termina.
Variável de classe
Uma variável declarada dentro da classe e fora de qualquer método é um campo (ou atributo) da classe. Ela fica no "corredor": todos os métodos da classe a enxergam.
public class Main {
static String escola = "Escola da Ana";
static void mostrarCabecalho() {
System.out.println("== " + escola + " ==");
}
static void mostrarRodape() {
System.out.println("Fim do boletim - " + escola);
}
public static void main(String[] args) {
mostrarCabecalho();
System.out.println("Bruno: 8.5");
mostrarRodape();
}
}
$ javac Main.java && java Main == Escola da Ana == Bruno: 8.5 Fim do boletim - Escola da Ana
- static
- Pelo mesmo motivo dos métodos: o campo pertence à classe e pode ser usado pelos métodos
static. Detalhes em static. - String escola = "Escola da Ana";
- Declaração comum, só que no nível da classe. Vale na classe inteira, em qualquer método, antes ou depois da declaração.
Campos são o assunto central da orientação a objetos, que começa em O que é POO e continua em Atributos. Aqui, o que importa é o escopo: a classe toda.
É tentador "resolver" o cannot find symbol subindo a variável para a classe. Funciona, mas qualquer método passa a poder mudá-la, e fica difícil descobrir quem mudou o quê. Prefira passar valores por parâmetro e devolvê-los com return. Use campos para o que realmente pertence à classe inteira.
Tempo de vida: quem lembra e quem esquece
O escopo diz onde a variável pode ser usada. Há também o quando: por quanto tempo o valor existe. Uma variável local é criada a cada chamada do método e destruída no fim dela. Um campo da classe vive enquanto o programa roda.
public class Main {
static int visitasDoSite = 0;
static void registrarVisita() {
int visitasLocal = 0;
visitasLocal++;
visitasDoSite++;
System.out.println("local = " + visitasLocal + ", da classe = " + visitasDoSite);
}
public static void main(String[] args) {
registrarVisita();
registrarVisita();
registrarVisita();
}
}
$ javac Main.java && java Main local = 1, da classe = 1 local = 1, da classe = 2 « a local nasceu de novo valendo 0; a da classe lembrou local = 1, da classe = 3
A cada chamada, visitasLocal nasce com 0, vira 1 e morre no fim do método. Já visitasDoSite continua existindo entre as chamadas e acumula. Se você precisa que um método "lembre" algo de uma chamada para a outra, uma variável local não serve.
Brinque com o código: chame registrarVisita() mais vezes. Depois mova a linha int visitasLocal = 0; para fora do método (na classe, como static int visitasLocal = 0;) e veja a coluna "local" começar a contar.
public class Main {
static int visitasDoSite = 0;
static void registrarVisita() {
int visitasLocal = 0;
visitasLocal++;
visitasDoSite++;
System.out.println("local = " + visitasLocal + ", da classe = " + visitasDoSite);
}
public static void main(String[] args) {
registrarVisita();
registrarVisita();
registrarVisita();
}
}
Sombreamento: o mesmo nome em dois níveis
Se um método declara uma variável local com o mesmo nome de um campo da classe, a local "faz sombra" no campo: dentro do método, o nome passa a significar a local. Isso se chama sombreamento (em inglês, shadowing). O campo continua lá, intacto, e pode ser alcançado pelo nome completo, NomeDaClasse.campo:
public class Main {
static String escola = "Escola da Ana";
static void mostrarCabecalho() {
String escola = "Escola do Bairro";
System.out.println("Variável local: " + escola);
System.out.println("Campo da classe: " + Main.escola);
}
public static void main(String[] args) {
mostrarCabecalho();
System.out.println("No main: " + escola);
}
}
$ javac Main.java && java Main Variável local: Escola do Bairro Campo da classe: Escola da Ana No main: Escola da Ana
O Java permite isso, mas é uma fonte clássica de bugs. Veja este: a Ana quer um método que mude o nome da escola.
public class Main {
static String escola = "Escola da Ana";
static void mudarNome(String novoNome) {
String escola = novoNome;
System.out.println("Mudei para: " + escola);
}
public static void main(String[] args) {
mudarNome("Colégio Ana Souza");
System.out.println("Nome atual: " + escola);
}
}
$ javac Main.java && java Main Mudei para: Colégio Ana Souza Nome atual: Escola da Ana « o campo não mudou!
Compila, roda, e está errado. O String na frente de escola = novoNome criou uma variável local nova, que fez sombra no campo e morreu no fim do método. O campo nunca foi tocado. A correção é tirar o tipo, para que a linha seja uma atribuição ao campo: escola = novoNome;.
Com objetos, o caso mais comum é um parâmetro com o mesmo nome de um atributo (nome e nome). Para diferenciar os dois, o Java tem a palavra this, assunto de uma aula só para ela: this.
Onde a sombra não é permitida
Dentro de um mesmo método, o Java proíbe reaproveitar um nome que ainda está "vivo", mesmo num bloco interno:
public class Main {
public static void main(String[] args) {
int total = 10;
if (total > 5) {
int total = 20;
System.out.println(total);
}
}
}
$ javac Main.java
Main.java:5: error: variable total is already defined in method main(String[])
int total = 20;
^
1 error
"A variável total já está definida no método main(String[])". Entre local e local, nada de sombra: só entre local e campo da classe. O mesmo vale para um parâmetro e uma local com o mesmo nome:
$ javac Main.java
Main.java:3: error: variable numero is already defined in method dobrar(int)
int numero = 5;
^
1 error
Formas diferentes de levar um valor de um lugar a outro
| Problema | Solução | Exemplo |
|---|---|---|
Usar depois de um if/for um valor calculado lá dentro | Declarar a variável antes do bloco | String msg; if (…) { msg = …; } else { msg = …; } |
Usar num método um valor do main | Passar por parâmetro | mostrarAluno(nome) |
Trazer de volta ao main um valor calculado num método | Devolver com return | double m = media(7, 8); |
| Algo que vários métodos usam e que deve durar o programa todo | Campo da classe | static String escola = "…"; |
| Um valor fixo, que nunca muda, usado em toda a classe | Constante | static final double NOTA_MINIMA = 7.0; (veja Constantes) |
A boa prática é o menor escopo possível: declare cada variável o mais perto possível de onde ela é usada, no bloco mais interno que der conta. Menos código enxerga a variável, menos código pode estragá-la.
Quando dá errado
$ javac Main.java
Main.java:8: error: cannot find symbol
System.out.println(mensagem);
^
symbol: variable mensagem
location: class Main
1 error
O que aconteceu: no ponto indicado, não existe variável com esse nome. As causas mais comuns, em ordem: (1) a variável foi declarada dentro de um bloco (if, for, while) e usada fora; (2) ela pertence a outro método; (3) erro de digitação (Mensagem, mensagen); (4) ela é usada antes da linha em que é declarada.
Como resolver: encontre a declaração e siga as chaves até o } que a fecha. Se o uso está fora, mova a declaração para fora do bloco ou passe o valor por parâmetro. Veja também Lendo Erros do javac.
O que aconteceu: você declarou de novo (com o tipo na frente) uma variável que ainda está viva no mesmo método, num bloco interno, ou com o mesmo nome de um parâmetro.
Como resolver: se a intenção era mudar o valor, tire o tipo (total = 20;). Se era outra variável, use outro nome (totalComBonus).
$ javac Main.java
Main.java:8: error: variable mensagem might not have been initialized
System.out.println(mensagem);
^
1 error
O que aconteceu: a declaração está no escopo certo, mas existe um caminho (um if sem else, por exemplo) em que nenhum valor foi dado antes do uso.
Como resolver: dê um valor inicial na declaração ou garanta uma atribuição em todos os caminhos.
$ java Main.java Mudei para: Colégio Ana Souza Nome atual: Escola da Ana
O que aconteceu: sombreamento sem querer. Uma linha como String escola = novoNome; dentro do método cria uma variável local nova, em vez de mudar o campo.
Como resolver: tire o tipo do começo da linha: escola = novoNome;. Desconfie sempre de uma declaração com o mesmo nome de um campo.
Referência
| Tipo de variável | Onde é declarada | Onde pode ser usada | Quanto tempo vive | Valor inicial automático? |
|---|---|---|---|---|
| De bloco | Dentro de { } de if, for, while…, ou nos parênteses do for | Da declaração até o } do bloco | Enquanto o bloco roda | Não |
| De método (local) | Direto no corpo do método | Da declaração até o } do método | Uma chamada | Não |
| Parâmetro | Nos parênteses do método | O corpo inteiro do método | Uma chamada | Recebe o argumento |
| De classe (campo) | Na classe, fora dos métodos | Todos os métodos da classe | O programa inteiro (static) | Sim (0, false, null; veja Atributos) |
Missão final: conserte a sombra
Missão: o método mudarNome deveria trocar o nome da escola, mas o main continua mostrando o nome antigo. Corrija uma única linha para o programa terminar com Nome atual: Colégio Ana Souza.
public class Main {
static String escola = "Escola da Ana";
static void mudarNome(String novoNome) {
String escola = novoNome;
System.out.println("Mudei para: " + escola);
}
public static void main(String[] args) {
mudarNome("Colégio Ana Souza");
System.out.println("Nome atual: " + escola);
}
}
public class Main {
static String escola = "Escola da Ana";
static void mudarNome(String novoNome) {
escola = novoNome;
System.out.println("Mudei para: " + escola);
}
public static void main(String[] args) {
mudarNome("Colégio Ana Souza");
System.out.println("Nome atual: " + escola);
}
}
Exercícios
1. Até onde vai?
Uma variável declarada dentro de um for existe até qual caractere?
2. Acessar o campo na sombra
Dentro de um método da classe Loja, uma variável local nome faz sombra no campo static String nome. Complete para imprimir o campo:
3. Atribuir, não declarar
Complete a linha que muda o campo static int pontos para 100, dentro de um método da mesma classe, sem criar variável nova:
4. No main: if (true) { int x = 5; } System.out.println(x);. O que acontece?
5. Um método tem int contador = 0; contador++; System.out.println(contador); e é chamado três vezes. O que aparece?
6. Qual destas situações o Java permite?
Resumo
- Escopo é a parte do código em que uma variável existe: da declaração até o
}que fecha o seu bloco. - Três níveis: bloco (
if,for…), método (locais e parâmetros) e classe (campos). De dentro se enxerga para fora, nunca o contrário. - Locais nascem a cada chamada e morrem no fim dela; campos
staticvivem o programa todo. - Usou fora do escopo:
cannot find symbol. Declarou de novo no mesmo método:already defined. Sem valor em algum caminho:might not have been initialized. - Uma local com o nome de um campo faz sombra; o campo continua acessível como
Classe.campo. Cuidado comTipo campo = …quando a intenção era mudar o campo. - Prefira o menor escopo possível; leve valores entre métodos por parâmetros e
return.