Traitement de l’effacement de types dans les constructeurs avec des génériques

J’essaie de créer un cours ne pouvant contenir qu’un seul des deux objects et je souhaite le faire avec des génériques. Voici l’idée:

public class Union { private final A a; private final B b; public Union(A a) { this.a = a; b = null; } public Union(B b) { a = null; this.b = b; } // isA, isB, getA, getB... } 

Bien sûr, cela ne fonctionnera pas car, en raison de l’effacement des types, les constructeurs ont la même signature de type. Je me rends compte qu’une solution consiste à avoir un seul constructeur prenant les deux, mais je veux qu’une des valeurs soit vide, il semble donc plus élégant d’avoir des constructeurs à un seul paramètre.

 // Ugly solution public Union(A a, B b) { if (!(a == null ^ b == null)) { throw new IllegalArgumentException("One must exist, one must be null!"); } this.a = a; this.b = b; } 

Y a-t-il une solution élégante à cela?


Edit 1: J’utilise Java 6.

Edit 2: La raison pour laquelle je veux faire cela est parce que j’ai une méthode qui peut renvoyer l’un des deux types. J’ai réalisé une version concrète sans générique, mais je me demandais si je pouvais la rendre générique. Oui, je me rends bien compte que le vrai problème est d’avoir une méthode avec deux types de retour différents, mais j’étais toujours curieux de savoir s’il existait un bon moyen de le faire.

Je pense que la réponse de durron597 est la meilleure, car elle indique que Union et Union devraient agir de la même manière mais qu’elles ne le font pas (ce qui est la principale raison pour laquelle j’ai décidé de ne plus poursuivre dans cette voie). C’est un problème beaucoup plus laid que le constructeur “laid”.

Pour ce que cela vaut, je pense que la meilleure option est probablement de rendre ce résumé abstrait (car les interfaces ne peuvent pas dicter la visibilité) et de getA éléments getA et getA , puis dans la classe d’implémentation, de meilleures méthodes nommées pour éviter les ! = problème. Je vais append ma propre réponse avec plus de détails.

Montage final: Pour ce que ça vaut, j’ai décidé d’utiliser des méthodes statiques comme pseudo constructeurs ( public static Union fromA(A a) et public static Union fromB(B b) ) est la meilleure approche. (avec rendre le constructeur réel privé). Union et Union ne seraient jamais comparées de manière réaliste si elles étaient simplement utilisées comme valeur de retour.

Une autre modification, six mois plus tard : je ne peux vraiment pas croire à quel point j’étais naïve lorsque j’ai posé cette question, les méthodes d’usine statiques sont donc à l’évidence le choix tout à fait correct et une évidence.

Tout cela mis à part, j’ai trouvé Functional Java très insortingguant. Je ne l’ai pas encore utilisé, mais j’ai trouvé ce produit. Either quand je cherchais ‘java disjunct union’ dans Google, c’est exactement ce que je cherchais. L’inconvénient est que Functional Java ne concerne que Java 7 et 8, mais heureusement, le projet sur lequel je travaille maintenant a utilisé Java 8.

Cela n’a aucun sens de faire cela. Quand cela aurait-il un sens? Par exemple (en supposant que votre code initial fonctionne pour le moment):

 Union union = new Union("Hello"); // stuff if (union.isA()) { ... 

Mais si vous le faisiez plutôt:

 Union union = new Union("Hello"); // stuff if (union.isA()) { ... 

Cela aurait un comportement différent , même si les classes et les données sont les mêmes. Votre concept de isB et isB est fondamentalement «gauche vs droite» – il est plus important de choisir entre gauche et droite que celui qui est une chaîne par rapport à celui qui est un entier. En d’autres termes, Union est très différente de Union , ce qui n’est probablement pas ce que vous voulez.

Pensez à ce qui arriverait si, par exemple, nous avions:

