Em um projeto recente, defini uma classe com o seguinte cabeçalho:
public class Node extends ArrayList<Node> {
...
}
No entanto, depois de discutir com meu professor de CS, ele afirmou que a classe seria "horrível para a memória" e "má prática". Não achei o primeiro particularmente verdadeiro e o segundo subjetivo.
Meu raciocínio para esse uso é que eu tinha uma ideia para um objeto que precisava ser definido como algo que pudesse ter profundidade arbitrária, em que o comportamento de uma instância pudesse ser definido por uma implementação customizada ou pelo comportamento de vários objetos semelhantes interagindo . Isso permitiria a abstração de objetos cuja implementação física seria composta de muitos subcomponentes interagindo.¹
Por outro lado, vejo como isso pode ser uma prática ruim. A idéia de definir algo como uma lista em si não é simples ou fisicamente implementável.
Existe algum motivo válido para eu não usar isso no meu código, considerando o meu uso?
¹ Se eu precisar explicar isso mais, ficaria feliz em; Estou apenas tentando manter essa pergunta concisa.
fonte
ArrayList<Node>
, em comparação com a implementaçãoList<Node>
?Respostas:
Francamente, não vejo a necessidade de herança aqui. Isso não faz sentido;
Node
é umArrayList
dosNode
?Se isso é apenas uma estrutura de dados recursiva, você simplesmente escreveria algo como:
O que faz sentido; O nó possui uma lista de nós filhos ou descendentes.
fonte
Item
serve eItem
opera independentemente das listas.Node
é umArrayList
deNode
comoNode
contém 0 a muitosNode
objetos. Sua função é a mesma, essencialmente, mas, em vez de ser uma lista de objetos semelhantes, ela possui uma lista de objetos semelhantes. O design original para a classe escrita era a crença de que qualquer umNode
pode ser expresso como um conjunto de subNode
s, o que ambas as implementações fazem, mas esse certamente é menos confuso. +1Children
apenas uma ou duas vezes, e geralmente é preferível escrevê-lo nesses casos por motivos de clareza do código.O argumento “horrível para a memória” está totalmente errado, mas é objetivamente uma “má prática”. Quando você herda de uma classe, não herda apenas os campos e métodos nos quais está interessado. Em vez disso, herda tudo . Todo método declarado, mesmo que não seja útil para você. E o mais importante, você também herda todos os seus contratos e garante que a classe fornece.
O acrônimo SOLID fornece algumas heurísticas para um bom design orientado a objetos. Aqui, o I nterface Segregação Princípio (ISP) eo L iskov Substituição Pricinple (LSP) tem algo a dizer.
O ISP nos diz para manter nossas interfaces o menor possível. Mas, ao herdar de
ArrayList
, você obtém muitos, muitos métodos. É significativo paraget()
,remove()
,set()
(substituir), ouadd()
(inserir) um nó filho em um índice específico? É sensato àensureCapacity()
lista subjacente? O que isso significa parasort()
um nó? Os usuários da sua classe realmente deveriam receber umsubList()
? Como você não pode ocultar os métodos que não deseja, a única solução é ter o ArrayList como uma variável de membro e encaminhar todos os métodos que você realmente deseja:Se você realmente deseja todos os métodos que vê na documentação, podemos seguir para o LSP. O LSP nos diz que devemos poder usar a subclasse onde quer que a classe pai seja esperada. Se uma função usa um
ArrayList
parâmetro as e passamos o nossoNode
, nada deve mudar.A compatibilidade das subclasses começa com coisas simples, como assinaturas de tipo. Quando você substitui um método, não pode tornar os tipos de parâmetro mais rigorosos, pois isso pode excluir usos legais da classe pai. Mas isso é algo que o compilador verifica para nós em Java.
Mas o LSP é muito mais profundo: precisamos manter a compatibilidade com tudo o que é prometido pela documentação de todas as classes e interfaces-pai. Em sua resposta , Lynn encontrou um desses casos em que a
List
interface (que você herdou porArrayList
) garante como os métodosequals()
ehashCode()
devem funcionar. PoishashCode()
você recebe um algoritmo específico que deve ser implementado exatamente. Vamos supor que você tenha escrito issoNode
:Isso requer que o
value
não possa contribuir parahashCode()
e não possa influenciarequals()
. AList
interface - que você promete honrar herdando dela - precisanew Node(0).equals(new Node(123))
ser verdadeira.Como a herança de classes facilita demais a quebra acidental de uma promessa feita por uma classe pai e, como geralmente expõe mais métodos do que você pretendia, geralmente é sugerido que você prefira composição do que herança . Se você deve herdar algo, é recomendável herdar apenas interfaces. Se você deseja reutilizar o comportamento de uma classe específica, pode mantê-lo como um objeto separado em uma variável de instância, para que todas as suas promessas e requisitos não sejam impostos a você.
Às vezes, nossa linguagem natural sugere uma relação de herança: um carro é um veículo. Uma motocicleta é um veículo. Devo definir classes
Car
eMotorcycle
que herdam de umaVehicle
classe? O design orientado a objetos não se trata de espelhar o mundo real exatamente em nosso código. Não podemos codificar facilmente as ricas taxonomias do mundo real em nosso código fonte.Um exemplo é o problema de modelagem empregado-chefe. Temos vários
Person
s, cada um com um nome e endereço. UmEmployee
é umPerson
e tem umBoss
. ABoss
também é aPerson
. Então, devo criar umaPerson
classe que é herdada porBoss
eEmployee
? Agora eu tenho um problema: o chefe também é funcionário e tem outro superior. Então parece queBoss
deveria se estenderEmployee
. Mas oCompanyOwner
é umBoss
mas não é umEmployee
? De qualquer forma, qualquer tipo de gráfico de herança será dividido aqui.OOP não é sobre hierarquias, herança e reutilização de classes existentes, é sobre generalizar o comportamento . OOP é sobre "Eu tenho vários objetos e quero que eles façam uma coisa em particular - e não me importo como." É para isso que servem as interfaces . Se você implementar a
Iterable
interface para o seu computadorNode
porque deseja torná-lo iterável, tudo bem. Se você implementar aCollection
interface porque deseja adicionar / remover nós filhos, etc., tudo bem. Mas herdar de outra classe, porque isso lhe dá tudo o que não é, ou pelo menos não, a menos que você tenha pensado cuidadosamente, conforme descrito acima.fonte
Estender um contêiner por si só é geralmente aceito como uma má prática. Há realmente muito pouca razão para estender um contêiner em vez de apenas ter um. Estender um contêiner de você mesmo o torna ainda mais estranho.
fonte
Além do que foi dito, há um motivo específico de Java para evitar esse tipo de estrutura.
O contrato do
equals
método para listas exige que uma lista seja considerada igual a outro objetoFonte: https://docs.oracle.com/javase/7/docs/api/java/util/List.html#equals(java.lang.Object)
Especificamente, isso significa que ter uma classe projetada como uma lista em si mesma pode tornar caras as comparações de igualdade (e cálculos de hash também se as listas forem mutáveis) e, se a classe tiver alguns campos de instância, eles deverão ser ignorados na comparação de igualdade. .
fonte
Em relação à memória:
Eu diria que isso é uma questão de perfeccionismo. O construtor padrão de se
ArrayList
parece com isso:Fonte . Este construtor também é usado no Oracle-JDK.
Agora considere construir uma lista vinculada individualmente com seu código. Você acabou de aumentar com sucesso o seu consumo de memória pelo fator 10x (para ser mais preciso, ainda que marginalmente mais alto). Para as árvores, isso pode facilmente ficar muito ruim sem requisitos especiais sobre a estrutura da árvore. Usar uma
LinkedList
ou outra classe como alternativa deve resolver esse problema.Para ser sincero, na maioria dos casos isso não passa de mero perfeccionismo, mesmo que ignoremos a quantidade de memória disponível. A
LinkedList
desaceleraria um pouco o código como alternativa, por isso é uma troca entre desempenho e consumo de memória de qualquer maneira. Ainda assim, minha opinião pessoal sobre isso seria não desperdiçar tanta memória de uma maneira que possa ser tão facilmente contornada quanto esta.EDIT: esclarecimentos sobre os comentários (@amon, para ser mais preciso). Esta seção da resposta não trata da questão da herança. A comparação do uso da memória é feita com uma Lista vinculada única e com o melhor uso da memória (em implementações reais, o fator pode mudar um pouco, mas ainda é suficientemente grande para resumir um pouco de memória desperdiçada).
Em relação às "más práticas":
Definitivamente. Esta não é a maneira padrão de implementar um gráfico por um motivo simples: um nó de gráfico possui nós filhos, não é como uma lista de nós filhos. Expressar com precisão o que você quer dizer com código é uma habilidade importante. Seja em nomes de variáveis ou estruturas de expressão como esta. Próximo ponto: mantendo as interfaces mínimas: você disponibilizou todos os métodos
ArrayList
para o usuário da classe por herança. Não há como alterar isso sem quebrar toda a estrutura do código. Em vez disso, armazene aList
variável como interna e disponibilize os métodos necessários por meio de um método de adaptador. Dessa forma, você pode facilmente adicionar e remover funcionalidades da classe sem estragar tudo.fonte
new Object[0]
use 4 palavras no total, umnew Object[10]
usaria apenas 14 palavras de memória.ArrayList
s não utilizados estão consumindo bastante memória.Parece que parece a semântica do padrão composto . É usado para modelar estruturas de dados recursivas.
fonte
public class Node extends ArrayList<Node> { public string Item; }
é a versão de herança da sua resposta. O seu é mais simples de raciocinar, mas ambos alcançam o seguinte: eles definem árvores não binárias.