2

对真正的 Java 泛型专家有一点困惑... ;)

假设我有以下两个接口:

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)。

(这远没有听起来那么微不足道,除非我现在真的很愚蠢......)

4

6 回答 6

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 被滥用。尽你所能记录下来。

这个馅饼说明了我的想法。您不能将此问题建模为在编译时具有类型安全性,因为Foo接口需要一种方法来声明方法返回用当前接口实现实例化的泛型类型。这会破坏继承,并且不能在 Java 中完成。

但是,使用超类型令牌,您可以在运行时检查处理器是否是正确的类型,如果您在 API 文档中明确声明只有处理器有权调用实例,则始终在语言级别保证这一点。如果客户端程序员不服从并使用不正确的类型调用,您的类仍然会抛出异常,但在运行时。setProcessor()FoosetProcessor()

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

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

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

这就是泛型的用武之地:另一整类错误被排除在外。回到你的例子,有人建议添加一个类型参数,Foo这样你就可以在编译时获得类型安全。然而,使用 Meriton 的实现,可以编写绕过编译器检查的不合逻辑的代码:

class SomeFoo implements Foo<WrongFoo> {}

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

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

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

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

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

于 2012-11-29T23:12:28.590 回答
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),而是该对象的实际类(上图:SomeFoo)。

这是不可能在 Java 中声明的。相反,正如您在上面看到的,您可以在类之外有一个通用方法(或者是该类的静态方法),它同时接受对象和处理器,并在两者上强制执行类型。


更新:如果您希望能够process使用 any调用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());
}
于 2012-11-30T10:27:55.217 回答
1

不,这不是 Java 泛型的设计目的。

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

于 2012-11-29T23:04:17.497 回答
1
interface Processor<F extends Foo<F>> {
    void process(F fooSubclass);
}

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

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

于 2012-11-29T23:10:29.730 回答
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 更新的答案显示了一个更优雅的解决方案,因为它不需要递归类型边界。

于 2012-11-30T00:05:47.777 回答
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,请在类上使用单例模式。

于 2012-11-30T00:09:36.517 回答