Imprimir

Neste artigo veremos dois assuntos que estão intimamente ligados e podem ser usados em conjunto: Classes Aninhadas e Classes Anônimas. Veremos teoria e prática de ambos os conceitos, assim como o uso de ambos de forma conjunta.

Classes Aninhadas

Iremos iniciar nosso artigo com as classes aninhadas, em teoria e prática. A palavra “aninhadas” em programação diz respeito que é uma subrotina encapsulada noutra. O escopo da subrotina aninhada é limitado a subrotina que a encapsula. Isso significa que ela pode ser chamada somente pela subrotina que a encapsula, ou pelas subrotinas diretamente ou indiretamente aninhadas pela mesma subrotina encapsuladora. O aninhamento é teoricamente ilimitado, ainda que na prática somente alguns níveis são aceitos, o que depende da implementação.

O trecho acima cita um conceito genérico de subrotinas aninhadas no contexto de desenvolvimento de software. Podemos usar esse mesmo conceito e aplicar a classes em Java, ou seja, as Classes Aninhadas são classes que estão dentro de outra classe, ou do inglês Nested Classes. Um classe pode ter uma ou várias classes aninhadas, e uma classe aninhada pode ter uma ou várias outras classes aninhadas e assim por diante.

Vejamos um exemplo de classe aninhada na Listagem 1.

Listagem 1. Nested Class Java

   
  public class EnclosingClass {
  . . .
   public class NestedClass {
   . . .
   
   }
  }

Temos a classe externa chamada “EclosingClass” e a classe aninhada chamada “NestedClass”. Elas são muito úteis quando a classe aninhada está ligada diretamente a classe externa e não há necessidade de criar uma classe externa já que ela não será utilizada em mais nenhum local, mas deve-se tomar cuidado com este recurso já que quando utilizado em demasia pode tornar o código bagunçado e de difícil manutenção.

Classes Aninhadas são dividas em quatro categorias: Estática, Não-Estática (Interna Comum), Interna a um método e Interna Anônima. Vejamos a Listagem 2.

Listagem 2. Static e Non-Static

  class OuterClass {
      ...
      static class StaticNestedClass {
          ...
      }
      class InnerClass {
          ...
      }
  }

Classes Aninhadas não estáticas tem acesso a membros privados da classe que a tem (classe externa), enquanto que classes aninhadas estáticas não tem acesso aos membros da classe externa. Veremos os outros casos mais a frente, sendo que o último caso de “Interna Anônima” será visto junto da seção sobre classes anônimas.

Porque usar classes aninhadas?

Porque deveríamos criar uma classe aninhada em vez de uma top-level? Quais as vantagens no seu uso?

  • Agrupamento de classes que são usadas em apenas um lugar: Se você possui uma classe B que com certeza será usada apenas dentro da classe A, o melhor é criar a classe B como interna a classe A.
  • Aumento de Encapsulamento: Sendo que a classe B e A são duas classes top-level e B precisa acessar os membros de A, poderíamos colocar a classe B dentro de A e colocar os membros de A como privados e ainda assim B conseguiria continuar acessando os membros de A por ser uma classe aninhada.
  • Código mais legível e de fácil manutenção: Colocar classes pequenas dentro de classes top-level, já que apenas esta a usará, faz com que a lógica da classe top-level seja mais fácil de ser identificada, consequentemente tornando o código mais legível e de fácil manutenção.

Classes Aninhadas Estáticas

Revendo os conceitos, é importante lembrar que uma classe estática não tem acesso aos membros da instância da classe externa, somente aos membros estáticos, ou seja, elas tem a mesma visibilidade de uma classe externa. Vejamos um exemplo na Listagem 3.

Listagem 3. Classe Aninhada Estática

public class Ex03 {  
     static int CountStatic = 0;  
     int CountNonStatic = 0;  
    
     public static class Inner {  
     }  
  }  

O exemplo acima demonstra de forma simples e fácil como utilizar uma classe aninhada estática (nested static class). Pelo fato da classe Inner ser estática ela não pode chamar diretamente a variável CountNonStatic pois isso causaria um erro de compilação como mostrado na Listagem 4.

