关于OOP:接口和抽象类在Java中的混淆

Interfaces and Abstract classes confusion in Java with examples

我很难理解什么时候使用接口而不是抽象类,反之亦然。另外,我很困惑什么时候用另一个接口扩展一个接口。对不起,这封长信,但这很让人困惑。

创建形状似乎是一个流行的起点。假设我们想要一种建模二维形状的方法。我们知道每个形状都有一个区域。以下两种实现之间的区别是什么:

带接口:

1
2
3
4
5
6
7
8
9
10
11
12
public interface Shape {
    public double area();
}

public class Square implements Shape{
    private int length = 5;
    public Square(){...}

    public double area()
         return length * length;
    }
}

使用抽象类:

1
2
3
4
5
6
7
8
9
10
11
abstract class Shape {
    abstract public double area();
}

public class Square extends Shape {
    private length = 5;
    public Square(){...}

    public double area(){
        return length * length;
    }

我理解抽象类允许您定义实例变量,并允许您提供方法实现,而接口不能做这些事情。但在本例中,这两个实现似乎是相同的。所以使用任何一个都可以吗?

但是现在假设我们想要描述不同类型的三角形。我们可以有等腰三角形、锐角三角形和直角三角形。对我来说,在这种情况下使用类继承是有意义的。使用"is-a"定义:直角三角形"is-a"三角形。三角形是一种形状。此外,抽象类应该定义所有子类中常见的行为和属性,因此这是完美的:

带抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
abstract Triangle extends Shape {
    private final int sides = 3;
}
class RightTriangle extends Triangle {
    private int base = 4;
    private int height = 5;

    public RightTriangle(){...}

    public double area() {
        return .5 * base * height
    }
}

我们也可以通过接口来实现,三角形和形状是接口。但是,与类继承(使用"is-a"关系来定义应该是子类的内容)不同,我不确定如何使用接口。我看到两种方式:

第一种方式:

1
2
3
4
5
6
7
8
9
10
11
12
  public interface Triangle {
      public final int sides = 3;
  }
  public class RightTriangle implements Triangle, Shape {
      private int base = 4;
      private int height = 5;

      public RightTriangle(){}
      public double area(){
          return .5 * height * base;
      }
  }

第二种方式:

1
2
3
4
5
6
7
8
9
10
public interface Triangle extends Shape {
     public final int sides = 3;
}
public class RightTriangle implements Triangle {
    ....

    public double area(){
         return .5 * height * base;
    }
}

在我看来,这两种方法都有效。但是你什么时候会用一种方式而不是另一种?与抽象类相比,使用接口来表示不同的三角形有什么好处吗?尽管我们把形状的描述复杂化了,但是使用接口和抽象类仍然是等价的。

接口的一个关键组件是,它可以定义可以在不相关的类之间共享的行为。所以一个可飞的接口将出现在类飞机和鸟中。因此,在这种情况下,很明显首选接口方法。

此外,要构建扩展另一个接口的混淆接口:在决定什么是接口时,什么时候应该忽略"is-a"关系?举个例子:链接。

为什么"verybadvampire"应该是类,"vampire"应该是接口?"吸血鬼"是一个"吸血鬼",所以我的理解是"吸血鬼"应该是一个超类(可能是抽象类)。"吸血鬼"类可以实现"致命"以保持其致命行为。此外,"吸血鬼"是一个"怪物",所以"怪物"也应该是一个阶级。"吸血鬼"类还可以实现一个名为"危险"的接口,以保持其危险行为。如果我们想创建一个新的怪物叫做"大老鼠",它是危险的,但不是致命的,那么我们可以创建一个"大老鼠"类,扩展"怪物"并实现"危险"。

上面的输出是否与使用"吸血鬼"作为接口(在链接中描述)的输出相同?我看到的唯一区别是,使用类继承和保留"is-a"关系可以消除很多混乱。但这并没有得到遵循。这样做的好处是什么?

即使你想让一个怪物分享吸血鬼的行为,你也可以重新定义物体的表现方式。如果我们想要一种新的吸血鬼怪物叫做"VerymildVampire",我们想要创造一种类似吸血鬼的怪物叫做"Chupacabra",我们可以这样做:

"吸血鬼"类扩展了"怪物"工具"危险"、"致命"、"吸血"
"VerymildVampire"类扩展了"Vampire"类
'chupacabra'类扩展'monster'实现'bloodsuckable'

但我们也可以这样做:

"VerymildVampire"扩展"Monster"工具危险、致命、吸血鬼
"丘帕卡布拉"扩展了"怪物"工具的危险,吸血鬼

第二种方法是创建一个"吸血鬼"接口,这样我们就可以更容易地定义一个相关的怪物,而不是创建一组定义吸血鬼行为的接口(如第一个例子)。但这打破了IS-A的关系。所以我很困惑…


使用抽象类或接口时请记住基本概念。

当要扩展的类与实现它的类更紧密地耦合时,即当两者都有父子关系时,使用抽象类。

例如:

1
2
3
4
5
       abstract class Dog {}

