发布于 2026-01-05 5 阅读
0

面向对象编程原理入门

面向对象编程原理入门

我经验不多,但参加过几次面试。

他们似乎总是问一些老生常谈的问题。

例如:

五年后你希望自己身处何方?

对于这类问题,我通常会事先准备好答案。

但我知道有一个问题肯定会被问到,但我总是答错。

这是:

面向对象编程的主要原则是什么?

这几乎是一个普遍存在的问题,从初级到高级的各种技术面试中都会被问到。面试官之所以会问这类问题,是因为这是衡量面试者经验和理解水平的一种简便方法。

说明以下三点很有帮助:

  1. 面试者是否为面试做了准备?

    如果立即得到答复,通常是个好兆头。

  2. 面试者在编写代码之前是否经过深思熟虑?

    理解并应用面向对象编程(OOP)原则表明候选人已经超越了简单的代码编写阶段。他不会直接从网上复制粘贴代码,而是拥有结构化的代码和清晰的思维方式。

  3. 面试者理解得深刻还是肤浅?

    这是一个非常开放式的问题,你可以浅尝辄止,也可以深入探讨。如果能深入分析,可以获得额外加分。

替代文字

面向对象编程的四个原则是封装抽象继承多态

我知道这听起来很吓人,但相信我,其实并非如此。

这其实很简单,所以我想用简单明了的方式,结合例子,给你们解释清楚。

封装

封装将数据及其相关方法绑定到同一个类中。它还通过将字段设为私有,并仅允许通过其相关方法访问这些字段来保护数据。

假设我们有一个小型程序。

它包含几个可以相互通信的对象,这些对象遵循程序中定义的规则。

封装是指每个对象都保持其状态私有,这意味着任何外部类都不能直接修改它。相反,它们只能调用一系列公共函数——称为方法。

在我们继续之前,我们引入了一些你可能感到困惑的关键词,让我来解释一下:

  1. 状态:状态代表对象的数据(值)。假设银行账户是一个对象,那么账号、金额等就是该银行账户的状态。
  2. 私有:只能在类内部访问的数据。
  3. 受保护:只能在类及其子类中访问的数据。
  4. 公共的:可以在类外部访问的数据或函数(方法)。

违反封装原则

想象一下,我们有一个单人角色扮演游戏,其中有一个负责管理任务的职业,叫做Quest

public class Quest {
    // possible options for status
    private static final int NOT_PICKED = 0;
    private static final int PICKED = 1;
    private static final int COMPLETED = 2;

    // state
    public String title;
    public int reward;
    public int status;

    // Constructor
    public Quest(String title, int reward, int status){
        this.title = title;
        this.reward = reward;
        this.status = status;
    }

    // Other behavior
}
Enter fullscreen mode Exit fullscreen mode

我们的客户端代码大致如下所示:

public static void client() {
    Quest quest = new Quest('Goblin Slaying', 190, 3);
}
Enter fullscreen mode Exit fullscreen mode

想象一下,如果其中一名玩家获得了该客户端的访问权限,并且想要通过更改游戏状态和奖励来作弊。

public static void client() {
    Quest quest = new Quest('Goblin Slaying', 190, 3);
    quest.reward = 1000000000;
    quest.status = 5;
}
Enter fullscreen mode Exit fullscreen mode

目前这段代码是有效的,因为我们的变量是公开可访问的。但另一个问题是,我们的“黑客”将状态值设置为了 5,而 5 并不存在,因此我们的游戏崩溃了。

修正我们的示例

解决这个问题的方法是将所有变量设为私有,并且只能通过它们各自的方法访问。

public class Quest {
    private static final int NOT_PICKED = 0;
    private static final int PICKED = 1;
    private static final int COMPLETED = 2;

    private String title;
    private int reward;
    private int status;

    public Quest(String title, int reward, int status){
        this.title = title;
        this.reward = reward;
        this.status = status;
    }

    public String getTitle() {
        return title;
    }

    public int getReward() {
        return reward;
    }

    public int getStatus() {
        return status;
    }

    public void setQuestNotPicked(){
        this.status = NOT_PICKED;
    }

    public void setQuestPicked(){
        this.status = PICKED;
    }

    public void setQuestCompleted(){
        this.status = COMPLETED;
    }

