Java Classes Internas e Anônimas

Até aqui, cada classe ficava "solta" no arquivo. Mas o Java permite declarar uma classe dentro de outra, e até dentro de um método. Isso serve para classes pequenas que só fazem sentido junto de outra: o motor de um carro, o item de um pedido. A forma mais usada é a classe anônima, uma classe sem nome criada na hora, que você vai encontrar em muito código antigo e que hoje, na maioria dos casos, virou lambda.

Você vai aprender
  • Os quatro tipos de classe aninhada: interna, static aninhada, local e anônima.
  • Quando cada uma enxerga os dados da classe de fora.
  • Como criar uma classe anônima com Comparator e Runnable.
  • Como uma lambda substitui uma classe anônima.
  • Que arquivos .class o javac gera para cada uma (Externa$Interna.class).

Esta aula usa Interfaces e static.


A ideia: peças que só existem dentro de algo

Um carro tem motor, mas ninguém vai à concessionária comprar "um motor de Fusca" avulso: ele só faz sentido dentro do carro, e conversa o tempo todo com o carro. Já o cardápio de um restaurante tem "itens": um item não pertence a um pedido específico, mas faz sentido agrupar a ideia de "item" junto da ideia de "pedido".

Uma classe declarada dentro de outra se chama classe aninhada (nested class). A de fora é a classe externa. Há quatro tipos:

TipoOnde é declaradaEnxerga os atributos do objeto de fora?Exemplo
Interna (inner)No corpo da classe, sem staticSimCarro.Motor
static aninhadaNo corpo da classe, com staticNãoPedido.Item
LocalDentro de um métodoSim (e as variáveis do método)class Placar no main
AnônimaNuma expressão new …() { }Sim (e as variáveis do método)new Comparator<String>() { … }

1. Classe interna: o motor do carro

Uma classe interna é declarada dentro de outra, sem static. Todo objeto dela fica ligado a um objeto da classe externa e enxerga os atributos dele, até os private:

public class Main {
    public static void main(String[] args) {
        Carro fusca = new Carro("Fusca");
        fusca.ligar();

        Carro.Motor outro = fusca.new Motor();
        outro.girar();
    }
}

class Carro {
    private String modelo;
    private Motor motor;

    Carro(String modelo) {
        this.modelo = modelo;
        this.motor = new Motor();
    }

    void ligar() {
        System.out.println("Ligando o " + modelo + "...");
        motor.girar();
    }

    class Motor {
        void girar() {
            System.out.println("Motor do " + modelo + ": vrum!");
        }
    }
}
class Motor
Declarada dentro das chaves de Carro. É um membro da classe, como um atributo ou método.
modelo
Dentro de Motor, modelo é o atributo do carro dono deste motor, mesmo sendo private.
new Motor()
Dentro do Carro, cria um motor ligado a this (este carro).
Carro.Motor
De fora, o nome completo do tipo é Carro.Motor.
fusca.new Motor()
De fora, um motor precisa de um carro dono; a sintaxe (estranha, e rara) é objetoExterno.new Interna().
$ java Main.java
Ligando o Fusca...
Motor do Fusca: vrum!          « o motor leu o modelo do carro dele
Motor do Fusca: vrum!          « o outro motor também pertence ao fusca

Use uma classe interna quando a peça precisa dos dados do "dono" e não tem sentido sozinha. Na prática, ela costuma ser private, usada só pela classe externa.


2. Classe static aninhada: o item do pedido

Com static, a classe aninhada não fica ligada a um objeto da externa. Ela é só uma classe comum, "guardada" dentro da outra para organizar o código. O nome completo dela usa a externa como prefixo:

public class Main {
    public static void main(String[] args) {
        Pedido.Item cafe = new Pedido.Item("Café", 2, 18.90);
        Pedido.Item pao = new Pedido.Item("Pão", 6, 0.75);
        System.out.println(cafe.total());
        System.out.println(pao.total());
    }
}

class Pedido {
    String cliente;

    static class Item {
        String produto;
        int quantidade;
        double preco;

