本文最后更新于:2020年8月12日 下午

文章介绍面向对象编程的基础概念,主要关于其特性、优势,以及 OOP 的使用方法与误区。

面向过程编程及面向过程编程语言

面向过程编程以过程(或方法)作为组织代码的基本单元。它最主要的特点就是数据和方法相分离。面向过程编程语言最大的特点就是不支持丰富的面向对象编程特性,比如继承、多态、封装。

面向对象编程

面向对象编程以类为组织代码的基本单元,一般来说,它具有以下优势。

  • 对于大规模复杂的程序的开发,程序的处理流程并非单一的一条主线,而是错综复杂的网状结构。面向对象编程更能应对这种复杂类型的程序开发;
  • 面向对象编程具有更加丰富的特性,如封装、抽象、继承、多态。代码易扩展、易复用、易维护;

封装

访问权限控制

封装主要关于信息隐藏、数据保护。编程语言需要提供访问权限控制这样的语法机制来支持。Java 使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限:

  • private : 访问权限限制的最窄的修饰符。同一类内可见。使用对象:变量、方法。 不能修饰类(外部类)
  • default (默认): 通常称为“默认访问权限“或者“包访问权限”,不使用任何修饰符。同一包内可见。使用对象:类、接口、变量、方法。
  • protected : 介于public 和 private 之间的一种访问修饰符。同一包内的类和所有子类可见。使用对象:变量、方法。 不能修饰类(外部类)
  • public : 访问限制最宽的修饰符。允许跨包访问,所有类可见。使用对象:类、接口、变量、方法

封装的意义

如果对类中属性的访问不做限制,任何代码都可以访问、修改类中的属性,虽然看起来更加灵活,但是过度灵活也意味着不可控。属性可以随意被修改,修改逻辑可能散落在代码中的各个角落,影响代码的可读性、可维护性

通过有限的方法,暴露必要的操作,能提高类的易用性。如果把类属性都暴露给类的调用者,调用者想要正确地操作这些属性,就要对业务细节有足够的了解。这对于调用者来说也是一种负担。如果将属性封装起来,只暴露必要的方法给调用者使用,调用者就不需要了解太多的业务细节,可以降低使用成本,减少用错的概率。

抽象

抽象关注的是如何隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,不需要知道实现细节。

面向对象编程常常借助编程语言提供的接口或者抽象类这两种语法机制实现抽象这一特性。

抽象有时候会被排除在面向对象特性之外。因为抽象是一个非常通用的设计思想,不单单用在面向对象编程中,也可以用来指导架构设计等。这个特性也并不需要编程语言提供特殊的语法机制来支持,只需要提供“函数”这一语法机制,就可以实现抽象特性。

抽象的意义

面对复杂的系统,人脑能承受的信息是有限的,所以要忽略一些非关键的实现细节。抽象作为一种只关注功能点不关注实现的设计思路,可以过滤掉非必要的信息。

抽象作为一个非常宽泛的设计思想,在代码设计中起到重要的指导作用。很多设计原则都体现了抽象这种设计思想,比如基于接口而非实现编程开闭原则(对扩展开放、对修改关闭)、代码解耦(降低代码的耦合性)等。

换一个角度考虑,定义类的方法的时候,不要暴露太多的实现细节,以保证在修改方法的实现的时候,不用再修改其定义。

继承

继承用来表示类之间的 is-a 关系,比如猫是一种哺乳动物。继承可以分为两种模式,单继承和多继承

Java 使用 extends 关键字来实现继承,C++ 使用冒号(class B : public A),Python 使用 paraentheses()。有些编程语言只支持单继承,不支持多重继承,比如 Java。有些编程语言两种方式都支持,比如 C++、Python。

菱形继承

多重继承存在钻石问题,即菱形继承。假设类 B 和类 C 继承自类 A,都重写了类 A 中的同一个方法,而类 D 同时继承了类 B 和类 C,那么此时类 D 会继承 B、C 的方法,那对于 B、C 重写的 A 中的方法,类 D 继承哪一个就无法确定。

Java支持多接口实现,因为接口中的方法是抽象的,即便一个类实现了多个接口,且这些接口中存在同名方法,但在实现接口的时候,这个同名方法需要由这个实现类自己来实现,所以避免了二义性的问题。

继承的意义

继承最大的优点是代码复用。假如两个类有一些相同的属性和方法,就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。两个子类可以重用父类中的代码。不过,通过其他方式,比如利用组合关系而不是继承关系,也可以达到代码复用的目的。

