2015-08-07 3 views
10

Ich versuche, eine Klasse zu machen, die nur eines von zwei Objekten enthalten kann, und ich möchte dies mit Generika tun. Hier ist die Idee:Behandlungstyp Löschen in Konstruktoren mit Generika

public class Union<A, B> { 

    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... 

} 

Natürlich wird dies allerdings nicht, da arbeiten aufgrund Typ Löschung die Konstrukteure haben die gleiche Art Signatur. Ich stelle fest, dass eine Lösung darin besteht, einen einzelnen Konstruktor zu haben, der beides akzeptiert, aber ich möchte, dass einer der Werte leer ist, daher scheint es eleganter zu sein, Einzelparameterkonstruktoren zu haben.

// 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; 
} 

Gibt es eine elegante Lösung?


Edit 1: Ich bin mit Java 6.

Edit 2: Der Grund, warum ich dies machen wollen, weil ich eine Methode, die kann einen von zwei Typen zurückgeben. Ich machte eine konkrete Version ohne Generika, aber ich fragte mich, ob ich es generisch machen könnte. Ja, mir ist klar, dass eine Methode mit zwei verschiedenen Rückgabetypen das eigentliche Problem ist, aber ich war immer noch neugierig, ob es einen guten Weg dafür gibt.

Ich denke durron597's answer am besten ist, weil es darauf hinweist, dass Union<Foo, Bar> und Union<Bar, Foo> sollten die gleichen handeln, aber sie nicht (das ist der Hauptgrund, warum ich dies zu stoppen entschieden verfolgt). Das ist ein viel hässlicheres Problem als der "hässliche" Konstruktor.

Für das, was es wert ist, ich denke, die beste Option ist wahrscheinlich diese abstrakte machen (weil Schnittstellen nicht Sichtbarkeit diktieren kann) und machen die isA und getA Sachen protected, und dann in der implementierenden Klasse haben eine bessere genannten Methoden die vermeiden <A, B>! = <B, A> Problem. Ich werde meine eigene Antwort mit mehr Details hinzufügen.

Finale edit: Für das, was es wert ist, habe ich beschlossen, dass die Verwendung von statischen Methoden als Pseudo-Konstrukteure (public static Union<A, B> fromA(A a) und public static Union<A, B> fromB(B b)) ist der beste Ansatz (zusammen mit der realen Konstruktor privat machen). Union<A, B> und Union<B, A> würde nie realistisch miteinander verglichen werden, wenn es nur als Rückgabewert verwendet wird.

Ein anderes bearbeitet, 6 Monate out: Ich kann wirklich nicht glauben, wie naiv ich war, als ich diese gefragt, sind statische Factory-Methoden so offensichtlich die absolute richtige Wahl und eindeutig ein Kinderspiel .

All das beiseite habe ich Functional Java gefunden, um sehr faszinierend zu sein. Ich habe es noch nicht benutzt, aber ich fand das Either beim googeln von 'java disjunct union', es ist genau das, wonach ich gesucht habe. Der Nachteil ist, dass Functional Java nur für Java 7 und 8 gedacht ist, aber glücklicherweise verwendete das Projekt, an dem ich gerade arbeite, Java 8.

+11

Sie können Ihren "hässlichen Lösung" -Konstruktor hinter Fabrikmethoden verstecken. – khelwood

+0

sollten Sie hier die Schnittstelle verwenden – HuStmpHrrr

+1

Statische Fabriken sind der Weg zu gehen im Allgemeinen. Wenn Sie jedoch zwei Felder haben, die sich gegenseitig ausschließen, ist das immer verdächtig. – biziclop

Antwort

5

Es macht keinen Sinn, dies zu tun. Wann würde das jemals Sinn machen? Zum Beispiel (unter der Annahme, für den Augenblick, Ihr ursprünglicher Code arbeitete):

