在最终方法中使用泛型 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 来自 process
的 A
的实例。这是因为对 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);
}
这就够了吗?我在您的单元测试中所做的唯一更改如下:
- 重命名变量以防止冲突
- 类型变量
a
、b
、c
分别为 AImpl
、BImpl
、CImpl
。没有通用参数,但也不是纯接口。
说出代码:
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
}
}
如您所见,类 A
、B
和 C
保持不变,既没有泛型类型参数也没有覆盖方法。此外,您得到的是编译错误而不是运行时错误。
权衡是您必须使用客户端代码中的静态帮助器方法。但是,您不需要使用泛型类型参数或强制转换。
如果您不喜欢或不能采用这种方法,您可以使用其他技巧(同样老套,但更容易出错),它不需要静态辅助方法:
在A
中,将process()
签名更改为:
public <T extends A> T process(T self, A a)
在您的客户端中,将调用更改为:
A resultAAA = instance.process(instance, a);
其中第一个参数必须与调用该方法的引用相同。
考虑以下不可变的 类:
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 来自 process
的 A
的实例。这是因为对 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);
}
这就够了吗?我在您的单元测试中所做的唯一更改如下:
- 重命名变量以防止冲突
- 类型变量
a
、b
、c
分别为AImpl
、BImpl
、CImpl
。没有通用参数,但也不是纯接口。
说出代码:
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
}
}
如您所见,类 A
、B
和 C
保持不变,既没有泛型类型参数也没有覆盖方法。此外,您得到的是编译错误而不是运行时错误。
权衡是您必须使用客户端代码中的静态帮助器方法。但是,您不需要使用泛型类型参数或强制转换。
如果您不喜欢或不能采用这种方法,您可以使用其他技巧(同样老套,但更容易出错),它不需要静态辅助方法:
在
A
中,将process()
签名更改为:public <T extends A> T process(T self, A a)
在您的客户端中,将调用更改为:
A resultAAA = instance.process(instance, a);
其中第一个参数必须与调用该方法的引用相同。