代码之家  ›  专栏  ›  技术社区  ›  Marcelo Glasberg

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

  •  3
  • Marcelo Glasberg  · 技术社区  · 9 年前

    考虑以下不可变类:

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

    我想修改源代码以转换 运行时间 错误输入到 编译时间 错误或警告,而不必重载或重写 过程 方法

    然而 客户端/测试 代码不能更改(没有强制转换或泛型参数)。

    编辑: 这个问题没有真正的解决办法。所以我接受了一个显而易见的答案: 过程 方法这是最适合客户端代码的方法,尽管这是一个维护噩梦。也许有一天可以修改Java类型系统,这样就可以编写“this的类型”了。然后我们可以写一些 public final this process(A a) 。请参阅中的建议 this page (在评论部分)如果你感兴趣。

    6 回复  |  直到 9 年前
        1
  •  2
  •   Bohemian    9 年前

    使用自引用类型:

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

        2
  •  1
  •   scottb    9 年前

    自Java5以来,您的代码允许在子类中重写的方法具有协变返回类型。这是什么意思?

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

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

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

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

        3
  •  1
  •   Paul Boddington    9 年前

    这是一个令人讨厌的问题。无法表示返回类型与调用方法的实例具有相同的类型。我推荐scottb的协变返回类型解决方案。您只需更改的签名 process 在里面 A 成为

    public A process(A a)
    

    然后在每个子类中用一行重写。E、 g.英寸 C :

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

    如果您不想在每个子类中反复执行此操作,可以使用 静止的 方法。

    public static <T extends A> T process(T t, A a) { 
        Class<?> clazz = t.getClass();
        return (T) createObjectOfType(clazz);
    }
    
        4
  •  1
  •   fps    9 年前

    我相信我已经找到了满足你所有要求的方法。技巧是强制编译器注意到您要调用 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
    
        }
    }
    

    如你所见 A , B C 保持不变,既没有泛型类型参数,也没有重写方法。此外,您得到的是编译错误,而不是运行时错误。

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

    如果您不喜欢或不能采用这种方法,可以使用另一种技巧(同样粗糙,但更容易出错),它不需要静态助手方法:

    1. 在里面 A. 改变 进程() 签名至:

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

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

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

        5
  •  0
  •   Brett Okken    9 年前

    类的实例 A 将始终返回 A. 从…起 process 。这是因为 getClass() 将始终返回 A. (因为这是对象的实例。同样,类的实例 B 将始终返回类型为的对象 B 因为 getClass 将返回 B .

    获得运行时错误而不是编译时错误的原因是您忽略了 Class 创建要返回的新实例时。

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

        6
  •  0
  •   Judge Mental    9 年前

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

    1. 重命名变量以避免冲突
    2. 类型化变量 a , b , c AImpl , BImpl , CImpl 分别地没有泛型参数,但也没有纯接口。

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