Prefira composição a herança.
Na composição o acoplamento entre as duas classes é menor. O que isso significa? Que a hierarquia de ambas pode correr em paralelo. A classe que compõe pode ter filhos, com variações diferentes da mesma funcionalidade. Por exemplo, é muito fácil substituir uma LinkedList por um ArrayList nesse caso:
public class Turma {
public List<Aluno> alunos = new LinkedList<Aluno>();
// resto dos métodos aqui
}
Mas é muito difícil nesse caso:
public class Turma extends ArrayList<Aluno> {
//resto dos métodos aqui
}
Note que no segundo caso, grandes são as chances de Turma<Aluno> usar os métodos protected de ArrayList<Aluno>, equanto no primeiro caso é impossível. Dependendo do quão acoplada as duas classes estiverem essa mudança pode ter um impacto profundíssimo em seu código.
A composição também tem a vantagem de poder ser definida em Runtime, como o colega falou. No caso da composição, é possível fazer isso:
public class Turma {
public List<Aluno> alunos = null;
public Turma(int numero) {
alunos = AlunosDAO.carregarTurmaPorNumero(alunos);
}
}
Que tipo de lista será retornado por carregarTurmaPorNumero? Não sabemos. O método pode decidir se vai retornar um ArrayList, uma coleção do Hibernate, um ArrayList encapsulado pelo Collections.unmodifiableCollection, qualquer coisa. O importante é que retorne algum tipo de list. Tente imaginar como fazer isso através do extends. Impossível, a turma seria sempre um ArrayList de alunos.
Melhor do que isso, você pode ter um arquivo de configuração que retorna uma lista real na aplicação de produção, e uma lista falsa, de testes, no JUnit.
Outra vantagem da composição ocorre quando você não quer ter acesso a todos os métodos da classe que você está compondo. Por exemplo, na classe turma ali em cima, temos o método clear(), herdado de ArrayList. Vamos supor que consideremos (mais tarde) esse método perigoso, e decidamos não te-lo no código. No caso da herança, só o que podemos fazer é sobrescrever o método e lançar um UnsupportedOperationException. A interface da classe turma fica poluída com um método que lança uma exceção, sempre que é chamado. Péssimo, não? No caso da composição, simplesmente não fazemos a implementação desse método.
Finalmente, é possível compor funcionalidades de 3 ou 4 classes separadas. O que aconteceria na herança é que essas 3 ou 4 classes provavelmente se misturariam em uma única classe, pouco coesa, difícil de manter e virtualmente impossível de ser aproveitada em algum outro lugar. Essas classes separadas, por serem menores, tem um objetivo claro e podem ser até mesmo reaproveitadas em outras hierarquias de classes, maximizando assim o reuso do código. Considere por exemplo que você queria que sua turma seja salva em um arquivo XML.
Usando herança, vc tem duas possibilidades:
- Fazer seu XMLHelper ser pai de ArrayList. O que nesse caso é impossível, já que herdamos de uma classe que era do Java. Isso mostra uma grave limitação da herança;
- Fazer XMLHelper ser filho de ArrayList, e Turma ser filho de XMLHelper. Agora pergunta-se, a classe XMLHelper deveria ser um ArrayList?? Tudo indica que não. Vários métodos vão ser inflados ali.
Com composição:
- Adiciona-se XMLHelper como atributo de Turma, pronto!
Note que com a composição, XMLHelper não sofreu modificação. Por se tratar de uma classe separada, a classe Aluno também poderia ter um XMLHelper associado. Imagine no primeiro caso, se optassemos pela opção #2 e quiséssemos também salvar o aluno em XML. Teríamos agora o Aluno filho de XMLHelper, que seria um ArrayList! Logo, o Aluno também se tornaria um ArrayList! Péssimo, não acha?
Os exemplo aqui são um pouco extremos. É meio óbvio que turma não é um ArrayList.
Na prática, isso ocorre mas de maneira mais sutil, pois nem sempre é possível dizer com muita precisão se algo “é ou não é” determinada coisa. Quando vc precisar reaproveitar a funcionalidade, seu código já está acoplado demais e isso se tornará difícil. O importante aqui é entender que herança cria um relacionamento forte. Quanto mais métodos protegidos o filho usar, ou quando mais funcionalidade da classe pai ele estender, mais forte se torna esse relacionamento.
Herança também tende a reduzir a coesão, ou seja, suas classes ficarem maiores e com funcionalidades que fogem de seu escopo principal. Prefira sempre um conjunto de classes pequenas, mas fácil de associar (para você efetivamente brincar de playmobil com o seu código) do que um elefantão branco multifuncional.