Стирание типа обработки в конструкторах с дженериками

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

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

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


Изменить 1: Я использую Java 6.

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

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

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

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

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

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

Ответ 1

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

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

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

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

Это будет другое поведение, хотя классы и данные будут одинаковыми. Ваша концепция isA и isB в основном "левая и правая" - это более важно, какой из них остается справа от того, какой из них представляет собой String vs, который является целым. Другими словами, 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();
}

Вы можете даже сделать метод "is" в абстрактном классе:

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

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


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

Ответ 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;
        }
    ...
}

Ответ 3

Если вы должны использовать конструктор, то, скорее всего, нет элегантного решения.

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

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

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

}

Ответ 4

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

Эта структура данных больше похожа на кортеж, с дополнительным индексом, указывающим на один значительный элемент. Лучшее слово для него, вероятно, "вариант". Фактически, 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){ ... }

Ответ 5

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

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

// I include this because if it 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<?>, ?> или что-то еще, или, по крайней мере, для хранения значений. Я не знаю. Весь этот код воняет. Он был создан из плохо разработанного метода, который нуждался в нескольких типах возврата.