理解面向对象编程及面向对象编程语言的关键就是理解其四大特性:封装、抽象、继承、多态。不过,对于这四大特性,光知道它们的定义是不够的,咱们还要知道每一个特性存在的意义和目的,以及它们能解决哪些编程问题。java
封装也叫做信息隐藏或者数据访问保护。类经过暴露有限的访问接口,受权外部仅能经过类提供的方式(或者叫函数)来访问内部信息或者数据。python
对于封装这个特性,咱们须要编程语言自己提供必定的语法机制来支持。这个语法机制就是访问权限控制。private
、public
等关键字就是 Java
语言中的访问权限控制语法。private
关键字修饰的属性只能类自己访问,能够保护其不被类以外的代码直接访问。若是 Java
语言没有提供访问权限控制语法,全部的属性默认都是 public
的,那任意外部代码均可以经过相似 wallet.id=123
; 这样的方式直接访问、修改属性,也就没办法达到隐藏信息和保护数据的目的了,也就没法支持封装特性了。编程
封装的意义是什么?它能解决什么编程问题?设计模式
若是咱们对类中属性的访问不作限制,那任何代码均可以访问、修改类中的属性,虽然这样看起来更加灵活,但从另外一方面来讲,过分灵活也意味着不可控,属性能够随意被以各类奇葩的方式修改,并且修改逻辑可能散落在代码中的各个角落,势必影响代码的可读性、可维护性。好比某个同事在不了解业务逻辑的状况下,在某段代码中“偷偷地”重设了 wallet
中的 balanceLastModifiedTime
属性,这就会致使 balance
和 balanceLastModifiedTime
两个数据不一致。数组
除此以外,类仅仅经过有限的方法暴露必要的操做,也能提升类的易用性。若是咱们把类属性都暴露给类的调用者,调用者想要正确地操做这些属性,就势必要对业务细节有足够的了解。而这对于调用者来讲也是一种负担。相反,若是咱们将属性封装起来,暴露少量的几个必要的方法给调用者使用,调用者就不须要了解太多背后的业务细节,用错的几率就减小不少。这就比如,若是一个冰箱有不少按钮,你就要研究很长时间,还不必定能操做正确。相反,若是只有几个必要的按钮,好比开、停、调节温度,你一眼就能知道该如何来操做,并且操做出错的几率也会下降不少。架构
封装主要讲的是如何隐藏信息、保护数据,而抽象讲的是如何隐藏方法的具体实现,让调用者只须要关心方法提供了哪些功能,并不须要知道这些功能是如何实现的。编程语言
在面向对象编程中,咱们常借助编程语言提供的接口类(好比 Java
中的 interface
关键字语法)或者抽象类(好比 Java
中的 abstract
关键字语法)这两种语法机制,来实现抽象这一特性。ide
public interface IPictureStorage { void savePicture(Picture picture); Image getPicture(String pictureId); void deletePicture(String pictureId); void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo); } public class PictureStorage implements IPictureStorage { // ...省略其余属性... public void savePicture(Picture picture) { ... } public Image getPicture(String pictureId) { ... } public void deletePicture(String pictureId) { ... } public void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo) { ... } }
在上面的这段代码中,咱们利用 Java
中的 interface
接口语法来实现抽象特性。调用者在使用图片存储功能的时候,只须要了解 IPictureStorage
这个接口类暴露了哪些方法就能够了,不须要去查看 PictureStorage
类里的具体实现逻辑。函数
实际上,抽象这个特性是很是容易实现的,并不须要非得依靠接口类或者抽象类这些特殊语法机制来支持。换句话说,并非说必定要为实现类(PictureStorage
)抽象出接口类(IPictureStorage
),才叫做抽象。即使不编写 IPictureStorage
接口类,单纯的 PictureStorage
类自己就知足抽象特性。性能
之因此这么说,那是由于,类的方法是经过编程语言中的“函数”这一语法机制来实现的。经过函数包裹具体的实现逻辑,这自己就是一种抽象。调用者在使用函数的时候,并不须要去研究函数内部的实现逻辑,只须要经过函数的命名、注释或者文档,了解其提供了什么功能,就能够直接使用了。好比,咱们在使用 C
语言的 malloc()
函数的时候,并不须要了解它的底层代码是怎么实现的。
抽象有时候会被排除在面向对象的四大特性以外,为何呢?
抽象这个概念是一个很是通用的设计思想,并不仅仅用在面向对象编程中,也能够用来指导架构设计等。并且这个特性也并不须要编程语言提供特殊的语法机制来支持,只须要提供“函数”这一很是基础的语法机制,就能够实现抽象特性、因此,它没有很强的“特异性”,有时候并不被看做面向对象编程的特性之一。
抽象的意义是什么?它能解决什么编程问题
若是上升一个思考层面的话,抽象及其前面讲到的封装都是人类处理复杂性的有效手段。在面对复杂系统的时候,人脑能承受的信息复杂程度是有限的,因此咱们必须忽略掉一些非关键性的实现细节。而抽象做为一种只关注功能点不关注实现的设计思路,正好帮咱们的大脑过滤掉许多非必要的信息。
除此以外,抽象做为一个很是宽泛的设计思想,在代码设计中,起到很是重要的指导做用。不少设计原则都体现了抽象这种设计思想,好比基于接口而非实现编程、开闭原则(对扩展开放、对修改关闭)、代码解耦(下降代码的耦合性)等。咱们在讲到后面的内容的时候,会具体来解释。
换一个角度来考虑,咱们在定义(或者叫命名)类的方法的时候,也要有抽象思惟,不要在方法定义中,暴露太多的实现细节,以保证在某个时间点须要改变方法的实现逻辑的时候,不用去修改其定义。举个简单例子,好比 getAliyunPictureUrl()
就不是一个具备抽象思惟的命名,由于某一天若是咱们再也不把图片存储在阿里云上,而是存储在私有云上,那这个命名也要随之被修改。相反,若是咱们定义一个比较抽象的函数,好比叫做 getPictureUrl()
,那即使内部存储方式修改了,咱们也不须要修改命名。
继承是用来表示类之间的 is-a
关系,好比猫是一种哺乳动物。从继承关系上来说,继承能够分为两种模式,单继承和多继承。单继承表示一个子类只继承一个父类,多继承表示一个子类能够继承多个父类,好比猫既是哺乳动物,又是爬行动物。
为了实现继承这个特性,编程语言须要提供特殊的语法机制来支持,好比 Java
使用 extends
关键字来实现继承,C++
使用冒号(class B : public A
),Python
使用 paraentheses()
,Ruby
使用 <
。不过,有些编程语言只支持单继承,不支持多重继承,好比 Java
、PHP
、C#
、Ruby
等,而有些编程语言既支持单重继承,也支持多重继承,好比 C++
、Python
、Perl
等。
为何 Java
不支持多重继承呢?
Java
不支持多重继承的缘由:
多重继承有反作用:钻石问题(菱形继承)。
假设类B
和类C
继承自类A
,且都重写了类A
中的同一个方法,而类D
同时继承了类B
和类C
,那么此时类D
会继承B
、C
的方法,那对于B
、C
重写的A
中的方法,类D
会继承哪个呢?这里就会产生歧义。
考虑到这种二义性问题,Java
不支持多重继承。
继承存在的意义是什么?它能解决什么编程问题?
继承最大的一个好处就是代码复用。假如两个类有一些相同的属性和方法,咱们就能够将这些相同的部分,抽取到父类中,让两个子类继承父类。这样,两个子类就能够重用父类中的代码,避免代码重复写多遍。不过,这一点也并非继承所独有的,咱们也能够经过其余方式来解决这个代码复用的问题,好比利用组合关系而不是继承关系。
若是咱们再上升一个思惟层面,去思考继承这一特性,能够这么理解:咱们代码中有一个猫类,有一个哺乳动物类。猫属于哺乳动物,从人类认知的角度上来讲,是一种 is-a
关系。咱们经过继承来关联两个类,反应真实世界中的这种关系,很是符合人类的认知,并且,从设计的角度来讲,也有一种结构美感。
继承的概念很好理解,也很容易使用。不过,过分使用继承,继承层次过深过复杂,就会致使代码可读性、可维护性变差。为了了解一个类的功能,咱们不只须要查看这个类的代码,还须要按照继承关系一层一层地往上查看“父类、父类的父类……”的代码。还有,子类和父类高度耦合,修改父类的代码,会直接影响到子类。
因此,继承这个特性也是一个很是有争议的特性。不少人以为继承是一种反模式,咱们应该尽可能少用,甚至不用。
多态是指,子类能够替换父类,在实际的代码运行过程当中,调用子类的方法实现。对于多态这种特性,纯文字解释很差理解,咱们仍是看一个具体的例子。
public class DynamicArray { private static final int DEFAULT_CAPACITY = 10; private int size = 0; private int capacity = DEFAULT_CAPACITY; private Integer[] elements = new Integer[DEFAULT_CAPACITY]; public int size() { return this.size; } public Integer get(int index) { return elements[index];} //...省略n多方法... public void add(Integer e) { ensureCapacity(); elements[size++] = e; } protected void ensureCapacity() { //...若是数组满了就扩容...代码省略... } } public class SortedDynamicArray extends DynamicArray { @Override public void add(Integer e) { ensureCapacity(); for (int i = size-1; i>=0; --i) { //保证数组中的数据有序 if (elements[i] > e) { elements[i+1] = elements[i]; } else { break; } } elements[i+1] = e; ++size; } } public class Example { public static void test(DynamicArray dynamicArray) { dynamicArray.add(5); dynamicArray.add(1); dynamicArray.add(3); for (int i = 0; i < dynamicArray.size(); ++i) { System.out.println(dynamicArray[i]); } } public static void main(String args[]) { DynamicArray dynamicArray = new SortedDynamicArray(); test(dynamicArray); // 打印结果:一、三、5 } }
多态这种特性也须要编程语言提供特殊的语法机制来实现。在上面的例子中,咱们用到了三个语法机制来实现多态。
第一个语法机制是编程语言要支持父类对象能够引用子类对象,也就是能够将 SortedDynamicArray
传递给 DynamicArray
。
第二个语法机制是编程语言要支持继承,也就是 SortedDynamicArray
继承了 DynamicArray
,才能将 SortedDyamicArray
传递给 DynamicArray
。
第三个语法机制是编程语言要支持子类能够重写(override
)父类中的方法,也就是 SortedDyamicArray
重写了 DynamicArray
中的 add()
方法。
经过这三种语法机制配合在一块儿,咱们就实现了在 test()
方法中,子类 SortedDyamicArray
替换父类 DynamicArray
,执行子类 SortedDyamicArray
的 add()
方法,也就是实现了多态特性。
对于多态特性的实现方式,除了利用”继承加方法重写”这种实现方式以外,咱们还有其余两种比较常见的的实现方式,一个是利用接口类语法,另外一个是利用 duck-typing
语法。不过,并非每种编程语言都支持接口类或者 duck-typing
这两种语法机制,好比 C++
就不支持接口类语法,而 duck-typing
只有一些动态语言才支持,好比 Python
、JavaScript
等。
接下来,先来看如何利用接口类来实现多态特性。
public interface Iterator { String hasNext(); String next(); String remove(); } public class Array implements Iterator { private String[] data; public String hasNext() { ... } public String next() { ... } public String remove() { ... } //...省略其余方法... } public class LinkedList implements Iterator { private LinkedListNode head; public String hasNext() { ... } public String next() { ... } public String remove() { ... } //...省略其余方法... } public class Demo { private static void print(Iterator iterator) { while (iterator.hasNext()) { System.out.println(iterator.next()); } } public static void main(String[] args) { Iterator arrayIterator = new Array(); print(arrayIterator); Iterator linkedListIterator = new LinkedList(); print(linkedListIterator); } }
在这段代码中,Iterator
是一个接口类,定义了一个能够遍历集合数据的迭代器。Array
和 LinkedList
都实现了接口类 Iterator
。咱们经过传递不一样类型的实现类(Array
、LinkedList
)到 print(Iterator iterator)
函数中,支持动态的调用不一样的 next()
、hasNext()
实现。
具体点讲就是,当咱们往 print(Iterator iterator)
函数传递 Array
类型的对象的时候,print(Iterator iterator)
函数就会调用 Array
的 next()
、hasNext()
的实现逻辑;当咱们往 print(Iterator iterator)
函数传递 LinkedList
类型的对象的时候,print(Iterator iterator)
函数就会调用 LinkedList
的 next()
、hasNext()
的实现逻辑。
刚刚讲的是用接口类来实现多态特性。再来看下,如何用 duck-typing
来实现多态特性。咱们仍是先来看一段代码。这是一段 Python
代码。
class Logger: def record(self): print(“I write a log into file.”) class DB: def record(self): print(“I insert data into db. ”) def test(recorder): recorder.record() def demo(): logger = Logger() db = DB() test(logger) test(db)
从这段代码中能够发现,duck-typing
实现多态的方式很是灵活。Logger
和 DB
两个类没有任何关系,既不是继承关系,也不是接口和实现的关系,可是只要它们都有定义了 record()
方法,就能够被传递到 test()
方法中,在实际运行的时候,执行对应的 record()
方法。
也就是说,只要两个类具备相同的方法,就能够实现多态,并不要求两个类之间有任何关系,这就是所谓的 duck-typing
,是一些动态语言所特有的语法机制。而像 Java
这样的静态语言,经过继承实现多态特性,必需要求两个类之间有继承关系,经过接口实现多态特性,类必须实现对应的接口。
多态特性存在的意义是什么?它能解决什么编程问题?
多态特性能提升代码的可扩展性和复用性。为何这么说呢?咱们回过头去看讲解多态特性的时候,举的第二个代码实例(Iterator
的例子)。
在那个例子中,咱们利用多态的特性,仅用一个 print()
函数就能够实现遍历打印不一样类型(Array
、LinkedList
)集合的数据。当再增长一种要遍历打印的类型的时候,好比 HashMap
,咱们只需让 HashMap
实现 Iterator
接口,从新实现本身的 hasNext()
、next()
等方法就能够了,彻底不须要改动 print()
函数的代码。因此说,多态提升了代码的可扩展性。
若是咱们不使用多态特性,咱们就没法将不一样的集合类型(Array
、LinkedList
)传递给相同的函数(print(Iterator iterator)
函数)。咱们须要针对每种要遍历打印的集合,分别实现不一样的 print()
函数,好比针对 Array
,咱们要实现 print(Array array)
函数,针对 LinkedList
,咱们要实现 print(LinkedList linkedList)
函数。而利用多态特性,咱们只须要实现一个 print()
函数的打印逻辑,就能应对各类集合数据的打印操做,这显然提升了代码的复用性。
除此以外,多态也是不少设计模式、设计原则、编程技巧的代码实现基础,好比策略模式、基于接口而非实现编程、依赖倒置原则、里式替换原则、利用多态去掉冗长的 if-else
语句等等。
1. 关于封装特性
封装也叫做信息隐藏或者数据访问保护。类经过暴露有限的访问接口,受权外部仅能经过类提供的方式来访问内部信息或者数据。它须要编程语言提供权限访问控制语法来支持,例如 Java
中的 private
、protected
、public
关键字。封装特性存在的意义,一方面是保护数据不被随意修改,提升代码的可维护性;另外一方面是仅暴露有限的必要接口,提升类的易用性。
2. 关于抽象特性
封装主要讲如何隐藏信息、保护数据,那抽象就是讲如何隐藏方法的具体实现,让使用者只须要关心方法提供了哪些功能,不须要知道这些功能是如何实现的。抽象能够经过接口类或者抽象类来实现,但也并不须要特殊的语法机制来支持。抽象存在的意义,一方面是提升代码的可扩展性、维护性,修改实现不须要改变定义,减小代码的改动范围;另外一方面,它也是处理复杂系统的有效手段,能有效地过滤掉没必要要关注的信息。
3. 关于继承特性
继承是用来表示类之间的 is-a
关系,分为两种模式:单继承和多继承。单继承表示一个子类只继承一个父类,多继承表示一个子类能够继承多个父类。为了实现继承这个特性,编程语言须要提供特殊的语法机制来支持。继承主要是用来解决代码复用的问题。
4. 关于多态特性
多态是指子类能够替换父类,在实际的代码运行过程当中,调用子类的方法实现。多态这种特性也须要编程语言提供特殊的语法机制来实现,好比继承、接口类、duck-typing
。多态能够提升代码的扩展性和复用性,是不少设计模式、设计原则、编程技巧的代码实现基础。
参考:理论二:封装、抽象、继承、多态分别能够解决哪些编程问题?