Union<String, Integer> union = new Union("Hello"); 
// stuff 
if (union.isA()) { ... 

Aber wenn du getan hast, statt:

Union<Integer, String> union = new Union("Hello"); 
// stuff 
if (union.isA()) { ... 

würde anderes Verhalten, obwohl die Klassen Diese hat und die Daten sind gleich. Ihr Konzept von isA und isB sind im Grunde "links vs rechts" - es ist wichtiger, welche links oder rechts ist, als welche ist eine Zeichenfolge vs welche eine Ganzzahl ist. Mit anderen Worten, Union<String, Integer> ist sehr verschieden von Union<Integer, String>, was wahrscheinlich nicht das ist, was Sie wollen.

Überlegen Sie, was passieren würde, wenn wir zum Beispiel hatte, sagen:

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

Die Tatsache, dass etwas, ein A spielt keine Rolle, es sei denn, die Sie interessieren, ob es ein Linker oder ein Recht, in In diesem Fall solltest du es so nennen.


Wenn diese Diskussion nicht über vs rechts nach links ist, dann ist die einzige Sache, die Ihre speziellen Typen mit ankommt, wenn die Klasse zu schaffen. Es wäre sinnvoller, einfach eine Schnittstelle zu haben;

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

Sie auch das „ist“ Methode in einer abstrakten Klasse tun könnte:

public abstract class AbstractUnion<A, B> { 
    public boolean isA() { return getB() == null; } 
    public boolean isB() { return getA() == null; } 
} 

Und dann, wenn Sie tatsächlich die Klasse instanziiert, werden Sie bestimmte Typen sowieso verwenden ...

public UnionImpl extends AbstractUnion<String, Integer> { 
    private String strValue; 
    private int intValue 

    public UnionImpl(String str) { 
    this.strValue = str; 
    this.intValue = null; 
    } 

    // etc. 
} 

Dann, wenn Sie tatsächlich Ihre Implementierungstypen gewählt haben, werden Sie wirklich wissen, was Sie bekommen.


Abgesehen: wenn nach allen oben genannten Lesen, auch dann noch, wie Sie in Ihrer ersten Frage beschreiben wollen, der richtige Weg, es zu tun mit statischen Factory-Methoden mit einem privaten Konstruktor wie beschrieben @JoseAntoniaDuraOlmos's answer here. Ich hoffe jedoch, dass Sie darüber nachdenken, was Sie eigentlich in einem echten Anwendungsfall für Ihre Klasse benötigen.

+1

Sehr guter Punkt! Union sollte sich wie Union verhalten

+0

Guten Ruf über 'A, B' und' B, A', ich bin mir nicht sicher, der beste Ansatz, um damit umzugehen. 'ist (Klasse clazz)' möglicherweise? Scheint noch hässlicher ... –

+0

Das einzige Problem ist, Ihre Skizze bringt keine Verbesserung. Ihre "Union " unterscheidet zwischen dem linken und dem rechten Typ-Argument so radikal wie OPs. Der Typ von 'getA()' ist 'A', von dem statisch bekannt ist, dass er der Wert des linken Typparameters ist. Wenn man 'isA()' fragt, fragt man nicht nach dem _Typ_ der Instanz, sondern nur nach seinem_State_: ob es erlaubt ist, 'getA()' oder 'getB()' aufzurufen, und beide Typen sind vor dem Ausführen behoben das Programm. –

2

ich einen privaten Konstruktor verwenden würde und 2 statische Schöpfer

public class Union<A, B> { 

     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 <A, B> Union<A, B> unionFromA(A a) { 
      Union<A,B> u = new Union(a, null); 
      return u; 
     } 

     public static <A, B> Union<A, B> unionFromB(B b) { 
      Union<A,B> u = new Union(null, b); 
      return u; 
     } 
    ... 
} 
+1

Factory-Methoden überprüfen nicht, ob der Parameter nicht null ist. –

+1

@JoseAntonioDuraOlmos: Es war nicht im ersten Konstruktor Beispiel, aber es ist einfach zu überprüfen. –

+0

Es stimmt, die Frage war in dieser Hinsicht unklar; mach es früher und später noch einmal. –

2

Wenn Sie einen Konstruktor dann höchstwahrscheinlich verwenden müssen, gibt es keine elegante Lösung.

Mit Factory-Methoden können Sie eine elegante Lösung haben, die für a und b final erhält.
Die Factory-Methoden verwenden den "hässlichen" Konstruktor, aber das ist in Ordnung, da es Teil der Implementierung ist. Die öffentliche Schnittstelle behält alle Ihre Anforderungen bei, um von Konstruktoren zu Factory-Methoden zu wechseln.

Dies wird mit der Absicht Union<A,B> machen mit Union<B,A> gemäß durron597's answer austauschbar gemacht.
Das ist nicht vollständig möglich, wie ich später mit einem Beispiel zeigen werde, aber wir können ziemlich nah kommen.

public class Union<A, B> { 

    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 <A, B> Union<A, B> valueOfA(A a) { 
     if (a == null) { 
      throw new IllegalArgumentException(); 
     } 
     Union<A, B> res = new Union<>(a, null); 
     return res; 
    } 

    public static <A, B> Union<A, B> valueOfB(B b) { 
     if (b == null) { 
      throw new IllegalArgumentException(); 
     } 
     Union<A, B> 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> C get(Class<C> 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; 
    } 
} 

Hier sind Beispiele dafür, wie nicht zu verwenden und wie es zu benutzen.
zeigt die Begrenzung dieser Lösung. Der Compiler kann keinen falschen Parameter erkennen, der für eine Methode verwendet wird, die eine Union mit einem String akzeptieren soll. Wir müssen auf die Überprüfung des Laufzeittyps zurückgreifen.

public class Test { 

    public static void main(String[] args) { 
     Union<String, Integer> alfa = Union.valueOfA("Hello"); 
     Union<Integer, String> beta = Union.valueOfB("Hello"); 
     Union<HashMap, String> gamma = Union.valueOfB("Hello"); 
     Union<HashMap, Integer> delta = Union.valueOfB(13); 
     // Union<A,B> compared do Union<B,A>. 
     // 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<A,B> compared do Union<C,A>. 
     // Prints true because both unions contain equal objects 
     System.out.println(alfa.equals(gamma)); 

     // Union<A,B> compared to Union<C,D> 
     // 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 compiler 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 compiler to generate an error for this line. 
     useUnionAsParm2(delta); 
    } 

    /** 
    * Prints a string contained in an Union. 
    * 
    * This is an example of how not to do it. 
    * 
    * @param parm Union containing a String 
    */ 
    public static void useUnionAsParm(Union<String, Integer> parm) { 
     System.out.println(parm.get(String.class)); 
    } 

    /** 
    * Prints a string contained in an Union. Correct example. 
    * 
    * @param parm Union containing a String 
    */ 
    public static void useUnionAsParm2(Union<? extends Object, ? extends Object> parm) { 
     System.out.println(parm.get(String.class)); 
    } 

} 
1

"Union" ist das falsche Wort hier. Wir sprechen nicht über die Vereinigung zweier Typen, die alle Objekte in beiden Typen einschließen, möglicherweise mit Überlappung.

Diese Datenstruktur ähnelt eher einem Tupel mit einem zusätzlichen Index, der auf ein signifikantes Element zeigt. Ein besseres Wort dafür ist wahrscheinlich "Option". In der Tat ist ein besonderer Fall davon.

So könnte ich es Entwurf so

interface Opt2<T0,T1> 

    int ordinal(); // 0 or 1 
    Object value(); 

    default boolean is0(){ return ordinal()==0; } 

    default T0 get0(){ if(is0()) return (T0)value(); else throw ... } 

    static <T0,T1> Opt2<T0,T1> of0(T0 value){ ... } 
0

Als durron597's answer weist darauf hin, Union<Foo, Bar> und Union<Bar, Foo> sollen, konzeptionell gesehen, gleich verhalten, aber sehr unterschiedlich verhalten. Es spielt keine Rolle, welches A ist und welches B ist, es spielt eine Rolle, welcher welcher Typ ist.

Hier ist, was ich denke, kann am besten sein,

// 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<A, B> { 

    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; 
    } 
} 

Und die Umsetzung. Wo dies verwendet wird (außer com.company.utils), können nur die Methoden mit eindeutigen Namen gefunden werden.

package com.company.domain; 

import com.company.utils.Union; 

public class FooOrBar extends Union<Foo, Bar> { 

    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(); } 

} 

