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

落花浮王杯 提交于 2019-12-12 14:11:55

问题


Consider the following immutable classes:

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

Class A has a method called process that gets a parameter of type A, and then returns a value of the type of the calling object:

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

This is the (very simple) test code:

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.

    }

I want to modify the source code to convert those runtime errors into compile time errors or warnings, without having to overload or override the process method.

However, the client/test code must not change (no casts or generic parameters).

Edit: There is no real solution to this question. So I accepted the obvious answer about overriding the process method. This is what works best for the client code, even though it's a maintenance nightmare. Maybe one day Java type system could be modified so that it would be possible to write "the type of this". Then we could write something like public final this process(A a). See the suggestion in this page (in the comments section) if you're interested.


回答1:


Since Java 5, your code is allowed to have co-variant return types for methods that are overridden in subclasses. What does this mean?

It means that the return type of an overridden method must be a subclass of the original method. This enables subclasses to return types that are of the appropriate type rather than the parent type.

Say you have a method in A that you want to use in subclass B, but you want it to return a B instance. Use this pattern:

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

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

Class B overrides the method in class A (it can invoke it, if needed, by making a call to super.myMethod()) and returns a B instance. This is allowed because B is a subtype of A (and in class design language, B -is an- A).




回答2:


Use a self-referencing type:

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

To avoid generic parameters entirely in the client code, you would have to create a second set of generic classes whose fixed-type implementations are A, B, C etc, like this:

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

The problem here is that b instance A won't be true, but it may feel close enough that the client code won't care.




回答3:


This is an annoying problem. There is no way to express that the return type has the same type as the instance on which the method is called. I would recommend the covariant return types solution of scottb. All you need to do is change the signature of process in A to be

public A process(A a)

and then override in each subclass with a one-liner. E.g. in C:

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

If you don't want to do this over and over in every subclass, there is a solution using a static method instead.

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



回答4:


I believe I've found a way to satisfy all your requirements. The trick is to force the compiler to notice that you want to invoke the process() method only on instances whose type is equal to or is a subtype of the type returned by process().

To achieve this, I'm using a static helper method. Please read the comments in the code, as they explain the trick being used:

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

    }
}

As you see, classes A, B and C are left untouched, with neither generic type params nor overriden methods. Besides, you are getting compilation errors instead of runtime errors.

The trade-off is that you have to use the static helper method from your client code. However, you don't need to use generic type parameters or casts.

If you don't like or can't take this approach, you could use this other trick (equally hacky, though more error-prone), which doesn't require a static helper method:

  1. In A, change process() signature to:

    public <T extends A> T process(T self, A a)
    
  2. In your client, change invocations to:

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

    where the first argument must be the same reference on which the method is being invoked.




回答5:


An instance of class A will always return an instance of A from process. This is because the call to getClass() will always return A (since that is what the object is an instance of. Likewise, an instance of class B will always return an object of type B because getClass will return B.

The reason you are getting runtime errors instead of compile time errors is because you are ignoring the generic information in Class when creating a new instance to return.

Ultimately the problem is that your api advertises that the type of object returned can be controlled by the caller, when in fact it is determined by the type of object the method is called on.




回答6:


Would this suffice? The only changes I made in your unit test are the following:

  1. Renamed variables to not collide
  2. Typed variables a, b, c as AImpl, BImpl, CImpl respectively. No generic parameters, but not pure interfaces either.

Voici the code:

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


来源:https://stackoverflow.com/questions/29188497/use-of-generics-in-a-final-method-that-returns-a-value-of-the-same-type-of-its-o

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!