       class Breed1 extends Dog {}

       class Breed2 extends Dog {}

Breed1Breed2都是狗的类型,并且作为狗有一些共同的行为。

然而,当实现类具有从类到实现的特性时,将使用接口。

1
2
3
4
5
6
7
8
     interface Animal {
         void eat();
         void noise();
     }

     class Tiger implements Animal {}

     class Dog  implements Animal {}

TigerDog是两个不同的类别,但吃的和发出的声音都是不同的。因此,他们可以使用来自Animal的食物和噪音。


这是在设计类层次结构时会遇到的一个问题,这些层次结构有点像普通的那样复杂。但一般来说,在使用抽象类和接口时,您需要知道的事情很少

抽象类

  • 允许您利用使用构造函数和构造函数重写的能力
  • 限制具有多个继承的类(如果要设计复杂的API,这尤其有用)
  • 实例变量和方法实现
  • 利用方法超级调用的能力(使用super调用父抽象类的实现)

界面

  • 启用多继承-可以实现n个接口
  • 只允许表示概念方法(没有方法体)

通常使用'-able'子句的接口(如功能)。Eg:

  • Runnable
  • Observable
  • 将抽象类用于类似is-a(演进格式)的内容。Eg:

  • Number
  • Graphics
  • 但是硬性和快速性的规则不容易创建。希望这有帮助


    如果要使一个或多个方法不是抽象的,请使用抽象类。

    如果你想保持所有的抽象,使用一个接口。


    这是个好问题。这个问题有很多好的和坏的答案。典型的问题是,抽象类和接口之间的区别是什么?让我们看看在哪里使用抽象类以及在哪里使用接口。

    抽象类的使用位置:在OOP方面,如果存在继承层次结构,那么应该使用抽象类来建模设计。enter image description here

    在何处使用接口:当您必须使用一个公共契约连接不同的契约(非相关类)时,您应该使用一个接口。以集合框架为例。enter image description here

    队列、列表、集合与它们的实现具有不同的结构,但它们仍然共享一些常见的行为,如add()、remove()。所以我们可以创建一个名为collection的接口,并且我们已经在接口中声明了公共行为。如您所见,arraylist实现了来自list和randomaccess接口的所有行为,这样我们就可以轻松地添加新契约,而无需更改现有逻辑。这被称为"接口编码"。


    这是一个经常出现的问题,但没有一个"正确"的答案能让每个人满意。

    类表示is-a关系,接口表示can-do行为。我通常遵循一些经验法则:

    • 坚持使用类(抽象/具体),除非您确定需要接口。
    • 如果您确实使用接口,请将它们分割成非常具体的功能。如果一个接口包含的方法不止几个,那么您就错了。

    此外,大多数人和形状的例子(或吸血鬼的事!)通常是现实世界模型的糟糕例子。"正确"的答案取决于您的应用程序需要什么。例如,您提到:

    1
    class Vampire extends Monster implements Dangerous, Lethal, BloodSuckable

    您的应用程序真的需要所有这些接口吗?有多少种不同类型的Monsters?除了Vampire之外,您是否还有实现BloodSuckable的类?

    不要太过笼统,在您不需要接口的时候提取它们。这可以追溯到经验法则:坚持使用一个简单的类,除非您的用例需要一个接口。


    这里有很多问题。但我认为基本上你是在问接口和抽象类。

    通过接口,您可以拥有实现多个接口的类。但是,如果您想将接口用作API,那么它是不持久的。接口一旦发布,就很难修改,因为它会破坏别人的代码。

    对于抽象类,只能扩展一个类。但是,抽象类对于API是持久的,因为在以后的版本中,您仍然可以修改,而不必破坏其他人的代码。同样,对于抽象类,您可以有预定义的实现。例如,在三角形示例中,对于抽象类,您可能有一个方法countedges(),它默认返回3。


    你的体型很好。我是这样看的:

    只有当有共享的方法或成员变量时,才有抽象类。对于您的Shape示例,您只有一个未实现的方法。在这种情况下,总是使用一个接口。

    比如说你上了一个Animal班。每只动物都会跟踪它有多少肢体。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public abstract class Animal
    {
        private int limbs;
        public Animal(int limbs)
        {
            this.limbs = limbs;
        }

        public int getLimbCount()
        {
            return this.limbs;
        }

        public abstract String makeNoise();
    }

    因为我们需要跟踪每个动物有多少肢体,所以在超类中使用成员变量是有意义的。但每只动物发出的噪音都不同。

    所以我们需要把它变成一个抽象类,因为我们有成员变量、实现方法以及抽象方法。

    对于第二个问题,你需要问问自己。

    三角形总是一个形状吗?

    如果是这样,则需要从形状界面延伸三角形。

    因此,最后——使用第一组代码示例,选择接口。对于最后一组,选择第二种方式。