    // other behavior

}
Enter fullscreen mode Exit fullscreen mode

由于这些改动,我们现在无法更改奖励和称号,但我们可以通过相应的功能获取它们。

最后,我们的代码对黑客来说是安全的。

封装的好处

  • 封装可以保护对象免受客户端的未经授权的访问。
  • 封装允许访问某一层级,而不会泄露该层级以下的复杂细节。
  • 它能减少人为错误。
  • 简化应用程序的维护
  • 使应用程序更容易理解。

封装技巧

为了实现最佳封装,对象数据几乎总是应该限制在 `<T>`private或 `<T>` 级别protected。如果您选择将访问级别设置为 `<T>` public,请务必了解此选择的后果。

抽象

抽象是面向对象编程中的一个概念,它只“显示”必要的属性,而“隐藏”不必要的信息。

抽象是封装的延伸,但它们之间有什么区别?

  • 封装——指的是隐藏数据,例如使用getter`and`setter等。
  • 抽象——指的是使用抽象类和接口等来隐藏具体实现。

但我现在更困惑了,什么是抽象类和接口?

抽象类

抽象类其实就是一个普通的类,但它有一些特殊的例外情况:

  • 抽象类可以包含没有方法体的方法。
  • 在实例化对象之前,抽象类必须在某种程度上被继承为非抽象类。
  • 不允许直接实例化抽象类的对象。

那么,我为什么要使用这类课程呢?

它们允许我们做很多事情:

  1. 整合通用行为(与仅定义契约的接口不同)
  2. 为函数提供默认(以及可选的可重写)实现。

我们来看一个简单的例子:

//abstract parent class
abstract class Animal{
   //abstract method
   public abstract void sound();
}
//Dog class extends Animal class
public class Dog extends Animal{

   public void sound(){
    System.out.println("Woof");
   }
   public static void main(String args[]){
    Animal obj = new Dog();
    obj.sound();
   }
}
Enter fullscreen mode Exit fullscreen mode

抽象类与具体类

  1. 抽象类只有被另一个类继承后才有用。
  2. 如果在一个类中声明了一个抽象方法,那么这个类也必须被声明为抽象类。具体类中不能包含抽象方法。反之则不一定成立:如果一个类没有任何抽象方法,它也可以被标记为抽象类。
  3. 它也可以有非抽象的方法(具体的方法)。

接口

替代文字

接口是一种抽象类型,用于指定类必须实现的行为。

将接口视为一份契约。

你必须遵守一系列规则,但细节并不重要。

例如,我们有一份关于狗的行为规范合同,名为DogInterface“它”,合同内容大致如下:

public interface DogInterface {
    public void bark();
    public void wiggleTail();
    public void eat();
}
Enter fullscreen mode Exit fullscreen mode

这里我们规定,所有实现该接口的类都DogInterface必须包含这三个方法。正如你所见,这里完全没有实现细节,它没有说明狗应该如何吠叫、摇尾巴或进食。也就是说,我们现在可以创建遵循相同约定的不同狗类。

public class LabradorRetriever implements DogInterface {

    public void bark() {
        System.out.println("Woof, woof!!");    
    }

    public void wiggleTail() {
        System.out.println("wiggles tail");
    }

    public void eat() {
        System.out.println("eats food");
    }
}
Enter fullscreen mode Exit fullscreen mode

你可能会问:“我为什么要使用界面呢?”

简而言之,接口保证一个类将拥有x 个方法。

这听起来可能过于简单,但实际上就是如此。

尽管如此,这个小功能对我们帮助极大。想象一下,我们有一个货运服务程序,客户端代码使用这个Car类来运输货物。一段时间后,我们的程序变得流行起来,我们需要支持飞机、火车、轮船、卡车等等……

一种方法是创建一个名为“交通工具”的通用接口TransportInterface,供所有交通工具(飞机、汽车等)使用。这样,我们的客户端代码就不必担心类中Airplane缺少某些通用方法。

应该使用抽象类还是接口?

一如既往,这要视情况而定。

需要考虑的因素有很多。

但一般来说,当需要子类实现或重写某个功能时,会使用抽象类。而接口则只允许描述功能,不涉及实现。此外,在大多数编程语言中,一个类只能继承一个抽象类,但可以利用多个接口。

回到抽象层面

