如何使用编译时泛型验证替换运行时instanceof检查

时间:2012-11-29 22:57:06

标签: java validation generics instanceof

真正的Java Generics专家有一点难题......;)

假设我有以下两个接口:

interface Processor {
    void process(Foo foo);
}

interface Foo {
    Processor getProcessor();
}

,例如,以下两个实现类:

static class SomeProcessor implements Processor {
    static final SomeProcessor INSTANCE = new SomeProcessor();

    @Override
    public void process(Foo foo) {
        if (foo instanceof SomeFoo) { // <-- GET RID OF THIS ?
            // process ((SomeFoo) foo)
        }
    }
}

class SomeFoo implements Foo {
    @Override
    public Processor getProcessor() {
        return SomeProcessor.INSTANCE;
    }
}

是否有某种方法可以使这两个接口通用,这样我就不需要在instanceof函数中进行标记process()检查,并且仍然在我的代码中的其他地方使用以下构造?

foo.getProcessor().process(foo);

(当然,我不知道我正在处理的Foo的子类是什么)

换句话说:我正在寻找一种在对象中定义函数的方法,使得它只能返回另一个处理包含该函数的对象类型的对象。注意:我不只是讨论处理包含函数的对象的最小公分母超类(上图:Foo),而是该对象的实际类(上图:SomeFoo)。

(除非我现在真的很傻,否则这听起来并不是那么微不足道......)

6 个答案:

答案 0 :(得分:2)

使用泛型可以做的是:

interface Processor<T extends Foo> {
  void process(T foo);
}

interface Foo {
  Processor<? extends Foo> getProcessor();
}

然后

class SomeProcessor implements Processor<SomeFoo> {
  public void process(SomeFoo foo) {
    // TODO
  }
}

class SomeFoo implements Foo {
  public Processor<? super SomeFoo> getProcessor() {
    return processor;
  }
}

这显然意味着你需要一个带有通配符的对应Foo.setProcessor(),这反过来意味着你最终会在某个地方进行未经检查的强制转换。从语言的角度来看,这是不安全的,没有办法解决这个问题。

您可以使用超类型令牌检查处理器实例化,但这会在运行时发生,因此在编译时您无法保证API被滥用。只需记录下来就可以了。

This pastie说明了我的想法。您无法对此问题进行建模以在编译时具有类型安全性,因为Foo接口需要一种方法来声明方法返回使用当前接口实现实例化的泛型类型。这会破坏继承,无法用Java完成。

然而,使用super type tokens,如果处理器是正确的类型,则可以检查在运行时始终保证语言级别,如果您在API文档中明确说明只有处理者有权在setProcessor()实例上调用Foo。如果客户端程序员违反并使用不正确的类型调用setProcessor(),那么您的类仍然会抛出异常,但是在运行时。

附录:为什么不应该参数化Foo

我想添加一个小段来解释为什么我不喜欢meriton的答案(当前接受的答案),以及涉及参数化Foo类型以使其变为Foo<T>的所有其他答案。

软件失败:这不是一件坏事或不寻常的事,只是发生了。它越早越好,所以我们可以修复它并避免损失(通常是金钱,但实际上是有人可能关心的)。这是今天选择Java的一个令人信服的理由:因为在编译时检查了类型,所以一大类错误永远不会生产。

这是泛型进入的地方:另外一整类错误被遗漏了。回到你的情况,有人建议在Foo中添加一个类型参数,这样你就可以在编译时获得类型安全性。但是,使用meriton的实现,可以编写绕过编译器检查的不合逻辑的代码:

class SomeFoo implements Foo<WrongFoo> {}

有人可能会说,判断程序是否语义正确并不是编译器的工作 - 而且我同意 - 但是一个好的语言可以让编译器发现问题并提示解决方案。在这里使用类型参数只是大喊一个明智的软件。

最好不要依赖脆弱的约定,而是最好坐下来关闭IDE并思考如何改进设计,以便客户端代码不会失败。最终,这是软件失败的主要原因:不是因为语言是强烈或动态类型的,而是因为开发人员误解了API。使用强类型语言是一种防止一种类型的误用的方法。

您的界面非常奇怪,因为它定义了getProcessor(),但没有说明谁负责设置处理器。如果它是提供自己的处理器的Foo,那么类型安全只会被一个非常愚蠢的开发人员打破;但由于它可以在运行时检查(参考我的带有超类型令牌的演示),因此可以通过良好的开发过程(单元测试)轻松保证。如果定义setProcessor()或等价物,结论不会改变太多。

您正在寻找的API无法用Java描述 - 顺便说一下,我认为对于每一种面向对象的语言都是如此,因为它打破了主要的继承规则,即父类不了解他们的孩子(这反过来带来多态性)。使用通配符(正如我在演示中暗示的那样)是您可以使用Java泛型的最接近的,提供类型安全性并且易于理解。

我鼓励您只向处理器添加一个类型参数,并为您的代码编写好的文档和单元测试,而不是以类型安全的名义强制执行Java规则。在这里买任何东西。

