Por que preferir classes internas não estáticas ao invés de estáticas?


37

Esta pergunta é sobre se uma classe aninhada em Java deve ser uma classe aninhada estática ou uma classe aninhada interna. Pesquisei por aqui e no Stack Overflow, mas não consegui encontrar realmente nenhuma dúvida sobre as implicações de design dessa decisão.

As perguntas que encontrei estão perguntando sobre a diferença entre classes aninhadas estáticas e internas, o que é claro para mim. No entanto, ainda não encontrei um motivo convincente para usar uma classe aninhada estática em Java - com exceção das classes anônimas, que não considero para esta pergunta.

Aqui está minha compreensão do efeito do uso de classes aninhadas estáticas:

  • Menos acoplamento: geralmente obtemos menos acoplamento, pois a classe não pode acessar diretamente os atributos de sua classe externa. Menos acoplamento geralmente significa melhor qualidade do código, testes mais fáceis, refatoração etc.
  • Único Class: o carregador de classes não precisa cuidar de uma nova classe a cada vez, criamos um objeto da classe externa. Acabamos de receber novos objetos para a mesma classe repetidamente.

Para uma classe interna, geralmente considero que as pessoas consideram o acesso aos atributos da classe externa um profissional. Eu imploro para diferir nesse aspecto do ponto de vista do design, pois esse acesso direto significa que temos um alto acoplamento e, se quisermos extrair a classe aninhada em sua classe de nível superior separada, só podemos fazê-lo depois de transformar essencialmente em uma classe aninhada estática.

Portanto, minha pergunta se resume a isso: Estou errado ao supor que o acesso ao atributo disponível para classes internas não estáticas leva a um acoplamento alto, portanto a uma qualidade de código mais baixa, e deduzo disso que as classes aninhadas (não anônimas) devem geralmente ser estático?

Ou, em outras palavras: existe uma razão convincente para alguém preferir uma classe interna aninhada?


2
do tutorial Java : "Use uma classe aninhada não estática (ou classe interna) se precisar de acesso aos campos e métodos não públicos de uma instância envolvente. Use uma classe aninhada estática se você não precisar desse acesso."
Gnat

5
Obrigado pela citação do tutorial, mas isso apenas reitera qual é a diferença, não porque você deseja acessar os campos da instância.
24414 Frank

é uma orientação geral, sugerindo o que preferir. Eu adicionei uma explicação mais detalhada sobre como eu interpretá-lo de esta resposta
mosquito

11
Se a classe interna não estiver fortemente acoplada à classe anexa, provavelmente não deve ser uma classe interna em primeiro lugar.
Kevin Krumwiede 5/10

Respostas:


23

Joshua Bloch no Item 22 de seu livro "Effective Java Second Edition" diz quando usar que tipo de classe aninhada e por quê. Existem algumas citações abaixo:

Um uso comum de uma classe de membro estático é como uma classe auxiliar pública, útil apenas em conjunto com sua classe externa. Por exemplo, considere uma enumeração descrevendo as operações suportadas por uma calculadora. A operação enum deve ser uma classe de membro estática pública da Calculatorclasse. Os clientes de Calculatorpoderiam então se referir a operações usando nomes como Calculator.Operation.PLUSe Calculator.Operation.MINUS.

Um uso comum de uma classe de membro não estático é definir um adaptador que permita que uma instância da classe externa seja vista como uma instância de alguma classe não relacionada. Por exemplo, as implementações da Mapinterface de tipicamente usar classes membro nonstatic para implementar as suas vistas de recolha , que são retornados por Map's keySet, entrySete valuesmétodos. Da mesma forma, implementações das interfaces de coleção, como Sete List, geralmente usam classes de membros não estáticos para implementar seus iteradores:

// Typical use of a nonstatic member class
public class MySet<E> extends AbstractSet<E> {
    ... // Bulk of the class omitted

    public Iterator<E> iterator() {
        return new MyIterator();
    }

    private class MyIterator implements Iterator<E> {
        ...
    }
}

Se você declarar uma classe de membro que não requer acesso a uma instância anexa, sempre coloque o staticmodificador em sua declaração, tornando-a uma classe de membro estática em vez de não-estática.


11
Não tenho muita certeza de como / se esse adaptador altera a exibição das MySetinstâncias de classe externa ? Eu poderia imaginar algo como um tipo dependente de caminho sendo uma diferença, ou seja myOneSet.iterator.getClass() != myOtherSet.iterator.getClass(), mas, novamente, em Java isso realmente não é possível, pois a classe seria a mesma para cada um.
12764 Frank

