【问题标题】:Handling type erasure in constructors with generics使用泛型处理构造函数中的类型擦除
【发布时间】:2026-02-21 01:05:01
【问题描述】:

我正在尝试创建一个只能容纳两个对象之一的类,我想用泛型来做到这一点。思路如下:

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's answer 是最好的,因为它指出Union&lt;Foo, Bar&gt;Union&lt;Bar, Foo&gt; 应该采取相同的行动,但他们不会(这是我决定停止追求这一点的主要原因)。 这是一个比“丑陋”构造函数更丑陋的问题。

对于它的价值,我认为最好的选择可能是使这个抽象(因为接口不能决定可见性)并制作 isAgetA 的东西 protected,然后在实现类中更好地命名避免&lt;A, B&gt; != &lt;B, A&gt; 问题的方法。我将添加我自己的答案并提供更多详细信息。

最终编辑:不管怎样,我决定使用静态方法作为伪构造函数(public static Union&lt;A, B&gt; fromA(A a)public static Union&lt;A, B&gt; fromB(B b))是最好的方法(同时将真正的构造函数设为私有)。 Union&lt;A, B&gt;Union&lt;B, A&gt; 在仅用作返回值时,永远不会真正相互比较。

另一个编辑,6 个月后:当我问这个问题时,我真的不敢相信我是多么天真,静态工厂方法显然是绝对正确的选择,而且显然是不费吹灰之力。

除此之外,我发现Functional Java 非常有趣。我还没有使用它,但我在谷歌搜索'java disjunct union'时确实找到了这个Either,这正是我想要的。缺点是,Functional Java 仅适用于 Java 7 和 8,但幸运的是我现在正在从事的项目使用 Java 8。

【问题讨论】:

  • 您可以将“丑陋的解决方案”构造函数隐藏在工厂方法后面。
  • 静态工厂通常是要走的路。但是,如果您有两个相互排斥的字段,那总是很可疑。
  • 在我看来,这就像另一个 Either monad 的开始......所以你为什么不看看已经完成的大量实现。
  • 是的,您需要不同的方法名称。构造函数无法获得的东西。不过,我看不出有什么大不了的。
  • @JoseAntonioDuraOlmos Reification 与静态类型无关,这是一个静态类型问题。另外,只有关于如何为 java 完成类型擦除的细节才会导致类型擦除成为问题。静态 FP 语言也可以进行类型擦除,但不会受此影响。

标签: java generics constructor type-erasure


【解决方案1】:

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

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

但如果你这样做了,那么:

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

这会有不同的行为,即使类和数据相同。您对isAisB 的概念基本上是“左对右”——哪个是左对右比哪个是字符串与哪个是整数更重要。换句话说,Union&lt;String, Integer&gt;Union&lt;Integer, String&gt; 有很大不同,这可能不是你想要的。

考虑一下,例如,如果我们说:

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

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


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

【讨论】:

  • 非常好! Union 应该表现为 Union
  • 关于A, BB, A 的好电话,我不确定处理这个问题的最佳方法。 is(Class&lt;?&gt; clazz) 可能吗?好像更丑了……
  • 唯一的问题是,你的草图没有带来任何改进。您的 Union&lt;A, B&gt; 与 OP 一样,从根本上区分左右类型参数。 getA() 的类型是A,静态已知是左类型参数的值。询问isA() 不是询问实例的type,而只是询问它的state:调用getA()getB() 是否合法,两者都可以这些类型中的一部分甚至在运行程序之前就已修复。
  • 在这种情况下,接口和实现之间的分离与问题的主题无关。运行时多态性的存在并没有改变静态类型解析。
  • @MarkoTopolnik 我同意我的回答的后半部分没有解决我在回答的前半部分中提出的任何问题。我猜这只是试图……“帮助”。
【解决方案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;
        }
    ...
}

【讨论】:

  • 工厂方法不检查参数是否为空。
  • @JoseAntonioDuraOlmos : 它不在初始构造函数示例中,但很容易检查。
  • 没错,在这方面问题还不清楚;早一点做,晚一点。
【解决方案3】:

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

使用工厂方法,您可以获得一个优雅的解决方案,它为 a 和 b 保留 final。
工厂方法将使用“丑陋”的构造函数,但这没关系,因为它是实现的一部分。除了从构造函数转移到工厂方法之外,公共接口可以满足您的所有要求。

这样做的目的是使Union&lt;A,B&gt; 可以与Union&lt;B,A&gt; 互换,根据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 显示了此解决方案的局限性。编译器无法检测用于方法的错误参数,该方法旨在接受任何包含字符串的联合。我们不得不求助于运行时类型检查。

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

    【讨论】:

    • 我使用了 union 这个词,因为它类似于 C 中的 union 概念。另外,我使用的是 Java 6,所以java.util.Optional 不是一个选项(双关语是无意的,并不后悔)。但我确实有番石榴,所以它的版本是可用的(事实上,我使用的是Optional&lt;A&gt;,但我想在我的问题中保持简约)。
    • C union 和您的类之间的根本区别在于 C 是完全类型不安全的:您可以随时将值解释为任何已声明类型的实例。但是,您的目标是一个类型安全的解决方案(否则您只会返回 Object 并转换为您想要的任何类型)。
    【解决方案5】:

    正如durron597's answer 指出的那样,Union&lt;Foo, Bar&gt;Union&lt;Bar, Foo&gt; 从概念上讲应该表现相同但表现得非常不同。哪个是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&lt;Class&lt;?&gt;, ?&gt; 或其他东西,或者至少要保存这些值。我不知道。所有这些代码都很臭。它源于一个设计不佳的方法,需要多种返回类型。

    【讨论】:

    • 是的,这个FooOrBar 课程完全扼杀了你最初的想法。但我实际上并没有看到使用左右类型的位置概念有什么问题。您的方法将声明返回一个Union&lt;String, Integer&gt;,您将非常清楚哪个是左侧类型,哪个是右侧类型。请记住,您不能将静态类型系统变成动态类型系统,并且在静态上左右之间的区别非常明显。您不可能删除该订单。
    • 请记住您最初概念背后的关键思想:您正在解决一个不可能的“具有两种返回类型的方法”的问题,方法是将其转换为返回一种类型但其 状态空间的方法 分为“左”和“右”状态,每个状态都有分配给它的静态类型。这正是 Either monad 最适合的(你正在重新发明它)。
    • @Marko 什么是Either monad?
    • 试试我在上一条评论中根据您的问题给出的参考。这是一个很好的解释。
    • @Marko 我查看了Either,是的,我确实认为对于返回类型,这是最好的解决方案。我认为如果是 &lt;A, B&gt;&lt;B, A&gt; 交互,我认为不会真正出现。