【问题标题】:Java - declaring from Interface type instead of ClassJava - 从接口类型而不是类声明
【发布时间】:2026-01-01 15:30:02
【问题描述】:

在寻求正确掌握接口最佳实践的过程中,我注意到以下声明:

List<String> myList = new ArrayList<String>();

而不是

ArrayList<String> myList = new ArrayList<String>();

-据我了解,原因是它提供了灵活性,以防有一天您不想实现 ArrayList 但可能是另一种类型的列表。

按照这个逻辑,我设置了一个例子:

public class InterfaceTest {

    public static void main(String[] args) {

        PetInterface p = new Cat();
        p.talk();

    }

}

interface PetInterface {                

    public void talk();

}

class Dog implements PetInterface {

    @Override
    public void talk() {
        System.out.println("Bark!");
    }

}

class Cat implements PetInterface {

    @Override
    public void talk() {
        System.out.println("Meow!");
    }

    public void batheSelf() {
        System.out.println("Cat bathing");
    }

}

我的问题是,我无法访问 batheSelf() 方法,因为它只存在于 Cat。这让我相信,如果我只使用在接口中声明的方法(而不是来自子类的额外方法),我应该只从接口声明,否则我应该直接从类声明(在本例中为 Cat)。我的假设是否正确?

【问题讨论】:

    标签: java interface


    【解决方案1】:

    当通过interfaceclassclass时,应该优选前者,但仅当存在适当的类型 em>时,应该首选。

    StringimplementsCharSequence 为例。在所有情况下,您不应该盲目地使用CharSequence 而不是String,因为这会拒绝您进行trim()toUpperCase() 等简单操作。

    但是,采用String 的方法只关心其char 值的序列应该改用CharSequence,因为在这种情况下这是合适的类型。事实上,replace(CharSequence target, CharSequence replacement)String 类中就是这种情况。

    另一个例子是java.util.regex.Pattern 及其Matcher matcher(CharSequence) 方法。这允许从Pattern 创建一个Matcher,不仅适用于String,还适用于所有其他CharSequence

    库中一个很好的例子,说明应该使用interface,但不幸的是没有使用,也可以在Matcher 中找到:它的appendReplacementappendTail 方法只接受StringBuffer .自 1.5 以来,此类已在很大程度上被其更快的表亲 StringBuilder 所取代。

    StringBuilder 不是StringBuffer,因此我们不能将前者与Matcher 中的append… 方法一起使用。但是,它们都是implementsAppendable(也在1.5中引入)。理想情况下,Matcherappend… 方法应该接受任何Appendable,然后我们就可以使用StringBuilder,以及所有其他可用的Appendable

    因此,我们可以看到当存在适当类型时如何通过接口引用对象是一种强大的抽象,但前提是这些类型存在。如果该类型不存在,那么如果有意义,您可以考虑定义自己的类型。例如,在这个Cat 示例中,您可以定义interface SelfBathable。然后,您可以接受任何SelfBathable 对象(例如Parakeet),而不是引用Cat

    如果创建新类型没有意义,那么您可以通过class 引用它。

    另见

    • Effective Java 2nd Edition,Item 52:通过接口引用对象

      如果存在适当的接口类型,则参数、返回值和字段都应使用接口类型声明。如果您养成使用接口类型的习惯,您的程序将会更加灵活。如果不存在适当的接口,则完全可以通过类引用对象。

    相关链接

    【讨论】:

    • 谢谢。这澄清了很多。
    • 您提到的@polygenelubricants - “例如,您可以定义接口SelfBathable。然后您可以接受任何SelfBathable对象(例如Parakeet)而不是指Cat”。由于我对java完全陌生,而且我很长一段时间都在想这个问题,我真的很想知道如何通过定义一个新的接口来解决这个问题。
    【解决方案2】:

    是的,你是对的。您应该声明为提供您使用的方法的最通用类型。

    这就是多态性的概念。

    【讨论】:

      【解决方案3】:

      您是正确的,但如果需要,您可以从界面投射到所需的宠物。例如:

      PetInterface p = new Cat();
      ((Cat)p).batheSelf();
      

      当然,如果您尝试将宠物扔给狗,则无法调用 batheSelf() 方法。它甚至不会编译。所以,为了避免问题,你可以有这样的方法:

      public void bathe(PetInterface p){
          if (p instanceof Cat) {
              Cat c = (Cat) p;
              c.batheSelf();
          }
      }
      

      使用instanceof 时,请确保在运行时不会尝试让狗自己洗澡。这会引发错误。

      【讨论】:

      • 可以这样做,但这是个坏主意。例如,在这种情况下,它将只有猫可以自己洗澡的假设嵌入到 bathe 方法中。
      • 我可以看到第二个代码 sn-p 是个坏主意的原因,但是第一个代码 sn-p 是否可取?它对我有用,但我想知道这是否是好的做法?
      【解决方案4】:

      是的,你是对的。通过让 Cat 实现“PetInterface”,您可以在上面的示例中使用它并轻松添加更多种类的宠物。如果您确实需要特定于 Cat,则需要访问 Cat 类。

      【讨论】:

        【解决方案5】:

        您可以在 Cat 中从 talk 调用方法 batheSelf

        【讨论】:

          【解决方案6】:

          通常,您应该更喜欢接口而不是具体类。按照这些思路,如果您可以避免使用 new 运算符(它总是需要一个具体的类型,就像在您的新 ArrayList 示例中一样),那就更好了。

          这一切都与管理代码中的依赖关系有关。最好只依赖高度抽象的东西(如接口),因为它们也往往非常稳定(参见http://objectmentor.com/resources/articles/stability.pdf)。因为它们没有代码,所以只有在 API 更改时才必须更改它们……换句话说,当您希望该接口向世界呈现不同的行为时,即设计更改时。

          另一方面,课程一直在变化。依赖于类的代码并不关心它是如何做的,只要 API 的输入和输出不改变,调用者就不应该关心。

          你应该努力根据开闭原则确定你的类的行为(参见http://objectmentor.com/resources/articles/ocp.pdf),这样即使你添加了功能,现有的接口也不需要改变,你可以指定一个新的子接口。

          避免使用新运算符的旧方法是使用抽象工厂模式,但这也带来了一系列问题。更好的是使用像 Guice 这样的工具来进行依赖注入,并且更喜欢构造函数注入。在开始使用依赖注入之前,请确保您了解依赖倒置原则(请参阅http://objectmentor.com/resources/articles/dip.pdf)。我见过很多人注入了不适当的依赖项,然后抱怨该工具对他们没有帮助......它不会让你成为一名优秀的程序员,你仍然必须适当地使用它。

          示例:您正在编写一个帮助学生学习物理的程序。在这个程序中,学生可以将球放在各种物理场景中并观察它的行为:从悬崖上的大炮中射出它,把它放在水下,在深空等。问题:你想包括一些关于重量的东西Ball API 中的球...应该包含 getMass() 方法还是 getWeight() 方法?

          重量取决于球碰巧所处的环境。调用者可以方便地调用一个方法并获得球碰巧在哪里的重量,但是如何编写此方法?每个球实例必须不断跟踪它的位置以及当前的引力常数是多少。所以你应该更喜欢 getMass(),因为质量是球的内在属性,不依赖于它的环境。

          等等,如果你只使用 getWeight(Environment) 会怎样?这样,球实例就可以将其当前的 g 从环境中取出并继续……更好的是,您可以使用 Guice 在 Ball 的构造函数中注入 Environment!这是我经常看到的滥用类型,人们最终指责 Guice 无法像他们希望的那样无缝地处理依赖注入。

          问题不在于 Guice,而在于 Ball API 设计。重量不是球的固有属性,因此它不是应该从球中获得的属性。相反,Ball 应该使用 getMass() 方法实现 MassiveObject 接口,并且 Environment 应该有一个名为 getWeightOf(MassiveObject) 的方法。环境固有的是它自己的引力常数,所以这要好得多。而Environment现在只依赖于一个简单的接口,MassiveObject……但它的工作是包含对象,所以这是应该的。

          【讨论】:

            【解决方案7】:

            为什么不简单地这样做!

            Cat c = new Cat();
            PetInterface p = (PetInterface)c;
            p.talk();
            c.batheSelf();
            

            现在我们有了一个对象,可以使用 2 个引用对其进行操作。
            引用 p 可用于调用接口中定义的函数,而 c 只能用于调用类(或超类)中定义的函数。

            【讨论】: