Java最佳实践:对象转换 vs 接口转换

7
假设我们有以下玩具接口:
interface Speakable
{
    public abstract void Speak();
}

interface Flyer
{
    public abstract void Fly();
}

我们有一个实现了这两个接口的类:

class Duck implements Speakable, Flyer
{
    public void Speak()
    {
        System.out.println("quack quack don't eat me I taste bad.");
    }

    public void Fly()
    {
        System.out.println("I am flying");
    }
}

目前我看到有多种方法可以在Duck上调用方法,但我无法决定哪种是最佳实践。考虑以下情况:

public class Lab 
{
        private static void DangerousSpeakAndFly(Object x)
        {
            Speakable temp  = (Speakable) x;
            temp.Speak();
            Flyer temp2= (Flyer) x;
            temp2.Fly();
        }

        public static void main(String[] args) 
        {
            Duck daffy= new Duck();
            DangerousSpeakAndFly(daffy);
        }
}

这个程序会按预期运行,因为传递给函数的对象恰好可以转换为FlyerSpeakable,但我看到这样的代码会感到不安,因为它不允许编译时类型检查,由于紧密耦合,当传递不同类型的对象(不能转换为任一接口)作为参数时,或者如果Duck的实现在以后更改,不能再实现Flyer时,它可能会抛出意外异常。
我经常看到像这样的Java代码,有时甚至出现在教科书中(例如O'Reilly的《Head First Design Patterns》第300页),所以我肯定没有领会其中的优点。
如果我要编写类似的代码,我会尽量避免向不能保证的类型或接口进行向下转型。例如,在这种情况下,我会这样做:
interface SpeakingFlyer extends Flyer, Speakable
{

}

class BuzzLightyear implements SpeakingFlyer
{
    public void Speak()
    {
        System.out.println("My name is Buzz");
    }
    public void Fly()
    {
        System.out.println("To infinity and beyond!");
    }
}

这将使我能够:

private static void SafeSpeakAndFly(SpeakingFlyer x)
{
    x.Speak();
    x.Fly();
}

public static void main(String[] args) 
{
    BuzzLightyear bly= new BuzzLightyear();
    SafeSpeakAndFly(bly);
}

这是否是一种不必要的过度设计?这样做有哪些缺陷?

我认为这种设计将SafeSpeakAndFly()函数与其参数分离,并通过编译时类型检查可避免出现讨厌的错误。

为什么第一种方法在实践中被广泛使用,而后者则不是呢?


我几年前也曾经问过自己同样的问题,而这篇帖子就是答案... https://dev59.com/3XRC5IYBdhLWcg3wMd5S#384067 - ΦXocę 웃 Пepeúpa ツ
2个回答

12
我经常看到像这样编写的Java代码,有时甚至在教科书中(例如O'Reilly出版的《Head First Design Patterns》第300页),所以我肯定错过了其中的优点。
该书最初于2004年出版,当时Java可能还不支持泛型。因此,不安全的强制转换是非常常见的。也许,如果我在Java中没有参数化多态性的支持,我会先检查参数是否是我想要转换的类型的实例,然后再进行实际的转换:
private static void dangerousSpeakAndFly(Object x) {
    if (x instanceof Speakable) {
        Speakable temp  = (Speakable) x;
        temp.Speak();
    }
    if (x instanceof Flyer) {
        Flyer temp2= (Flyer) x;
        temp2.Fly();
    }
}

有了泛型,我们可以做到这一点:
private static <T extends Speakable & Flyer> void reallySafeSpeakAndFly(T x) {
    x.Speak();
    x.Fly();
}

在这里,编译器可以确保我们没有传递未实现 SpeakableFlyer 接口的对象,并且可以在编译时检测到此类无礼的尝试。

为什么第一种方法在实践中被广泛使用,而后者则不是呢?

可能是因为你看到了很多遗留代码,我猜测。 :)


3
非常信息丰富且具有指导意义。噢,对了,修复损坏的遗留代码= 我生活中的故事。 - ForeverStudent

7

您可以使用类型交集使方法具有通用性,强制参数同时是可说话的(Speakable)飞行器(Flyer)

private <T extends Speakable & Flyer> static void DangerousSpeakAndFly(T x) { 
    // use any of `Speakable` or `Flyer` methods of `x`
}

因此,您不需要进行强制转换或创建额外的接口。

很好的解释 +1 第一个答案 - ForeverStudent
此外,我建议接受@Konstantin的答案,因为它更具信息量和解释性。 - Alex Salauyou

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接