Uma breve introdução às classes internas do Java
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:
-
uma classe estática
Nested
; e -
uma classe não-estática
Inner
.
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:
-
uma instância envolvente é necessária; e
-
a instância envolvente deve conter a instância de
Inner
.
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:
-
aninhada; e
-
não estática.
É 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.