Eine weitere Idee kann ein Map<Class<?>, ?> oder etwas, oder zumindest die Werte zu halten. Ich weiß es nicht. All dieser Code stinkt. Es wurde aus einer schlecht entworfenen Methode geboren, die mehrere Rückgabetypen benötigte.

+0

Ja, diese 'FooOrBar' Klasse tötet Ihre ursprüngliche Idee vollständig. Aber ich sehe eigentlich nichts falsch daran, die Positionskonzepte des linken und rechten Typs zu verwenden. Ihre Methode deklariert die Rückgabe einer 'Union 'und Sie werden genau wissen, was übrig ist und welcher Typ richtig ist. Denken Sie daran, dass Sie das statische System nicht in ein dynamisches System umwandeln können, und statisch ist der Unterschied zwischen links und rechts sehr klar. Sie können diese Reihenfolge möglicherweise nicht entfernen. –

+0

Erinnern Sie sich an die Grundidee Ihres ursprünglichen Konzepts: Sie lösen das Problem einer unmöglichen "Methode mit zwei Rückgabetypen", indem Sie sie in eine Methode umwandeln, die einen Typ zurückgibt, deren _Statraum_ jedoch in "links" und "rechts" zerfällt Jedem von diesen ist ein statischer Typ zugeordnet. Dies ist genau das, wofür sich die "Entweder" -Monade perfekt eignet (und Sie sind auf dem Weg, sie neu zu erfinden). –

+0

@Marko Was ist eine "Entweder" Monade? –