J'aimerais que ma BallUserInterfaceFactory
renvoie une instance d'une interface utilisateur ayant le type générique approprié. Je suis coincé dans l'exemple ci-dessous en obtenant l'erreur:
Correspondance liée: la méthode générique getBaseballUserInterface (BASEBALL) de type BallUserInterfaceFactory n'est pas applicable pour les arguments (BALL). Le type inféré BALL n'est pas un substitut valide du paramètre lié
public class BallUserInterfaceFactory {
public static <BALL extends Ball> BallUserInterface<BALL> getUserInterface(BALL ball) {
if(ball instanceof Baseball){
return getBaseballUserInterface(ball);
}
//Other ball types go here
//Unable to create a UI for ball
return null;
}
private static <BASEBALL extends Baseball> BaseballUserInterface<BASEBALL> getBaseballUserInterface(BASEBALL ball){
return new BaseballUserInterface<BASEBALL>(ball);
}
}
Je comprends qu’elle ne peut pas garantir que BALL soit une balle de baseball. Il existe donc une incompatibilité de type de paramètre dans l’appel de la méthode getBaseballUserInterface.
Si je transtypes le paramètre ball dans l'appel à la méthode getBaseballUserInterface, le message d'erreur suivant s'affiche:
Incompatibilité de type: impossible de convertir de
BaseballUserInterface<Baseball>
àBallUserInterface<BALL>
Parce qu'il ne peut pas garantir que ce que je retourne est le même type de balle.
Ma question est, quelle est la stratégie pour faire face à cette situation?
(Pour être complet, voici les autres classes requises dans l'exemple)
public class Ball {
}
public class Baseball extends Ball {
}
public class BallUserInterface <BALL extends Ball> {
private BALL ball;
public BallUserInterface(BALL ball){
this.ball = ball;
}
}
public class BaseballUserInterface<BASEBALL extends Baseball> extends BallUserInterface<BASEBALL>{
public BaseballUserInterface(BASEBALL ball) {
super(ball);
}
}
C'est une très bonne question.
Vous pouvez lancer bruyamment
return (BallUserInterface<BALL>)getBaseballUserInterface((Baseball)ball);
La réponse est théoriquement imparfaite, puisque nous forçons BASEBALL=Baseball
.
Cela fonctionne en raison de l'effacement. En fait, il dépend de l’effacement.
J'espère qu'il y a une meilleure réponse qui est reification safe .
C'est un mauvais modèle. Plutôt que d'utiliser une méthode générique et une échelle si, vous devriez plutôt utiliser la surcharge. La surcharge élimine le besoin de ladder if et le compilateur peut s’assurer que la bonne méthode est appelée plutôt que d’attendre le moment de l’exécution.
par exemple.
public class BallUserInterfaceFactory {
public static BallUserInterface<Baseball> getUserInterface(
Baseball ball) {
return new BallUserInterface<Baseball>(ball);
}
public static BallUserInterface<Football> getUserInterface(
Football ball) {
return new BallUserInterface<Football>(ball);
}
}
De cette façon, vous obtenez également l'avantage supplémentaire des erreurs de compilation si votre code ne peut pas créer un BallUserInterface
pour la balle appropriée.
Pour éviter l’échelle if, vous pouvez utiliser une technique appelée double dispatch. En substance, nous utilisons le fait que l'instance sait à quelle classe elle appartient et appelle la méthode de fabrique appropriée pour nous. Pour que cela fonctionne, Ball
doit avoir une méthode qui retourne le BallInterface
approprié.
Vous pouvez rendre la méthode abstraite ou fournir une implémentation par défaut qui lève une exception ou retourne null. Ball et Baseball devraient maintenant ressembler à ceci:
public abstract class Ball<T extends Ball<T>> {
abstract BallUserInterface<T> getBallUserInterface();
}
.
public class Baseball extends Ball<Baseball> {
@Override
BallUserInterface<Baseball> getBallUserInterface() {
return BallUserInterfaceFactory.getUserInterface(this);
}
}
Pour rendre les choses un peu plus propres, il est préférable de rendre le paquet getBallUserInterface
privé et de fournir un getter générique dans BallUserInterfaceFactory
name__. L'usine peut alors gérer des vérifications supplémentaires, comme pour les exceptions nulles et éventuelles. par exemple.
public class BallUserInterfaceFactory {
public static BallUserInterface<Baseball> getUserInterface(
Baseball ball) {
return new BallUserInterface<Baseball>(ball);
}
public static <T extends Ball<T>> BallUserInterface<T> getUserInterface(
T ball) {
return ball.getBallUserInterface();
}
}
Comme indiqué dans les commentaires, l'un des problèmes de ce qui précède est que les classes Ball
doivent connaître l'interface utilisateur, ce qui est hautement indésirable. Vous pouvez toutefois utiliser le modèle de visiteur, ce qui vous permet d’utiliser la double répartition, mais également de découpler les différentes classes Ball
et l’interface utilisateur.
Tout d’abord, les classes de visiteurs nécessaires et les fonctions d’usine:
public interface Visitor<T> {
public T visit(Baseball ball);
public T visit(Football ball);
}
public class BallUserInterfaceVisitor implements Visitor<BallUserInterface<? extends Ball>> {
@Override
public BallUserInterface<Baseball> visit(Baseball ball) {
// Since we now know the ball type, we can call the appropriate factory function
return BallUserInterfaceFactory.getUserInterface(ball);
}
@Override
public BallUserInterface<Football> visit(Football ball) {
return BallUserInterfaceFactory.getUserInterface(ball);
}
}
public class BallUserInterfaceFactory {
public static BallUserInterface<? extends Ball> getUserInterface(Ball ball) {
return ball.accept(new BallUserInterfaceVisitor());
}
// other factory functions for when concrete ball type is known
}
Vous noterez que le visiteur et la fonction usine doivent utiliser des caractères génériques. Ceci est nécessaire pour la sécurité du type. Puisque vous ne savez pas quel type de balle a été passé, la méthode ne peut pas être sûre de l'interface utilisateur renvoyée (à part qu'il s'agit d'une interface utilisateur de balle).
Deuxièmement, vous devez définir une méthode abstraite accept
sur Ball
qui accepte un Visitor
name__. Chaque implémentation concrète de Ball
doit également implémenter cette méthode pour que le modèle de visiteur fonctionne correctement. L'implémentation a exactement la même apparence, mais le système de types garantit l'envoi des méthodes appropriées.
public interface Ball {
public <T> T accept(Visitor<T> visitor);
}
public class Baseball implements Ball {
@Override
public <T> T accept(Visitor<T> visitor) {
return visitor.visit(this);
}
}
Enfin, un peu de code pouvant rassembler tout cela:
Ball baseball = new Baseball();
Ball football = new Football();
List<BallUserInterface<? extends Ball>> uiList = new ArrayList<>();
uiList.add(BallUserInterfaceFactory.getUserInterface(baseball));
uiList.add(BallUserInterfaceFactory.getUserInterface(football));
for (BallUserInterface<? extends Ball> ui : uiList) {
System.out.println(ui);
}
// Outputs:
// ui.BaseballUserInterface@37e247e2
// ui.FootballUserInterface@1f2f0ce9
public class BaseballUserInterface extends BallUserInterface<Baseball> {
public BaseballUserInterface(Baseball ball) {
super(ball);
}
}
Vous utilisez BallUserInterface à la suite de la méthode d'usine. Donc, on peut cacher quelle boule de béton est utilisée:
public class BallUserInterfaceFactory {
public static BallUserInterface<?> getUserInterface(Ball ball) {
if(ball instanceof Baseball){
return getBaseballUserInterface((Baseball)ball);
}
return null;
}
private static BaseballUserInterface getBaseballUserInterface(Baseball ball){
return new BaseballUserInterface(ball);
}
}
Si le client est intéressé par le type de la balle, vous devez proposer une méthode d'usine avec la balle en béton comme paramètre:
public static BaseballUserInterface getUserInterface(Baseball ball){
return new BaseballUserInterface(ball);
}