Uma breve introdução às classes internas do Java

Marcio Endo
May 21, 2023

Se você desenvolve aplicações na linguagem Java então deve escrever códigos como o abaixo:

for (var elem : collection) {
  consume(elem);
}

Ou talvez:

collection.stream()
    .filter(this::someFilter)
    .forEach(this::consume);

Saiba que, internamente, esses códigos criam instâncias de classes internas do Java. Ao menos nos casos em que collection é uma ArrayList ou um HashSet por exemplo.

Em outras palavras, ainda que você nunca tenha escrito uma classe interna, saiba que, muito provavelmente, você as usa diariamente.

Neste artigo veremos um pouco mais sobre as classes internas do Java.

Bora lá.

O que é uma classe interna?

Antes da definição formal, vejamos um exemplo primeiro.

Considere a seguinte classe TopLevel:

public class TopLevel {
  public static class Nested {}

  public class Inner {}
}

Ela declara duas classes aninhadas:

Vamos criar instâncias das duas últimas.

Criando uma instância de Nested

Primeiro, vamos criar uma instância da classe Nested:

var nested = new TopLevel.Nested();

O trecho acima compila sem erros. Quando executado no JShell, imprime:

jshell> var nested = new TopLevel.Nested();
nested ==> TopLevel$Nested@506e1b77

O JShell imprime o valor retornado pelo método toString da instância.

Lembrando que, como não sobrescrevemos o método toString, a implementação padrão é dada por:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

Em seguida, criaremos uma instância de Inner.

Crianda uma instância de Inner

Agora, vamos criar uma instância de Inner.

O código abaixo não compila:

// erro de compilação
var inner = new TopLevel.Inner();

System.out.println(inner);

O JShell apresenta a seguinte mensagem de erro:

jshell> var inner = new TopLevel.Inner();
|  Error:
|  an enclosing instance that contains TopLevel.Inner is required
|  var inner = new TopLevel.Inner();
|              ^------------------^

Em uma tradução livre, a mensagem diz que:

Corrigindo o erro de compilação

Vamos, então, seguir as instruções da mensagem de erro.

Primeiro, criamos a instância envolvente:

var topLevel = new TopLevel();

Essa deve conter a instância de Inner:

var inner = topLevel.new Inner();

Ou, se preferir, tudo em uma linha só:

var inner = new TopLevel().new Inner();

No JShell confirmamos que essas formas compilam sem erros:

jshell> var topLevel = new TopLevel();
topLevel ==> TopLevel@c4437c4

jshell> var inner = topLevel.new Inner();
inner ==> TopLevel$Inner@3f91beef

jshell> var inner = new TopLevel().new Inner();
inner ==> TopLevel$Inner@1a6c5a9e

Mas hein?

Classe interna: definição 'formal'

Toda classe que é simultaneamente:

É uma classe interna (ou inner class em inglês).

Classe interna requer uma instância da classe envolvente

Como vimos, para criar uma instância de uma classe interna, precisa-se necessariamente de uma instância da classe envolvente.

Isto explica a necessidade da sintaxe apresentada anteriormente:

var inner = new TopLevel().new Inner();

Eu não tenho dados concretos e, portanto, a próxima afirmação é um mero "achismo" meu...

Suspeito que essa última sintaxe seja raramente utilizada.

Por outro lado, como apontei na introdução do artigo, classes internas são amplamente utilizadas.

Então como as instâncias de classes internas são criadas na prática?

Exemplo prático de classe interna: iteradores

Mais uma vez, uma instância de uma classe interna está contida em uma instância da sua classe envolvente.

A implicação prática disso é que a instância da classe interna tem acesso direto ao estado de sua instância envolvente.

Eu mencionei, no início do artigo, a instrução for-each do Java. Isto não foi por acaso; um caso de uso prático para classes internas é a implementação de iteradores.

Nossa classe

Considere a seguinte classe:

public class PoemaConcreto {
  private final String[] versos = {
      "No meio do caminho tinha uma pedra",
      "tinha uma pedra no meio do caminho",
      "tinha uma pedra",
      "no meio do caminho tinha uma pedra."
  };
}

Sua estrutura interna é um array de strings.

É possível imaginar esta classe como uma versão bastante simplificada de um ArrayList<String>.

Vamos fazer com que nossa classe possa ser usada em um for-each.

A interface java.lang.Iterable

Para isto, é necessário implementar Iterable<String>.

public class PoemaConcreto implements Iterable<String> {
  private final String[] versos = {
      "No meio do caminho tinha uma pedra",
      "tinha uma pedra no meio do caminho",
      "tinha uma pedra",
      "no meio do caminho tinha uma pedra."
  };

  @Override
  public Iterator<String> iterator() {
    // implementação...
  }
}

Com isso, temos que implementar o método iterator.

O nosso iterador

Uma implementação possível para o nosso iterador é apresentada a seguir:

public class PoemaConcreto implements Iterable<String> {
  private final String[] versos = {
      "No meio do caminho tinha uma pedra",
      "tinha uma pedra no meio do caminho",
      "tinha uma pedra",
      "no meio do caminho tinha uma pedra."
  };

  @Override
  public Iterator<String> iterator() {
    return new IteratorImpl();
  }

  private class IteratorImpl implements Iterator<String> {
    private int index;

    @Override
    public boolean hasNext() {
      return index < versos.length;
    }

    @Override
    public String next() {
      if (hasNext()) {
        return versos[index++];
      } else {
        throw new NoSuchElementException();
      }
    }
  }
}

Vejamos essa implementação em mais detalhes.

O iterador é uma classe interna

O iterador foi implementado como uma classe interna:

public class PoemaConcreto implements Iterable<String> {
  ...
  private class IteratorImpl implements Iterator<String> {
    ...
  }
}

A classe IteratorImpl é aninhada e não-estática.

Classe interna acessa diretamente estado da classe envolvente

Observe o método next de IteratorImpl:

@Override
public String next() {
  if (hasNext()) {
    return versos[index++];
  } else {
    throw new NoSuchElementException();
  }
}

O método acessa diretamente o atributo versos da classe envolvente.

Criando instância da classe interna

Por fim, observe o método iterator da classe envolvente:

@Override
public Iterator<String> iterator() {
  return new IteratorImpl();
}

Ele retorna uma instância da classe interna. Ao contrário dos exemplos anteriores, isso é feito diretamente aqui. Isto porque já estamos dentro de uma instância da classe envolvente.

O método também pode ser escrito da seguinte forma:

@Override
public Iterator<String> iterator() {
  return this.new IteratorImpl();
}

Note a palavra reservada this.

Isto deixa explícito que a nova instância da classe interna ficará associada à instância atual da classe envolvente.

Breve introdução

Como dito no título do artigo, esta foi uma breve introdução às classes internas do Java.

Em outras palavras, há muito mais o que pode ser dito sobre as classes internas.

Até o próximo artigo

Por hoje é isto. Espero que tenha gostado.

O código para todos os exemplos pode ser encontrado no GitHub.

Se tiver comentários, dúvidas ou correções sobre este post, por favor, envie um email.

Siga-me no Twitter.