在最终方法中使用泛型 returns 与其对象相同类型的值

Use of generics in a final method that returns a value of the same type of its object

考虑以下不可变的 类:

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

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

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 方法。

但是,client/test 代码不得更改(无转换或通用参数)。

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

class A 的实例将始终 return 来自 processA 的实例。这是因为对 getClass() 的调用将始终 return A (因为这是对象的实例。同样, class B 的实例将始终 return 类型为 B 的对象,因为 getClass 将 return B.

您收到运行时错误而不是编译时错误的原因是因为您在为 return.

创建新实例时忽略了 Class 中的通用信息

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

自 Java 5 起,您的代码允许在 subclasses 中重写的方法具有协变 return 类型。这是什么意思?

表示重写方法的return类型必须是原方法的subclass。这使得子 class 到 return 类型是适当的类型而不是父类型。

假设您在 A 中有一个方法,您希望在子 class B 中使用它,但您希望它在 return 一个 B 实例中使用。使用此模式:

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

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

Class B 覆盖 class A 中的方法(如果需要,它可以通过调用 super.myMethod() 来调用它)和 returns a B实例。这是允许的,因为 B 是 A 的子类型(并且在 class 设计语言中,B - 是 - A)。

使用自引用类型:

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 不会是真的,但它可能感觉足够接近以至于客户端代码不会关心。

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

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

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

  1. 重命名变量以防止冲突
  2. 类型变量 abc 分别为 AImplBImplCImpl。没有通用参数,但也不是纯接口。

说出代码:

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

我相信我已经找到了满足您所有要求的方法。诀窍是强制编译器注意到您只想在类型等于 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 保持不变,既没有泛型类型参数也没有覆盖方法。此外,您得到的是编译错误而不是运行时错误。

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

如果您不喜欢或不能采用这种方法,您可以使用其他技巧(同样老套,但更容易出错),它不需要静态辅助方法:

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

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

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

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