面向对象编程原理入门
我经验不多,但参加过几次面试。
他们似乎总是问一些老生常谈的问题。
例如:
五年后你希望自己身处何方?
对于这类问题,我通常会事先准备好答案。
但我知道有一个问题肯定会被问到,但我总是答错。
这是:
面向对象编程的主要原则是什么?
这几乎是一个普遍存在的问题,从初级到高级的各种技术面试中都会被问到。面试官之所以会问这类问题,是因为这是衡量面试者经验和理解水平的一种简便方法。
说明以下三点很有帮助:
-
面试者是否为面试做了准备?
如果立即得到答复,通常是个好兆头。
-
面试者在编写代码之前是否经过深思熟虑?
理解并应用面向对象编程(OOP)原则表明候选人已经超越了简单的代码编写阶段。他不会直接从网上复制粘贴代码,而是拥有结构化的代码和清晰的思维方式。
-
面试者理解得深刻还是肤浅?
这是一个非常开放式的问题,你可以浅尝辄止,也可以深入探讨。如果能深入分析,可以获得额外加分。
面向对象编程的四个原则是封装、抽象、继承和多态。
我知道这听起来很吓人,但相信我,其实并非如此。
这其实很简单,所以我想用简单明了的方式,结合例子,给你们解释清楚。
封装
封装将数据及其相关方法绑定到同一个类中。它还通过将字段设为私有,并仅允许通过其相关方法访问这些字段来保护数据。
假设我们有一个小型程序。
它包含几个可以相互通信的对象,这些对象遵循程序中定义的规则。
封装是指每个对象都保持其状态私有,这意味着任何外部类都不能直接修改它。相反,它们只能调用一系列公共函数——称为方法。
在我们继续之前,我们引入了一些你可能感到困惑的关键词,让我来解释一下:
- 状态:状态代表对象的数据(值)。假设银行账户是一个对象,那么账号、金额等就是该银行账户的状态。
- 私有:只能在类内部访问的数据。
- 受保护:只能在类及其子类中访问的数据。
- 公共的:可以在类外部访问的数据或函数(方法)。
违反封装原则
想象一下,我们有一个单人角色扮演游戏,其中有一个负责管理任务的职业,叫做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
}
我们的客户端代码大致如下所示:
public static void client() {
Quest quest = new Quest('Goblin Slaying', 190, 3);
}
想象一下,如果其中一名玩家获得了该客户端的访问权限,并且想要通过更改游戏状态和奖励来作弊。
public static void client() {
Quest quest = new Quest('Goblin Slaying', 190, 3);
quest.reward = 1000000000;
quest.status = 5;
}
目前这段代码是有效的,因为我们的变量是公开可访问的。但另一个问题是,我们的“黑客”将状态值设置为了 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
}
由于这些改动,我们现在无法更改奖励和称号,但我们可以通过相应的功能获取它们。
最后,我们的代码对黑客来说是安全的。
封装的好处
- 封装可以保护对象免受客户端的未经授权的访问。
- 封装允许访问某一层级,而不会泄露该层级以下的复杂细节。
- 它能减少人为错误。
- 简化应用程序的维护
- 使应用程序更容易理解。
封装技巧
为了实现最佳封装,对象数据几乎总是应该限制在 `<T>`private或 `<T>` 级别protected。如果您选择将访问级别设置为 `<T>` public,请务必了解此选择的后果。
抽象
抽象是面向对象编程中的一个概念,它只“显示”必要的属性,而“隐藏”不必要的信息。
抽象是封装的延伸,但它们之间有什么区别?
- 封装——指的是隐藏数据,例如使用
getter`and`setter等。 - 抽象——指的是使用抽象类和接口等来隐藏具体实现。
但我现在更困惑了,什么是抽象类和接口?
抽象类
抽象类其实就是一个普通的类,但它有一些特殊的例外情况:
- 抽象类可以包含没有方法体的方法。
- 在实例化对象之前,抽象类必须在某种程度上被继承为非抽象类。
- 不允许直接实例化抽象类的对象。
那么,我为什么要使用这类课程呢?
它们允许我们做很多事情:
- 整合通用行为(与仅定义契约的接口不同)
- 为函数提供默认(以及可选的可重写)实现。
我们来看一个简单的例子:
//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();
}
}
抽象类与具体类
- 抽象类只有被另一个类继承后才有用。
- 如果在一个类中声明了一个抽象方法,那么这个类也必须被声明为抽象类。具体类中不能包含抽象方法。反之则不一定成立:如果一个类没有任何抽象方法,它也可以被标记为抽象类。
- 它也可以有非抽象的方法(具体的方法)。
接口
接口是一种抽象类型,用于指定类必须实现的行为。
将接口视为一份契约。
你必须遵守一系列规则,但细节并不重要。
例如,我们有一份关于狗的行为规范合同,名为DogInterface“它”,合同内容大致如下:
public interface DogInterface {
public void bark();
public void wiggleTail();
public void eat();
}
这里我们规定,所有实现该接口的类都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");
}
}
你可能会问:“我为什么要使用界面呢?”
简而言之,接口保证一个类将拥有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");
}
}
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");
}
}
正如你所看到的,有很多常见的状态和行为,我们不妨将它们全部提取到一个名为“Human
public class Human {
protected String name;
protected String last_name;
protected String gender;
public void eatSandwich(){
System.out.println("Eating a sandwich");
}
}
PS:我已将所有变量设置为受保护变量,以便子类可以访问它们。
这样我们就可以将所有常见的状态和行为继承到我们的Student类Teacher中。
public class Student extends Human {
private String classroom;
private String year;
public void attendClass(){
System.out.println("Attend Class");
}
}
public class Teacher extends Human {
private String schedule;
private String subject;
public void teachClass(){
System.out.println("Teaching a class");
}
}
恭喜!我们已经删除了重复代码,这是一个好兆头。
回到继承的问题上来
继承并非那么简单,实际上有不同类型的继承。
- 单继承:这是最简单的继承方式,我们从一个父类继承子类。就像我们上面提到的教师和学生的例子一样。
- 多级继承:当一个继承来自子类时,它就变成了多级继承。例如,假设我们在上面的例子中添加了一个名为 `Intern` 的类
Intern。`Intern` 的状态和行为与 `Intern` 相同,Teacher但有一些额外的字段,因此我们直接继承自 `Intern`Teacher。这就是多重继承,因为现在 `Intern`Intern同时继承自 `Intern`Teacher和`Intern`Human。 - 多重继承:指一个类继承多个父类。大多数编程语言通常不允许多重继承,但有些语言支持。
- 层次继承:是指多个子类继承自同一个父类。例如,我们有一个类 A
Dog和Cat另一个类 B,它们都继承自另一个类Animal。 - 混合遗传:当存在两种以上遗传方式的组合时,称为混合遗传。
继承的好处
- 代码重用性
- 一个超类可以用于表示层次结构中子类的数量。
- 所有基类都不需要做任何更改,只需要在父类中进行更改即可。
- 继承可以避免重复和数据冗余。
- 使用继承可以避免空间复杂度和时间复杂度。
预防
我并不是说一定要继承类,这样做会使代码耦合度很高。另一种方法是使用组合或聚合。
您可以在这里阅读更多相关信息。
多态性
多态性是指一个对象能够呈现多种形态的能力。
让我们来分析一下这个词:
- Poly = 多:polygon = 多边形,polystyrene = 许多苯乙烯,polyglot = 多种语言,等等。
- Morph = 变化或形态,morphology = 研究生物形态,Morpheus = 希腊梦神,能够变成任何形态。
所以,多态性是指(在编程中)为不同的底层形式(数据类型)提供相同接口的能力。
经典的例子是Shape类以及所有可以从该类继承的类(正方形、圆形、十二面体、不规则多边形、扁平状等等)。
利用多态性,每个类都将拥有不同的底层数据。一个点形状只需要两个坐标(当然,假设它位于二维空间中)。一个圆需要圆心和半径。一个正方形或矩形需要左上角和右下角的两个坐标,以及(可能)一个旋转坐标。一个不规则多边形需要一系列线段。
通过让类负责其代码和数据,可以实现多态性。在本例中,每个类都有自己的Draw()函数,客户端代码只需执行以下操作:
shape.Draw()
为了获得任何形状的正确行为。这与过去代码与数据分离的方式截然不同,过去你会有诸如 ` drawSquare()and` 和 `.`之类的函数drawCircle()。
多态性的类型
面试中通常会问这样的问题:
什么是多态性?
你按部就班地回答了他们,然后为了扰乱你的思路,他们会问:
“多态性有哪些不同类型?”
现在你有两种选择:
- 回答这个问题,因为你很聪明。
- 编造一些东西。
很遗憾,我也曾被问过这个问题,但我当时没能回答。所以我们今天要讨论一下多态性的类型。
多态性有两种类型:动态多态性和静态多态性。
动态多态性
这也称为:
- 运行时多态性
- 动态绑定
- 运行时绑定
- 后期绑定
- 方法重写
这就是标准的多态性。它的基本形式是,当子类中有一个与父类中的方法同名且参数相同的方法时,这些方法的形式会有所不同(这被称为方法重写)。
例如,让我们用上面的例子来说明一下我们刚才提到的形状。
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.
}
}
public class Circle extends Shape {
private int radius;
@Override
public void draw() {
// draw a circle code
}
}
public class Square extends Shape {
@Override
public void draw() {
// specifically draw a square
}
}
我们的客户端代码大致如下所示:
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
}
如您所见,我们的类Circle和Square类都继承自Shape类。它们也重写了类draw()中的方法Shape。
静态多态性
这也称为:
- 编译时多态性
- 静态绑定
- 编译时绑定
- 早期结合
- 方法重载
这基本上是指同一个类中一个方法有多种形式的情况。
我们来看一个例子:
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);
}
}
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
}
多态性的益处
- 它有助于程序员重用代码,也就是说,一旦编写、测试和实现,类就可以根据需要重复使用。这节省了大量时间。
- 单个变量可以存储多种数据类型。
- 代码易于调试。
结论
面向对象编程的四个原则是:
- 封装:意味着隐藏数据,例如使用
getter和setter - 抽象:使用接口和抽象类隐藏实现过程。
- 继承:一种基于另一个对象或类,并保留类似实现方式的对象或类的机制。
- 多态性:一个对象能够呈现多种形态的能力。
恭喜!现在你可以回答面试中最常被问到的问题之一,并且大放异彩了。
如果您有任何疑问,请随时在下方评论区留言。
文章来源:https://dev.to/tamerlan_dev/oop-principles-for-dummies-2mbd



