使用超类类型作为子类实例

9
我知道这个问题被问过很多次,但通常的答案在我看来都不够令人满意。
给定以下类层次结构:
class SuperClass{}
class SubClass extends SuperClass{}

为什么人们使用这种模式来实例化子类:
SuperClass instance = new SubClass();

请使用这个:

SubClass instance = new SubClass();

现在,我通常看到的答案是这样做是为了将instance作为参数发送到需要超类实例的方法中,就像这里一样:

void aFunction(SuperClass param){}

//somewhere else in the code...
...
aFunction(instance);
...

但是我可以将SubClass的实例发送到aFunction,而不管它所持有的变量类型!这意味着以下代码将编译并且不会出现错误(假设先前提供了aFunction的定义):

SubClass instance = new SubClass();
aFunction(instance);

实际上,据我所知,在运行时变量类型是没有意义的。它们仅由编译器使用!

将变量定义为SuperClass的另一个可能的原因是,如果它有几个不同的子类,并且该变量应在运行时切换到其中的几个子类引用,但例如我只看到这种情况发生在类中(不是超级,不是子类。只是类)。绝对不足以需要一般模式...


这是因为他们不想知道实例是哪种子类。他们只关心超类的上下文。 - Patryk Dobrowolski
换句话说,它取决于你目前的需求,而不是最佳实践。对吗? - user1545072
5个回答

8
这种编码方式的主要论据是基于 Liskov替换原则,该原则指出,如果X是类型T的子类型,则任何T类型的实例都应该能够被X所替换。
这样做的好处很简单。假设我们有一个程序,它有一个属性文件,看起来像这样:
mode="Run"

你的程序应该长这样:

public void Program
{
    public Mode mode;

    public static void main(String[] args)
    {
        mode = Config.getMode();
        mode.run();
    }
}

简单来说,这个程序将使用配置文件定义程序启动的模式。在Config类中,getMode()可能像这样:

public Mode getMode()
{
    String type = getProperty("mode"); // Now equals "Run" in our example.

    switch(type)
    {
       case "Run": return new RunMode();
       case "Halt": return new HaltMode();  
    }
}
为什么这样做是必要的 现在,因为您拥有类型为Mode的引用,只需更改mode属性的值就可以完全改变程序的功能。如果使用public RunMode mode,则无法使用此类型的功能。 为什么这是一个好事情 这种模式非常流行,因为它使程序具有可扩展性。这意味着,如果作者想要实现这种功能,只需要进行最小量的更改即可。我是说,您只需更改配置文件中的一个单词,就可以完全改变程序的流程,而不需要编辑任何一行代码。这是可取的。

这确实是一个很好的答案,但这里的优势取决于特定的设计(例如将引导模式的实现作为自定义对象而不是HashMap),但我可以看出模块化/可扩展的应用程序可以从这种做法中受益。感谢您的回答。 - user1545072
另外,这个问题(像其他大多数答案一样)处理的是参数而不是变量。所以基本上它没有回答我的问题。但我想真正的答案是局部变量对此没有任何好处(除非它们的意思是保存参数的值),而字段显然与公共方法有着深刻的联系,就像获取器和设置器中一样,所以无论参数是否可以从成为超类中获益,字段也必须成为超类才能获得。 - user1545072
1
在我看来,我认为更重要的是遵循这些实践,这样将来就会为您打开这些大门。如果您深入了解一个应用程序,并且您的老板转身说“如果我在这里输入不同的单词,我希望程序完全改变”,而您一直遵循这些原则,那么您已经为此奠定了所有基础。 - christopher

3
在许多情况下,这并不真的很重要,但被视为良好的风格。您将提供给对参考文献用户的信息限制在必要的范围内,即它是SuperClass类型的实例。变量引用的是SuperClassSubClass类型的对象并不(也不应该)重要。
更新:
这也适用于从未用作参数等的局部变量。正如我所说,通常并不重要,但被视为良好的风格,因为您稍后可能会将变量更改为保存参数或超类型的另一个子类型。在这种情况下,如果您先使用了子类型,则您进一步的代码(在该单个范围内,例如方法)可能无意中依赖于一个特定子类型的API,并且将变量更改为保存另一个类型可能会破坏您的代码。
我将扩展Chris的示例:
考虑以下内容:
RunMode mode = new RunMode();

...

您目前可能依赖于mode是一个RunMode的事实。

然而,稍后您可能想要更改该行为:

RunMode mode = Config.getMode(); //breaks

糟糕,它无法编译。好的,让我们进行更改。

Mode mode = Config.getMode(); 

这行代码现在可以编译,但是你的后续代码可能会出现问题,因为你偶然依赖于modeRunMode的一个实例。请注意,它可能会在运行时崩溃或破坏您的逻辑。