这篇文章的走向确实有些出乎意料,但有必要谈谈抽象类和接口。

简而言之:

抽象是封装的延伸,它使用抽象类或接口来隐藏实现细节。

遗产

替代文字

继承是指将一个对象或类建立在另一个对象或类的基础上,并保留类似的实现方式。

编程中一个常见的问题是对象太相似了。

物体通常非常相似,功能也类似,但它们并不完全相同。

现在,每个程序员都会面临一个难题。

我应该创建一个全新的类,还是把所有通用功能都放在一个基类中?”

我不会说哪个方案更好,但我们今天讨论的是面向对象编程(OOP)。这意味着我们将选择第二个方案,创建一个基类来存储所有通用功能。

学校范例

假设我们受命开发一个学校管理系统,其中包含学生和教师两个实体。在应用的第一个版本中,我们是这样实现它们的:

public class Student {
    private String name;
    private String last_name;
    private String gender;
    private String classroom;
    private String year;

    public void attendClass(){
        System.out.println("Attend Class");
    }

    public void eatSandwich(){
        System.out.println("Eating a sandwich");
    }
}
Enter fullscreen mode Exit fullscreen mode
public class Teacher {
    private String name;
    private String last_name;
    private String gender;
    private String schedule;
    private String subject;

    public void teachClass(){
        System.out.println("Teaching a class");
    }

    public void eatSandwich(){
        System.out.println("Eating a sandwich");
    }
}
Enter fullscreen mode Exit fullscreen mode

正如你所看到的,有很多常见的状态和行为,我们不妨将它们全部提取到一个名为“Human

public class Human {
    protected String name;
    protected String last_name;
    protected String gender;

    public void eatSandwich(){
        System.out.println("Eating a sandwich");
    }
}
Enter fullscreen mode Exit fullscreen mode

PS:我已将所有变量设置为受保护变量,以便子类可以访问它们。

这样我们就可以将所有常见的状态和行为继承到我们的StudentTeacher中。

public class Student extends Human {
    private String classroom;
    private String year;

    public void attendClass(){
        System.out.println("Attend Class");
    }
}
Enter fullscreen mode Exit fullscreen mode
public class Teacher extends Human {
    private String schedule;
    private String subject;

    public void teachClass(){
        System.out.println("Teaching a class");
    }
}
Enter fullscreen mode Exit fullscreen mode

恭喜!我们已经删除了重复代码,这是一个好兆头。

回到继承的问题上来

继承并非那么简单,实际上有不同类型的继承。

  1. 单继承:这是最简单的继承方式,我们从一个父类继承子类。就像我们上面提到的教师和学生的例子一样。
  2. 多级继承:当一个继承来自子类时,它就变成了多级继承。例如,假设我们在上面的例子中添加了一个名为 `Intern` 的类Intern。`Intern` 的状态和行为与 `Intern` 相同,Teacher但有一些额外的字段,因此我们直接继承自 `Intern` Teacher。这就是多重​​继承,因为现在 `Intern`Intern同时继承自 `Intern`Teacher和`Intern` Human
  3. 多重继承:指一个类继承多个父类。大多数编程语言通常不允许多重继承,但有些语言支持。
  4. 层次继承:是指多个子类继承自同一个父类。例如,我们有一个类 ADogCat另一个类 B,它们都继承自另一个类Animal
  5. 混合遗传:当存在两种以上遗传方式的组合时,称为混合遗传。

继承的好处

  1. 代码重用性
  2. 一个超类可以用于表示层次结构中子类的数量。
  3. 所有基类都不需要做任何更改,只需要在父类中进行更改即可。
  4. 继承可以避免重复和数据冗余。
  5. 使用继承可以避免空间复杂度和时间复杂度。

预防

我并不是说一定要继承类,这样做会使代码耦合度很高。另一种方法是使用组合或聚合。

您可以在这里阅读更多相关信息。

多态性

替代文字

多态性是指一个对象能够呈现多种形态的能力。

让我们来分析一下这个词:

  • Poly = 多:polygon = 多边形,polystyrene = 许多苯乙烯,polyglot = 多种语言,等等。
  • Morph = 变化或形态,morphology = 研究生物形态,Morpheus = 希腊梦神,能够变成任何形态。

所以,多态性是指(在编程中)为不同的底层形式(数据类型)提供相同接口的能力。

