面向对象编程(oop)的主要原则之一是使用抽象类和接口。程序员经常使用这些技术来强制封装,并在派生类之间共享属性和行为,而无需重复代码。
抽象类和接口有什么区别?
从高层次上看,抽象类是我们不能单独实例化的类,而接口是一个类必须遵守的规则的契约。通常,我们用“abstract”关键字实现抽象类,用“interface”关键字实现接口。虽然抽象类通常包含抽象方法,即没有实现必须被重写的方法,但它们也可以包含非抽象方法。可以将抽象方法视为c++中纯虚函数的类似物。抽象类的主要目的是提供属性和方法供派生类使用。这样,它们就充当了一种模板或蓝图。
另一方面,接口给出了一组指导类的准则,并且可以提供功能,但它们不包含任何实现。这可以类比为抽象类,但接口不能有非抽象方法或成员变量,就像抽象类一样。抽象类确实以某种方式定义了一个契约,但它们也可以提供非抽象方法。许多编程语言(如java)允许您直接使用“interface”关键字来实现接口。但是,我们也可以通过抽象类实现接口,以便重用抽象方法。值得注意的是,类只能从一个抽象类继承,但可以同时从多个接口继承。
您可以参考下表以了解抽象类和接口之间的主要区别。
抽象类 vs. 接口:摘要
抽象类 | 接口 |
---|---|
用于提供属性和方法给派生类以进行重写。 | 用于给类提供契约和功能。 |
可用于实现接口。 | 不能用于实现抽象类。 |
通常使用abstract关键字声明。 | 通常使用interface关键字声明。 |
可以包含抽象或非抽象方法。 | 只能包含抽象方法。 |
只能从一个抽象类继承。 | 类可以从多个接口继承。 |
可以有受保护的、公共的或私有的成员。 | 其成员默认始终为公共的。 |
为什么要使用抽象类和接口?
使用抽象类和接口有很多原因,但常见的原因是强制封装、继承、多态和松耦合。由于缺乏实现,外部代码无法直接修改抽象类方法。抽象方法的功能由抽象类控制,子类提供具体实现。此外,抽象类用于实现继承,因为派生类将继承抽象类的属性。
如前所述,接口的主要目的是为类提供一组指南。这使得我们能够实现多态性,可以将对象视为相同类的对象。抽象类也可以实现这一点,因为它们提供了一个可以被多个类继承的基类。接口还有助于鼓励松耦合,因为它们使得类能够根据指南而不是特定的实现进行交互。这将最大限度地减少类之间的依赖关系。
何时使用抽象类和接口?
最终,使用抽象类还是接口取决于项目的需求和您想要实现的目标。抽象类更适合于为多个相关类提供不完整的实现或行为。除此之外,如果您计划在将来进一步扩展类,抽象类也更有用,因为新的方法比接口更容易添加。然而,如果您想定义一个合同并为不太相关的类提供行为,那么接口更合适。当您需要一个类来实现多个合同时,接口也更可取。在这两种情况下,目标通常是使代码更可重用,模块化和灵活。
抽象类和接口的优缺点
现在我们已经介绍了抽象类和接口的基本知识以及它们之间的区别,现在是时候讨论每种方法的优点和缺点了。我们在下面的表格中总结了这些内容,首先是抽象类。
抽象类的优点和缺点
优点 | 缺点 |
---|---|
为重用代码提供了部分实现。 | 一些语言,如java和c++只允许从一个抽象类继承。 |
用于促进继承。 | 派生类与抽象类紧密耦合,因为它们必须继承其行为和属性。 |
抽象方法提供了灵活性,同时遵守合同。 | |
可以添加新的方法和属性而不会对派生类产生不利影响。 | |
可以使用各种访问修饰符来控制对内容的访问。 |
接下来,我们来看一下使用接口的优点和缺点。
接口的优点和缺点
优点 | 缺点 |
---|---|
可以支持多重继承。 | 不提供实现,这可能导致如果类表现相似,则使用重复代码。 |
通过依赖于合同而不是特定的实现来鼓励松耦合。 | 访问修饰符的使用受限,因为所有成员默认都是公共的。 |
通过确保不同类的对象遵守相同的规则来帮助实现多态性。 | 添加新方法通常需要修改实现接口的所有类,这比修改抽象类更具挑战性。 |
实施
此时展示抽象类和接口在实践中是如何工作的将是一个好主意。为此,我们将使用一个相对简单的示例来演示这两个概念,使用java、c#、c++和python。
java中的抽象类示例
首先,考虑以下代码:
abstract class shape {
public abstract void draw();
}
class circle extends shape {
@override
public void draw() {
system.out.println("drawing a circle.");
}
}
public class abstractclass {
public static void main(string[] args) {
circle circle = new circle();
circle.draw();
}
}
我们首先声明了”shape”抽象类,其中包含”draw()”抽象方法。然后,我们使用”circle”派生类扩展shape类,继承draw方法并提供自己的实现。我们使用”@override”表示该方法覆盖了shape类的抽象方法。
最后,我们声明了”abstractclass”类,其中包括”main()”方法。我们创建了circle类的一个实例,并调用draw方法,该方法将”drawing a circle”消息打印到控制台。您可以在下面的图像中看到结果。
java中抽象类的简单示例。
©jingzhengli.com
c++中的抽象类示例
c++中的实现类似,但也存在一些区别。请看以下代码:
#include
class shape {
public:
virtual void draw() = 0;
};
class circle : public shape {
public:
void draw() override {
std::cout << "drawing a circle." << std::endl;
}
};
int main() {
circle circle;
circle.draw();
return 0;
}
总体而言,大部分代码都是相同的。我们声明了shape和circle类,并打印相同的消息。然而,我们使用了纯虚函数,这在某种程度上类似于抽象方法,并使用”virtual”关键字和”= 0″进行声明。我们还在c++中使用了显式的”override”关键字。输出语句的语法也不同,但提供相同的结果。
需要注意的是,c++不支持接口的实现。我们能做的最好的办法是使用抽象类和纯虚函数来模拟接口,就像这里所做的一样。
c++中使用抽象类的示例。
©jingzhengli.com
c#中的抽象类示例
现在,让我们看看如何使用c#来实现这个例子。作为示例,我们有以下代码块:
using system;
abstract class shape
{
public abstract void draw();
}
class circle : shape
{
public override void draw()
{
console.writeline("drawing a circle.");
}
}
class program
{
static void main(string[] args)
{
circle circle = new circle();
circle.draw();
console.readline();
}
}
尽管名称不同,但这段代码与java示例相比更相似。两者都使用了abstract关键字和类似的方法。然而,我们在这里使用”console.writeline”和”console.readline”来写入和打印指定的字符串,还使用”program”类作为程序的起点。
在c#中实现抽象类。
©jingzhengli.com
python中的抽象类示例
由于python没有内置的抽象方法功能,因此情况有所不同。但是,我们可以通过使用抽象方法装饰器来绕过这个问题。这些装饰器为我们提供了一种在不更改源代码的情况下修改函数的方式。例如,我们有以下代码块:
from abc import abc, abstractmethod
class shape(abc):
@abstractmethod
def draw(self):
pass
class circle(shape):
def draw(self):
print("drawing a circle.")
circle = circle()
circle.draw()
首先,我们必须导入abc模块,它是“抽象基类”模块。然后像往常一样定义shape类,以及draw方法,我们使用装饰器将其声明为一个抽象方法。然后定义circle类,它继承了draw方法,并像以前一样提供了自己的print方法。最后,我们创建了一个circle类的实例并打印相同的消息。
与c++类似,python也不支持创建接口,但这是一种类似的方法。
python实现抽象类的方法。
©jingzhengli.com
java中的接口示例
接下来,让我们看一个使用接口的示例。考虑以下代码:
interface drawable {
void draw();
}
class circle implements drawable {
@override
public void draw() {
system.out.println("drawing a circle.");
}
}
class rectangle implements drawable {
@override
public void draw() {
system.out.println("drawing a rectangle.");
}
}
public class interfaceexample {
public static void main(string[] args) {
drawable circle = new circle();
drawable rectangle = new rectangle();
circle.draw();
rectangle.draw();
}
}
我们定义了”drawable”接口,并声明了”draw()”方法。然后,我们定义了”circle”类,它实现了接口并重写了draw方法。同样地,我们定义了”rectangle”类,它也继承了draw方法。最后,我们定义了公共类”interfaceexample”,其中包含用于启动程序的”main()”方法。我们分别分配了”circle”和”rectangle”变量的实例,并调用这些方法来生成输出中的消息。
在java中实现接口的一个简单示例。
©jingzhengli.com
c#中的接口示例
虽然我们不能在c++或python中实现接口,但我们可以在c#中实现。为了展示这一点,这里有一些示例代码。
interface idrawable
{
void draw();
}
class circle : idrawable
{
public void draw()
{
console.writeline("drawing a circle.");
}
}
class rectangle : idrawable
{
public void draw()
{
console.writeline("drawing a rectangle.");
}
}
class interfaceexample
{
static void main(string[] args)
{
idrawable circle = new circle();
idrawable rectangle = new rectangle();
circle.draw();
rectangle.draw();
}
}
我们在这里使用了”interface”关键字,就像在java中一样。但是,与java中使用”implements”关键字不同,c#使用冒号。两种方法都使用了重写,但c#使用了一个关键字而不是注解。
使用c#实现接口的基本示例。
©jingzhengli.com
总结
总的来说,通过了解抽象类和接口之间的区别,您可以使代码更加灵活和可重用。虽然两者都引入了一定程度的抽象,但接口更关注定义合同,而抽象类可以包含抽象和具体方法。一个类还可以继承多个接口,但只能继承一个抽象类。虽然两者都鼓励多态性,但接口在类之间提供了更松散的耦合。如果您打算扩展和添加新方法,使用抽象类更容易。如果您想从多个类继承,并且这些类之间没有太多关联,那么接口更可取。java和c#直接支持接口,但其他像python和c++这样的语言可以模拟它们。最终,您的决策将取决于项目的具体需求和任何设计考虑。