学好面向对象编程语言的关键,在于掌握它们的共通结构与特性

引言

自从软件开发进入到互联网时代,面向对象编程语言的发展,可以说是日新月异。但不管编程语言如何发展,凡是支持面向对象编程技术的,都必然包含了共有的一些典型结构。

对于程序员来说,只掌握一门编程语言就指望能吃一辈子,这明显是不可能的。快速学会新语言并用于工作,是程序员的基本软技能之一。

幸好,我们可以回归到面向对象编程语言的根本结构与特性,只要把握好这些共通之处,那新语言再让人眼花缭乱也不怕。

在下文中将按照面向对象编程语言的基本结构和高级结构,对这两方面进行描述。因为不同语言之间的细节千差万别,所以力求找到技术点的最大公约数。

但解释语言特性又不能不贴代码,因此决定采用较为普及,面向对象特性也比较齐全的Java语言作为示例。因为面向对象编程的英文缩写为OOP,因此下文中也以OOP进行表示。



基本结构

毫无疑问,OOP中最重要的概念是类。它的英文单词是Class,就是分类,或是将具有相同特点的事物归为一类的意思。

所以不管是学习哪门语言,只要看到这个单词,或是包含了class的单词,我们都知道这是在定义类。

从定义上看,类是将同种事物的特征抽取之后,进行表示的方式。从形式上看,类中包含有属性,这用以将一个类与其他类区分开来;类还包含了方法,它可以让类与类之间产生关联,从而通过协作实现系统功能。

废话不多说,先来看一个类的定义:

 class MyCar {
     int speed;
 
     void Drive() {
         System.out.println("run for " + this.speed + "km/h");
     }
 }

好了,我们定义了一辆车的类,它有一个速度属性,还有一个驾驶操作方法,如果要让一辆车跑起来,那么可以用下面的方式:

 MyCar one = new MyCar();
 one.speed = 100; // 设定时速
 one.Drive();

我们从MyCar类派生了一个实例one,运行结果会显示run for 100 km/h,一切看起来都不错。不过且慢,程序里好像有一个隐患,就是我们对speed的操作,是可以在类外进行的。

这意味着什么呢?就是说任何人都可以修改这个公开的值。想象一下,你开着车以百公里时速跑得正欢,然后忽然有人把你的速度降到零。会发生什么的画面太美,我实在不敢想……

那就不能把这个值公开,让别人有机可乘。在java里可以使用private字段限定属性值的访问范围,这样只能在类内修改这个值,保证了驾驶员的人身安全。

代码修改之后:

 class MyCar {
     private int speed;
 ​
     public MyCar(int speed) {
         this.speed = speed;
     }
 ​
     void Drive() {
         System.out.println("run for " + this.speed + "km/h");
     }
 }

speed被限定访问权限之后,我们增加了一个构造方法public MyCar(int speed),可以在生成实例的时候对speed值进行设置。

调用方法也修改如下:

 MyCar one = new MyCar(100);
 one.Drive();

这回驾驶员可以放心地开车上路了。这就是通过private或public关键字限定属性与方法的访问范围,从而实现信息的隐藏与公开,这就是类的一个重要特性:封装

当然,还有一个限定访问范围的关键字protected,不过这个关键字在不同的语言里存在细微的差别,甚至有的编程语言里就没有这个词。因此在本文中就不再讨论差异之处,而private与public的意义则通常都是一致的。

继承

好了,我们已经有了车,开着还不错。但我们还希望跑不同的路况时,能够开不同的车。这一个类显然还不够用,怎么办?

那就通过继承的方式,对车辆实现进一步细分。例如在城市里代步出行,我们想开能源环保舒适型小轿车,而去野外郊游时则想开SUV。

所以继承这个概念,它是在事物基本的共性之外,又再添加进差异化信息的能力。有了继承,对系统进行扩展、升级就会方便的多。因为自然界里事物的发展也不会是动辄就推倒从来的,而是逐渐进化的方式。

先从代码看一下继承的代码示例:

 class MyCarSUV extends MyCar {
     // SUV的独特属性
     // ...
     
     public MyCarSUV(int speed) {
         super(speed);   
     }
 }
 ​
 class MyCarClean extends MyCar {
     // 新能源车的独特属性
     // ...
     
     public MyCarClean(int speed) {
         super(speed);
     }
 }

MyCarSUV类和MyCarClean类就是从MyCar类继承而来。在面向对象的术语体系中,MyCar这种位于上一层级的类有的称之为父类,而有的称之为超类,其下的则都称为子类。

从我个人来说,我比较偏向于“超类-子类”的说法。因为“父类-子类”这个隐喻暗含有新生与消亡之意,但子类并不是更新换代,而是在共性之外再添加新特性。这仅是一家之言,无须过多在意。

可以看到,超类中包含的属性speed与方法Drive(),在子类中可以直接拿来就用。这就是继承机制所带来的代码重用优点,减少了代码的冗余,同时代码也更具表现力。

多态

现在家里停着两辆车了,我们也都知道出门郊游时要开SUV,上班通勤开能源环保车。不过要写成代码的时候,如果为不同场景的出行就单独增加一个方法,这扩展起来就涉及到多处修改,相当不便。

那么,我们当然希望调用端代码能够越简洁越好。这个时候就可以利用OOP语言的多态特性了。它可以使用超类的方法去操作子类的实例,从而实现调用端的统一化处理。

