Home > Net >  Cannot override method that takes in parameter of inner class of generic
Cannot override method that takes in parameter of inner class of generic

Time:10-29

Ok, I'll try to explain this as cleanly as I can.

I've created a generic abstract controller class that has a method hasCreatePermissions that looks something like this:

public abstract class ApplicationController<
        AppEntity extends ApplicationEntity,
        AppService extends ApplicationService<AppEntity>,
        DTOManager extends ApplicationDTOManager
> {

    // Other methods, properties, etc...
    
    public boolean hasCreatePermissions(DTOManager.CreationRequest requestBody, Optional<UUID> requestingUser) {
        return false;
    }


}

Essentially, I want any class that overrides this method to be able to use its own DTOManager class as the parameter when it overrides this method.

The generic ApplicationDTOManager class looks like

public abstract class ApplicationDTOManager {

    public abstract class CreationRequest {}

    public abstract class CreationResponse {}

}

and any class that inherits ApplicationDTOManager can add classes that extend CreationRequest and CreationResponse for their own implementation of respective DTOs.

However, lets say I try to extend it with a UserResource class (assume UserDTOManager exists with an implementation for CreationRequest):

@RestController
public class UserResource extends ApplicationController<
    User,
    UserService<User>,
    UserDTOManager
> {
    
    @Override
    public boolean hasCreatePermissions(UserDTOManager.CreationRequest requestBody, Optional<UUID> requestingUser) {
        // Stuff
    }


}

I'm told that this does not override any super class methods. Why? Is there any way to achieve this as I did not want to pass too many generics to my ApplicationController class, but also cannot have a constructor.

CodePudding user response:

class ApplicationController<
       AppEntity extends ApplicationEntity,

No, stop right there. This is declaring a type variable with the bound rules: "Must be either ApplicationEntity or any subtype thereof" and you named it AppEntity. This is going to make your head go in circles when you read the code later, 'I keep confusing reified types with type variables' comprises 95% of all confusion about generics. I know it seems unreadable, but there really is just only one way to go about it, and that is to use single capital letters for all your type vars. So let's fix that right now:

public abstract class ApplicationController<
        E extends ApplicationEntity,
        S extends ApplicationService<E>,
        M extends ApplicationDTOManager> {

Which then immediately lets us see a big problem in the next line:

public boolean hasCreatePermissions(M.CreationRequest requestBody) {}

Of course that can't work. M is a type variable, at compile time the compiler has no idea what type it is. It could be bound to some type that doesn't even exist yet right now. You can't ask for a reified inner type on a type variable. You can of course simply talk about ApplicationDTOManager.CreationRequest and that's presumably what you want.

Alternatively, you're thinking that subtypes of ApplicationDTOManager may also want to subclass ApplicationDTOManager.CreationRequest. This is possible, but then all ApplicationDTOManager types need to carry their associated CreationRequest type as a type variable. We thus fix a few things:

public class ApplicationDTOManager<R extends ApplicationDTOManager.CreationRequest> {
  public static class CreationRequest {}
}

You may have a non-static inner class named CreationRequest. I'm going to stop you again on that - non-static inners have an invisible inner field of their outer's type, and combining that invisible voodoo magic with generics just doesn't work. Don't do it. You can explicitly make that field if you must have it, and make a constructor that takes it. This is what javac generates for you if you don't add static to your inner classes. But by making it explicit, you take control of the generics, which you have to here, and avoid confusion which given the nature of the question seems pertinent.

I'm told that this does not override any super class methods. Why?

Java's method names include all their erased types. The name of this method:

class List<T extends Number> {
  int hello(String name, boolean[] hi, T arg) throws SQLException {}
}

is, as far as the JVM is concerned, hello(Ljava/lang/String;[ZLjava/lang/Number;)I.

Yeah, no, really. javap -c -v a class file and you'll see it. (I is integer, [ is array, Z is boolean, and Ltxt; encodes ref type names in JVM style, e.g. with slashes and dollars instead of dots). It's written name(params)ret.

If you then subtype something and introduce a method whose erased JVM name is identical, you're overriding. If you don't, it is not an override. Merely an overload. Overrides are dynamically dispatched. But overloads are not - The names are all linked up at compile time. However, for any given 'JVM method name', the lookup is done dynamically based on the receiver type. In other words:

class Fruit {
  void hi(Fruit f) { System.out.println("Fruit Fruit"); }
  void hi(Apple a) { System.out.println("Fruit Apple"); }
}

class Apple extends Fruit {
  void hi(Fruit f) { System.out.println("Apple Fruit"); }
  void hi(Apple a) { System.out.println("Apple Apple"); }
}

Fruit f = new Fruit();
Fruit a = new Apple();
a.hi(a);

Will print Apple Fruit. You'd think it should print Apple Apple perhaps - we are calling apple's hi passing an apple, no? But the invocation a.hi(a) is invoking the method named hi(Lfruit;)V (because the type of a is Fruit). The receiver variable (a) has compile time type Fruit, but its real type is Apple. So, which of the hi(Lfruit;)V methods is chosen is done with dynamic dispatch - you get apple's hi(Fruit). Deciding between going with hi(Fruit) and hi(Apple) is done by the compiler only. Given that the type of the expression a is Fruit, you get hi(Fruit). The fact that if you resolve this expression, you get an object whose .getClass() returns Apple.class, doesn't change this.

Hence, what you wrote, does not override. Different name, even if you erase.

Toss generics in the mix and it gets funky. But you can do this.

public abstract class ApplicationDTOManager<I extends CreationRequest, O extends CreationResponse> {

    public abstract static class CreationRequest {}

    public abstract static class CreationResponse {}

}

public abstract class ApplicationController<
        E extends ApplicationEntity,
        S extends ApplicationService<AppEntity>,
        I extends CreationRequest,
        O extends CreationResponse,
        M extends ApplicationDTOManager<I, O>
    >
// heck that's probably too many, at some point generics aren't worth it

{
  public abstract boolean hasCreatePermissions(I requestBody);
}

@RestController
public class UserResource extends ApplicationController<
    User,
//     UserService<User>, // this seems wrong!
    UserService, // UserService should extends ApplicationService<User>
    UserDTOManager.CreationRequest,
    UserDTOManager.CreationResponse,
    UserDTOManager> {
    
    @Override
    public boolean hasCreatePermissions(UserDTOManager.CreationRequest requestBody, Optional<UUID> requestingUser) {
        // Stuff
    }


}

Not sure all this is worth the pain, but, if you insist on linking all this together with generics, the above is the only way. You cannot express the notion 'has an inner type that is a subtype of CreationRequest' is a generics bound.

CodePudding user response:

If you override a method you cannot have a different signature, If the method you override requires a DTOManager.CreateRequest you cannot use a child class within the override method.

You have to "support" all types of input that the parent method could take.

I believe that you need this because the permission validation relies on methods or fields of the child class. If so you should implement it inside the child class.

  • Related