Il concetto a cui inizialmente fai riferimento nella tua domanda è chiamato tipi di ritorno covarianti .
I tipi di restituzione covarianti funzionano perché si suppone che un metodo restituisca un oggetto di un certo tipo e che i metodi di sovrascrittura possano effettivamente restituire una sottoclasse di esso. In base alle regole di sottotipizzazione di un linguaggio come Java, se S
è un sottotipo di T
, allora ovunque sia visualizzato T
possiamo passare un S
.
In questo modo è possibile restituire un S
quando si esegue l'override di un metodo che prevede un T
.
Il tuo suggerimento di accettare che un metodo che sovrascrive usi argomenti che sono sottotipi di quelli richiesti dal metodo sovrascritto è molto più complicato in quanto porta a una mancanza nel sistema dei tipi.
Da una parte, con le stesse regole di sottotitoli sopra menzionate, molto probabilmente funziona già per quello che vuoi fare. Ad esempio
interface Hunter {
public void hunt(Animal animal);
}
Niente impedisce alle implementazioni di questa classe di ricevere qualsiasi tipo di animale, in quanto tale soddisfa già i criteri nella tua domanda.
Ma supponiamo di poter sovrascrivere questo metodo come suggerito:
class MammutHunter implements Hunter {
@Override
public void hunt(Mammut animal) {
}
}
Ecco la parte divertente, ora puoi fare questo:
AnimalHunter hunter = new MammutHunter();
hunter.hunt(new Bear()); //Uh oh
Come per l'interfaccia pubblica di AnimalHunter
dovresti essere in grado di cacciare qualsiasi animale, ma come per la tua implementazione di MammutHunter
accetti solo Mammut
oggetti. Pertanto il metodo sovrascritto non soddisfa l'interfaccia pubblica. Abbiamo appena rotto la solidità del sistema di tipi qui.
Puoi implementare ciò che vuoi usando i generici.
interface AnimalHunter<T extends Animal> {
void hunt(T animal);
}
Quindi potresti definire il tuo MammutHunter
class MammutHunter implements AnimalHunter<Mammut> {
void hunt(Mammut m){
}
}
E usando la covarianza e la contravarianza generiche puoi rilassare le regole a tuo favore quando necessario. Ad esempio, potremmo assicurarci che un cacciatore di mammiferi possa cacciare solo felini in un dato contesto:
AnimalHunter<? super Feline> hunter = new MammalHunter();
hunter.hunt(new Lion());
hunter.hunt(new Puma());
Supponendo che MammalHunter
implementa AnimalHunter<Mammal>
.
In tal caso ciò non sarebbe stato accettato:
hunter.hunt(new Mammut()):
Anche se i mammut sono mammiferi, non sarebbero accettati a causa delle restrizioni sul tipo controvariante che stiamo usando qui. Quindi, puoi ancora esercitare un controllo sui tipi per fare cose come quelle che hai citato.