如何使具有有界类型参数的方法排除一个子类?

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

让我们假设长臂猿、猩猩、大猩猩、黑猩猩和人类都是类人猿。我已经相应地模拟了这种关系。

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

我现在想编写一个带有有界类型参数的 hunt() 方法来狩猎类人猿。

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

让我们假设猎杀非人类类人猿没有错,因为它们只是动物。但是,猎杀人类是错误的,因为那是谋杀。如何重写上述有界类型参数以将人类排除为合法参数?我对这里的例外不感兴趣。如果使用人工参数调用,我根本不希望 hunt() 方法进行编译。

我认为没有办法做到这一点。您可以创建一个排除 Humans 的 Ape 子类型,或者您可以在方法本身中检查 Humans 作为一种类型(这将违反开闭原则):

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

你不能这样做。没有办法指定一个界限说 "any subclass, except this one".

即使可以,也不会阻止您调用它:

chimp.hunt((Ape) human);

会绕过它。

你所能做的就是在运行时检查它。

您可以通过一些工具,例如 Error Prone(由 Google 编写,我是一名员工,我做出贡献;其他工具可用)编写编译时检查以确保参数既不是 Ape 也不是 Human;但这超出了 "pure" Java.

的能力

您不能按照预期的方式排除特定的子类。但是,您可以做的是创建一个接口 'isHuntable',由您想要狩猎的所需动物实现,并将其用作方法的类型,而不是通用类型绑定。另一个可能不太优雅的解决方案是在您的层次结构中创建另一个级别,例如 'Monkeys',它从 Ape 扩展,所有动物都从它扩展,并且您将 Monkey 类型用于您的方法。不过,我会采用第一种方法。您可以使用显式类型检查,但您会在代码中违反 'open-closed' 原则,因此最好将这些检查用于类型系统。

只是对界面行为契约的概念进行了一些扩展,这是一种功能强大的工具,但遗憾的是未得到充分利用。你在 Apes 之间拥有的是一种 "is-a" 关系,它与你的对象紧密相连。另一方面,"Huntable" 并不是该层次结构的固有或定义特征,而是您希望添加到该层次结构子集的附加 condition/behaviour/check。通过向您打算拥有该行为的子集添加合同(接口实现)可以更好地实现这一点。这将允许在未来以最少的重构、最大的语义清晰度、更少的错误以及不将类型与行为紧密绑定的方式添加更多契约(接口实现)。简而言之,您不会违反 Liskov Substitution 原则,也不会违反开闭原则,也不会违反任何 SOLID 原则。

我会让你的 hunt 方法成为非静态的;毕竟,这是对对象本身的操作。然后,您可以定义所有 Apes 的行为:

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

当然,人类推翻了这个基本想法:

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

添加对 #isHuntable 的检查是可取的,因为与隐喻保持一致,很高兴知道您是否被允许在这样做之前打猎。此外,我对人类狩猎进行了例外处理,因为它违反了您(好吧,我)期望 #hunt 实现的一般行为。

为了过度工程和 SOLID 的各种概念,您可以使用 Huntable 接口实现此目的,该接口可以扩展 Living 接口:

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
}

总的来说,这让你的泛型变得不必要了。如果您走那条路,您将被迫进行类型转换和手动检查,如其他答案所示。因此,虽然它回避了最初的问题,但我认为这是解决潜在问题(XY 问题?)的更好方法。更重要的是,关于人类的行为仍然存在于描述人类的 class 中。

编辑:

为了与静态方法和检查异常保持一致,虽然我强烈建议反对它(根据我的评论),但您可以使用方法重载,因此 Human对象通过不同的签名:

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

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

HuntingException 将扩展 Exception 而不是 RuntimeExceptionException/Error/Throwable 您自己创建的对象本质上都是已检查的异常。除此之外,作为开发人员,您自己不会产生编译器错误。您可以生成警告,但您会面临同样的陷阱。

你可以看出为什么这很愚蠢,并且仍然可以通过强制转换在没有编译器错误的情况下猎杀人类:

Main.hunt((Ape) someHuman);

所以现在你回到在你的#hunt 方法中添加类型检查的兔子洞,这不会抛出编译器错误(或者必须 总是 抛出一个).如果你总是抛出它,那么使用你的代码的开发人员(很可能还有你自己)将在每次调用 #hunt 时自动填充 try-catch 块,只是为了处理 Human 的错误。这增加了很多"code noise",这只会让事情变得混乱。

简而言之,运行时异常是针对像 "hunting a human" 这样的开发人员错误的,并且有太多的理由可以列出为什么将其作为静态方法(更不用说 main class)从设计观点。