第一章 对象的概念笔记

481 阅读20分钟

第一章 对象的概念

面向对象编程(Object-Oriented Programming OOP)是一种编程思维方式和编码架构。本章讲述 OOP 的基本概述。

--编程语言就是软件的思想结构。

抽象

汇编语言是对底层机器语言的轻微抽象。接着出现的“命令式”语言(如 FORTRAN,BASIC 和 C)是对汇编语言的抽象。 ———所有编程语言都提供抽象机制

程序员必须要在机器模型(“解决方案空间”)和问题模型("问题空间")之间建立一种关联。在面向对象的程序设计中程序员可利用一些工具表达“问题空间”内的元素。由于这种表达非常具有普遍性,所以不必受限于特定类型的问题。我们把问题中的元素以及他们在计算机的表示称作对象。还有一些在问题中没有对应的对象体。可以通过添加新的对象类型,进行灵活的调整,以便与特定的问题配合。

创建好一个类后,我们可以根据需求生成许多对象。随后,可将那些对象作为问题中的元素进行处理。但当我们进行面向对象的程序设计师,面临最大的一项挑战就是:如何在问题中的元素与计算机中的元素建立理想的“一对一”映射关系。

类似“银行出纳员”这样的经典问题。在这个例子里,我们有一系列出纳员、客户、帐号、交易和货币单位等许多"对象”。每类成员(元素)都具有一些通用的特征:每个帐号都有一定的余额;每名出纳都能接收客户的存款;等等。与此同时,每个成员都有自己的状态;每个帐号都有不同的余额;每名出纳都有一个名字。所以在计算机程序中,能用独一无二的实体分别表示出纳员、客户、帐号以及交易。这个实体便是“对象”,而且每个对象都隶属一个特定的“类”,那个类具有自己的通用特征与行为。

OOP( Object-Oriented Programming)面向对象,允许我们根据问题来描述问题,而不是根据运行解决方案来解决问题。每个对象都可以有自己的状态并且可以进行特定的操作,与现实世界的"对象"或者“物体”相相似;他们都有自己的特征和行为。

面向对象的五大特征

1.万物皆对象。 你可以将对象想象成一种特殊的变量。它存储数据,但可以在你对其“发出请求”时执行本身的操作。

你可以从需要解决的问题身上抽象出概念性的东西,然后再程序中表示为一个对象。

2.程序是一组对象,通过传递消息来告知彼此该做什么。 要请求一个对象的方法,你需要像该对象发送消息。

3.每个对象都有自己的存储空间,可容纳其他对象。你可以通过封装现有对象来制作新型对象。所以,尽管对象的概念非常简单,但在程序中却可以复杂到任意高度。

4.每个对象都属于一种类型。对象都是某个“类”的一个“实例”。一个类最重要的特征就是“能将什么消息发给它?”

比如,饮料这个类型,可以包含可乐芬达雪碧。每个可乐,芬达,雪碧又有各自不同的生产日期,和产地。这时候,可乐,芬达,雪碧就是饮料类的实例。

5.**同一类的对象都能接受相同的消息。**可乐的一个对象也属于类型为“饮料”的一个对象,所以一个可乐完全能接受发送给”饮料“的消息这意味着可让程序代码统一指挥“饮料”,令其自动控制所有符合“饮料”描述的对象,其中自然包括“可乐”。 这一特性称为对象的“可替换性”,是OOP最重要的概念之一

一个对象具有自己的状态,行为和标识。这意味着对象有自己的内部数据(提供状态)、方法 (产生行为),并彼此区分(每个对象在内存中都有唯一的地址)。


接口

这里的“接口”不是方法也不是interface而是抽象的指某一类的通用特征。就像USB一样,不是让你手机适配电脑,而是先制定出一个标准(USB标准),让手机和电脑适配这个标准。 定义一个类,这个类要有通用特征,比如定义了水果类,那么这个类具有可食用性这个特征,香蕉和苹果都属于水果都具有可食用性,那么这个可食用性就是“接口”。

我们需要通过“接口”来向对象发出请求,令其解决一些实际问题。比如完成一次计算,在屏幕上输出一段文字等等。每个对象仅能接受特定的请求。我们向对象发出的请求是通过它的“接口”定义的,对象的“类型”或“类”规定了它的接口形式。----"类型”与“接口”的对应关系是面向对象程序设计的基础

以下段代码为例,其中类型名称为Light,可向Light对象发出的请求包括 打开 on、关闭 off、变得更明亮 brighten 或者变得更暗淡 dim

Light lt = new Light();
lt.on();
lt.off();
lt.brighten();
lt.dim();

