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.
- Os quatro tipos de classe aninhada: interna,
staticaninhada, local e anônima. - Quando cada uma enxerga os dados da classe de fora.
- Como criar uma classe anônima com
ComparatoreRunnable. - Como uma lambda substitui uma classe anônima.
- Que arquivos
.classojavacgera 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:
| Tipo | Onde é declarada | Enxerga os atributos do objeto de fora? | Exemplo |
|---|---|---|---|
| Interna (inner) | No corpo da classe, sem static | Sim | Carro.Motor |
static aninhada | No corpo da classe, com static | Não | Pedido.Item |
| Local | Dentro de um método | Sim (e as variáveis do método) | class Placar no main |
| Anônima | Numa 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 sendoprivate. - new Motor()
- Dentro do
Carro, cria um motor ligado athis(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>. Onewcria 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étodorun()(é 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):
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ônima | Lambda |
|---|---|
new Comparator<String>() { | (a, b) -> a.length() - b.length() |
new Runnable() { | () -> 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
| Arquivo | De onde veio | Regra do nome |
|---|---|---|
Carro$Motor.class | A classe interna Motor | Externa$Interna |
Pedido$Item.class | A static aninhada Item | Externa$Aninhada (igual) |
Main$1Placar.class | A classe local Placar | Externa$ + número + nome |
Main$1.class, Main$2.class | As duas classes anônimas, na ordem em que aparecem | Externa$ + número (não há nome) |
| (nenhum) | A lambda | Nã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
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.
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.
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
| Escrita | Tipo | Como criar um objeto |
|---|---|---|
class Externa { class Interna { } } | Interna | Dentro: new Interna(); fora: externa.new Interna() |
class Externa { static class Aninhada { } } | static aninhada | new Externa.Aninhada() |
void m() { class Local { } } | Local | new Local(), só dentro do método |
new Interface() { … } | Anônima | Criada junto com a declaração |
(a, b) -> … | Lambda (não é classe) | Só para interfaces funcionais |
Externa$Interna.class, Externa$1.class | Arquivos gerados pelo javac | |
Exercícios
1. Criar a aninhada
Item é uma classe static dentro de Pedido, com construtor sem parâmetros. Complete:
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":
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. staticaninhada: 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
javacgeraExterna$Interna.class,Externa$1.class(anônimas) eExterna$1Local.class; lambdas não geram arquivo.