@Frank É uma classe de adaptador bastante típica - permite visualizar o conjunto como um fluxo de seus elementos (um Iterator).
usar o seguinte comando

@immibis obrigado, estou bem ciente do que o iterador / adaptador faz, mas essa pergunta era sobre como ele é implementado.
Frank

@ Frank eu estava dizendo como isso muda a visão da instância externa. É exatamente isso que um adaptador faz - muda a maneira como você vê algum objeto. Nesse caso, esse objeto é a instância externa.
usar o seguinte comando

10

Você está correto ao supor que o acesso ao atributo disponível para classes internas não estáticas leva a um acoplamento alto, portanto, a uma qualidade de código mais baixa e as classes internas (não anônimas e não locais ) geralmente devem ser estáticas.

As implicações da decisão de design para tornar a classe interna não estática são apresentadas no Java Puzzlers , Puzzle 90 (a fonte em negrito na citação abaixo é minha):

Sempre que você escrever uma classe de membro, pergunte-se: Essa classe realmente precisa de uma instância anexa? Se a resposta for não, faça-o static. Às vezes, as classes internas são úteis, mas podem facilmente introduzir complicações que dificultam a compreensão de um programa. Eles têm interações complexas com genéricos (quebra-cabeça 89), reflexão (quebra-cabeça 80) e herança (este quebra-cabeça) . Se você declara Inner1ser static, o problema desaparece. Se você também declara Inner2ser static, pode realmente entender o que o programa faz: um bom bônus de fato.

Em resumo, raramente é apropriado que uma classe seja uma classe interna e uma subclasse de outra. Mais geralmente, raramente é apropriado estender uma classe interna; se necessário, pense muito sobre a instância anexa. Além disso, prefira staticclasses aninhadas a não static. A maioria das classes de membros pode e deve ser declarada static.

Se você estiver interessado, uma análise mais detalhada do Puzzle 90 é fornecida nesta resposta em Stack Overflow .


Vale ressaltar que acima é essencialmente uma versão estendida das orientações fornecidas no tutorial Java Classes and Objects :

Use uma classe aninhada não estática (ou classe interna) se precisar de acesso aos campos e métodos não públicos de uma instância envolvente. Use uma classe aninhada estática se você não precisar desse acesso.

Portanto, a resposta à pergunta que você fez em outras palavras, por tutorial é que a única razão convincente para usar non-static é quando é o acesso a campos e métodos não-públicas de uma instância encerrando necessário .

A redação do tutorial é um pouco ampla (esse pode ser o motivo pelo qual o Java Puzzlers tenta fortalecê-lo e reduzi-lo). Em particular, nunca foi realmente necessário acessar diretamente os campos da instância envolvente - no sentido de que maneiras alternativas como passar esses parâmetros como construtor / método sempre ficavam mais fáceis de depurar e manter.


No geral, meus (bastante dolorosos) encontros com a depuração de classes internas que acessam diretamente os campos da instância anexa causaram forte impressão de que essa prática se assemelha ao uso do estado global, juntamente com os males conhecidos associados a ele.

É claro que o Java faz com que os danos de um "quase global" fiquem contidos na classe envolvente, mas quando eu tive que depurar uma classe interna específica, parecia que um band-aid não ajudou a reduzir a dor: eu ainda tinha ter em mente a semântica e os detalhes "estrangeiros" em vez de se concentrar totalmente na análise de um objeto problemático em particular.


Por uma questão de integridade, pode haver casos em que o raciocínio acima não se aplique. Por exemplo, de acordo com minha leitura de javadocs map.keySet , esse recurso sugere um acoplamento rígido e, como resultado, invalida argumentos contra classes não estáticas:

Retorna uma Setvisualização das chaves contidas neste mapa. O conjunto é apoiado pelo mapa, portanto, as alterações no mapa são refletidas no conjunto e vice-versa ...

Não que isso acima, de alguma forma, facilitaria a manutenção, o teste e a depuração do código envolvido, mas pelo menos poderia permitir que alguém argumentasse que a complicação corresponde / é justificada pela funcionalidade pretendida.


Compartilho sua experiência sobre os problemas causados ​​por esse acoplamento alto, mas não estou de acordo com o uso do exigir do tutorial . Você diz a si mesmo que nunca realmente exigiu o acesso ao campo; portanto, infelizmente, isso também não atende totalmente à minha pergunta.
31414 Frank