多态

多态是同一个行为具有多个不同表现形式或形态的能力,同一个接口,使用不同的实例而执行不同操作。子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现

实现方式

继承 + 方法重写

public class DynamicArray {
  private static final int DEFAULT_CAPACITY = 10;
  protected int size = 0;
  protected int capacity = DEFAULT_CAPACITY;
  protected Integer[] elements = new Integer[DEFAULT_CAPACITY];
  
  public int size() { return this.size; }
  public Integer get(int index) { return elements[index];}
  
  public void add(Integer e) {
    ensureCapacity();
    elements[size++] = e;
  }
  
  protected void ensureCapacity() {
    // 如果数组满了就扩容...
  }
}

public class SortedDynamicArray extends DynamicArray {
  @Override
  public void add(Integer e) {
    ensureCapacity();
    int i;
    for (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.get(i));
    }
  }
  
  public static void main(String args[]) {
    DynamicArray dynamicArray = new SortedDynamicArray();
    test(dynamicArray); // 打印结果:1、3、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 等。

接口类语法

Iterator 是一个接口类,定义了一个可以遍历集合数据的迭代器。Array 和 LinkedList 都实现了接口类 Iterator。通过传递不同类型的实现类(Array、LinkedList)到 print(Iterator iterator) 函数中,支持动态的调用不同的 next()、hasNext() 实现

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);
  }
}

Java 通过继承实现多态特性,必须要求两个类之间有继承关系。通过接口实现多态特性类必须实现对应的接口

多态的意义

多态特性能提高代码的可扩展性和复用性。利用多态,一个 print() 函数可以实现遍历打印不同类型集合的数据。当需要再增加一种要遍历打印的类型的时候,比如 HashMap,只需让 HashMap 实现 Iterator 接口,重新实现自己的 hasNext()、next() 等方法就可以了,不需要改动 print() 函数的代码,提高了代码的可扩展性。

多态也是很多设计模式、设计原则、编程技巧的代码实现基础,比如策略模式、基于接口而非实现编程、依赖倒置原则里式替换原则、利用多态去掉冗长的 if-else 语句等等。

拓展

违反OOP风格的代码设计

滥用 getter、setter 方法

  • 设计实现类的时候,除非真的需要,否则尽量不要给属性定义 setter 方法;
  • 尽管 getter 方法相对 setter 方法要安全些,但是如果返回的是集合容器,也要防范集合内部数据被修改的风险。

Constants 类、Utils 类的设计问题

  • 对于这两种类的设计,尽量能做到职责单一,定义一些细化的小类,比如 RedisConstants、FileUtils,而不是定义一个大而全的 Constants 类、Utils 类;
  • 如果能将这些类中的属性和方法,划分归并到其他业务类中,能极大地提高类的内聚性和代码的可复用性。

接口与抽象类

面向对象编程中,抽象类和接口是两个经常使用的语法概念,是面向对象四大特性以及很多设计模式、设计思想、设计原则编程实现的基础,例如:

  • 使用接口来实现面向对象的抽象特性、多态特性基于接口而非实现的设计原则;
  • 使用抽象类来实现面向对象的继承特性和模板设计模式等。

继承关系是 is-a 的关系,接口表示 has-a 关系,表示具有某些功能。接口,有一个更形象的叫法:协议

抽象类

  • 抽象类不允许被实例化,只能被继承
  • 抽象类可以包含属性和方法。抽象类中的成员变量可以是各种类型的,接口中的成员变量只能是 public static final 类型的;
    • 抽象类中的方法可以有方法体;
    • 也可以不包含方法体。不包含代码实现的方法叫作抽象方法
  • 子类继承抽象类,必须实现抽象类中的所有抽象方法

接口

  • 接口是隐式抽象的,声明接口时不必使用abstract关键字;

  • 接口没有构造方法,接口不能实例化;

  • 接口支持多继承;

  • 接口中没有成员变量只有方法声明,没有方法实现,实现接口的类必须实现接口中的所有方法

    • 接口不包含任何非常量成员,任何 field 都是隐含着 public static final 的意义;
  • 接口没有非静态方法实现,要么是隐式的抽象方法(声明时不需要abstract关键字),要么是静态方法;

    • JDK 1.8版本及以上,接口里可以有静态方法和方法体了,只有静态方法可以在接口中实现

