2015-08-07 2 views
10

Я пытаюсь создать класс, который может содержать только один из двух объектов, и я хочу сделать это с помощью дженериков. Вот эта идея:Удаление типа стирания в конструкторах с генераторами

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

} 

Конечно, это не сработает, потому что из-за стирания типа конструкторы имеют подпись того же типа. Я понимаю, что одно решение состоит в том, чтобы иметь один конструктор, который принимает оба, но я хочу, чтобы одно из значений было пустым, поэтому кажется более элегантным иметь конструкторы с одним параметром.

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

Есть ли элегантное решение?


Edit 1: Я использую Java 6.

Edit 2: Поэтому я хочу, чтобы сделать это, потому что у меня есть метод, который может возвращать один из двух типов. Я сделал конкретную версию без каких-либо дженериков, но задавался вопросом, могу ли я сделать ее обобщенной. Да, я понимаю, что наличие метода с двумя разными типами возврата является реальной проблемой, но мне все еще было любопытно, был ли хороший способ сделать это.

Я думаю durron597's answer лучше, потому что это указывает на то, что Union<Foo, Bar> и Union<Bar, Foo> должны действовать так же, но они этого не делают (что является основной причиной, почему я решил прекратить преследовать это). Это очень уродливая проблема, чем «уродливый» конструктор.

Для чего это стоит, я думаю, что лучший вариант, вероятно, сделать это аннотацию (потому что интерфейсы не могут диктовать видимость) и сделать isA и getA материалом protected, а затем в реализующем классе имеют более названные методы, чтобы избежать <A, B>! = <B, A> выпуск. Я добавлю свой собственный ответ с более подробной информацией.

Final редактирование: Для чего это стоит, я решил, что использование статических методов, как псевдо-конструкторы (public static Union<A, B> fromA(A a) и public static Union<A, B> fromB(B b)) является лучшим подходом (наряду с созданием реального конструктора частным). Union<A, B> и Union<B, A> никогда не сравнивались бы реалистично друг с другом, когда они просто используются как возвращаемое значение.

Другой редактировать, 6 месяцев в: Я действительно не могу поверить, насколько наивным я был, когда я спросил это, статические фабричные методы настолько очевидно, что абсолютное правильный выбор и ясно ежу понятно ,

Все, что в стороне, я нашел Functional Java, чтобы быть очень интригующим. Я еще не использовал его, но я нашел это Either, когда googling «java disjunct union», это именно то, что я искал. Недостатком является то, что функциональная Java предназначена только для Java 7 и 8, но, к счастью, проект теперь я работаю над используемой Java 8.

+11

Вы можете скрыть свой конструктор «уродливого решения» за заводскими методами. – khelwood

+0

Вы должны использовать интерфейс здесь – HuStmpHrrr

+1

Статические заводы - это путь в общем. Хотя, если у вас есть два поля, которые являются взаимоисключающими, это всегда подозрительно. – biziclop

ответ

5

Это не имеет никакого смысла для этого. Когда это когда-нибудь будет иметь смысл? Например (в предположении, на данный момент, ваш исходный код работал):

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

Но если вы сделали, вместо этого:

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

Это будет иметь различное поведение, даже несмотря на классы и данные одинаковы. Ваша концепция isA и isB в основном «левая и правая» - это более важно, какой из них слева и справа, чем тот, который является строкой, с которой является целым. Другими словами, Union<String, Integer> сильно отличается от Union<Integer, String>, что, вероятно, не то, что вы хотите.

Рассмотрим, что произойдет, если мы, к примеру, было, скажем:

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

Факт то, являющейся A не имеет значения, если вы не заботитесь о том, является ли это левый или правый, в в этом случае вы должны это назвать.


Если это обсуждение не касается левого и правого, то единственное, что имеет значение, это использование ваших конкретных типов при создании класса. Было бы разумнее просто иметь интерфейс;

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

Можно даже сделать «это» метод в абстрактном классе:

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

И тогда, когда вы на самом деле экземпляр класса, вы в любом случае использовать определенные типы ...

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

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

    // etc. 
} 

Затем, когда вы на самом деле выбрали ваши типы реализации, вы на самом деле знаете, что вы получаете.


В стороне: если, прочитав все вышесказанное, вы все еще хотите сделать это так, как вы описали в вашем первоначальном вопросе, правильный способ сделать это со статическими методами фабричных с частным конструктором, как описано @JoseAntoniaDuraOlmos's answer here. Тем не менее, я надеюсь, вы еще раз подумаете о том, что вам действительно нужно, чтобы ваш класс работал в реальном использовании.