通过声明一个引用,如lt(变量名)和new关键字,我们创建了一个Light类型的对象,再用等号将其赋值引用。

为了向对象发送消息,我们使用句点符号.lt和接口名称on连接起来。

public class Light {
    public String on() {
       return "开灯";
    }
    public String off() {
        return "关灯";
    }
    public String brighten() {
        return "变亮";
    }
    public String dim() {
        return "变暗";
    }
}
class test{
    public static void main(String[] args) {
        Light light =new Light();
        System.out.println(light.on());
        System.out.println(light.off());
        System.out.println(light.brighten());
        System.out.println(light.dim());
    }
}

服务提供

在开发或者理解程序设计时,我们可以将对象看成是“服务提供者”。程序本身将为用户提供服务,并且它能通过调用其他对象的服务来实现这一点。我们的最终目标是开发或调用工具库中已有的一些对象,提供理想的服务来解决问题

我们可以将一个程序分解,抽象成一组服务。软件设计的基本原则是:高内聚低耦合,每个组件内部作用明确,各个组件功能紧密相关

例如:在支票打印模块中,你需要设计一个可以同时读取文本格式又能正确识别不同打印机型号的对象。正确的做法是提供三个或更多对象:一个对象检查所有排版布局的目录;一个或一组可以识别不同打印机型号的对象展示通用的打印界面;第三个对象组合上述两个服务来完成任务。

在良好的面向对象设计中,每个对象功能单一且高效。这样的程序设计可以提高我们代码的复用性,同时也提高了代码的可读性。


封装

编程的侧重领域可以分成两部分,一部分是研发另一部分是应用。应用程序员调用研发程序员写好的基础工具类,来做快速开发。研发程序开发一个工具类,该工具类仅向应用程序员公开必要的内容,并隐藏内部实现的细节。这样可以有效的避免该工具类被随意的修改从而减少程序出错的可能。如果工具类的创建者将类的内部所有信息都公开给调用者,那么有些使用规则就不容易被遵守。因为前者无法保证后者是否会按照正确的规则来使用,甚至是改变该工具类。只有设定访问控制,才能从根本上阻止这种情况的发生。

Java中有三个显式关键字来设置类中的访问权限: public(公开),private(私有)和protected(受保护)。这些访问修饰符决定了谁能使用它们修饰的方法、变量或类。

  1. public(公开)表示任何人都可以访问和使用该元素;
  2. private(私有)除了类本身和类内部的方法,外界无法直接访问该元素。private 是类和调用者之间的屏障。任何试图访问私有成员的行为都会报编译时错误;
  3. protected(受保护)类似于 private,区别是子类(下一节就会引入继承的概念)可以访问 protected 的成员,但不能访问 private 成员;
  4. default(默认)如果你不使用前面的三者,默认就是 default 访问权限。default 被称为包访问,因为该权限下的资源可以被同一包(库组件)中其他类的成员访问。

复用

一个类创建好,并通过测试后,理应是可复用的。但是由于程序员没有足够的编程经验和远见,导致我们代码的复用性并不强。

代码和设计方案的复用性是面向对象程序设计的优点之一。我们可以通过重复使用某个类的对象来达到复用性。同时,我们也可以将一个类的对象,作为另一个类的成员变量来使用。新的类可以由任意数量和任意类型的其他对象构成。这里就涉及到了“组合”和“聚合”的概念。

组合和聚合

  • 组合 :用来表示“拥有”关系,例如“汽车拥有引擎”,“人有五官”。 组合关系中,整件拥有部件的生命周期,所以整件删除时,部件一定会跟着删除。而且,多个整件不可以同时共享同一个部件。

  • 聚合:动态的组合,例如“办公室里有职员”,“公交车里有乘客”。 聚合关系中,整件不会拥有部件的生命周期,所以整件删除时,部件不会被删除。再者,多个整件可以共享同一个部件。

  • 组合和聚合区别:两个类生命周期不同步,则是聚合关系,生命周期同步就是组合关系。

//聚合关系
class Car{
    Engine engine;
    void setEngine(Engine engine) {
   this.engine = engine;
  }
}
//组合关系
class Car{
    Engine engine;
    Car(){
        engine = new Engine();
    }
}

使用“组合”关系可以给我们程序带来极大的灵活性。通常新建类中,成员对象会使用private访问权限,这样程序员则无法对其直接访问,我们就可以在不影响客户代码的前提下,从容修改那些成员。 我们也可以在“运行时"改变成员对象从而动态地改变程序的行为,这进一步增大了灵活性。

继承

“继承“给面向对象编程带来了极大的便利。它在概念上允许我们将各种各样的数据和功能封装到一起,这样便可恰当表达”问题空间“的感念,而不用受制于必须使用底层机器语言。

