【问题标题】:Use of generics in a final method that returns a value of the same type of its object在最终方法中使用泛型,返回与其对象相同类型的值
【发布时间】:2015-05-25 03:49:03
【问题描述】:

考虑以下不可变类:

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.

    }

我想修改源代码,将那些 runtime 错误转换为 compile time 错误或警告,而不必重载或覆盖 process 方法。

但是,client/test 代码不得更改(无强制转换或泛型参数)。

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

【问题讨论】:

  • 方法process不使用a。你的意思是a.getClass()
  • @pbabcdefp 不,OP 的真正含义是 A.getClass()、B.getClass() 或 C.getClass(),具体取决于对象的实际运行时类型。
  • 只是澄清一下,“没有泛型参数”是指在 trst 代码中的方法调用,还是在类定义上,或其他什么?
  • @marcg 这奇怪地让我想起了实数、整数和自然数...... ;) 你的问题很棘手。您是否考虑过双重调度方法?
  • @Magnamag 是的,这是提出这个问题的更好方法,我从那里链接到这里。关于双重调度,我猜是关于根据对象类型运行不同的代码,但这里的代码是相同的(process 方法的内部结构不会改变)。我只是想找到一种方法让编译器看到c.process(...)返回一个C类型的值,例如。

标签: java hierarchy class-hierarchy generics


【解决方案1】:

使用自引用类型:

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

【讨论】:

  • 我相信他说他不希望解决方案使用泛型类型参数。
  • 解决方案可能在内部使用泛型类型参数。我只是不想在客户端/测试代码中使用它们。例如,我不想在客户端代码中写B&lt;B&gt; = new B&lt;&gt;(); 之类的东西。
  • 根据目前提供的信息,这似乎是解决方案。
  • 这很聪明。但是,正如你注意到的,我需要b instance A == true。否则它会破坏A resultBBA = b.process(a); 行。
  • @marcg,如果您尝试将 C 是一个 B 是一个没有通用参数和 F 有界多态性的 A 结合起来,擦除(有史以来最糟糕的想法)将会得到您。原因是不允许您以两种不同的具体类型实现泛型接口。
【解决方案2】:

从 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)。

【讨论】:

  • 正如我所说,我不想重载或覆盖process 方法。我在层次结构中有几十个类,方法代码都是一样的。所以它会非常重复。 process 方法应该被认为是最终的。
  • @marcg 不会那么重复。您不需要编写整个方法。您只需要return (B) super.process(a);。 scottb 的答案是正确的(它确实将运行时异常转换为编译错误)。
  • 如果我有 20 个类和 30 个方法,那么我必须创建 600 个方法,如果只有 Java 类型系统可以实现看起来如此简单的事情,其中​​ 570 个是无用的。也许 Java 类型系统是专门为 Collection 用例开发的......无论如何,我已经知道重复的解决方案,这就是为什么在我的问题中我特别声明我不想覆盖该方法。
  • 但是,如果完美的解决方案确实不可能,我应该认为这是正确的答案,你是对的。但我想我会再等一会儿,看看是否有人提出了突破......
  • 我相信这与生成器模式出现的问题相同。看看(重复的解决方案在标题“讨厌的解决方案”下):weblogs.java.net/node/642849
【解决方案3】:

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

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

【讨论】:

    【解决方案4】:

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

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

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

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

    【讨论】:

    • 是的。但我不想使用静态方法。我想在不修改客户端/测试代码的情况下获得解决方案。
    • @marcg 以及替代的 A resultAAA = a.process(a, a); 怎么样?
    • @marcg 我以为您愿意更改签名:“是的,我想修改签名、实现或两者兼而有之。”否则,你问的是不可能的事情。你必须假设会有一些取舍。
    • 是的,我愿意更改签名,但不会更改客户端/测试代码。正如我在问题中所说:the client/test code must not change。所以最终我的权衡是接受覆盖process 方法。维护很糟糕,但是干净的客户端代码对我来说更重要。考虑我想添加xy。那么sum = x.add(y)最好,而sum = x.add(x,y)很奇怪,sum = U.add(x,y)不是很OOP吧?
    • @marc 我同意你的观点,因为它不是非常 OOP,但是,至少可以说,重写 570 个方法只是用于键入而不是创建 30 个静态方法并从客户端调用它们是非常乏味的。使用 U.add(x,y) 并没有那么糟糕。在 Java 8 之前,这是 Java 中的“函数式”编程风格,即 Google Guava 库几乎都是基于接收回调的静态方法。
    【解决方案5】:

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

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

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

    【讨论】:

    • 是的,布雷特。我想要那些错误。我的问题是我希望它们成为编译时错误。如果我写b.process(...),编译器就知道b 的类型是B。所以编译器原则上可以告诉我C result = b.process(c) 在编译时是错误的。
    • 编译器在编译时无法判断C result = b.process(c) 是错误的。 process 方法签名表明这是正确的。问题在于方法签名或实现(或两者的某种组合)。
    • 是的,我想修改签名和/或实现。
    【解决方案6】:

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

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

    【讨论】:

    • 如果A resultAAA = a.process(a) 那么我应该可以写A newResult = resultAAA.process(a)。然而这是不可能的,因为接口A 没有方法。结果将被重新用于进一步处理。
    • 接口ABC在这里没用,至少它们可以处于继承关系。 AImplBImplCImpl 不能,因为我在上面的评论中表达的原因:他们将以多种方式实现 Processor&lt; T &gt;,Java 由于类型擦除而拒绝允许。不过,您可以将任意数量的 process() 调用链接在一起。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-12-24
    • 1970-01-01
    • 2012-06-09
    • 2018-12-24
    • 2018-03-20
    • 2019-10-26
    • 1970-01-01
    相关资源
    最近更新 更多