+1

Очень хорошая точка! Union должен вести себя как Union

+0

Хороший вызов о 'A, B' и' B, A', я не уверен, что лучший подход к этому. 'есть (класс clazz)' возможно? Кажется еще более уродливым ... –

+0

Единственная проблема заключается в том, что ваш эскиз не приносит никаких улучшений. Ваш 'Union ' дискриминирует радикально между аргументом left и right type как OP. Тип 'getA()' - 'A', который статически известен как значение параметра левого типа. Запрашивая 'isA()' не спрашивает об _type_ экземпляра, но только о его _state_: является ли законным вызывать 'getA()' или 'getB()', и оба этих типа фиксируются до того, как они будут запущены программа. –

2

Я хотел бы использовать приватный конструктор и 2 статических создатели

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

Методы фабрики не проверяют, что параметр не равен нулю. –

+1

@JoseAntonioDuraOlmos: Это был не пример исходного конструктора, но его легко проверить. –

+0

Правда, вопрос был неясным в этом отношении; делая это одним способом раньше, а другой позже. –

2

Если необходимо использовать конструктор, то, скорее всего, существует не элегантное решение.

С фабричными методами вы можете иметь изящное решение, которое сохраняет финал для a и b.
Методы фабрики будут использовать «уродливый» конструктор, но это нормально, поскольку это часть реализации. Открытый интерфейс сохраняет все ваши требования, за исключением перехода от конструкторов к заводским методам.

Это сделано с целью сделать Union<A,B> взаимозаменяемым с Union<B,A> согласно durron597's answer.
Это нецелесообразно, так как я покажу вам пример, но мы можем приблизиться.

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

Вот примеры использования и того, как его не использовать.
useUnionAsParm2 показывает ограничение этого решения. Компилятор не может обнаружить неправильный параметр, используемый для метода, который предназначен для принятия любого Союза, содержащего String. Мы должны прибегнуть к проверке типа времени выполнения.

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

«Союз» - это неправильное слово здесь. Мы не говорим о объединении двух типов, которые будут включать все объекты в обоих типах, возможно, с перекрытием.

Эта структура данных больше похожа на кортеж, с дополнительным указателем, указывающим на один значительный элемент. Лучшее слово для него, вероятно, «вариант». Фактически, это java.util.Optional.

Таким образом, я мог бы разработать это так

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

Я использовал слово union, потому что он похож на идею объединения в C. Кроме того, я использую Java 6, поэтому 'java.util.Optional' не вариант (каламбур непреднамеренно и не жалеет). Но у меня есть guava, поэтому его версия доступна (на самом деле, я использую 'Необязательный ' вместо этого, но я хотел быть минималистичным в моем вопросе). –

+0

Принципиальное различие между C 'union' и вашим классом состоит в том, что C полностью небезопасен: вы можете в любое время интерпретировать значение как экземпляр любого из объявленных типов. Вы, однако, стремитесь к типичному решению (иначе вы просто возвращаете «Объект» и бросаете любой желаемый тип). –

0

Как durron597's answer указывает, Union<Foo, Bar> и Union<Bar, Foo> должны, концептуально говоря, ведут себя так же, но ведут себя очень по-разному. Не имеет значения, что такое A, а это B, это вопрос, какой тип.

Вот что я думаю, что может быть лучше,

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

И реализация. Там, где это используется (кроме com.company.utils), можно найти только методы с однозначными именами.

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

} 

Другая идея может быть Map<Class<?>, ?> или что-то, или, по крайней мере, для хранения значений. Я не знаю. Весь этот код воняет. Он был создан из плохо разработанного метода, который требовал нескольких типов возврата.

+0

Да, этот класс «FooOrBar» полностью убивает вашу оригинальную идею. Но я не вижу ничего плохого в использовании позиционных понятий левого и правого типов. Ваш метод объявит о возврате 'Union ', и вы прекрасно поймете, что осталось, и это правильный тип. Помните, что вы не можете превратить систему статического типа в динамическую, и статически разница между левым и правым очень понятна. Вы не можете удалить этот заказ. –

+0

Помните о ключевой идее вашей оригинальной концепции: вы решаете проблему невозможного «метода с двумя типами возврата», преобразуя его в метод, который возвращает один тип, но чье _state space_ разделяется на «левые» и «правильные» состояния , каждый из которых имеет назначенный ему статический тип. Это именно то, что монада «Либо» идеально подходит (и вы на своем пути, чтобы изобретать ее). –

+0

@ Марко, что такое монаха 'Либо '? –