Classes aninhadas: o que são e quando usar?

Como receber notificações de que a um atributo da sessão HTTP foi mudado? Registramos um listener, o padrão observer. Como fazemos para registrar uma ação de saída da aplicação ao clicar o botão “Sair“? Mesmo padrão: passamos um ActionListener para o addActionListener:

public class Aplicacao {
    private JButton botaoSair;
    private void montaBotaoSair() {
        botaoSair = new JButton("Sair");
        botaoSair.addActionListener(new AcaoSaida());
    }
}

Onde a ação de saída pode estar definida numa outra classe:

public class AcaoSaida implements ActionListener {
    public void actionPerformed(ActionEvent e) {
        System.exit(0);
    }
}

Será mesmo que precisamos de uma classe pública (ou mesmo package-protected) chamada AcaoSaida? Quem mais vai utilizá-la? Talvez seja uma melhor opção criar essa classe de tal forma que apenas a Aplicacao possa enxergá-la. Para isso, podemos cria-la dentro da propria classe Aplicacao, de forma aninhada**:

public class Aplicacao {
    private JButton botaoSair;
    private void montaBotaoSair() {
        botaoSair = new JButton("Sair");
        botaoSair.addActionListener(new AcaoSaida());
    }

    private class AcaoSaida implements ActionListener {
        public void actionPerformed(ActionEvent e) {
            System.exit(0);
        }
    }
}

Considerando que a ação de saída estará exclusivamente vinculada ao botão “Sair” e que, portanto, iremos instanciar apenas um objeto a partir dessa classe, podemos definir a classe no próprio momento em que decidimos instanciar o objeto. Como não precisaremos reaproveitar o comportamento da classe AcaoSaida em nenhum outro ponto dessa classe, podemos simplesmente abreviar o trabalho de sequer definir um nome para ela:

public class Aplicacao {
    private JButton botaoSair;
    private void montaBotaoSair() {
        botaoSair = new JButton("Sair");
        botaoSair.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                System.exit(0);
            }
        });
    }
}

A sintaxe do Java permite darmos new em uma interface, como é o caso de new ActionListener desde que, após os parênteses do construtor, você implemente todas os métodos abstratos. Qual será o nome dessa classe? Não há! Por este motivo ela é denominada classe anônima. Repare que isto é basicamente um truque do compilador: será gerado um arquivo, o Aplicacao$1.class, contendo o bytecode referente a esta classe anônima.

Aqui são viáveis as 3 alternativas. O fator mais importante para decidirmos qual estratégia usar foi o quão reaproveitável será a lógica definida dentro da classe. Mas, ainda que essa lógica seja utilizada num único ponto, poderíamos sim, por organização ou gosto, tê-la mantido como uma classe top-level. A decisão, nesse caso, é até um pouco subjetiva, podendo ser refatorada quando necessária.

Mas e se a ação de sair necessitasse o nome do usuário para poder criar uma mensagem melhor? Se ela fosse uma classe top-level, como a AcaoSaida, poderíamos receber como argumento no construtor uma String ou entidade Usuario. Se ela precisasse de muitos outros dados, esse construtor pode começar a ficar muito grande, mostrando aí uma possível dependência mais forte. Mais ainda: se a ação que será disparada precisasse devolver algum valor, como resolver? Um getter na nossa classe AcaoSaida? Talvez vários getters, dependendo do caso.

Em situações como essas, onde o listener parece estar tão intimamente ligado ao objeto que registra os listeners, podemos tirar proveito das classes aninhadas, pois acessam os atributos das classes a qual elas pertencem**. Elas podem acesssar tanto o this referente ao seu objeto, quanto ao this da classe que a contém (outer class, enclosing class, classe externa).

Como exemplo de cenário onde o uso de classes privadas é mais apropriado, o curso online de Design Patterns da Caelum propõe uma situação para o uso do padrão State. Temos uma classe Conta cujos métodos de saque e depósito tem comportamentos diferentes dependendo de a conta ter saldo positivo ou negativo. Caso o saldo seja positivo, no depósito a conta deve descontar 2% do valor. Caso negativo, no depósito deve-se descontar 5% e o saque deve ser impossibilitado com o lançamento de uma exceção.

A implementação dessa solução pode ser estruturada com duas classes que definem o comportamento da conta Positiva e Negativa, possibilitando que a conta trabalhe de forma diferente nesses casos (cobrando uma taxa a mais de qualquer saque no caso do estado negativo). Ambas assinam o contrato de serem estados de uma conta, definido pela interface EstadoDaConta.

