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

For*_*ent 7 java oop inheritance casting downcast

假设我们有以下玩具界面:

interface Speakable
{
    public abstract void Speak();
}

interface Flyer
{
    public abstract void Fly();
}
Run Code Online (Sandbox Code Playgroud)

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

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");
    }
}
Run Code Online (Sandbox Code Playgroud)

在这一点上,我看到了不同的方法来调用方法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);
        }
}
Run Code Online (Sandbox Code Playgroud)

这个程序将按预期运行,因为传递给函数的对象恰好可以转换为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!");
    }
}
Run Code Online (Sandbox Code Playgroud)

这将允许我这样做:

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

public static void main(String[] args) 
{
    BuzzLightyear bly= new BuzzLightyear();
    SafeSpeakAndFly(bly);
}
Run Code Online (Sandbox Code Playgroud)

这是不必要的矫枉过正吗?这样做有什么陷阱?

我觉得这个设计将SafeSpeakAndFly()函数与其参数分离,并且由于编译时类型检查而保持令人讨厌的错误.

为什么第一种方法在实践中如此广泛地使用而后者不是?

Kon*_*kov 11

我看到Java代码一直都是这样编写的,有时候是在教科书中(例如O'Reilly的"Head First Design Patterns"第300页)所以我必须要有一个我缺失的优点.

这本书最初发表于2004年,我认为Java当时并不支持Generics.因此,不安全的铸造是当时非常常用的.可能,如果我没有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();
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,拥有泛型可以让我们这样做:

private static <T extends Speakable & Flyer> void reallySafeSpeakAndFly(T x) {
    x.Speak();
    x.Fly();
}
Run Code Online (Sandbox Code Playgroud)

在这里,编译器可以确保我们不及格的东西,不执行SpeakableFlyer,可以检测到这种时髦的尝试在编译时.

为什么第一种方法在实践中如此广泛地使用而后者不是?

我想,你可能已经看过很多遗留代码了.:)

  • 非常翔实和有启发性.哦,是的,修复破碎的遗留代码=我生命中的故事 (2认同)

Ale*_*you 7

可以执行一个参数是在同一时间SpeakableFlyer制造通用型交叉点的方法:

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

因此,您不需要转换或创建其他接口.