@Frank Bem, como você pode ver, minha interpretação da orientação tutorial (que parece ser suportada pelo Java Puzzlers) é como, use classes não estáticas somente quando você puder provar que isso é necessário e, em particular, provar que maneiras alternativas são pior . Vale notar que na minha experiência formas alternativas sempre acabou melhor
mosquito

Essa é a questão. Se é sempre melhor, por que nos incomodamos?
31414 Frank

@Frank Outra resposta fornece exemplos convincentes de que nem sempre é assim. De acordo com minha leitura de map.keySet javadocs , esse recurso sugere acoplamento rígido e, como resultado, invalida argumentos contra classes não estáticas "O conjunto é apoiado pelo mapa, de modo que as alterações no mapa são refletidas no conjunto e vice-versa ... "
gnat

1

Alguns equívocos:

Menos acoplamento: geralmente temos menos acoplamento, pois a classe [estática interna] não pode acessar diretamente os atributos de sua classe externa.

IIRC - Na verdade, pode. (É claro que, se forem campos de objetos, ele não terá um objeto externo para examinar. No entanto, se ele obtiver esse objeto de qualquer lugar, poderá acessar diretamente esses campos).

Classe Única: O carregador de classes não precisa cuidar de uma nova classe a cada vez, criamos um objeto da classe externa. Acabamos de receber novos objetos para a mesma classe repetidamente.

Não sei bem o que você quer dizer aqui. O carregador de classes está envolvido apenas duas vezes no total, uma vez para cada classe (quando a classe é carregada).


Divagam-se a seguir:

Em Javaland, parecemos um pouco assustados em criar classes. Eu acho que tem que parecer um conceito significativo. Geralmente pertence ao seu próprio arquivo. Precisa de clichê significativo. Precisa de atenção ao seu design.

Versus outras linguagens - por exemplo, Scala, ou talvez C ++: Não há absolutamente nenhum drama criando um pequeno suporte (classe, estrutura, qualquer que seja) a qualquer momento que dois bits de dados pertençam juntos. As regras de acesso flexível ajudam você a reduzir o padrão, permitindo manter o código relacionado fisicamente mais próximo. Isso reduz sua 'distância mental' entre as duas classes.

Podemos afastar as preocupações de design sobre o acesso direto, mantendo a classe interna em particular.

(... Se você perguntasse 'por que uma classe interna pública não estática ?', Essa é uma pergunta muito boa - acho que nunca encontrei um caso justificado para isso).

Você pode usá-los sempre que ajudar na legibilidade do código e evitar violações DRY e clichê. Não tenho um exemplo pronto, porque isso não acontece com tanta frequência na minha experiência, mas acontece.


Classes internas estáticas ainda são uma boa abordagem padrão , btw. Não estático possui um campo oculto extra gerado por meio do qual a classe interna se refere à externa.


0

Pode ser usado para criar um padrão de fábrica. Um padrão de fábrica é frequentemente usado quando os objetos criados precisam de parâmetros adicionais do construtor para funcionar, mas são tediosos para fornecer sempre:

Considerar:

public class QueryFactory {
    @Inject private Database database;

    public Query create(String sql) {
        return new Query(database, sql);
    }
}

public class Query {
    private final Database database;
    private final String sql;

    public Query(Database database, String sql) {
        this.database = database;
        this.sql = sql;
    } 

    public List performQuery() {
        return database.query(sql);
    }
}

Usado como:

Query q = queryFactory.create("SELECT * FROM employees");

q.performQuery();

O mesmo poderia ser alcançado com uma classe interna não estática:

public class QueryFactory {
    @Inject private Database database;

    public class Query {
        private final String sql;

        public Query(String sql) {
            this.sql = sql;
        }

        public List performQuery() {
            return database.query(sql);
        }
    }
}

Use como:

Query q = queryFactory.new Query("SELECT * FROM employees");

q.performQuery();

As fábricas se beneficiam de ter métodos nomeados especificamente, como create, createFromSQLou createFromTemplate. O mesmo pode ser feito aqui também na forma de várias classes internas:

public class QueryFactory {

    public class SQL { ... }
    public class Native { ... }
    public class Builder { ... }

}

É necessária menos passagem de parâmetro da fábrica para a classe construída, mas a legibilidade pode sofrer um pouco.

Comparar:

Queries.Native q = queries.new Native("select * from employees");

vs

Query q = queryFactory.createNative("select * from employees");
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.