2015-08-07 24 views
10

Sto provando a creare una classe che possa contenere solo uno di due oggetti e voglio farlo con i generici. Ecco l'idea:Gestione del tipo di cancellazione nei costruttori con generici

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

} 

Naturalmente questo non funziona, però, perché a causa della cancellazione di tipo costruttori hanno la stessa firma tipo. Mi rendo conto che una soluzione è di avere un singolo costruttore che prenda entrambi, ma voglio che uno dei valori sia vuoto, quindi sembra più elegante avere costruttori a parametro singolo.

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

C'è una soluzione elegante a questo?


Edit 1: Sto usando Java 6.

Edit 2: Il motivo che voglio fare questo è perché ho un metodo che può restituire uno di due tipi. Ho fatto una versione concreta senza farmaci generici, ma mi chiedevo se potevo renderla generica. Sì, mi rendo conto che avere un metodo con due diversi tipi di ritorno è il vero problema, ma ero comunque curioso di sapere se c'era un buon modo per farlo.

penso durron597's answer è migliore, perché sottolinea che Union<Foo, Bar> e Union<Bar, Foo> dovrebbe agire lo stesso ma non lo fanno (che è il motivo principale per cui ho deciso di smettere di perseguire questo). Questo è un problema molto più brutto rispetto al costruttore "brutto".

Per quello che vale Credo che l'opzione migliore è probabilmente rendere questo abstract (perché le interfacce non possono dettare la visibilità) e rendere il isA e getA roba protected, e poi nella classe che implementa avere metodi più chiamati per evitare il <A, B>! = <B, A> problema. Aggiungerò la mia risposta con maggiori dettagli.

montaggio finale: Per quel che vale, ho deciso che l'utilizzo di metodi statici, come costruttori di pseudo (public static Union<A, B> fromA(A a) e public static Union<A, B> fromB(B b)) è l'approccio migliore (insieme a fare il costruttore vero e privato). Union<A, B> e Union<B, A> non sarebbero mai realisticamente confrontati l'uno con l'altro quando è solo utilizzato come valore di ritorno.

Un'altra modifica, 6 mesi fuori: Davvero non riesco a credere quanto ingenuo ero quando ho chiesto questo, metodi factory statici sono così ovviamente la corretta scelta assoluta e chiaramente un gioco da ragazzi .

Tutto questo a parte ho trovato Functional Java molto intrigante. Non l'ho ancora usato, ma ho trovato questo Either quando ho cercato su Google 'java disjunct union', è esattamente quello che stavo cercando. Lo svantaggio è che Java funzionale è solo per Java 7 e 8, ma fortunatamente il progetto su cui sto lavorando ora utilizza Java 8.

+11

È possibile nascondere il costruttore della "brutta soluzione" dietro i metodi di fabbrica. – khelwood

+0

dovresti usare l'interfaccia qui – HuStmpHrrr

+1

Le fabbriche statiche sono la strada da percorrere in generale. Anche se hai due campi che si escludono a vicenda, è sempre sospetto. – biziclop

risposta

5

Non ha proprio senso farlo. Quando mai avrebbe avuto senso? Per esempio (assumendo, per il momento, il codice iniziale lavorato):

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

Ma se l'avete fatto, invece:

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

Ciò avrebbe un comportamento diverso, anche se le classi e la i dati sono gli stessi Il tuo concetto di isA e isB sono fondamentalmente "left vs right" - è più importante quale è left o right rispetto a quale one è uno String vs quale è un Integer. In altre parole, Union<String, Integer> è molto diverso da Union<Integer, String>, che probabilmente non è quello che desideri.

considerare che cosa accadrebbe se, per esempio, avevamo, dire:

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

Il fatto di qualcosa di essere un A non importa, a meno che non vi preoccupate se si tratta di una sinistra o destra, in nel qual caso dovresti chiamarlo così.


Se questa discussione non riguarda sinistra e destra, l'unica cosa che conta è utilizzare i tipi specifici durante la creazione della classe. Avrebbe più senso avere semplicemente un'interfaccia;

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

Si potrebbe anche fare la "è" il metodo in una classe astratta:

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

E poi, quando in realtà un'istanza della classe, si utilizzerà tipi specifici in ogni caso ...

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

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

    // etc. 
} 

Poi, quando hai effettivamente scelto i tipi di implementazione, ci troveremo a sapere cosa si sta ottenendo.