Listagem 4. Chamando CountNonStatic

  public class Ex03 {  
     static int CountStatic = 0;  
     int CountNonStatic = 0;  
    
     public static class Inner {  
        public void doInner() {  
           System.out.println( CountNonStatic );  
        }  
     }  
  }  
  Erro gerado: Erro de compilação: non-static variable CountNonStatic cannot be referenced from a static context

Você precisaria de uma instância de Ex03 para usar a variável CountNonStatic, pois a classe Inner está em um contexto diferente da variável CountNonStatic. Por outro lado, a Listagem 5 compila sem problemas:

Listagem 5. Chamando CountStatic

  public class Ex03 {  
                     static int CountStatic = 0;  
                     int CountNonStatic = 0;  
                    
                     public static class Inner {  
                        public void doInner() {  
                           System.out.println( CountStatic );  
                        }  
                     }  
                  }  

A vantagem de utilizar-se uma classe estática aninhada está no fato de poder acessar membros privados da classe externa (que sejam estáticos também, é claro).

Vejamos um exemplo mais completo do seu uso em detalhes na Listagem 6.

Listagem 6. Exemplo completo Classe Aninhada Estática

  import java.util.*;  
  public class Week {  
     private int weeknr;  
     private int year;  
     public Week(int weeknr, int year) {  
        this.weeknr = weeknr;  
        this.year = year;  
     }  
     public Iterator getDays() {  
        return new DayIterator(this);  
     }  
     public int getWeeknr() {  
        return weeknr;  
     }  
     public int getYear() {  
        return year;  
     }  
     public static class DayIterator implements Iterator {  
        private int index = 0;  
        private Calendar cal = null;  
    
        DayIterator (Week aWeek) {  
           cal = new GregorianCalendar();  
           cal.clear();  
           cal.set(Calendar.YEAR, aWeek.getYear());  
           cal.set(Calendar.WEEK_OF_YEAR, aWeek.getWeeknr());  
        }  
        public boolean hasNext() {  
           return index < 7;  
        }  
        public Object next() {  
           cal.set(Calendar.DAY_OF_WEEK, index++);  
           return cal.getTime();  
        }  
        public void remove() {  
           // not implemented  
        }  
     }  
    
     public static void main(String[] args) {  
     // list the days of the week  
     if (args.length < 2) {  
        System.err.println("Usage: java Week <weeknr> year>");  
        System.exit(1);  
     } else {  
        try {  
          int weeknr = Integer.parseInt(args[0]);  
          int year = Integer.parseInt(args[1]);  
          Week wk = new Week(weeknr, year);  
          for (Iterator i=wk.getDays();i.hasNext();) {  
            System.err.println(i.next());  
          }  
        } catch (NumberFormatException x) {  
          System.err.println("Illegal week or year");  
        }  
     }  
     }  
  }

Vamos destrinchar a classe acima e entender seu funcionamento. Mostrando apenas as assinaturas dos métodos sem levar em consideração seu “corpo” temos a Listagem 7.