        Item(String produto, int quantidade, double preco) {
            this.produto = produto;
            this.quantidade = quantidade;
            this.preco = preco;
        }

        double total() {
            return quantidade * preco;
        }
    }
}
$ java Main.java
37.8
4.5

Repare: new Pedido.Item(…), sem precisar de nenhum pedido. Como não há "pedido dono", a classe static não enxerga os atributos de objeto da externa. Tentando usar o cliente dentro de Item:

$ javac Main.java
Main.java:25: error: non-static variable cliente cannot be referenced from a static context
            System.out.println("Cliente: " + cliente);
                                             ^
1 error

É o mesmo erro da aula static: dentro de algo static, não existe "este pedido". Na dúvida entre interna e static, a recomendação dos livros é: prefira static, a menos que a classe precise mesmo do objeto de fora. Ela é mais simples e não guarda uma referência escondida ao objeto externo. Um exemplo da biblioteca: Map.Entry (veja HashMap).


3. Classe local: dentro de um método

Uma classe pode ser declarada dentro de um método, como uma variável. Ela só existe ali:

public class Main {
    public static void main(String[] args) {
        class Placar {
            int pontos;

            void marcar(int p) {
                pontos = pontos + p;
                System.out.println("Placar: " + pontos);
            }
        }

        Placar jogo = new Placar();
        jogo.marcar(3);
        jogo.marcar(2);
    }
}
$ java Main.java
Placar: 3
Placar: 5

Classes locais são raras. Elas aparecem quando um método precisa de uma pequena estrutura só para ele. Hoje, para "um grupo de dados só aqui", um record local costuma ser mais prático.


4. Classe anônima: criada na hora, sem nome

Às vezes você precisa de um objeto que implemente uma interface uma única vez, num lugar só. Criar uma classe com nome, num lugar separado, só para isso, dá trabalho. A classe anônima junta tudo numa expressão: declara a classe e cria o objeto, ali mesmo.

O exemplo clássico é ordenar uma lista com um critério próprio. O Collections.sort aceita um Comparator, uma interface com o método compare(a, b) (devolve negativo se a vem antes, positivo se vem depois; veja Comparable e Comparator). A Ana quer as frutas da menor para a maior palavra:

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> frutas = new ArrayList<>(List.of("melancia", "uva", "banana", "kiwi"));

        Comparator<String> porTamanho = new Comparator<String>() {
            @Override
            public int compare(String a, String b) {
                return a.length() - b.length();
            }
        };

        Collections.sort(frutas, porTamanho);
        System.out.println(frutas);

        Runnable tarefa = new Runnable() {
            @Override
            public void run() {
                System.out.println("Tarefa rodando!");
            }
        };
        tarefa.run();
    }
}
new Comparator<String>()
Parece criar um objeto de uma interface, o que é proibido… mas logo depois vêm as chaves.
{ … }
O corpo de uma classe sem nome que implementa Comparator<String>. O new cria um objeto dela na mesma hora.
public int compare(String a, String b)
A implementação do único método da interface: compara pelo tamanho.
};
Fecha o corpo da classe e o comando de atribuição. Esse ; depois da chave é fácil de esquecer.
new Runnable() { … }
A mesma ideia com Runnable, a interface de "uma tarefa para rodar", com o método run() (é a base das Threads).
$ java Main.java
[uva, kiwi, banana, melancia]       « 3, 4, 6 e 8 letras
Tarefa rodando!

Uma classe anônima pode implementar uma interface ou estender uma classe (inclusive abstrata: new Forma("x") { double area() { return 1; } }). Ela não pode ter construtor (não tem nome para isso) e serve para um objeto só.

Missão: mude a classe anônima para ordenar da maior para a menor palavra. Dica: troque a ordem da subtração no compare. A saída deve ser [melancia, banana, kiwi, uva].

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> frutas = new ArrayList<>(List.of("melancia", "uva", "banana", "kiwi"));

        Comparator<String> porTamanho = new Comparator<String>() {
            @Override
            public int compare(String a, String b) {
                return a.length() - b.length();
            }
        };

        Collections.sort(frutas, porTamanho);
        System.out.println(frutas);
    }
}
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> frutas = new ArrayList<>(List.of("melancia", "uva", "banana", "kiwi"));

        Comparator<String> porTamanho = new Comparator<String>() {
            @Override
            public int compare(String a, String b) {
                return b.length() - a.length();
            }
        };

        Collections.sort(frutas, porTamanho);
        System.out.println(frutas);
    }
}