抽象类 OR 接口

  • 抽象类是一种自下而上的设计思路,先有子类的代码重复,然后抽象成上层的父类(抽象类);
  • 如果表示 is-a 的关系,为了解决代码复用的问题,使用抽象类
  • 接口是一种自上而下的设计思路,一般是先设计接口,再考虑具体的实现;
  • 如果表示 has-a 关系,为了解决抽象而非代码复用的问题,使用接口

基于接口而非实现编程

“基于接口而非实现编程”,也可以表达为“基于抽象而非实现编程”。软件开发时一定要有抽象意识、封装意识、接口意识。越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性、扩展性、可维护性。

本质上看,“接口”是一组“协议”或者“约定”,是功能提供者给使用者的一个“功能列表”。“接口”在不同的应用场景下会有不同的解读,比如服务端与客户端之间的“接口”,类库提供的“接口”,甚至是一组通信的协议都可以叫作“接口”。落实到具体的编码,“基于接口而非实现编程”原则中的“接口”,可以理解为编程语言中的接口或者抽象类

这条原则能有效地提高代码质量。可以将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。上游系统面向接口而非实现编程,不依赖不稳定的实现细节,当实现发生变化时,上游系统的代码基本上不需要改动,以此降低耦合性,提高扩展性

应用原则

编写代码时遵从基于接口而非实现编程的原则:

  • 函数的命名不能暴露任何实现细节
  • 封装具体的实现细节
  • 为实现类定义抽象的接口。具体的实现类依赖统一的接口定义,使用者依赖接口而非具体的实现类来编程。

越不稳定的系统,越要在代码的扩展性、维护性上下功夫。不过,如果业务场景中,某个功能只有一种实现方式,未来也不可能被其他实现方式替换,没有必要为其设计接口,也没有必要基于接口编程,那么直接使用实现类就可以了。

组合优于继承

继承的缺点

继承层次越来越深,会使得继承关系会越来越复杂,引发以下问题:

  • 代码的可读性变差。因为要搞清楚某个类具有哪些方法、属性,必须阅读父类的代码、父类的父类的代码,一直追溯到最顶层父类的代码;
  • 破坏了类的封装特性,降低可维护性。父类的实现细节暴露给子类,子类的实现依赖父类的实现,两者高度耦合。父类代码的修改会影响所有子类的逻辑。

组合的优势

可以利用组合(composition)、接口、委托(delegation)三个技术,解决继承存在的问题。

注:组合,java代码复用的一种方法。使用多个已有的对象组合为一个功能更加复杂强大的新对象。体现的是整体与部分、拥有的关系。因为在对象之间,各自的内部细节是不可见的,所以也说这种方式的代码复用是黑盒的。

public interface Flyable {
  void fly()
}
public class FlyAbility implements Flyable {
  @Override
  public void fly() { //... }
}
//省略Tweetable/TweetAbility/EggLayable/EggLayAbility

public class Ostrich implements Tweetable, EggLayable {//鸵鸟
  private TweetAbility tweetAbility = new TweetAbility(); //组合
  private EggLayAbility eggLayAbility = new EggLayAbility(); //组合
  //... 省略其他属性和方法...
  @Override
  public void tweet() {
    tweetAbility.tweet(); // 委托
  }
  @Override
  public void layEgg() {
    eggLayAbility.layEgg(); // 委托
  }
}

继承主要有三个作用:表示 is-a 关系,支持多态特性,代码复用。这三个作用都可以通过其他技术来达成。

  • is-a 关系可以通过组合接口的 has-a 关系来替代;
  • 多态特性可以利用接口来实现;
  • 代码复用可以通过组合和委托来实现。

理论上讲,通过组合、接口、委托三种技术,可以替代继承。在项目中可以不用或者少用继承关系,特别是一些复杂的继承关系。

组合 OR 继承

继承改写成组合意味着要做更细粒度的类的拆分,定义更多的类和接口。类和接口的增多会增加代码的复杂程度和维护成本。实际的项目开发要根据具体的情况,具体地选择该用继承还是组合。

如果类之间的继承结构稳定,继承层次比较浅,继承关系不复杂,可以使用继承。反之,就尽量使用组合。

一些设计模式会固定使用继承或者组合。装饰者模式、策略模式、组合模式等都使用了组合关系,而模板模式使用了继承关系。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!

Java 常用集合 上一篇
Java 基础 下一篇