Eu entendo o polimorfismo como a capacidade de nos referirmos a um tipo de objeto, sendo específico somente até onde for necessário.
Esse caso da Conta é um ótimo exemplo.
Suponha que nosso programa tenha duas classes: ContaCorrente e ContaPoupanca. Se temos na classe Banco a necessidade de um método chamado adicionaConta, que irá receber como parâmetro a conta a ser adicionada no Banco, teríamos que criar na verdade dois métodos com sobrecarga, um recebendo como argumento a ContaCorrente e o outro recebendo como argumento a ContaPoupanca. Ou ainda dois métodos, um adicionaContaCorrente e outro adicionaContaPoupanca.
Percebe o código duplicado? Temos uma ação idêntica ocorrendo em duas classes, que apesar de próximas, têm pequena diferenças. Mas sabemos que ambas têm uma coisa em comum, correto? Tanto ContaCorrente como ContaPoupanca são Contas!
Portanto, se pudermos informar ao método adicionaConta que ele pode receber como argumento uma Conta, não importa se é ContaCorrente ou ContaPoupanca, desde que seja uma Conta, acabamos com esse problema de código duplicado. Isso é o polimorfismo! Podemos nos referir a diferentes tipos de objeto de uma única maneira. Isto é, não precisamos ser específicos demais, podemos "generalizar" e dizer que o argumento deve ser uma Conta.
Como isso vai ser feito, depende do propósito! Podemos usar herança para obter polimorfismo, mas também podemos usar interfaces.
Nesse caso da conta, acho que o ideal seria o uso de interface, pois a Conta não tem uma implementação padrão. Queremos apenas definir como deve ser uma Conta. Por exemplo, deve ter os métodos getSaldo, deposita, saca.
Por tanto, temos a Interface Conta, e ContaCorrente e ContaPoupanca implementam essa interface, e portanto podem ser referenciadas como uma Conta.
EDIT.: Um pouco de código sempre é bom:
Essa é a interface Conta. Repare que ela diz quais métodos as classes devem ter, mas não os implementam.
public interface Conta {
void deposita(double valor);
void saca(double valor);
double getSaldo();
}
Aqui temos uma implementação, a ContaCorrente. Ela implementa os métodos da interface.
public class ContaCorrente implements Conta {
private double saldo;
public void deposita(double valor) {
this.saldo += valor * 0.9;
}
public void saca(double valor) {
this.saldo -= valor * 1.1;
}
public double getSaldo() {
return this.saldo;
}
}
E aqui temos uma outra implementação, a ContaPoupanca.
public class ContaPoupanca implements Conta {
private double saldo;
public void deposita(double valor) {
this.saldo += valor;
}
public void saca(double valor) {
this.saldo -= valor;
}
public double getSaldo() {
return this.saldo;
}
}
Agora, podemos nos referir a uma Conta, e chamar seus métodos, sem especificar o tipo de Conta.
public void adicionaConta(Conta c) {
//codigo
}
Esse método pode receber como argumento uma Conta. Não importa se é ContaCorrente ou ContaPoupanca, pois estamos usando Polimorfismo.