Listagem 7. Mostrando apenas assinaturas dos métodos da Listagem 6

  import java.util.*;  
  public class Week {  
     private int weeknr;  
     private int year;  
     public Week(int weeknr, int year) {  
    
     
     public Iterator getDays(){}
   
     public int getWeeknr(){}  
     
     public int getYear(){}
       
     public static class DayIterator implements Iterator {}  
    
     public static void main(String[] args){}    
       
  }

A classe Week tem uma função bem simples. No construtor é passado o número da semana e o ano desejado e com esses parâmetros podemos começar a trabalhar. O método getWeeknr() apenas retorna o número da semana que foi passado, assim como o getYear() retorna o ano que foi passado, sem realizar nenhum tratamento. Mas o ponto principal está na classe aninhada chamada DayIterator, onde sua função é retornar um Iterator para que possamos percorrer os dias da semana naquele determinado ano.

Perceba que a classe aninhada DayIterator não tem acesso aos membros da classe Week, isso porque eles são membros de instância. Neste caso optamos por usar uma classe aninhada estática apenas para não precisar criar um novo “.java” separado para controlar a iteração entre os dias, pois como sabemos que apenas a classe Week irá utilizar essa funcionalidade, é muito melhor criar o DayIterator interno a ela e facilitar a manutenção e legibilidade do código.

Classe Interna Comum (Classe Não Estática)

Uma classe interna comum só pode ser instanciada se estiver dentro de um objeto da classe externa. Vejamos a Listagem 8.

Listagem 8. Instanciando a classe interna de forma errada

  public class Outer {  
     public static void main(String[] args) {  
        Inner i = new Inner();  
  }  
    
     class Inner {  
     }  
  }

Logo acima citamos que a classe interna não pode ser instanciada sem que seja por um objeto da classe externa, e no exemplo da Listagem 8 ocorrerá um erro por este motivo. Acontece que o método main é estático, ou seja, da classe e não do objeto. Sendo assim não podemos instanciar a classe Inner a partir deste método estático.

Veja como faríamos da maneira correta na Listagem 9.

Listagem 9. Instanciando a classe interna de forma correta

  public class Outer {  
     public static void main(String[] args) {  
        Outer o = new Outer();  
        Inner i = o.new Inner();  
  }  
    
     class Inner {  
     }  
  }

Acima temos a criação do objeto “o” do tipo Outer e posteriormente (a partir do objeto “o”) a criação do objeto “i” do tipo Inner, usando o objeto “o”. O código mostrado na Listagem 9 é a forma correta de realizar a instanciação de uma classe interna, diferente da Listagem 8.

Uma classe interna é considerada um membro da classe externa, sendo esta um membro então deve ter acesso a todos os outros membros da classe externa, sejam eles private ou não. Observe a Listagem 10.

Listagem 10. Acessando membros privados da classe externa

  public class Outer {  
      private int x = 0;  
     public static void main(String[] args) {  
        Outer o = new Outer();  
        Inner i = o.new Inner();  
        o.print();  
     }  
     public void print() {  
         System.out.println("x before "+x);  
         Inner i = new Inner();  
         i.print();  
     }  
     class Inner {  
         public void print() {  
        x++;  
        System.out.println("x after: "+x);  
         }  
     }  
  }  

Acima temos a classe externa Outer e a classe interna Inner. Perceba que a classe Inner usa o atributo “x” que foi declarado como private na classe Outer, ou seja, ela tem total acesso a mesma. Se você executar o código da Listagem 10 teremos como saída o que está na Listagem 11.

Listagem 11. Saída da Listagem 10

  x before 0
  x after: 1               

Podemos ainda instanciar a classe interna fora da classe externa, porém seguindo a mesma regra de que isso deve ser feito através de um objeto da classe externa. Vejamos a Listagem 12.

Listagem 12. Instanciando a classe interna de fora da classe externa

  public class TestOuter {  
     public static void main(String[] args) {  
        Outer o = new Outer();  
        Outer.Inner i = o.new Inner();  
     }  
  }  
  class Outer {  
      class Inner { }  
  }  

A nossa classe TestOuter possui um método estático chamado main , perceba que dentro deste método estamos criando o objeto Outer e a partir dele conseguimos criar o objeto Inner.

A classe interna em alguns casos pode precisar referenciar a classe externa, e isso é possível através da palavra reservada “this”, precedida do nome da classe externa. Observe a Listagem 13.

Listagem 13. Referenciando a classe externa com 'this'

  public class TestOuter {  
     public static void main(String[] args) {  
        Outer o = new Outer();  
        Outer.Inner i = o.new Inner();  
        i.see();  
     }  
  }  
  class Outer {  
      private int x = 10;  
      class Inner {  
     public void see() {  
         System.out.println(x);  
         System.out.println(this);      
         System.out.println(Outer.this);   
     }  
      }  
  }

No exemplo da Listagem 13 estamos usando o “this” de duas formas:

  1. Em “System.out.println(this)” estamos referenciando a classe Inner;
  2. Em “System.out.println(Outer.this)” estamos referenciando a classe Outer;

Então o nome da classe na frente do “this” faz toda a diferença na hora de apontar a referencia correta. Também vale ressaltar que classes aninhadas comuns podem possuir os seguintes modificadores presentes na Listagem 14.

Listagem 14. Modificadores de acesso

   
  final
 
 abstract
 
 public
 
 private
 
 protected
 
 static
 
 Strictfp
  

Classe Interna a um método

Vimos nas seções acima Classes Internas Estáticas e Não estáticas, todas dentro de um escopo onde a classe externa as delimita, mas agora veremos um escopo mais restrito ainda, classes internas dentro de um método. Observe a Listagem 15.

Listagem 15. Classe interna dentro de um método

  public class TestOuter {  
     public static void main(String[] args) {  
        Outer o = new Outer();  
        o.see();  
     }  
  }  
  class Outer {  
      private int x = 10;  
      public void see() {  
     System.out.println("before "+x);  
     class Inner {  
         public Inner() {  
        x = 0;  
         }  
     }  
     System.out.println("after "+x);  
      }  
  }

Se a pergunta fosse: Qual a saída do código acima? Você responderia como a Listagem 16?

Listagem 16. Saída da Listagem 15

  before 10
  after 10

Acontece que o construtor da classe interna Inner não foi chamado e por conta disso a variável x não recebeu o valor “0”. Vejamos como ficaria chamando o construtor e atribuindo o valor zero a variável x na Listagem 17.

Listagem 17. Chamando construtor da classe Inner

  public class TestOuter {  
     public static void main(String[] args) {  
        Outer o = new Outer();  
        o.see();  
     }  
  }  
  class Outer {  
      private int x = 10;  
      public void see() {  
     System.out.println("before "+x);  
     class Inner {  
         public Inner() {  
        x = 0;  
         }  
     }  
     Inner i = new Inner();  
     System.out.println("after "+x);  
      }  
  }  
  

Agora teremos a seguinte saída da Listagem 18.

Listagem 18. Saída da Listagem 17

  before 10
  after 0

Lembre-se que uma classe Interna a um método, só pode ser instanciada dentro deste próprio método, não há como realizar a instanciação dessa classe de fora do método ou mesmo de fora da classe externa, o escopo é muito restrito neste caso. Por esse fato o modificador padrão de acesso de uma classe de método é private, visto que esse não pode ser instanciado de nenhum local fora do método. Outros modificadores de podem ser aplicados a este tipo de classe são: abstract e final.

Há uma exceção a ser notada quando utilizamos classes de métodos, não podemos fazer referência as variáveis do método, estranho mas é isso mesmo, não podemos usar as variáveis declaradas no método. Vejamos a Listagem 19.

Listagem 19. Usando variáveis de método em classes de método

  public class TestOuter {  
     public static void main(String[] args) {  
        Outer o = new Outer();  
        o.see();  
     }  
  }  
    
  class Outer {  
      private int x = 10;  
      public void see() {  
     int y = 5;  
     System.out.println("before "+x);  
     class Inner {  
         public String s = "string inner";  
         public Inner() {  
        x = y;  
         }  
     }  
     Inner i = new Inner();  
     System.out.println("after "+x);  
      }  
  }

O código acima lança uma exceção pelo fato da variável y está sendo utilizada dentro da classe Inner. O seguinte erro será mostrado:

  local variable y is accessed from within inner class; needs to be declared final

E como o próprio erro sugere, nós devemos declarar a variável y como final para poder utilizá-la dentro da classe, como mostra a Listagem 20.

Listagem 20. Declarando y como final

  public class TestOuter {  
     public static void main(String[] args) {  
        Outer o = new Outer();  
        o.see();  
     }  
  }  
    
  class Outer {  
      private int x = 10;  
      public void see() {  
     final int y = 5;  
     System.out.println("before "+x);  
     class Inner {  
         public String s = "string inner";  
         public Inner() {  
        x = y;  
         }  
     }  
     Inner i = new Inner();  
     System.out.println("after "+x);  
      }  
  }  

Classes Internas Anônimas

Classe anônima nada mais é do que a herança de determinada classe em um local exclusivo, ou seja, apenas naquele determinado ponto eu preciso redefinir minha classe-pai. Veja a Listagem 21.

Listagem 21. Usando Classe Anônima

  class Car {  
     public void run() { ... }  
  }  
    
  class Gol {  
    
  Car car = new Car() {  
     public void run() { ... }  
  };  
  }

Temos acima a classe Car e a classe Gol. A classe Gol possui em seu corpo a instanciação de um objeto Car, mais ainda, ela além de instanciar um objeto Car muda o comportamento interno da classe, aplicando a herança ao mesmo tempo. Vejamos outro exemplo na Listagem 22.

Listagem 22. Classe Anônima, exemplo 2

  class Empregado {  
     public void trabalhar() {  
         System.out.println("trabalhar");  
     }  
  }  
    
  class QuadroFuncionario {  
     Empregado mgr  = new Empregado() {  
        public void trabalhar() {  
           System.out.println("mandar");  
        }  
     };  
    
     Empregado peao = new Empregado() {  
        public void trabalhar() {  
           System.out.println("executar");  
        }  
     };  
  }  

Este segundo exemplo fica bem mais fácil de entender o uso deste tipo de classe. Temos uma superclasse chama Empregado que possui a ação de “trabalhar()” onde imprime uma mensagem “trabalhar”.

Dentro da classe QuadroFuncionario nós criamos dois tipos de funcionários: peao e mgr (gerente). O mgr muda a ação de trabalhar() imprimindo uma mensagem “mandar” e o peao também muda mas imprimindo a mensagem “executar”.

Se fossemos fazer uma herança “comum”, teríamos que criar uma classe “Peao extends Empregado” e mudar o comportamento do método trabalhar, e também criaríamos uma classe “Mgr extends Empregado”, isso é o que normalmente se faz quando há necessidade do uso de tais comportamentos distintos em vários locais distintos, mas em nosso caso só precisamos desses comportamentos dentro da classe QuadroFuncionario e a melhor prática é usar classes anônimas.

Vejamos um exemplo erro do uso de classes anônimas na Listagem 23.

Listagem 23. Uso errado e classes anônimas

  class Empregado {  
     public void trabalhar() {  
         System.out.println("trabalhar");  
     }  
  }  
    
  class QuadroFuncionario {  
     Empregado mgr  = new Empregado() {  
        public void trabalhar() {  
           System.out.println("mandar");  
        }  
    
        public void demite() {  
               System.out.println("demite");  
        }  
     };  
    
     public void work() {  
         mgr.trabalhar();      
         mgr.demite();  
     }  
  }

O erro do código acima está no método work(), pois ele está tentando executar o método demite() que não existe no objeto “mgr”. Acontece que mesmo você criando o método demite() o que importa é a declaração na superclasse e nesse caso esse método não existe. Assim como na herança você precisaria referir-se a classe “Mgr” em vez de “Empregado” para usar o método demite(), se fosse o caso.

Classes anônimas são classes internas, por isso esta seção ficou por último já que abrange dois assuntos distintos mas que estão diretamente ligados. Você não tem como criar uma classe anônima sem que seja uma classe interna, perderia o objetivo.

As classes anônimas ainda tem algo muito interessante, o uso de Interfaces, como podemos ver na Listagem 24.

Listagem 24. Usando interface direto em classe anônimas

  public class Ex04 {  
     public static void main(String[] args) {  
        acao( new Evento() {  
           public void clicar() {  
              System.out.println("clicou");  
           }  
           public void arrastar() {  
              System.out.println("arrastou");  
           }  
        });  
     }  
     public static void acao(Evento e) {  
        e.clicar();  
        e.arrastar();  
     }  
  }  
  interface Evento {  
     public abstract void clicar();  
     public abstract void arrastar();  
  }

O que você provavelmente percebeu com o código acima é que estamos fazendo uma instanciação da interface Evento como uma classe anônima, mas como isso é possível se o java não permite que uma interface seja instanciada?

Verdade, e não permite mesmo e continua não permitindo. Acontece que ao usar a interface Evento como uma classe anônima, é criada uma classe que estende a interface Evento e não a interface é instanciada diretamente, tanto que você é obrigado a usar todos os métodos que estão na interface.

Vimos neste artigo o conceito de Classes Aninhadas ou Nested Class, muito importante para aumentar a produtividade de um projeto, a legibilidade do código e prover maior facilidade de manutenção (quando usado de forma correta). Aproveitamos para entrar em outro assunto que tem tudo haver com Classes Aninhadas, as Classes Anônimas, que são utilizadas em demasia por listeners, você já deve ter se deparado muito com a construção de um código que possui um listener para o click de um botão e geralmente é usado uma classe anônima para facilitar tal trabalho e evitar código desnecessário.