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.

Você vai aprender
  • 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 defined e might 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 escolaNo JavaQuem enxerga
Mural do corredorVariável de classe (um campo)Todos os métodos da classe
Quadro de uma salaVariável de método (local, ou um parâmetro)Só o método onde foi declarada
Bilhete numa carteiraVariá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 } do main (linha 9). Pode ser usada dentro do if, porque o if está "dentro" do main.
String mensagem
Declarada dentro das chaves do if: vale só até o } da linha 7.
linha 6
Ainda dentro do if: mensagem existe. Funciona.
linha 8
Fora do if: mensagem já 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.

Não transforme tudo em variável de classe

É 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;.

Você vai reencontrar a sombra

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

ProblemaSoluçãoExemplo
Usar depois de um if/for um valor calculado lá dentroDeclarar a variável antes do blocoString msg; if (…) { msg = …; } else { msg = …; }
Usar num método um valor do mainPassar por parâmetromostrarAluno(nome)
Trazer de volta ao main um valor calculado num métodoDevolver com returndouble m = media(7, 8);
Algo que vários métodos usam e que deve durar o programa todoCampo da classestatic String escola = "…";
Um valor fixo, que nunca muda, usado em toda a classeConstantestatic 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

"cannot find symbol … symbol: variable"
$ 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.

"variable … is already defined in method …"

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

"variable … might not have been initialized"
$ 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.

O método "mudou" o valor, mas nada mudou
$ 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ávelOnde é declaradaOnde pode ser usadaQuanto tempo viveValor inicial automático?
De blocoDentro de { } de if, for, while…, ou nos parênteses do forDa declaração até o } do blocoEnquanto o bloco rodaNão
De método (local)Direto no corpo do métodoDa declaração até o } do métodoUma chamadaNão
ParâmetroNos parênteses do métodoO corpo inteiro do métodoUma chamadaRecebe o argumento
De classe (campo)Na classe, fora dos métodosTodos os métodos da classeO 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:

System.out.println();

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 static vivem 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 com Tipo 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.