答案 1 :(得分:2)

从类型安全的角度来看,所有递归边界的东西都是不必要的。就这样就足够了:

interface Processor<F> {
    void process(F foo);
}

interface Foo<F> {
    Processor<F> getProcessor();
}

class SomeFoo implements Foo<SomeFoo> {
    @Override
    SomeProcessor getProcessor() { ... }
}

class SomeProcessor implements Processor<SomeFoo> {
    @Override
    void process(SomeFoo foo) { ... }
}

// in some other class:
<F extends Foo<? super F>> void process(F foo) {
    foo.getProcessor().process(foo);
}
  

换句话说:我正在寻找一种方法来定义一个函数   对象使得它只能返回另一个处理它的对象   包含该函数的对象的类型。注意:我不只是说话   关于处理一些最不常见的分母超类   包含函数的对象(上图:Foo),但该对象是实际的   class(上图:SomeFoo)。

用Java声明是不可能的。相反,如上所示,您可以在类外部使用泛型方法(或者是该类的静态方法),它同时接受对象和处理器,并在两者上强制执行类型。


更新:如果您希望能够使用process调用Foo,那么我们也可以将meriton的getThis()概念集成到此代码中(同样,没有递归边界):

interface Foo<F> {
    Processor<F> getProcessor();
    F getThis();
}

class SomeFoo implements Foo<SomeFoo> {
    SomeProcessor getProcessor() { ... }
    SomeFoo getThis() { return this; }
}


// in some other class:
<F> void process(Foo<F> foo) {
    foo.getProcessor().process(foo.getThis());
}

答案 2 :(得分:1)

不,这不是Java Generics的目的。

另外,在我看来,需要进行类型检查表明设计存在问题。 我建议尝试重新设计它,这样就没有必要了。

答案 3 :(得分:1)

interface Processor<F extends Foo<F>> {
    void process(F fooSubclass);
}

interface Foo<F extends Foo<F>> {
    Processor<F> getProcessor();
}

我没有测试过这是完全正确的,但是这种具有泛型类型的模式本身可能与编译时Java泛型一样接近。

答案 4 :(得分:1)

这比我想象的要糟糕。我的看法:

interface Processor<F extends Foo<F>> {
    void process(F foo);
}

interface Foo<F extends Foo<F>> {
    Processor<F> getProcessor();
}

interface SomeFoo extends Foo<SomeFoo> {
    @Override
    SomeProcessor getProcessor();
}

interface SomeProcessor extends Processor<SomeFoo> {
    @Override
    void process(SomeFoo foo);
}

现在,以下将编译:

<F extends Foo<F>> void process(F foo) {
    foo.getProcessor().process(foo);
}

void process(Foo<?> foo) {
    foo.getProcessor().process(foo);
}

没有,因为编译器无法知道传递的foo的实际类型是其类型参数的子类型,因为有人可以写:

    class Bar implements Foo<SomeFoo> { ... }

我们可以通过要求foo的子类型实现对其类型参数的转换来解决这个问题:

abstract class Foo<F extends Foo<F>> {
    abstract Processor<F> getProcessor();

    abstract F getThis();
}

class SomeFoo extends Foo<SomeFoo> {
    @Override
    SomeFoo getThis() {
        return this;
    }

    @Override
    Processor<SomeFoo> getProcessor() {
        return new SomeProcessor();
    }
}

现在,我们可以写:

<F extends Foo<F>> void process(Foo<F> foo) {
    foo.getProcessor().process(foo.getThis());
}

并使用

调用此方法
Foo<?> foo = ...;
process(foo);

为了方便使用,我建议将辅助方法移到Foo类中:

abstract class Foo<F extends Foo<F>> {
    abstract Processor<F> getProcessor();

    abstract F getThis();

    void processWith(Processor<F> p) {
        p.process(getThis());
    }
}

更新:我认为newaccts更新的答案显示了一个更优雅的解决方案,因为它不需要递归类型边界。

答案 5 :(得分:1)

以下是您的代码完全“通用化”,但稍有改动:INSTANCE变量不是静态的。

interface Processor<T extends Foo<T>> {
    void process(T foo);
}

interface Foo<T extends Foo<T>> {
    Processor<T> getProcessor();
}

static class SomeProcessor<T extends Foo<T>> implements Processor<T> {

    final SomeProcessor<T> INSTANCE = new SomeProcessor<T>();

    @Override
    public void process(T foo) {
        // it will only ever be a SomeFoo if T is SomeFoo
    }
}

class SomeFoo implements Foo<SomeFoo> {
    @Override
    public Processor<SomeFoo> getProcessor() {
        return new SomeProcessor<SomeFoo>().INSTANCE;
    }
}

没有编译器错误或警告。

INSTANCE是一个实例变量,因为类类型没有通过静态任何东西。如果你真的只想要一个INSTANCE,请使用该类的单例模式。