经典的例子是Shape类以及所有可以从该类继承的类(正方形、圆形、十二面体、不规则多边形、扁平状等等)。

利用多态性,每个类都将拥有不同的底层数据。一个点形状只需要两个坐标(当然,假设它位于二维空间中)。一个圆需要圆心和半径。一个正方形或矩形需要左上角和右下角的两个坐标,以及(可能)一个旋转坐标。一个不规则多边形需要一系列线段。

通过让类负责其代码和数据,可以实现多态性。在本例中,每个类都有自己的Draw()函数,客户端代码只需执行以下操作:

shape.Draw()
Enter fullscreen mode Exit fullscreen mode

为了获得任何形状的正确行为。这与过去代码与数据分离的方式截然不同,过去你会有诸如 ` drawSquare()and` 和 `.`之类的函数drawCircle()

多态性的类型

面试中通常会问这样的问题:

什么是多态性?

你按部就班地回答了他们,然后为了扰乱你的思路,他们会问:

“多态性有哪些不同类型?”

现在你有两种选择:

  1. 回答这个问题,因为你很聪明。
  2. 编造一些东西。

很遗憾,我也曾被问过这个问题,但我当时没能回答。所以我们今天要讨论一下多态性的类型。

多态性有两种类型:动态多态性静态多态性

动态多态性

这也称为:

  • 运行时多态性
  • 动态绑定
  • 运行时绑定
  • 后期绑定
  • 方法重写

这就是标准的多态性。它的基本形式是,当子类中有一个与父类中的方法同名且参数相同的方法时,这些方法的形式会有所不同(这被称为方法重写)。

例如,让我们用上面的例子来说明一下我们刚才提到的形状。

public class Shape {
    protected int width;
    protected int height;

    // some more functionality for shape

    public void draw(){
        // draws a shape using the width and height.
    }
}
Enter fullscreen mode Exit fullscreen mode
public class Circle extends Shape {
    private int radius;

    @Override
    public void draw() {
        // draw a circle code
    }
}
Enter fullscreen mode Exit fullscreen mode
public class Square extends Shape {

    @Override
    public void draw() {
        // specifically draw a square
    }
}
Enter fullscreen mode Exit fullscreen mode

我们的客户端代码大致如下所示:

public static void main(String[] args) {
        Shape shape = new Shape();
        Shape circle = new Circle();
        Shape square = new Square();

        shape.draw(); // calls the draw method in shape class
        circle.draw(); // calls the draw method in circle class
        square.draw(); // calls the draw method in square class
    }
Enter fullscreen mode Exit fullscreen mode

如您所见,我们的类CircleSquare类都继承自Shape类。它们也重写了类draw()中的方法Shape

静态多态性

这也称为:

  1. 编译时多态性
  2. 静态绑定
  3. 编译时绑定
  4. 早期结合
  5. 方法重载

这基本上是指同一个类中一个方法有多种形式的情况

我们来看一个例子:

public class Calculator {
    void add(int a, int b) {
        System.out.println(a+b);
    }
    void add(int a, int b, int c) {
        System.out.println(a+b+c);
    }
}
Enter fullscreen mode Exit fullscreen mode
public static void main(String args[]) {
        Calculator calculator = new Calculator();

        // method with two parameters gets called
        calculator.add(10, 20); // output: 30

        // method with three parameters get called
        calculator.add(10, 20, 30); //output: 60
    }
Enter fullscreen mode Exit fullscreen mode

多态性的益处

  • 它有助于程序员重用代码,也就是说,一旦编写、测试和实现,类就可以根据需要重复使用。这节省了大量时间。
  • 单个变量可以存储多种数据类型。
  • 代码易于调试。

结论

面向对象编程的四个原则是:

  1. 封装:意味着隐藏数据,例如使用gettersetter
  2. 抽象:使用接口和抽象类隐藏实现过程。
  3. 继承:一种基于另一个对象或类,并保留类似实现方式的对象或类的机制。
  4. 多态性:一个对象能够呈现多种形态的能力。

恭喜!现在你可以回答面试中最常被问到的问题之一,并且大放异彩了。

如果您有任何疑问,请随时在下方评论区留言。

文章来源:https://dev.to/tamerlan_dev/oop-principles-for-dummies-2mbd