How can I make a method with a bounded type parameter exclude one subclass?

338 Views Asked by At

Let us assume that gibbons, orangutans, gorillas, chimpanzees and humans are all apes. I have modeled this relationship accordingly.

class Ape {}
class Gibbon extends Ape {}
class Orangutan extends Ape {}
class Gorilla extends Ape {}
class Chimpanzee extends Ape {}
class Human extends Ape {}

I now want to write a hunt() method with a bounded type parameter for hunting apes.

public static <T extends Ape> void hunt(T type) {}

Let us assume that there is nothing wrong with hunting non-human apes because they are just animals. However, hunting humans is wrong because it is murder. How can I re-write the above bounded type parameter to exclude humans as a legal parameter? I am not interested in exceptions here. I do not want the hunt() method to compile at all if invoked with a human parameter.

4

There are 4 best solutions below

0
tony _008 On BEST ANSWER

You can't exclude a specific subclass the way you intend there. What you can do however, is create an interface 'isHuntable', implemented by the required animals you want to hunt, and you use that as a type for your method instead of the generic type bound. Another maybe less elegant solution is to create another level in your hierarchy called 'Monkeys' for example which extend from Ape, having all the animals extending from it, and you use that Monkey type for your method. I'd go with the first approach though. You could use explicit type checking, but you'd be breaking the 'open-closed' principle in you code, so it's better to leverage those checks to the type system.

Just extending a bit on the concept of interface behaviour-contract which is powerful tool which is sadly underused. What you have among your Apes is a "is-a" relationship which tightly couples your subjects. Being "Huntable" on the other hand, is not an inherent or defining characteristic of the structure of that hierarchy, and is more an added condition/behaviour/check you'd like to add to a subset of that hierarchy. That is better achieved by adding contracts (interface implementation) to the subset you intend to have that behaviour. That will allow to add further contracts (interface implementations) in the future with minimal refactorings, maximal semantic clarity, less bugs and without tightly bounding a type to a behaviour. In short, you don't break the Liskov Substitution principle, nor the open closed principle, nor any SOLID principle.

5
ejdebruin On

I don't believe there's a way to do this. You could make a sub-type of Ape that excludes Humans, or you can check for Humans as a type in the method itself (which would violate the Open-Closed principle) :

public static <T extends Ape> void hunt(T type) {
  if (type instanceOf Human) {
     throw new IllegalArgumentException("It's immoral to hunt Humans.  You monster.");
  }
  . . .
}
4
Andy Turner On

You cannot do this. There is no way to specify a bound to say "any subclass, except this one".

And even if you could, it wouldn't stop you invoking it:

chimp.hunt((Ape) human);

would get around it.

All you can do is to check this at runtime.

You could, via some tool such as Error Prone (written by Google, I am an employee, and I contribute; other tools are available) write a compile-time check to ensure that the argument is neither Ape nor Human; but that's outside the capabilities of "pure" Java.

3
Rogue On

I would make your hunt method non-static; it's an action on the object itself, after all. Then, you can define the behavior for all Apes:

public void hunt() {
    this.die(); // oh no!
}

And of course, humans override that basic idea:

@Override
public void hunt() {
    throw new IllegalMurderException("Human horn hunting is against intergalactic law");
}

Adding a check for #isHuntable would be advisable, as keeping with the metaphor it's nice to know if you're allowed to hunt something before doing so. Additionally, I went with an exception for human hunting as it violates the general behavior of what you (well, I) would expect #hunt to achieve.

For the sake of overengineering and various concepts of SOLID, you can achieve this with a Huntable interface, which could extend a Living interface:

public interface Living {
    public void die();
}

public interface Huntable extends Living {
    default public void hunt() {
        this.die();
    }
    //could use this for a generic exception in the interface too
    public boolean isHuntable(); 
}

public interface Ape extends Huntable {} // already Huntable

public interface Human extends Ape {
    //And finally, we break that behavioral contract like above
}

Overall this makes your generics unnecessary. You'll be forced into a type cast and manual check if you go that route, as other answers have shown. So while it sidesteps the original question, I think this is a better solution to the underlying problem (XY problem?). More importantly, behavior concerning humans remains in a class describing humans.

Edit:

In keeping with the static method and a checked exception, while I strongly recommend against it (per my comments), you can use a method overload so Human objects go through a different signature:

public static void hunt(Ape example) { ... }

public static void hunt(Human example) throws HuntingException {
    throw new HuntingException("cannot hunt humans");
}

HuntingException would extend Exception instead of RuntimeException. Exception/Error/Throwable objects that you make yourself are all essentially checked exceptions. Short of that, there's no creating compiler errors yourself as a developer. You can generate warnings, but you face the same pitfalls.

You can kind of see why this is silly, and a human is still able to be hunted without a compiler error at all by just casting:

Main.hunt((Ape) someHuman);

So now you go back down the rabbit hole of adding type checks in your #hunt method, which won't throw a compiler error (or will have to always throw one). If you always throw it, then developers using your code (and quite possibly yourself) will just auto fill try-catch blocks around every single invocation of #hunt just to handle an error for Human. This adds a lot of "code noise", which just makes things messy.

In short, runtime exceptions are for developer errors like "hunting a human", and there's too many reasons to list for why having this as a static method (let alone main class) is not good from a design standpoint.