Variáveis de fora: só as "efetivamente finais"

Classes locais e anônimas podem usar as variáveis do método onde estão, com uma condição: a variável não pode mudar depois de receber o valor. Ela precisa ser final ou efetivamente final (veja final):

Este código tem um erro de propósito
public class Main {
    public static void main(String[] args) {
        String saudacao = "Olá";
        saudacao = "Oi";
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println(saudacao);
            }
        };
        r.run();
    }
}
$ javac Main.java
Main.java:8: error: local variables referenced from an inner class must be final or effectively final
                System.out.println(saudacao);
                                   ^
1 error

"Variáveis locais usadas por uma classe interna precisam ser final ou efetivamente finais." O objeto anônimo pode viver mais que o método (pode ser guardado e rodado depois); por isso ele guarda uma cópia do valor, e o Java exige que esse valor nunca mude, para a cópia não ficar diferente do original. Apagando a linha saudacao = "Oi";, compila e mostra Olá. A mesma regra vale para as lambdas.


Lambdas: o substituto moderno

Java 8+ Repare quanto código a classe anônima precisou para dizer uma coisa só: "compare pelo tamanho". Comparator e Runnable têm um único método abstrato; interfaces assim se chamam interfaces funcionais. Para elas, o Java 8 criou uma forma curta, a lambda: só os parâmetros, uma seta -> e o corpo.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> frutas = new ArrayList<>(List.of("melancia", "uva", "banana", "kiwi"));
        Collections.sort(frutas, (a, b) -> a.length() - b.length());
        System.out.println(frutas);

        Runnable tarefa = () -> System.out.println("Tarefa rodando!");
        tarefa.run();
    }
}
$ java Main.java
[uva, kiwi, banana, melancia]
Tarefa rodando!

A mesma saída, com uma linha no lugar de seis. Compare as duas formas:

Classe anônimaLambda
new Comparator<String>() {
  public int compare(String a, String b) {
    return a.length() - b.length();
  }
}
(a, b) -> a.length() - b.length()
new Runnable() {
  public void run() {
    System.out.println("Oi");
  }
}
() -> System.out.println("Oi")

Como a interface só tem um método, o compilador deduz o resto: qual método, os tipos dos parâmetros, o return. Tudo sobre lambdas está em Lambdas e Interfaces Funcionais.

Ainda há lugar para a classe anônima? Sim, quando a lambda não serve: a interface tem mais de um método abstrato, você precisa estender uma classe (não uma interface), ou o objeto precisa guardar estado próprio (atributos). E você vai lê-las em todo código anterior a 2014.


Os arquivos .class gerados

A JVM não conhece "classes dentro de classes": para ela, toda classe é um arquivo .class separado. O javac resolve isso dando nomes com $ (cifrão). A Ana compilou um arquivo com um exemplo de cada tipo (o Carro com Motor, o Pedido com Item, uma classe local Placar no main, duas classes anônimas de Runnable e uma lambda) e listou a pasta de saída:

$ javac -d out Main.java
$ ls out
Carro$Motor.class
Carro.class
Main$1.class
Main$1Placar.class
Main$2.class
Main.class
Pedido$Item.class
Pedido.class
$ java -cp out Main
primeira anônima
segunda anônima
uma lambda
ArquivoDe onde veioRegra do nome
Carro$Motor.classA classe interna MotorExterna$Interna
Pedido$Item.classA static aninhada ItemExterna$Aninhada (igual)
Main$1Placar.classA classe local PlacarExterna$ + número + nome
Main$1.class, Main$2.classAs duas classes anônimas, na ordem em que aparecemExterna$ + número (não há nome)
(nenhum)A lambdaNão gera arquivo: vira um método escondido dentro de Main.class