通过使用class关键字,这些概念形成了编程语言中的基本单元。在创建一个类以后,如果你还需要一个与其相似的类,你还是得重新创建一个新类。但我们若能利用现成的数据类型,对其进行”克隆“,再进行添加和修改,就便利很多了。”继承“正是针对这个目标而设计的。但继承并不完全等价于克隆。

”继承“通过原始类和派生类的概念来表达这种相似性。 创建原始类以表示核心思想。派生类来实现该核心的不同方式。一个原始类可以有多个派生类,但一个派生类只可以有一个原始类。原始类不仅仅描述一组对象的约束,它还涉及派生类。派生类之间具有相似性和差异性,个派生类可能包含比另一个派生类的更多的特征,并且还可以处理更多的消息。 原始类包含派生类的所有特征和行为,若原始类发生了变化,那么派生类也会反映出这种变化。如果派生类完全继承了原始类,这种继承的等价性是理解面向对象编程含义的基本门槛之一。

通过使用继承后,父类和子类的结构成为了主要模型,因此我们可以直接从真世界中对问题的描述过渡到用代码对问题进行描述。

从现有类型继承创建新类型。这种新类型不仅包含父类的所有成员(父类私有成员不可访问),而且还包含父类的接口。也就意味着,父类对象能接收的所有消息也能被子类接收。因此子类与父类是相同的类型。但是如果继承一个类而不做其他任何事,则来自父类接口的方法直接进入子类。这意味着父类和子类不仅具有相同的类型,而且具有相同的行为,这么做没什么特别意义。所有接口必定有某些具体实现,也就是说,当对象接收到特定消息,必须有可执行代码。

有两种方法来区分派生类和原始类。第一种方法:派生类中添加了新的接口。这些接口不是原始类接口的一部分。这意味着原始类不能满足需求,所以添加了更多的接口。

第二种方法:派生类改变了继承自原始类的接口,这称为覆盖。要想覆盖一个方法,只需要在派生类中重新定义这个方法即可。


“是一个”与“像一个”的关系

“是一个”

如果继承只覆盖原始类的方法而不添加新的方法,这样的话,原始类和派生类就是相同的类型了,因为他们具有相同的接口。这样会造成,你可以用一个派生类对象完全替代原始类对象,这叫做“纯粹替代”,也经常被称作“替代原则”。

“像一个”

有时你在派生类添加新的接口,从而扩展接口。虽然这种派生类仍然可以替代原始类,但是这种替代不完美,原因在于原始类无法访问新添加的接口。这种关系称为“像一个”关系。派生类不但拥有原始类的接口,而且包含其他方法,所以说不能说新旧类型完全相同。

多态

我们在处理类的层次结构时,通常把一个对象看成是它所属的父类,而不是把它当成具体对象。通过这种方式,我们可以编写出不局限于特定类型的代码。这样的代码不会受添加的新类型影响,并且添加新类型是扩展面向对象已处理新情况的常用方法。 通过派生类来扩展设计的这种能力是封装变化的基本方法之一。这种能力改善了我们的设计,且减少了软件的维护代价。

如果我们把派生类的对象统一一看成是它本身的原始类,那么编译器在编译时就无法准确的知道是操作哪一个派生类,程序员也并不需要知道那段代码会被执行,那我们就能添加一个新的不同执行方式的类而不需要更改调用它的方法,可是派生类仍旧会依据自身的具体类型来执行恰当代码。

public class Test {
    public static void main(String[] args) {
        Person person=new Person();
        Teacher teacher=new Teacher();
        Student student=new Student();
        new PersonController().relocate(person);//输出人类走动
        new PersonController().relocate(teacher);//输出老师走动
        new PersonController().relocate(student);//输出学生走动
    }
}
class PersonController{
    public void relocate(Person person){
        person.move();
    }
}
class Person {
    public void move() {
        System.out.println("人类走动");
    }
}
class Teacher extends Person {
    @Override
    public void move() {
        System.out.println("老师走动");
    }
}
class Student extends Person{
    @Override
    public void move() {
        System.out.println("学生走动");
    }
}

这段代码中StudentTeacher都是Person的派生类,且重写了move()方法,添加的PersonController提供了一个新的执行方式,relocate()方法。

relacate()方法可以接受任何Person,因此它与所操作对象的具体类型无关,无论传入任何Person程序都可以正确的执行。由于TeacherStudent都是一种Person所以relacate()方法能正确地执行。也就是说relacate()方法能接受任意发送给Person的消息。这种把派生类当成原始类来处理的过程叫做”向上转型“。 在面向对象的编程里,经常利用这种方法来给程序解耦。