public class Conta {
    private double saldo;
    private EstadoDaConta estado;

    private void atualizaEstado() {
        this.estado = saldo >= 0 ? new Positivo() : new Negativo();
    }

    public void deposita(double valor) {
        this.estado.deposita(valor);
        atualizaEstado();
    }

    public void saca(double valor) {
        this.estado.saca(valor);
        atualizaEstado();
    }

    private interface EstadoDaConta {
        void saca(double valor);
        void deposita(double valor);
    }

    private class Positivo implements EstadoDaConta {
        public void deposita(double valor) {
            saldo += valor * 0.98;
        }

        public void saca(double valor) {
            saldo -= valor;
        }
    }

    private class Negativo implements EstadoDaConta {
        public void deposita(double valor) {
            saldo += valor * 0.95;
        }

        public void saca(double valor) {
            throw new IllegalStateException();
        }
    }
}

Como as classes aninhadas podem acessar atributos privados da classe onde estão aninhadas, conseguimos encapsular novamente todo o estado de uma Conta. Dando acesso ao seu comportamento apenas pelos métodos públicos saca e deposita.

O interessante sobre a situação colocada pelo uso do design pattern state é que, ao contrário da situação do simples ActionListener para o botão “Sair”, o uso de classes aninhadas resolve um problema importante que é o afrouxamento do encapsulamento da classe agregadora. Usando classes aninhadas, pudemos manter o bom encapsulamento da classe Conta. Vale lembrar que exagerar no uso de classes aninhadas pode levar a implementações gigantes e de difícil manutenção, logo a opção por esta estrutura deve ser sempre bem justificada.

Um outro excelente exemplo de uso de classes aninhadas é a implementação de Iterators na api de Collections. Os iterators estão aninhados nas listas. Você pode ver isso na linha 780 da classe ArrayList, e dentro de seus métodos, o iterador acessa a array através de ArrayList.this.elementData (poderia utilizar apenas this.elementData, mas essa forma fica mais claro que o atributo elementData pertence a ArrayList, e não ao Itr, além de resolver possíveis conflitos no caso de atributos com mesmo nome nas duas classes).

Certamente as listas poderiam ter seus iteradores implementados de maneira não aninhada, mas o fortíssimo acoplamento entre as duas classes ficaria claramente indicado pelo excesso de métodos expostos apenas para compartilhar objetos e resultados.

** Há um pouco de detalhes sobre classes aninhadas que não citamos durante o artigo, para simplificar. No dia a dia e até na literatura, o termo classe interna é empregado genericamente para representar todo tipo de classe aninhada, porém ela é um subgrupo. Há dois tipos: as estáticas (utilizando o próprio modificador static, e dessa forma sem estar atrelada a um objeto da outter-class) e as internas (ou não estáticas), que vimos aqui. Mais ainda: quando declaramos uma classe dentro de um método, chamamos de classe local, que também pode ser estática ou interna, dependendo do método onde foi declarada. As classes anônimas se enquadram nos dois tipos de aninhadas, mas não possuem nome e não faz sentido declarar um modificador de acesso. Tem muitos outros detalhes que não cobrimos nesse curto post.

