Java基础1:面向过程与面向对象、三大基本特征、五大基本原则
一、前言
本文内容摘自《深入理解Java核心技术:写给Java工程师的干货笔记(基础篇)》一书,2022年出版,作者 张洪亮(@Hollis),阿里巴巴技术专家,著有《Java工程师成神之路》系列文章,《Java工程师成神之路》电子书已开源,可在阿里云开发者社区免费下载。书籍内容比电子书内容要丰富,内容有修改,有需要的读者可以购买正版书籍。
【如何成神:先搬砖,再砌砖,后造砖!】
本文由 @大白有点菜 原创,请勿盗用,转载请说明出处!如果觉得文章还不错,请点点赞,加关注,谢谢!
《Java工程师成神之路》下载地址为:
https://developer.aliyun.com/ebook/395?spm=a2c6h.20345107.ebook-index.24.4c927863j10ats
二、第1章 什么是面向对象
1、面向过程与面向对象
(1)面向过程(Procedure Oriented)
:是一种以过程为中心的编程思想,是一种自顶而下的编程模式。
代表:C语言。
简单说,程序员把问题分解成一个个步骤,每个步骤用函数实现,依次调用。
典型用法:简单算法,如冒泡排序。
优点:代码执行效率高。
缺点:代码重用性低、扩展能力差、后期维护难度大。
(2)面向对象(Object Oriented)
:是一种对现实世界的理解和抽象的方法。通过对现实世界的理解和抽象,运用封装、继承、多态等方法,通过抽象出对象的方式进行软件开发。
代表:Java、C#、C++、Python、Ruby、PHP。
简单说,程序员把问题分解成一个个步骤,对每个步骤进行相应的抽象,形成对象,通过不同对象之间的调用,组合解决问题。例如,要造车,先定义车的各种属性,将各种属性封装在一起,抽象成一个Car类。
优点:代码扩展性、可维护性高。
2、面向对象的三大基本特征
(1)封装(Encapsulation)
:把客观事务封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的类或者对象隐藏信息。
【举个例子(非书中例子)】
手机提供了计算器的功能,用户只知道计算器可以用来算数,但不清楚内部的逻辑处理算法,计算器对内部算法私有化了,只对外提供计算功能。
【封装举例(书中例子)】
定义矩形Rectangle类,通过封装的方式,给“矩形”定义了“长度”和“宽度”,“矩形”也就被抽象了。
/**
* 矩形
*/
public class Rectangle {
/**
* 长度
*/
private int length;
/**
* 宽度
*/
private int width;
/**
* 设置矩形的长度和宽度
* @param length 长度
* @param width 宽度
*/
public Rectangle(int length, int width) {
this.length = length;
this.width = width;
}
/**
* 获得矩形面积
* @return
*/
public int area() {
return this.length * this.width;
}
}
public class EncapsulationApp {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle(2, 4);
System.out.println(rectangle.area()); // 8
}
}
(2)继承(Inheritance)
:可以使现有类的所有功能,并在无须重新编写原来的类的情况下对这些功能进行扩展。通过继承创建的类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”、“超类”。
【继承举例(书中例子)】
定义一个正方形,因为有了矩形,所以直接继承Rectangle类。
/**
* 正方形,继承自矩形
*/
public class Square extends Rectangle {
/**
* 设置正方形边长
* @param length
*/
public Square(int length) {
super(length, length);
}
}
public class InheritanceApp {
public static void main(String[] args) {
Square square = new Square(6);
System.out.println(square.area()); // 36
}
}
(3)多态(Polymorphism)
:一个类实例的相同方法在不同情形下有不同的表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。
3、面向对象的五大基本原则
SOLID原则:
单一职责原则(Single-Responsibility Principle)
开放封闭原则(Open-Closed principle)
里氏替换原则(Liskov-Substitution Principle)
接口隔离原则(Interface-Segregation Principle)
依赖倒置原则(Dependecy-Inversion Principle)
(1)单一职责原则(Single-Responsibility Principle)
核心思想:一个类最好只做一件事,只有一个引起它变化的原因。
单一职责原则可以看做是高内聚、低耦合在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。
高内聚、低耦合是软件工程中的概念,是判断软件设计好坏的标准,主要用于程序的面向对象的设计,主要看类的内聚性是否高、耦合度是否低。目的是使程序模块的可重用性、移植性大大增强。
内聚是从功能角度来度量模块内的联系的,一个好的内聚模块应当恰好做一件事,它描述的是模块内的功能联系;耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度。
职责过多,可能引起它变化的原因就越多,这将导致职责依赖,相互之间就产生影响,从而大大损伤其内聚性和耦合度。
通常意义下的单一职责,就是指类只有单一功能,不要为类实现过多的功能点,以保证实体只有一个引起职责变化的原因。
交杂不清的职责会使得代码看起来特别别扭,牵一发而动全身,有失美感,甚至可能导致不可预期的系统错误风险。
(2)开放封闭原则(Open-Closed principle)
核心思想:软件实体应该是可扩展的且不可修改的。也就是说,对扩展开放、对修改封闭。
体现在两个方面:
1)对扩展开放,意味着当有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
2)对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对其进行任何尝试的修改。
实现开放封闭原则的核心思想就是对抽象(如接口、抽象类等)编程,因为抽象相对稳定。让类依赖于固定的抽象,所以修改就是封闭的;而通过面向对象的继承和多态机制,又可实现对抽象类的继承,通过覆写其方法来改变固有行为,实现新的拓展方法,所以就是开放的。
(3)里氏替换原则(Liskov-Substitution Principle)
核心思想:子类必须能够替换其基类。这一思想体现为对继承机制的约束规范,只有当子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。
在父类和子类的具体行为中,必须严格把握继承层次中的关系和特征,将基类替换为子类,程序的行为不会发生任何变化。同时,这一约束反过来则是不成立的,子类可以替换基类,但是基类不一定能替换子类。
里氏替换原则主要着眼于对抽象和多态建立在继承的基础上,因此只有遵循里氏替换原则,才能保证继承复用是可靠的。
里氏替换原则是关于继承机制的设计原则,若违反了里氏替换原则,则必然导致违反开放封闭原则。
里氏替换原则能够保证系统具有良好的拓展性,同时实现基于多态的抽象机制,能够减少代码冗余,避免运行期的类型判别。
(4)接口隔离原则(Interface-Segregation Principle)
核心思想:使用多个小的专门的接口,而不要使用一个大的总接口。
在父类和子类的具体行为中,必须严格把握继承层次中的关系和特征,将基类替换为子类,程序的行为不会发生任何变化。同时,这一约束反过来则是不成立的,子类可以替换基类,但是基类不一定能替换子类。
具体而言,接口隔离原则体现在:接口应该是内聚的,应该避免“胖”接口。一个类对另外一个类的依赖应该建立在最小的接口上,不要强迫依赖不用的方法,这是一种接口污染。
接口有效地将细节和抽象隔离,体现了对抽象编程的一切好处,接口隔离强调接口的单一性。而“胖”接口存在明显的弊端,会导致实现的类型必须完全实现接口的所有方法、属性等;而在某些时候,实现类型并非需要所有的接口定义,在设计上这是一种“浪费”,而且在实施上这会带来潜在的问题,对“胖”接口的修改将导致需要修改一连串的客户端程序,有时这是一种灾难。在这种情况下,将“胖”接口分解为多个具体的定制化方法,使得客户端仅仅依赖它们的实际调用的方法,从而避免客户端要依赖于它们根本用不到的方法。
分离接口的手段主要有以下两种:
1)委托分离,通过增加一个新的类型来委托客户的请求,隔离客户和接口的直接依赖,但是会增加系统的开销。
2)多重继承分离,通过接口多继承来实现客户的需求,这种是较好的方式。
(5)依赖倒置原则(Dependecy-Inversion Principle)
核心思想:程序依赖于抽象接口,而不是具体的实现。简单地说,就是要对抽象进行编程,不要对实现进行编程。
在面向过程的开发中,上层依赖于下层,当下层发生重大变化时,上层也要跟着变动,这就会导致模块的复用性降低,大大提高了开发的成本。
面向对象的开发很好地解决了这个问题,通过分离接口与实现,使得类与类、模块与模块之间只有依赖于接口,而不依赖于具体的实现类。在一般情况下,抽象的接口的变化概率是很小的,依赖接口而不依赖具体实现,即使实现细节不断变动,只要抽象不变,上层依赖就不需要改动。这大大降低了客户程序与实现细节的耦合度。