在返回其对象的相同类型的值的final方法中使用泛型

时间:2015-03-21 21:54:38

标签: java hierarchy class-hierarchy generics

考虑以下不可变类:

A
B extends A
C extends B
D extends C
...

A类有一个名为process的方法,它获取类型A的参数,然后返回调用对象类型的值:

public class A {

    public final <T extends A> T process(A a) {
        Class clazz = getClass();
        T result = createObjectOfType(clazz);
        return result;
        }
    }

public class B extends A { }

public class C extends B { }

这是(非常简单的)测试代码:

public void test()
    {
    A a = new A();
    B b = new B();
    C c = new C();

    // Returns type A:

    A resultAAA = a.process(a); // Works.
    A resultAAB = a.process(b); // Works.
    A resultAAC = a.process(c); // Works.

    B resultAAA = a.process(a); // Runtime error.
    B resultAAB = a.process(b); // Runtime error.
    B resultAAC = a.process(c); // Runtime error.

    C resultAAA = a.process(a); // Runtime error.
    C resultAAB = a.process(b); // Runtime error.
    C resultAAC = a.process(c); // Runtime error.

    // Returns type B:

    A resultBBA = b.process(a); // Works.
    A resultBBB = b.process(b); // Works.
    A resultBBC = b.process(c); // Works.

    B resultBBA = b.process(a); // Works.
    B resultBBB = b.process(b); // Works.
    B resultBBC = b.process(c); // Works.

    C resultBBA = b.process(a); // Runtime error.
    C resultBBB = b.process(b); // Runtime error.
    C resultBBC = b.process(c); // Runtime error.

    // Returns type C:

    A resultCCA = c.process(a); // Works.
    A resultCCB = c.process(b); // Works.
    A resultCCC = c.process(c); // Works.

    B resultCCA = c.process(a); // Works.
    B resultCCB = c.process(b); // Works.
    B resultCCC = c.process(c); // Works.

    C resultCCA = c.process(a); // Works.
    C resultCCB = c.process(b); // Works.
    C resultCCC = c.process(c); // Works.

    }

我想修改源代码以将运行时错误转换为编译时错误或警告,而不必重载或覆盖process方法。

但是,客户端/测试代码不得更改(没有强制转换或通用参数)。

编辑:此问题没有真正的解决方案。所以我接受了关于覆盖process方法的明显答案。这对客户端代码最有效,即使它是一个维护噩梦。也许有一天可以修改Java类型系统,这样就可以编写&#34;这个&#34;的类型。然后我们可以编写类似public final this process(A a)的内容。如果您有兴趣,请参阅this page中的建议(在评论部分)。

6 个答案:

答案 0 :(得分:2)

使用自引用类型:

public class A<T extends A<T>>{

    public final T process(A a) {
        Class clazz = getClass();
        T result = createObjectOfType(clazz);
        return result;
    }
}

public class B<T extends B<T>> extends A<T>{ }

public class C extends B<C> { }

要完全避免客户端代码中的泛型参数,您必须创建第二组泛型类,其固定类型实现为A,B,C等,如下所示:

public class BaseA<T extends BaseA<T>>{
    public final T process(BaseA a) {
        Class clazz = getClass();
        T result = createObjectOfType(clazz);
        return result;
    }
}
public class A extends BaseA<A> {}

public class BaseB<T extends BaseB<T> extends BaseA<BaseB<T>> {}

public class B extends BaseB<B> {}

public class C extends BaseB<C> {}

这里的问题是b instance A不是真的,但它可能感觉足够接近客户代码不会关心。

答案 1 :(得分:1)

从Java 5开始,允许您的代码为在子类中重写的方法提供共变量返回类型。这是什么意思?

这意味着重写方法的返回类型必须是原始方法的子类。这使子类能够返回适当类型的类型而不是父类型。

假设您在A中有一个要在子类B中使用的方法,但是您希望它返回一个B实例。使用这种模式:

class A {
    public A myMethod() { ... }
}

class B extends A {
    @Override public B myMethod() { ... }
}

B类重写A类中的方法(如果需要,可以通过调用super.myMethod()来调用它,并返回B实例)。这是允许的,因为B是A的子类型(在类设计语言中,B-is an-A)。

答案 2 :(得分:1)

这是一个恼人的问题。无法表达返回类型与调用方法的实例具有相同的类型。我会推荐scottb的协变返回类型解决方案。您需要做的就是将processA的签名更改为

public A process(A a)

然后使用单行覆盖每个子类。例如。在C

 @Override
 public C process(A a) { return (C) super.process(a); }

如果您不想在每个子类中反复执行此操作,则可以使用 static 方法来解决此问题。

public static <T extends A> T process(T t, A a) { 
    Class<?> clazz = t.getClass();
    return (T) createObjectOfType(clazz);
}

答案 3 :(得分:1)

我相信我找到了满足您所有要求的方法。诀窍是强制编译器注意到只想在类型等于或是process()返回的类型的子类型的实例上调用process()方法。

为实现这一目标,我使用了静态辅助方法。请阅读代码中的注释,因为它们解释了所使用的技巧:

public class GenericsNightmare {

    // Should reside in the same package as class A
    public static final class U { // Utils

        private U() {
            // No instances
        }

        public static <T extends A> T process(T target, A a) {
            // Here is enforced that returned type T
            // matches the instance type on which the method
            // is called, because it's being invoked on an
            // argument whose type is T as well
            return target.process(a);
        }

        // TODO Other 29 one-liner methods in a similar fashion ;)
    }

    // Should reside in the same package as class U
    public static class A {