20 Comentários

  1. Alberto Souza 08/03/2012 at 11:41 #

    Ótima explicação. Muito legal!

  2. Michael Nascimento Santos 08/03/2012 at 22:31 #

    Caros amigos da Caelum,

    Tenho amizade há muito tempo com muitos de vocês, mas irei refazer o pedido que sempre tenho que fazer quando vocês dão esses exemplos de classe Conta e afins: dá pra usar BigDecimal ao invés de double pelo amor de Deus?

  3. Roberto Souza 08/03/2012 at 23:42 #

    serve também enquanto não temos as closures né???? mas aí teria de escrever váááááários posts 😉

  4. Paulo Silveira 09/03/2012 at 01:50 #

    @Michael

    Seria interessante utilizar BigDecimal aí, assim como também seria interessante fazer ressalvas sobre a thread-safety dessas classes (pois não há), além de muitos outros detalhes importantes. Infelizmente, não cabe.

    Usamos do construtivismo não só nas nossas aulas, como também no blog e palestras. A partir do momento que você quer focar em um recurso da linguagem, deve minimizar todos os outros pontos de aprendizado.

    Não apenas na nossa experiência, utilizar um exemplo que envolvesse BigDecimal e fluent interface para explicar classes aninhadas a um iniciante não é uma boa forma de ensinar.

    Além disso, o blog já tem alguns posts que envolvem BigDecimal, inclusive um só sobre:
    http://blog.caelum.com.br/arredondamento-no-java-do-double-ao-bigdecimal/

    Nos posts mais avançados, onde aparecem dinheiro e valores, e sabemos que o nível de leitura exigido já garante um conhecimento maior, o BigDecimal usualmente aparece:
    http://blog.caelum.com.br/codigo-expressivo-e-programacao-funcional-em-java-com-lambdaj/
    http://blog.caelum.com.br/adequar-o-banco-as-entidades-ou-o-contrario/

    Sem dúvida vale deixar um link em cada um dos posts, como um warning, assim como os asteriscos no fim desse post. Como agora já está aqui nos comentários, acho que ficou ótimo.

  5. Eric Torti 09/03/2012 at 13:35 #

    @Alberto

    Valeu!

    @Michel e @Paulo,

    Concordo com o Paulo, o double não é adequado pra representar valores monetários. Mas, considerando que para o assunto tratado no post a precisão das operações matemáticas é um fator coadjuvante, optamos pela simplicidade para manter o foco central da explicação mais claro e acessível.

    @Roberto

    As closures realmente dão muito pano pra manga :]

  6. Rodrigo Facholi 09/03/2012 at 13:54 #

    Boa explicação Eric, me fez refletir bastante.

  7. Bruno Laturner 09/03/2012 at 16:40 #

    Uma vez precisei fazer uma integração do repositório de usuários da minha aplicação com o Spring Security, e acabei utilizando classes aninhadas dentro de classes aninhadas, por questão de não espalhar código de terceiros pela minha aplicação.

    A classe é usada como bean no spring-security.xml

    https://gist.github.com/2008251

  8. Eric Torti 13/03/2012 at 12:29 #

    @Rodrigo

    Valeu! É um assunto bacana mesmo.

    @Bruno

    Interessante o exemplo, Bruno. Criar o UserDetails on the fly com o usuário recebido no método. Valeu!

  9. Raphael 14/03/2012 at 10:41 #

    excelente explicação para o Fj-16! Bacana

  10. depp 14/03/2012 at 10:50 #

    lembrando que algumas variáveis vc só acessa com o modificador final… se não me engano só as locais.

  11. Alan 14/03/2012 at 13:39 #

    Acho que seria legal ja passar como BigDecimal ao invés de Double, pois novatos (como eu) já aprendem da maneira correta!

    Acho que tudo é uma questão de prática, e se você pratica da maneira correta, fica mais fácil !

  12. Felipe Mamud 14/03/2012 at 20:43 #

    Muito bom o post! Parabéns!

    OBS: O Link “linha 780 da classe ArrayList” está quebrado, faltando um “l” no fim da url. Está “.htm” só que o correto seria “.html”

    http://www.docjar.com/html/api/java/util/ArrayList.java.html

    Abraços,

  13. Paulo Silveira 14/03/2012 at 23:01 #

    valeu Felipe! corrigido o link

  14. Eric Torti 15/03/2012 at 00:56 #

    @Raphael

    Verdade! No FJ-16, a gente discute justamente os listeners como classes aninhadas. Valeu!

    @Depp

    De fato, as classes anônimas e locais tem acesso apenas variávies final do escopo local. Bem observado. Valeu!

    @Alan

    Realmente o double não serve como boa representação para valores monetários. Mas nesse caso, pra simplificar o código do exemplo e manter o foco na questão das classes aninhadas, optamos por usar double. Ainda assim, valeu a sugestão!

    @Felipe

    Obrigado pelo aviso!

  15. Marcio Silva 16/03/2012 at 09:28 #

    Classes aninhadas é uma questão difícil de se sistematizar (definir uma regra formal para seu uso) porque é um aspecto mais da implementação do que da modelagem. Isso parece que todos já sacaram.

    Eu costumo dizer que uma classe aninhada é bom quando o design interno sugere dois componentes e a implementação (considerando os critérios de coesão/acoplamento) sugere apenas um.

  16. João Henrique 28/06/2012 at 12:00 #

    Foi exatamente isso que a minha excelente instrutora Cecília da caelum me disse.

    Mas é sempre muito bom rever conceitos e distinguir necessidades.

    Classes anônimas são usadas exatamente por não terem a necessidades de serem reutilizadas dentro da classe mais de uma vez.

  17. Edgar Delmiro 12/05/2015 at 08:20 #

    Belíssimo post, muito bem explicado!

Deixe uma resposta