O argumento "horrível para a memória" está completamente errado, mas é uma "má prática" objetivamente . Quando você herda de uma classe, você não apenas herda os campos e métodos nos quais está interessado. Em vez disso, você herda tudo . Todo método que ele declara, mesmo que não seja útil para você. E o mais importante, você também herda todos os contratos e garantias que a classe oferece.
A sigla SOLID fornece algumas heurísticas para um bom design orientado a objetos. Aqui, o I Princípio de Segregação da Interface (ISP) e o L iskov Substituição do Pricinple (LSP) tem algo a dizer.
O ISP nos diz para manter nossas interfaces o menor possível. Mas, herdando de ArrayList
, você obtém muitos, muitos métodos. É significativo para get()
, remove()
, set()
(substituir) ou add()
(inserir) um nó filho em um índice específico? É sensato a ensureCapacity()
da lista subjacente? O que significa sort()
a Node? Os usuários da sua turma realmente deveriam receber um subList()
? Como não é possível ocultar os métodos que você 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:
private final ArrayList<Node> children = new ArrayList();
public void add(Node child) { children.add(child); }
public Iterator<Node> iterator() { return children.iterator(); }
Se você realmente quiser todos os métodos que você vê na documentação, podemos passar para o LSP. O LSP nos diz que devemos poder usar a subclasse sempre que a classe pai for esperada. Se uma função usar um parâmetro ArrayList
como e passarmos o nosso Node
, nada deve mudar.
A compatibilidade de subclasses começa com coisas simples, como assinaturas de tipos. Quando você substitui um método, não pode tornar os tipos de parâmetro mais restritos, pois isso pode excluir os usos legais da classe pai. Mas isso é algo que o compilador nos verifica em Java.
Mas o LSP é muito mais profundo: temos que manter a compatibilidade com tudo o que é prometido pela documentação de todas as classes e interfaces pai. Na sua resposta , Lynn encontrou um desses casos em que a interface List
(que você herdou via ArrayList
) garante como os métodos equals()
e hashCode()
devem funcionar. Para hashCode()
, você recebe até mesmo um algoritmo específico que deve ser implementado exatamente. Vamos supor que você tenha escrito este Node
:
public class Node extends ArrayList<Node> {
public final int value;
public Node(int value, Node... children) {
this.value = Value;
for (Node child : children)
add(child);
}
...
}
Isso exige que value
não contribua para o hashCode()
e não possa influenciar equals()
. A interface List
- que você promete honrar herdando dela - exige que new Node(0).equals(new Node(123))
seja verdadeiro.
Como a herança de classes torna muito fácil quebrar acidentalmente uma promessa feita por uma classe pai e, como ela geralmente expõe mais métodos do que você pretendia, geralmente é sugerido que você prefira composição sobre herança . Se você precisar herdar algo, é recomendável herdar somente interfaces. Se você quiser reutilizar o comportamento de uma determinada classe, poderá mantê-lo como um objeto separado em uma variável de instância, dessa forma, todas as promessas e requisitos não serão impostos a você.
Às vezes, nossa linguagem natural sugere um relacionamento de herança: um carro é um veículo. Uma motocicleta é um veículo. Devo definir classes Car
e Motorcycle
que herdam de uma classe Vehicle
? O design orientado a objetos não se refere ao espelhamento do mundo real exatamente em nosso código. Não podemos codificar facilmente as taxonomias ricas 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. Um Employee
é um Person
e tem um Boss
. Um Boss
também é um Person
. Então, devo criar uma classe Person
que é herdada por Boss
e Employee
? Agora eu tenho um problema: o chefe também é funcionário e tem outro superior. Portanto, parece que Boss
deve estender Employee
. Mas o CompanyOwner
é um Boss
, mas não é um Employee
? Qualquer tipo de gráfico de herança irá de alguma forma quebrar aqui.
OOP não é sobre hierarquias, herança e reutilização de classes existentes, trata-se de generalizar o comportamento . OOP é sobre "Eu tenho um monte de objetos e quero que eles façam uma coisa em particular - e eu não me importo como." Isso é o que interfaces são para. Se você implementar a Iterable
interface para o seu Node
, porque você quer torná-lo iterável, está tudo bem. Se você implementar a interface Collection
porque deseja adicionar / remover nós filhos etc., tudo bem. Mas herdar de outra classe, porque acontece de lhe dar tudo o que não é, ou pelo menos não, a menos que você tenha dado um pensamento cuidadoso, conforme descrito acima.