Então, a questão é que o polimorfismo é uma técnica de generalização de tipos concretos por meio de interfaces abstratas. Seu uso é fundamental dentro da lógica de programação orientado a objetos, e principalmente quando fazemos uso de frameworks.
Pense no seguinte problema:
interface Forma {
void desenhaCentradoEm(double x, double y);
}
class Quadrado implements Forma {
...
}
class Circulo implements Forma {
...
}
class Triangulo implements Forma {
...
}
class Canvas {
void adiciona(Forma forma, double x, double y) {
forma.desenhaCentradoEm(x, y);
}
}
Observe que cada uma das formas possui uma lógica de desenho específica. Mas a classe Canvas consegue resolver o problema de desenha-las de forma generalista com o mesmo código, isso porque os aspectos específicos de cada uma são resolvidos pelo método desenhaCentradoEm, o qual possui implementação distinta em cada uma delas.
Podemos portanto fazer algo como:
canvas.adiciona(new Quadrado(), 10, 20);
canvas.adiciona(new Triangulo(), 15, 10);
canvas.adiciona(new Circulo(), 5, 5);
Não sei se havia entendido exatamente qual a tua dúvida. Tu querias saber se seria possível fazer algo como o código abaixo?
...
Quadrado quadrado = new Quadrado();
...
canvas.adiciona(quadrado, 10, 20);
Sim, é perfeitamente válido, pois todo quadrado é uma forma, mas nem toda forma é um quadrado. O chamado uppercasting é sempre uma operação segura, e portanto, realizada automaticamente.
Há várias situações em que isso é desejável; por exemplo, quando um sub-tipo de uma classe abstrata necessariamente faz uso de uma determinada implementação de uma interface. Dessa forma, quando você instancia um objeto pelo seu tipo concreto, você pode chamar seus métodos específicos, que adicionem outras funcionalidades não descritas na interface pública, não obstante pode também passa-lo para qualquer método que espere seu super-tipo.
Contudo, pense no caso de uma estrutura de dados, como por exemplo uma lista. Você poderia querer em dado momento mudar de uma ArrayList para uma LinkedList. Se você construiu toda a interface da sua aplicação usando um tipo concreto, seu código é muito menos flexível e, portanto, menos reutilizável. Por este motivo é sempre melhor usar uma interface mais abstrata a uma implementação concreta quando se trata de dados ou métodos públicos.
abstract class Documento {
...
}
class Pessoa {
private List<Documento> documentos;
void adiciona(Documento documento);
void remove(Documento document);
}
Perceba que o código anterior nos dá mais liberdade que algo como:
class Cpf {
...
}
class Pessoa {
private ArrayList<Cpf> documentos;
void adiciona(Cpf cpf);
void remove(Cpf cpf);
}
Mesmo porque, de forma legítima, uma pessoa só pode ter um CPF. Por outro lado, uma pessoa pode ter até 27 RGs, habilitação de trânsito, passaporte, carteira de conselho profissional, etc.