(A opção -d out grava os .class na pasta out; veja Pacotes.) Esses nomes aparecem também em mensagens de erro e rastros de exceção (numa linha como at Main$1.run(…)): quando vir um $ com número, pense "classe anônima". E, no terminal, cuidado: o $ é especial para o Bash, então para usar esses nomes num comando escreva-os entre aspas simples ('Main$1.class').


Quando dá errado

"local variables referenced from an inner class must be final or effectively final"

O que aconteceu: uma classe anônima (ou local) usou uma variável do método que muda de valor em algum lugar.

Como resolver: não mude a variável; se precisar de outro valor, crie outra variável (String mensagem = saudacao + "!";) e use essa.

"non-static variable … cannot be referenced from a static context" numa classe aninhada

O que aconteceu: uma classe static aninhada tentou usar um atributo de objeto da classe externa.

Como resolver: passe o dado como parâmetro (ou no construtor) da classe aninhada. Tirar o static também resolve, mas aí cada objeto dela precisa de um objeto externo dono.

"';' expected" depois da classe anônima

O que aconteceu: faltou o ; depois da chave que fecha a classe anônima (};). Lembre-se: a classe anônima é parte de uma expressão (Runnable r = …;), e todo comando termina em ;.

Como resolver: feche com };. Ou, se a interface for funcional, troque por uma lambda.


Referência

EscritaTipoComo criar um objeto
class Externa { class Interna { } }InternaDentro: new Interna(); fora: externa.new Interna()
class Externa { static class Aninhada { } }static aninhadanew Externa.Aninhada()
void m() { class Local { } }Localnew Local(), só dentro do método
new Interface() { … }AnônimaCriada junto com a declaração
(a, b) -> …Lambda (não é classe)Só para interfaces funcionais
Externa$Interna.class, Externa$1.classArquivos gerados pelo javac

Exercícios

1. Criar a aninhada

Item é uma classe static dentro de Pedido, com construtor sem parâmetros. Complete:

Pedido.Item i = ;

2. O nome do arquivo

Que arquivo o javac gera para a classe Roda declarada dentro de Bicicleta?

3. Lambda no lugar da anônima

Complete a lambda equivalente a um Runnable anônimo que mostra "Oi":

Runnable r = ;

4. Qual tipo de classe aninhada não enxerga os atributos de objeto da classe externa?

5. Quando a classe anônima não pode ser trocada por uma lambda?

6. Um arquivo Main.java tem duas classes anônimas e uma lambda. Quantos arquivos Main$…class o javac gera?

Missão final: modernize o código: troque as duas classes anônimas por lambdas, sem mudar a saída ([uva, kiwi, banana, melancia] e Tarefa rodando!). Depois, o import java.util.Comparator; pode até sair.

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> frutas = new ArrayList<>(List.of("melancia", "uva", "banana", "kiwi"));

        Collections.sort(frutas, new Comparator<String>() {
            @Override
            public int compare(String a, String b) {
                return a.length() - b.length();
            }
        });
        System.out.println(frutas);

        Runnable tarefa = new Runnable() {
            @Override
            public void run() {
                System.out.println("Tarefa rodando!");
            }
        };
        tarefa.run();
    }
}
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> frutas = new ArrayList<>(List.of("melancia", "uva", "banana", "kiwi"));

        Collections.sort(frutas, (a, b) -> a.length() - b.length());
        System.out.println(frutas);

        Runnable tarefa = () -> System.out.println("Tarefa rodando!");
        tarefa.run();
    }
}

Resumo

  • Interna: dentro da classe, sem static; cada objeto pertence a um objeto externo e enxerga os dados dele.
  • static aninhada: só organização; não enxerga os atributos de objeto da externa. Prefira esta quando der.
  • Local: declarada dentro de um método; só existe ali.
  • Anônima: new Interface() { … } declara e cria um objeto de uma classe sem nome. Usa variáveis do método só se forem efetivamente finais.
  • Para interfaces com um só método (Comparator, Runnable), a lambda substitui a classe anônima.
  • O javac gera Externa$Interna.class, Externa$1.class (anônimas) e Externa$1Local.class; lambdas não geram arquivo.