使用泛型处理构造函数中的类型擦除

Handling type erasure in constructors with generics

我正在尝试制作一个只能容纳两个对象之一的 class,我想用泛型来实现。这是想法:

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: I am using Java 6.

Edit 2: The reason I want to make this is because I have a method that can return one of two types. I made a concrete version with no generics but was wondering if I could make it generic. Yes, I realize that having a method with two different return types is the real issue at hand, but I was still curious if there was a good way to do this.

I think is best because it points out that Union<Foo, Bar> and Union<Bar, Foo> should act the same but they don't (which is the main reason why I decided to stop pursuing this). This is a much uglier issue than the "ugly" constructor.

For what it's worth I think the best option is probably make this abstract (because interfaces can't dictate visibility) and make the isA and getA stuff protected, and then in the implementing class have better named methods to avoid the <A, B> != <B, A> issue. I will add my own answer with more details.

Final edit: For what it's worth, I decided that using static methods as pseudo constructors (public static Union<A, B> fromA(A a) and public static Union<A, B> fromB(B b)) is the best approach (along with making the real constructor private). Union<A, B> and Union<B, A> would never realistically be compared to each other when it's just being used as a return value.

Another edit, 6 months out: I really can't believe how naive I was when I asked this, static factory methods are so obviously the absolute correct choice and clearly a no-brainer.

All that aside I have found Functional Java to be very intriguing. I haven't used it yet but I did find this Either when googling 'java disjunct union', it's exactly what I was looking for. The downside though it that Functional Java is only for Java 7 and 8, but luckily the project I am now working on used Java 8.

我会使用私有构造函数和 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;
        }
    ...
}

如果您必须使用构造函数,那么很可能没有完美的解决方案。

使用工厂方法,您可以获得一个优雅的解决方案,为 a 和 b 保留 final。
工厂方法将使用 "ugly" 构造函数,但这没关系,因为它是实现的一部分。 public 接口保留了从构造函数到工厂方法的所有要求。

这是为了让 Union<A,B> 可以根据 Union<B,A> 互换。
这并非完全可能,因为我稍后将通过示例展示,但我们可以非常接近。

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 显示了此解决方案的局限性。编译器无法检测到用于接受任何包含字符串的联合的方法的错误参数。我们必须求助于运行时类型检查。

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

}

这样做真的没有任何意义。这什么时候有意义?例如(假设,目前,您的初始代码有效):

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

但是如果你这样做了,相反:

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

这会有 不同的行为,即使 classes 和数据相同。您对 isAisB 的概念基本上是 "left vs right" - 哪个是左与右比哪个是字符串与哪个是整数更重要。换句话说,Union<String, Integer>Union<Integer, String>有很大的不同,这可能不是你想要的。

考虑一下如果我们假设会发生什么:

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

某物是 A 的事实并不重要,除非你关心它是左派还是右派,在这种情况下你应该这样称呼它。


如果此讨论不是关于左与右的,那么唯一重要的是在创建 class 时使用您的特定类型。简单地拥有一个接口会更有意义;

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

您甚至可以在摘要中执行 "is" 方法 class:

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

然后,当您实际实例化 class 时,无论如何您将使用特定类型...

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

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

  // etc.
}

然后,当您真正选择了实施类型时,您就会真正知道自己得到了什么。


另外:如果在阅读了以上所有内容之后,您仍然想按照最初问题中描述的方式执行此操作,那么正确的方法是使用带有私有构造函数的静态工厂方法,如所述。但是,我希望您进一步考虑在实际用例中您实际需要 class 做什么。

"Union" 是这里的错误词。我们不是在谈论两种类型的联合,这将包括两种类型中的所有对象,可能有重叠。

这个数据结构更像是一个元组,有一个指向一个重要元素的额外索引。一个更好的词可能是 "option"。其实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){ ... }

正如 指出的那样,从概念上讲,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<?>, ?> 之类的,或者至少要保持这些值。我不知道。所有这些代码都很臭。它诞生于一个设计糟糕的方法,需要多个 return 类型。