示例代码:

 enum RoadCondtion {
     CITY,   //城市路段
     OUTSIDE //野外路段
 }
 ​
 public class Main {
     static void travel(RoadCondtion cond) {
         MyCar one = null; //声明一个超类引用,根据路况要求再派生实例
 ​
         switch(cond) {
             case CITY:
                 one = new MyCarClean(80); //派生能源环保车实例
                 break;
             case OUTSIDE:
                 one = new MyCarSUV(160);  //派生SUV车实例
                 break;
         }
 ​
         one.Drive(); //无须关注内部细节,驾驶方法都一样
     }
 ​
     public static void main(String[] argv) {
         travel(RoadCondtion.CITY); //城市出行
         travel(RoadCondtion.OUTSIDE); //野外郊游
     }
 ​
 }

多态使得程序在实现具有相同业务逻辑的功能时,可以大大简化重复的代码。多态特性在公用类库与框架的实现中具有非常重要的作用。因为它将固定流程的操作都确定下来,这些流程中所处理的对象具有统一的方法就可以。而对象包含的差异性,则可以通过继承的方式去单独处理。

简单说,就是只要车辆能保证驾驶方法都一样,那么对于司机来说,他可以开着任何一辆车上路。而不同的车之间所具有的差异性,则可以是在子类中进行个性化处理,而对于司机来说则不必关心这些细节问题。



高级结构

包在OOP编程语言中的作用,就是对类进行组织和管理,避免类名重复造成的命名冲突问题,也方便对类进行查找与访问。

有了包的机制,对于OOP语言开发的大型程序,就提供了非常方便的管理手段。包与文件系统目录的结构类似,在java语言中则是通过点分隔符“.”,将代码的功能自顶向下进行分类。

我们先来看怎样创建一个包:

 // Vehicle.java
 package vehicle; //定义包名
 ​
 public interface Vehicle {
     public void Drive();
 }

我们创建了一个名为vehicle的包,并且为Drive()公共方法创建了一个机动车接口,即只要是机动车辆都具备可驾驶能力。然后我们在另一个文件中去引用包名下的接口并且实现它。

 // Main.java
 package vehicle;
 ​
 import vehicle.Vehicle; //引用接口声明
 ​
 class MyCar implements Vehicle {
     private int speed;
 ​
     public MyCar(int speed) {
         this.speed = speed;
     }
 ​
     public void Drive() {
         System.out.println("run for " + this.speed + "km/h");
     }
 }

通过package与import关键字,我们就可以创建公共类库,并且在业务逻辑中使用类库的方法。

异常

异常机制是OOP语言中,用来处理程序运行中出现的各类错误的。包括IO读写失败、数据库连接失败、内存溢出等。

在结构化编程语言中,对于运行时的各种意外情况,只能通过条件判断语句来处理。而产生的问题,要么代码是深度嵌套的,阅读起来极其费神;要么使用goto语句,导致程序运行不可控。如果业务代码还包含了多重子函数调用,那隐患更难处理。

而在OOP语言的异常处理中,可以专注于业务逻辑实现。当运行时出现问题,则会被异常机制捕捉,这样可以直接跳转到处理方法中。既保护了程序的稳定运行,也为排查问题提供了方便。

我们来看怎样捕捉一个除零错误产生的异常:

 try {
             int a;
 ​
             a = 100 / 0;
         }catch(Exception e) {
             System.out.println("zero problem: " + e);
         }

这段代码运行之后的结果是:

 zero problem: java.lang.ArithmeticException: / by zero

有了异常处理,除零错误就不会导致程序直接退出,还能记录下出错原因并且继续运行。

不过有一点需要注意的是,程序员不要将异常机制当作代码的护身符。以为有了异常,就可以不关注代码质量。异常的本意是为了处理意料之外的情况,并不是为糟糕的代码质量兜底。例如上面示例中的除零错误就是不应该发生的,这种问题在编写程序的过程中就应该解决掉。

垃圾回收

垃圾回收一般都是OOP语言的内置功能。它能够将已经分配出去,并且不再使用的内存区域重新收回。这个机制的好处,是可以帮助程序员将注意力从内存的分配/释放工作中解放出来,从而更好地完成业务功能。

但程序员同样需要注意的是,不能因为OOP语言支持垃圾回收机制,在写程序时就无所顾忌,将运行时环境想象成内存无限的场景。

因为垃圾回收的原理都是基于引用计数的方式,而如果大量的内存实例都被栈区和静态区所引用,那么就是垃圾回收机制也无能为力了。

而频繁地分配对象实例,则会导致系统内存管理一直处于“抖动”状态,拖慢系统性能,影响了程序的运行效率。所以,编程时充分考虑内存的合理分配与使用,仍然是非常重要的。



结语

OOP语言包括的结构与特性并不止上述这些,还有泛型、反射、重写与重载等。不过学习OOP最好是由易及难,不然一上来就面对潮水般的术语,只怕热情很快就被淹没了。

而掌握了OOP的共通结构与特性,接下来学习各种框架和类库时就会得心应手了。相信程序员面临的一大痛点,就是新的框架和功能类库层出不穷,学习起来感觉一直是在疲于应付。

其实只要充分掌握好OOP的理念,框架和类库的代码组织结构就几乎不用劳神,只要将重点放在理解它们实现的功能上就好。短期内是完全可以做到快速掌握并上手工作的。

对于设计模式也是这样,有的程序员在工作中应用设计模式时,会变成“唯设计模式论”,恨不得给每段代码都套到一个模式里。这样搞得程序沉重不堪,导致设计过载了。

如果能从基本结构上理解OOP,那么就会理解每个设计模式的意义,以及它所适用的场景。程序员可以做到有的放矢,将设计模式的威力发挥到最大。

学习好面向对象编程的基本理念,可以帮助你成为更加优秀的程序员!


举报
评论 0