 List> myList; for(Union element : myList) { if(element.isA()) { // What does this even mean? 

Le fait que quelque chose soit un A n’a pas d’importance, sauf si vous vous souciez de savoir si c’est une gauche ou une droite, auquel cas vous devriez l’appeler ainsi.


Si cette discussion ne traite pas de gauche contre droite, la seule chose qui compte est d’utiliser vos types spécifiques lors de la création de la classe. Il serait plus logique d’avoir simplement une interface;

 public interface Union { boolean isA(); boolean isB(); A getA(); B getB(); } 

Vous pouvez même faire la méthode “is” dans une classe abstraite:

 public abstract class AbstractUnion { public boolean isA() { return getB() == null; } public boolean isB() { return getA() == null; } } 

Et puis, quand vous instanciez réellement la classe, vous utiliserez quand même des types spécifiques …

 public UnionImpl extends AbstractUnion { private Ssortingng strValue; private int intValue public UnionImpl(Ssortingng str) { this.strValue = str; this.intValue = null; } // etc. } 

Ensuite, lorsque vous aurez choisi vos types d’implémentation, vous saurez réellement ce que vous obtenez.


De plus, si, après avoir lu tout ce qui précède, vous souhaitez toujours procéder comme vous le décrivez dans votre question initiale, la bonne façon de procéder consiste à utiliser des méthodes fabriques statiques avec un constructeur privé, comme décrit ici par la réponse de JoseAntoniaDuraOlmos . Cependant, j’espère que vous approfondirez ce que vous avez réellement besoin de faire en classe.

J’utiliserais un constructeur privé et 2 créateurs statiques

 public class Union { private final A a; private final B b; // private constructor to force use or creation methods private Union(A a, B b) { if ((a == null) && (b == null)) { // ensure both cannot be null throw new IllegalArgumentException(); } this.a = a; this.b = b; } public static  Union unionFromA(A a) { Union u = new Union(a, null); return u; } public static  Union unionFromB(B b) { Union u = new Union(null, b); return u; } ... } 

Si vous devez utiliser un constructeur, il n’y a probablement aucune solution élégante.

Avec les méthodes d’usine, vous pouvez avoir une solution élégante qui conserve final pour a et b.
Les méthodes d’usine utiliseront le constructeur “laid” mais c’est correct car cela fait partie de l’implémentation. L’interface publique conserve toutes vos exigences, sauf pour passer des constructeurs aux méthodes d’usine.

Cela est fait dans l’intention de rendre l’ Union interchangeable avec l’ Union conformément à la réponse de durron597 .
Ce n’est pas tout à fait possible, comme je le montrerai plus tard avec un exemple, mais nous pouvons être assez proches.

 public class Union { private final A a; private final B b; private Union(A a, B b) { assert a == null ^ b == null; this.a = a; this.b = b; } public static  Union valueOfA(A a) { if (a == null) { throw new IllegalArgumentException(); } Union res = new Union<>(a, null); return res; } public static  Union valueOfB(B b) { if (b == null) { throw new IllegalArgumentException(); } Union res = new Union<>(null, b); return res; } public boolean isClass(Class clazz) { return a != null ? clazz.isInstance(a) : clazz.isInstance(b); } // The casts are always type safe. @SuppressWarnings("unchecked") public  C get(Class clazz) { if (a != null && clazz.isInstance(a)) { return (C)a; } if (b != null && clazz.isInstance(b)) { return (C)b; } throw new IllegalStateException("This Union does not contain an object of class " + clazz); } @Override public boolean equals(Object o) { if (!(o instanceof Union)) { return false; } Union union = (Union) o; Object parm = union.a != null ? union.a : union.b; return a != null ? a.equals(parm) : b.equals(parm); } @Override public int hashCode() { int hash = 3; hash = 71 * hash + Objects.hashCode(this.a); hash = 71 * hash + Objects.hashCode(this.b); return hash; } } 

Voici des exemples d’utilisation et d’utilisation.
useUnionAsParm2 montre les limites de cette solution. Le compilateur ne peut pas détecter un paramètre incorrect utilisé pour une méthode destinée à accepter toute Union contenant une chaîne. Nous devons recourir à la vérification du type à l’exécution.

 public class Test { public static void main(Ssortingng[] args) { Union alfa = Union.valueOfA("Hello"); Union beta = Union.valueOfB("Hello"); Union gamma = Union.valueOfB("Hello"); Union delta = Union.valueOfB( 13 ); // Union compared do Union. // Prints true because both unions contain equal objects System.out.println(alfa.equals(beta)); // Prints false since "Hello" is not an Union. System.out.println(alfa.equals("Hello")); // Union compared do Union. // Prints true because both unions contain equal objects System.out.println(alfa.equals(gamma)); // Union compared to Union // Could print true if a type of one union inherited or implement a //type of the other union. In this case contained objects are not equal, so false. System.out.println(alfa.equals(delta)); useUnionAsParm(alfa); // Next two lines produce comstackr error //useUnionAsParm(beta); //useUnionAsParm(gamma); useUnionAsParm2(alfa); useUnionAsParm2(beta); useUnionAsParm2(gamma); // Will throw IllegalStateException // Would be nice if it was possible to declare useUnionAsParm2 in a way //that caused the comstackr to generate an error for this line. useUnionAsParm2(delta); } /** * Prints a ssortingng contained in an Union. * * This is an example of how not to do it. * * @param parm Union containing a Ssortingng */ public static void useUnionAsParm(Union parm) { System.out.println(parm.get(Ssortingng.class)); } /** * Prints a ssortingng contained in an Union. Correct example. * * @param parm Union containing a Ssortingng */ public static void useUnionAsParm2(Union parm) { System.out.println( parm.get(Ssortingng.class) ); } } 

“Union” est le mauvais mot ici. Nous ne parlons pas de l’union de deux types, qui incluront tous les objects de l’un ou l’autre type, éventuellement avec chevauchement.

Cette structure de données s’apparente davantage à un tuple, avec un index supplémentaire pointant vers un élément significatif. Un meilleur mot pour cela est probablement “option”. En fait, java.util.Optional est un cas particulier.

Donc, je pourrais le concevoir comme ça

 interface Opt2 int ordinal(); // 0 or 1 Object value(); default boolean is0(){ return ordinal()==0; } default T0 get0(){ if(is0()) return (T0)value(); else throw ... } static  Opt2 of0(T0 value){ ... } 

Comme le souligne la réponse de durron597 , Union et Union devraient, sur le plan conceptuel, se comporter de la même manière mais se comporter de manière très différente. Peu importe qui est A et qui est B , peu importe quel type.

Voici ce que je pense être le meilleur,

 // I include this because if it's not off in its own package away from where its // used these protected methods can still be called. Also I specifically use an // abstract class so I can make the methods protected so no one can call them. package com.company.utils; public abstract class Union { private A a; private B b; protected Union(A a, B b) { assert a == null ^ b == null: "Exactly one param must be null"; this.a = a; this.b = b; } // final methods to prevent over riding in the child and calling them there protected final boolean isA() { return a != null; } protected final boolean isB() { return b != null; } protected final A getA() { if (!isA()) { throw new IllegalStateException(); } return a; } protected final B getB() { if (!isB()) { throw new IllegalStateException(); } return b; } } 

Et la mise en œuvre. Lorsque cela est utilisé (à l’exception de com.company.utils ), seules les méthodes portant des noms non équivoques peuvent être trouvées.

 package com.company.domain; import com.company.utils.Union; public class FooOrBar extends Union { public FooOrBar(Foo foo) { super(foo, null); } public FooOrBar(Bar bar) { super(null, bar); } public boolean isFoo() { return isA(); } public boolean isBar() { return isB(); } public Foo getFoo() { return getA(); } public Bar getBar() { return getB(); } } 

Une autre idée peut être une Map, ?> Ou quelque chose du genre, ou du moins conserver les valeurs. Je ne sais pas. Tout ce code pue. Il est né d’une méthode mal conçue nécessitant plusieurs types de retour.