早期绑定(静态绑定)

在传统意义上,编译器不能进行函数调用。由非OOP编译器产生的函数调用会引起所谓的“早期绑定"(静态绑定)。这意味着编译器生成对特定函数名的调用,该调用会被解析为将执行的代码的绝对地址。

后期绑定(静态绑定)

通过继承,程序直到运行时才能确定代码的地址,因此面向对象语言使用"后期绑定"(静态绑定)的概念。当向对象发送消息时,被调用的代码直到运行时才确定。编译器确保方法存在,并对参数和返回值类型检查,但是它不知道要执行的确切代码。


为了执行后期绑定,java使用了一个特殊的代码来替代绝对地址。这段代码使用对象中存储的信息来计算方法主体地址。因此,每个对象的行为根据特定代码位的内容而不同。当你向对象发送消息时,对象知道该如何处理这条消息。

发送消息给对象时,如果程序不知道接受的具体类型时什么,但最终执行是正确的,这就是对象的”多态性“。面向对象的程序设计语言是通过**“后期绑定”**的方式来实现对象的多态性的。编译器和运行时系统会负责对所有细节控制;我们只需知道要做什么,以及如何利用多态性来更好地设计程序。

单继承结构

除了C++以外几乎所有的面向对象语言,都有一个原始类,所有的类都默认从原始类继承。在Java中,这个原始类的名字就是Object

Java单继承结构有很多好处。由于所有对象都具有一个公共接口,因此他们最终都属于同一个原始类。对于完全面向对象编程,我们必须要构建自己的层次结构,以提供与其他OOP语言同样的便利。另外,单继承的结构使得垃圾收集器的实现更为容易。

由于运行时的类型信息会存在于所有对象中,所以我们永远不会遇到判断不了的对象类型情况。

集合

在面向对象的设计中,我们可以创建一个新类型的对象来引用、容纳其他的对象。但在Java中我们也可以通过“集合”来实现。

集合”这种类型的对象可以存储 任意类型、数量的其他对象 。它能根据需要自动扩容。 在Java中,不同类型的集合对应不同的需求。从设计角度来看,我们真正想要的是一个能够解决某个问题的集合。如果一种集合满足所有需求,那么我们就不需要剩下的额。之所以选择集合有以下两个原因:

  1. 集合可以提供不同类型的接口和外部行为。 堆栈、队列的应用场景和集合、列表不同,它们中的一种提供的解决方案可能比其他灵活得多。
  2. 不同的集合对某些操作有不同的效率。由于底层数据结构的不同,每种集合类型在执行相同的操作时会表现出效率上的差异

参数化类型机制

参数化类型机制可以使得编译器能够自动识别集合中class的具体类型并正确的执行。Java5版本中支持了参数化类型机制,称之为“泛型”。

对象创建与生命周期

每个对象的生存都需要资源,尤其是内存。为了资源的重复利用,当对象不再被使用时,我们应该及时释放资源,清理内存。

Java使用动态内存分配——在堆内存中动态的创建对象。 在这种方式下,直到程序运行我们才能确定需要创建的对象数量、生存时间和类型。什么时候需要,什么时候在堆内存中创建。 ** 开辟堆内存空间的时间取决于内存机制的设计。动态方法有这样一个一般性的逻辑假设:对象趋向于变得复杂,因此额外的内存查找和释放对对象的创建影响不大。 由于在堆内存创建对象的话编译器是不知道它的生命周期的。但是,Java的内存管理是建立在垃圾收集器上的, 垃圾收集器知道对象什么时候不再被使用并且自动释放内存 。

异常处理

异常处理机制将程序错误直接交给编程语言甚至是操作系统。“异常”(Exception)是一个从出错点“抛出”(thrown)后能被特定类型的异常处理程序捕获(catch)的一个对象。它不会干扰程序的正常运行,仅当程序出错的时候才被执行。另外,异常不像方法会返回错误值和方法设置用来表示发生错误的标志位那样可以被忽略。异常的发生是不会被忽略的,它终究会在某一时刻被处理。

Java的异常处理机在编程语言中脱颖而出。Java从一开始就内置了异常处理,因此你不得不使用它。这是Java语言唯一接受错误报告的方法。如果没有适当的异常处理代码,你将会收到一条编译时错误消息。这种有保障的一致性有时会让程序的错误处理更容易。

“异常机制”提供了一种可靠地从错误状况中恢复的方法,使得我们可以编写出更健壮的程序。有时你只要处理好抛出的异常情况并恢复程序的运行即可,无需退出。

2019年10月26日