很棒的答案,但我并没有询问方法签名。我询问的是使用我在原始帖子中描述的特定设计... - user1545072
@YekhezkelYovel 我也没有谈论方法签名 ;) 在一个方法中,还有使用本地变量的用户,他们不需要或者不应该知道确切的类型。 - Thomas
你确实做到了 :) 'Config.getMode()' 不是一个实例化,而是一个静态函数调用。它返回在其签名中声明的类型的实例,因此你谈论签名。不过你没有谈论方法,我的错。 ;) - user1545072
在一个方法中,如果不知道类型,只有在返回值时才有意义 - 而不是在本地实例化时。这就是为什么你谈论函数调用而不是实例化。这也是我发表烦人评论的原因 :) - user1545072
如果你只谈论实例化,那么是的,你不需要将新实例分配给超类型的变量。但是你的代码通常不仅涉及实例化,而且可能会在以后发生变化(这就是我更新的重点),这就是为什么使用超类型被认为是良好风格的原因。顺便说一下,你并不是在谈论实例化,而是在谈论赋值。;) - Thomas
@YekhezkelYovel,也许我真的不明白,因为:1.使用实例化(new SubClass())没有绕过使用子类型的方法,2.您的示例是关于赋值(SuperClass instance = ...)-但尽管如此,我引用您的话:“...需要一个通用模式...”->没有这种要求,这只是一种常见的约定和合理的约定,但您不必遵守它。 - Thomas

2

SuperClass instance = new SubClass1()

几行代码后,你可能会写 instance = new SubClass2();

但如果你这样写,SubClass1 instance = new SubClass1();

几行代码后,你就不能写 instance = new SubClass2()


仔细阅读我的帖子,我提到了这个答案并说明了为什么它不能令我满意。 - user1545072

1
它被称为多态性,是一个超类引用指向子类对象。
In fact, AFAIK variable types are meaningless at runtime. They are used 
only by the compiler!

我不确定你是从哪里读到这个的。在编译时,编译器只知道引用类型的类(因此在多态情况下是超类,正如您所述)。在运行时,Java 知道对象的实际类型(.getClass())。在编译时,Java 编译器仅检查被调用的方法定义是否在引用类型的类中。要调用哪个方法(函数重载)是根据对象的实际类型在运行时确定的。

Why polymorphism?

希望你能够在谷歌上寻找更多相关内容,以下是一个例子。你有一个通用方法draw(Shape s)。现在形状可以是RectangleCircle或任何CustomShape。如果你不在draw()方法中使用形状引用,你将不得不为每种类型的(子类)形状创建不同的方法。


谢谢你关于多态性的课程,但如果你看一下我给你的标签,你会发现其中一个是“多态性”。我知道多态性,我的论点是我提到的这种编码实践与多态性无关。如果你想知道我为什么这样认为,请再次查看我的原始帖子... - user1545072
我猜你是在我回答之后添加了最后一段,或者我没有注意到它。无论如何,重点是它完全是针对设计的特定情况。现实软件世界中发生的情况是软件不是一次性创建的。它是发布和升级的周期。你在一个类(没有子类、父类)中看到了这种模式,可能是因为这是一种通常预防措施(或者从某种意义上来说是一种设计惯例),假设代码将来会被修改,并且有可能在后期出现继承的情况。 - Aniket Thakur
1
换句话说,它取决于您目前的需求,而不是最佳实践,对吗?是的,它取决于您目前的需求,如果您确定代码的这个方面将来不会被修改,那么就可以这样设计。但是一般的约定是使用多态引用。 - Aniket Thakur
正如你所看到的,我从未编辑过原始帖子,所以你一定错过了。关于真实的软件世界,我想这取决于你创建的应用程序类型,无论是桌面应用、移动应用、服务器应用,它们建立在什么平台上等等。也许你一直使用这种做法,而其他人可能不会。我个人之所以问这个问题,是因为我在SO上经常看到这种做法,但在工作中却没有看到,这让我想知道那些使用它的人为什么这样做。 - user1545072
另外,你还将它带到了方法签名中,这也不是我的问题(虽然我确实也使用多态参数甚至接口类型参数,但我的问题是关于变量的声明,而不是参数)。 - user1545072
如果严格地说是关于声明变量,那么是的,你是正确的,这是一个选择问题,没有普遍的模式。 - Aniket Thakur

0
这是从设计角度来看的,你将拥有一个超类和可以扩展功能的多个子类。
需要编写子类的实现者只需关注要覆盖哪些方法。

好的,我猜这里几乎每个人都看过这个答案并跳过了自己的回答,因为他们大多数基本上说了同样的事情。再次提到,我在我的帖子中提到了这个答案,并解释了为什么它对我来说不够好。基本上,这取决于您代码的目标。如果您不编写可扩展的应用程序,则此操作将变得不必要且繁重。而且您确实说过“这是从设计角度来看的”,所以我基本上同意您的观点。谢谢。 - user1545072

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