parte: se, dopo aver letto tutto quanto sopra, si vuole ancora fare questo il modo di descrivere nella sua domanda iniziale, il modo giusto per farlo è con metodi factory statici con un costruttore privato, come descritto @JoseAntoniaDuraOlmos's answer here. Tuttavia, spero che tu pensi ulteriormente a ciò che effettivamente hai bisogno che la tua classe faccia in un vero caso d'uso.

+1

Ottimo punto! Union dovrebbe comportarsi come Union

+0

Buona chiamata su 'A, B' e' B, A', non sono sicuro che l'approccio migliore per gestirlo. 'è (Classe clazz)' eventualmente? Sembra ancora più brutto ... –

+0

L'unico problema è che il tuo schizzo non porta alcun miglioramento. Il tuo 'Unione ' discrimina in modo radicale tra l'argomento di tipo sinistro e destro come OP. Il tipo di 'getA()' è 'A', che è noto staticamente come valore del parametro di tipo sinistro. Chiedere 'isA()' non fa domande sul _type_ dell'istanza, ma solo sul suo _state_: se è legale chiamare 'getA()' o 'getB()', ed entrambi i tipi sono corretti prima ancora di essere in esecuzione il programma. –

2

avrei usato un costruttore privato e 2 creatori statiche

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

I metodi di fabbrica non verificano che il parametro non sia nullo. –

+1

@JoseAntonioDuraOlmos: non era nell'esempio iniziale del costruttore, ma è facile da controllare. –

+0

Vero, la questione non era chiara al riguardo; facendolo prima e dopo. –

2

Se è necessario utilizzare un costruttore, allora molto probabilmente non v'è una soluzione elegante.

Con i metodi di fabbrica si può avere una soluzione elegante che preserva finale per aeb.
I metodi factory useranno il costruttore "brutto" ma va bene dato che fa parte dell'implementazione. L'interfaccia pubblica mantiene tutte le tue esigenze salvo il passaggio dai costruttori ai metodi di fabbrica.

Questo è fatto con l'intento di rendere Union<A,B> intercambiabile con Union<B,A> come da durron597's answer.
Questo non è interamente possibile, come mostrerò più avanti con un esempio, ma possiamo avvicinarci molto.

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

Ecco alcuni esempi di come utilizzare e come non utilizzarlo.
useUnionAsParm2 mostra la limitazione di questa soluzione. Il compilatore non può rilevare un parametro errato utilizzato per un metodo che intende accettare qualsiasi unione contenente una stringa. Dobbiamo ricorrere al controllo del tipo di runtime.

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

"Unione" è la parola sbagliata qui. Non stiamo parlando dell'unione di due tipi, che includerà tutti gli oggetti in entrambi i tipi, eventualmente con sovrapposizioni.

Questa struttura dati è più simile a una tupla, con un indice aggiuntivo che punta a un elemento significativo. Una parola migliore è probabilmente "opzione". In effetti, java.util.Optional è un caso speciale di esso.

Quindi, potrei progettare come questo

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

Come durron597's answer punti fuori, Union<Foo, Bar> e Union<Bar, Foo> dovrebbe, concettualmente parlando, si comportano allo stesso, ma si comportano in modo molto diverso. Non importa quale sia A e che sia B, importa quale sia il tipo.

Ecco cosa penso possa essere migliore,

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

e l'implementazione. Dove viene utilizzato (ad eccezione di com.company.utils), è possibile trovare solo i metodi con nomi non ambigui.

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

} 

Un'altra idea può essere un qualcosa di Map<Class<?>, ?> o, o almeno per contenere i valori. Non lo so. Tutto questo codice puzza. È nato da un metodo mal progettato che aveva bisogno di più tipi di ritorno.

+0

Sì, questa classe 'FooOrBar' elimina completamente la tua idea originale. Ma in realtà non vedo nulla di sbagliato nell'usare i concetti posizionali di tipo sinistro e destro. Il tuo metodo dichiarerà di restituire un 'Union 'e saprai perfettamente che cosa è rimasto e quale è il tipo giusto. Ricorda che non puoi trasformare il sistema di tipo statico in uno dinamico, e staticamente la differenza tra sinistra e destra è molto chiara. Non è possibile rimuovere questo ordine. –

+0

Ricorda l'idea chiave dietro il tuo concetto originale: stai risolvendo il problema di un "metodo con due tipi di ritorno" impossibile trasformandolo in un metodo che restituisce un tipo, ma il cui spazio _stato_ segrega in stati "sinistro" e "giusto" , ognuno di questi ha un tipo statico assegnato ad esso. Questo è esattamente ciò per cui la monade "Either" è perfetta (e tu stai per reinventarlo). –

+0

@Marko che cos'è una monade 'Either'? –