        // Don't make this method public unless
        // you want runtime errors instead of
        // compilationn errors! 
        // In other words, this is to avoid the
        // well-known "heap pollution" problem
        final <T extends A> T process(A a) {
            try {
                @SuppressWarnings("unchecked")
                // The cast below is safe because we're being called
                // from within the static method of the utility class
                // (i.e. we already know we are of type T)
                Class<T> clazz = (Class<T>) this.getClass();
                T result = clazz.getConstructor().newInstance();
                return result;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static class B extends A {
    }

    public static class C extends B {
    }

    @SuppressWarnings("unused")
    public static void main(String[] args) {
        A a = new A();
        B b = new B();
        C c = new C();

        // Returns type A:

        // Use the static helper method to 
        // call the process() method, oh yes ;)

        A resultAAA = U.process(a, a); // Compiles
        A resultAAB = U.process(a, b); // Compiles
        A resultAAC = U.process(a, c); // Compiles

        B resultBAA = U.process(a, a); // Compilation error
        B resultBAB = U.process(a, b); // Compilation error
        B resultBAC = U.process(a, c); // Compilation error

        C resultCAA = U.process(a, a); // Compilation error
        C resultCAB = U.process(a, b); // Compilation error
        C resultCAC = U.process(a, c); // Compilation error

        // Returns type B:

        A resultABA = U.process(b, a); // Compiles
        A resultABB = U.process(b, b); // Compiles
        A resultABC = U.process(b, c); // Compiles

        B resultBBA = U.process(b, a); // Compiles
        B resultBBB = U.process(b, b); // Compiles
        B resultBBC = U.process(b, c); // Compiles

        C resultCBA = U.process(b, a); // Compilation error
        C resultCBB = U.process(b, b); // Compilation error
        C resultCBC = U.process(b, c); // Compilation error

        // Returns type C:

        A resultACA = U.process(c, a); // Compiles
        A resultACB = U.process(c, b); // Compiles
        A resultACC = U.process(c, c); // Compiles

        B resultBCA = U.process(c, a); // Compiles
        B resultBCB = U.process(c, b); // Compiles
        B resultBCC = U.process(c, c); // Compiles

        C resultCCA = U.process(c, a); // Compiles
        C resultCCB = U.process(c, b); // Compiles
        C resultCCC = U.process(c, c); // Compiles

    }
}

如您所见,类ABC保持不变,既没有泛型类型参数也没有覆盖方法。此外,您将收到编译错误而不是运行时错误。

权衡是您必须使用客户端代码中的静态帮助程序方法。但是,您不需要使用泛型类型参数或强制转换。

如果你不喜欢或不能采用这种方法,你可以使用另一个技巧(同样hacky,虽然更容易出错),这不需要静态帮助方法:

  1. A中,将process()签名更改为:

    public <T extends A> T process(T self, A a)
    
  2. 在您的客户端中,将调用更改为:

    A resultAAA = instance.process(instance, a);
    

    其中第一个参数必须与调用该方法的引用相同。

答案 4 :(得分:0)

A的实例将始终从A返回process的实例。这是因为对getClass()的调用将始终返回A(因为这是对象的实例。同样,类B的实例将始终返回类型为{的对象{1}}因为B将返回getClass

您获得运行时错误而不是编译时错误的原因是因为在创建要返回的新实例时忽略了B中的通用信息。

最终问题是你的api宣称返回的对象类型可以由调用者控制,而实际上它是由调用该方法的对象类型决定的。

答案 5 :(得分:0)

这样就够了吗?我在单元测试中所做的唯一更改如下:

  1. 将变量重命名为不碰撞
  2. 分别为abc键入变量AImplBImplCImpl。没有通用参数,但也没有纯接口。
  3. Voici代码:

    interface Processor< T extends Processor< ? extends T > > {
        T process( Processor< ? > p );
    }
    abstract class AbstractProcessor< T extends AbstractProcessor< ? extends T > > implements Processor< T > {
        public T process( Processor< ? > a ) {
            // ... actual processing
            return reproduce();
        }
        abstract T reproduce();
    }
    
    interface A {}
    interface B extends A {}
    interface C extends B {}
    
    class AImpl extends AbstractProcessor< AImpl > implements A {
        AImpl reproduce() { return new AImpl(); }
    }
    class BImpl extends AbstractProcessor< BImpl > implements B {
        BImpl reproduce() { return new BImpl(); }
    }
    class CImpl extends AbstractProcessor< CImpl > implements C {
        CImpl reproduce() { return new CImpl(); }
    }
    
    @org.junit.Test
    public void test()
    {
        AImpl a = new AImpl();
        BImpl b = new BImpl();
        CImpl c = new CImpl();
    
        // Returns type A:
    
        A resultAAA = a.process(a); // Works.
        A resultAAB = a.process(b); // Works.
        A resultAAC = a.process(c); // Works.
    
        B resultBAA = a.process(a); // Type error.
        B resultBAB = a.process(b); // Type error.
        B resultBAC = a.process(c); // Type error.
    
        C resultCAA = a.process(a); // Type error.
        C resultCAB = a.process(b); // Type error.
        C resultCAC = a.process(c); // Type error.
    
        // Returns type B:
    
        A resultABA = b.process(a); // Works.
        A resultABB = b.process(b); // Works.
        A resultABC = b.process(c); // Works.
    
        B resultBBA = b.process(a); // Works.
        B resultBBB = b.process(b); // Works.
        B resultBBC = b.process(c); // Works.
    
        C resultCBA = b.process(a); // Type error.
        C resultCBB = b.process(b); // Type error.
        C resultCBC = b.process(c); // Type error.
    
        // Returns type C:
    
        A resultACA = c.process(a); // Works.
        A resultACB = c.process(b); // Works.
        A resultACC = c.process(c); // Works.
    
        B resultBCA = c.process(a); // Works.
        B resultBCB = c.process(b); // Works.
        B resultBCC = c.process(c); // Works.
    
        C resultCCA = c.process(a); // Works.
        C resultCCB = c.process(b); // Works.
        C resultCCC = c.process(c); // Works.
    }