合理的“instanceof”?将它与接口而不是实现类型一起使用

Justified `instanceof`? Using it with an interface but not an implementation type

当代码中包含 Java instanceof 运算符时,许多人会扬起眉毛说这是禁忌。例如在这个other SO Q&A中,答案是:

Note that if you have to use that operator very often it is generally a hint that your design has some flaws. So in a well designed application you should have to use that operator as little as possible (of course there are exceptions to that general rule).

不过,instanceof什么时候可以用,什么时候不能用

,就不详细说了。

我对此进行了一些思考,并阐明了以下准则。我认为这可能已经在 Internet 上的某个地方讨论过,但我找不到。因此提出这个问题并征求您的意见:

Using instanceof on an interface is okay; using instanceof on an implementation is not okay

这里有一个关于 "okay" 案例的例子。

示例:动物目录,其中一些(但不是全部)会飞

Animal.java

public interface Animal {
    String getName();
    String makeNoise();
}

CanFly.java

public interface CanFly {
    float getMaxInAirDistanceKm();
}

Cat.java

public class Cat implements Animal {
    @Override
    public String getName() {
        return "Cat";
    }

    @Override
    public String makeNoise() {
        return "meow";
    }
}

BaldEgale.java

public class BaldEagle implements Animal, CanFly {
    @Override
    public String getName() {
        return "BaldEagle";
    }

    @Override
    public String makeNoise() {
        return "whistle";
    }

    @Override
    public float getMaxInAirDistanceKm() {
        return 50;
    }
}

Catalog.java

import java.util.ArrayList;
import java.util.List;

public class Catalog {
    private List<Animal> animals = new ArrayList<>();

    public void putAnimal(Animal animal) {
        animals.add(animal);
    }

    public void showList() {
        animals.forEach(animal -> {
            StringBuilder sb = new StringBuilder();
            sb.append(animal.getName() + ": ");
            sb.append(animal.makeNoise() + " ");

            // this block exemplifies some processing that is 
            //   specific to CanFly animals
            if (animal instanceof CanFly) {
                sb.append(String.format(" (can stay in air for %s km)",
                        ((CanFly) animal).getMaxInAirDistanceKm()));
            }
            System.out.println(sb.toString());
        });
    }

    public static void main(String[] args){

        Catalog catalog = new Catalog();
        Cat cat = new Cat();
        BaldEagle baldEagle = new BaldEagle();
        catalog.putAnimal(cat);
        catalog.putAnimal(baldEagle);

        catalog.showList();
    }
}

测试输出

Cat: meow 
BaldEagle: whistle  (can stay in air for 50.0 km)

2019-10-09 更新 添加 "not-okay" 案例的示例:

我们可以删除 CanFly 接口,在 showList() 方法中,我们将 instanceof 应用于具体实现 BaldEagle —— 像这样:

    public void showList() {
        animals.forEach(animal -> {
            StringBuilder sb = new StringBuilder();
            sb.append(animal.getName() + ": ");
            sb.append(animal.makeNoise() + " ");

            if (animal instanceof BaldEagle) {
                sb.append(String.format(" (can stay in air for %s km)",
                        ((BaldEagle) animal).getMaxInAirDistanceKm()));
            }
            System.out.println(sb.toString());
        });
    }

这种方法不好,因为代码现在依赖于实现,而不是接口。例如,它可以防止换出另一个代表 Bald Eagle 的实现(例如 BaldEagleImpl

我认为人们认为总有一个 "cleaner" 解决方案可以产生您想要的那种行为。

在您的示例中,我会说使用访问者设计模式在不使用 instanceOf 的情况下完全相同:

public interface Animal {
    String getName();
    String makeNoise();
    void accept(AnimalVisitor v);
}

public interface AnimalVisitor() {
    void visit(Cat a);
    void visit(BaldEagle a);
}

public interface CanFly {
    float getMaxInAirDistanceKm();
}

public class Cat implements Animal {
    void accept(Visitor v) {
        v.visit(this);
    }
}

public class BaldEagle implements Animal, CanFly {
    void accept(Visitor v) {
        v.visit(this);
    }
}

public class DisplayVisitor implements AnimalVisitor  {
    void visit(Cat a) {
       //build & display your string
    }

    void visit(BaldEagle a) {
       //build & display your string
    }
}

public class Catalog {
    private List<Animal> animals = new ArrayList<>();

    public void putAnimal(Animal animal) {
        animals.add(animal);
    }

    public void showList() {
        DisplayVisitor display = new DisplayVisitor();
        animals.forEach(a->a.accept(display));
    }
}

虽然我没有完全回答你的问题,但它表明在大多数情况下无需使用 instanceOf 即可完成相同的行为,只需以 OOP 方式思考并使用已知模式即可。

However, it does not further elaborate when the use of instanceof is okay

这不是您问题的直接答案,但我认为 instanceof 仅适用于所有其他选项都不可行的情况。

Using instanceof on an interface is okay; using instanceof on an implementation is not okay

我将其重新表述为 "using instanceof on an interface is less bad than using instanceof on an implementation",但这只是强耦合不好的一般规则的必然结果。通常有更好的选择。

当你想使用instanceof时,你应该首先考虑引入额外的接口或接口方法或者使用访问者模式(参见)。所有这些选项都是在 Java.

中实现所需行为的更简洁的方法

这并不总是很优雅,可能需要人工接口或导致接口膨胀,这就是为什么其他一些语言支持临时联合类型和代数数据类型的原因。但是 instanceof 也不是很好的模拟方式,因为 Java 的类型系统不会帮助您确保处理所有可能的选项。

首先,重要的是要注意面向对象编程范例是抵抗类型检查的来源,例如 instanceof。其他范例不一定具有这种阻力,甚至可能鼓励类型检查。所以这个问题真的只有在你尝试做 OOP 时才有意义。

如果您正在尝试进行 OOP,那么您应该尽可能多地利用多态性。多态性是 OOP 的主要武器。类型检查是多态性的对立面。

当然,抽象的类型检查比具体实现的类型检查更可取;但这只是重申依赖倒置原则(依赖于抽象,而不是具体化)。

在 OOP 中,每一次类型检查的使用都可以看作是错失了多态性的机会。