《Java编程思想》读书笔记-对象导论

42次阅读

共计 5657 个字符,预计需要花费 15 分钟才能阅读完成。

计算机是头脑延伸的工具,是一种不同类型的表达媒体。本文以背景性的和补充性的材料,介绍包括开发方法概述在内的面向对象程序设计 (Object-oriented Programming,OOP) 的基本概念。
本文通过概念 + 代码的方式,来帮助读者了解面向对象程序设计的全貌。
抽象过程
概念
机器模型:位于解空间内,是对问题建模的地方;可以这样理解,汇编语言和命令式语言,在解决问题时要基于计算机的架构;因此架构限定了解决方案,所以说机器模型是解空间。
实际待解决问题:问题空间,是问题存在的地方
如何抽象
抽象的类型和质量,决定了人们所能够解决的问题的复杂性。抽象的类型指的是“所抽象的是什么”。一种是在机器模型和实际待解决问题的模型之间建立联系的抽象;另一种是只针对待解决问题建模。而面向对象则是向程序员提供表示问题空间中元素的工具,我们将问题空间中的元素及其在解空间中的表示称为“对象”。
面向对象程序设计的特性:

万物皆是对象;
程序是对象的集合,对象间方法的调用是程序运行的基本表现;
对象可以包含其他对象;
每个对象都拥有其特定的类型;
某一特定类型的所有对象都可以接收同样的方法调用;

什么是对象?对象具有状态、行为和标识。每一个对象都可以拥有内部数据和方法,并且可以唯一的与其他对象区分开来
每个对象都应该都归属于一个类或接口
名词解释
对象:具有状态、行为和标识的实体。如银行存款账户是一个类,那么具体的每个人的银行存款账户就是这个类目下的对象。
类:可以看作类型来考虑。比如说鸟类,是动物中的其中一种类型。
类和对象
所有的对象都是唯一的,但同时具有相同的特性和行为的对象也都归属于某个特定的类。
类在 Java 中用关键词 class 表示。每个类的对象都具有某种共性和个性,如银行存款账户,每个账户中都有余额的属性,但每个账户中的余额又不同。在实际中,面向对象程序设计语言都用 class 关键字来表示数据类型,换而言之,每一个类都是一个数据类型。程序员可以自由地添加新的类(数据类型)来扩展编程语言,对实际问题进行处理。
对象的获取与方法调用
面向对象的挑战之一,就是在问题空间的元素和解空间的对象之间创建一对一的映射。
获取有用对象,必须以某种方式对对象进行请求,使对象完成各种任务。类型决定接口,而接口决定对象能满足的请求。就比如鸟类型,其提供的接口有飞翔,因此其能满足飞翔的请求。在接口确定了某一特定对象能够发出的请求后,接口的实现掌控着请求的具体行为的展现方式。在类型中,每一个可能的请求都有一个方法与之关联,当向对象发送请求时,与之关联的方法就会被调用。
以下代码是获取一个对象并调用其中的方法实例(你可以暂时不用理解,只需要知道形式即可,后面再反过来看就好)
public class Light {
// 开灯
public void on() {
System.out.println(“Light is on!”);
}
// 关灯
public void off() {
System.out.println(“Light is off!”);
}

// 这里是获取一个对象并调用其中方法的实例
public static void main(String [] args) {
// 这里是核心代码,开灯操作
Light lt = new Light();
lt.on();
}
}
### 对象是服务提供者
程序通过调用其他对象提供的服务来向用户提供服务。程序员的目标就是去创建(或者最好是从现有的代码库中寻找)能够提供解决问题所需服务的一系列对象。
为什么要把对象看作是服务提供者呢?

这是将问题分解为对象集合的一种合理方式。比如说,你正在创建一个簿记系统,那么,这个系统可以拆分为:我需要一个包括了预定义的簿记输入屏幕的对象、一个执行簿记计算的对象集合以及一个处理在不同的打印机上打印支票和开发票的对象。
它有助于提高对象的内聚性。就像上面所定义的簿记系统,每个对象都可以很好地完成一项任务,但是它并不试图做更多的事情。职能太多,可能会导致对象的内聚性降低。简而言之,每个对象只做它该做的事。

程序访问权限控制
将程序开发人员按照角色划分为类创建者和客户端程序员。类创建者创建新的数据类型,而客户端程序员在其应用中使用类创建者创建的新数据类型。如此一来,客户端程序员的主要目的就是收集各种用来实现快速应用开发的类;而类创建者的目的则是构建类,并向客户端程序员暴露必须的部分。
为什么类创建者需要对类的某些部分进行隐藏呢?或者说,为什么需要进行访问权限控制呢?

让客户端程序员无法触及他们不该触及的部分,让客户端程序员分清楚,哪些东西对他们来说是必须的,哪些是可以忽略的。
允许库设计者可以改变类内部的工作方式而不用担心会影响到客户端程序员。因为对客户端程序员所提供的那一部分可见的内容总是不变的,而库设计者改变的是其中隐藏的部分。

Java 的访问权限控制关键字

修饰符
类内部
同包
子类
任何地方

private

×
×
×




×
×

protected



×

public



访问权限关键字的修饰范围

public:可以修饰外部类、属性、方法;
protected:只能修饰属性和方法;
private:只能修饰属性、方法、内部类;

复用具体实现
代码复用是面向对象程序设计语言所提供的最了不起的优点之一。
代码复用的基本方式:

直接使用该类的一个对象;
可以将某个类的一个对象置于一个新类中,作为新类的成员出现;

类之家的关系
关系是指事物之间存在单向或者相互的作用力或者影响力的状态。
在两个类之间存在有关系和没关系两种情况,在有关系的情况下,其关系包括以下六种类型

类关系
英文名
描述
强权方
UML 图表示
示例说明

继承
extends
父类与子类之间的关系:is-a
父类
空心三角 + 实线,空心三角指向父类
鸟是动物

实现
implements
接口与实现类之间的关系:can-do
接口
空心三角 + 虚线,空心三角指向接口
鸟实现了飞翔的接口

组合
composition
比聚合更强的关系:contains-a
整体
实心菱形 + 实线,实心菱形指向整体
人类的头和身体是强组合关系

聚合
aggregation
暂时组装的关系:has-a
组装方
空心菱形 + 实线,空心菱形指向组装方
狗和牵狗的绳子是聚合关系

依赖
dependency
一个类依赖于另一个类:depends-a
被依赖方
箭头 + 虚线,箭头指向被依赖方
人喂小狗,小狗是喂这个动作的被依赖方

关联
association
类与类之间存在互相平等的使用关系:links-a
平等
实线
人与信用卡的关系,人用信用卡,信用卡可以读取个人信息

继承关系
以现有类为基础,复制它,然后通过添加和修改这个副本来创建新类。当源类发生变化时,被修改的副本也会反应出这种变动。生物学中对科目的定义,用于解释继承关系再恰当不过。
相关名词定义
父类:又称源类、基类、超类;
子类:又称导出类、继承类;
父类和子类
父类和子类之间的类型层次结构同时体现了他们之间的相似性和差异性。当继承现有类型时,也就创造了新的类型,同时子类又归属于父类的类型。这个新的类型不仅包括现有类型的所有成员,而且更重要的是它复制了父类的接口,这意味着所有对父类对象的调用同时可可以对子类对象发起,这遵循了编程原则之一的里氏替换原则。
如果只是简单地继承一个类而不做其他任何事情,那么在父类接口中的方法将会直接继承到子类中。当需要使父类和子类产生差异时,有以下两种方式:

直接在子类中添加新的方法;在采用该种方案时需要仔细考虑是否存在父类也需要这些额外方法的可能性。
覆写父类中的某个方法;该种方案需要在子类中定义与父类需覆写方法同名、同返回值类型、同方法参数类型的方法。

那么继承是否应该只覆写父类的方法呢?
如果继承只覆写了父类的方法,那么子类对象可以完全替代父类对象,这通常称之为替代原则,在这种情况下的类关系称为 is-a;但有时又的确需要在子类中添加新的接口,这种情况下父类无法访问新添加的接口,这种情况下类关系为 is-like-a,这时这种父类与子类之间的关系,被视为非存粹替代
多态
在处理类层次关系的时候,如果把任意一个特定类型的对象可以当作其基类对象来对待,就使得人们可以编写出不依赖于特定类型的代码。
相关概念
前期绑定:编译器将产生对一个具体函数名字的调用,而在运行时需要将这个调用解析到将要被执行的代码的绝对地址(意味着运行前就需要知道具体代码的位置)。
后期绑定:编译器只确保调用的方法存在,而且调用参数和返回值类型正确;在运行时,通过特殊代码,解析具体将要执行的代码的具体位置。
多态的实现理念
通过导出新的子类而轻松扩展设计的能力,是对改动进行封装的基本方式之一。
在试图将子类对象当作其基类对象来看待时,需要解决的一个问题是:编译器无法精确地了解哪一段代码将会被执行。在 OOP 程序设计中,程序直到运行时才能够确定代码的位置。
OOP 程序设计语言使用了后期绑定的概念:编译器确保调用方法的存在,并对调用参数和返回值执行类型检查,但并不知道将被执行的确切代码。Java 使用一小段特殊的代码来替代绝对地址调用,这段特殊代码用来计算方法体的具体位置。Java 默认是动态绑定的。
向上转型
把子类对象看作父类对象的过程,称作向上转型。原因是在类图中,父类总是位于类图的顶部,把子类对象视为父类对象,即将子类类型向上推导。
单根继承
Java 中所有的类最终都继承自单一的基类:Object
单根继承结构保证所有对象都具备某些功能。Object 是任何类的默认父类,是在哲学方向上继续宁的延伸思考。

我是谁?getClass()说明本质上是谁,而 toString()是当前类的名片。

我从哪里来?Object()构造方法是生产对象的基本方式;clone()是繁殖对象的另一种方式。

我到哪里去?finalize()方法说明了对象的最终归属

我是否是独一无二的?hashCode()和 equals()就是判断与其他元素是否相同的一组方法。

与其他人如何协调?wait()和 notify()方法是对象间通信和协作的一组方法。

容器
通常来说,如果不知道在解决某个特定问题时需要多少对象,或者他们将存活多久,那么就不可能知道如何存储这些对象。
在 Java 标准类库中提供了大量容器。不同的容器提供了不同类型的接口和外部行为,同时对某些操作具有不同的效率。如 List 中的 ArrayList 和 LinkedList 由于底层实现的不同,具备不同的应用场景。
参数化类型
由于容器只存储 Object,所以将对象引入置入容器时,被向上转型为 Object,在取出类型时会丢失其类型。在一定程度上可以使用向下转型的方式来获取其实际类型,但是这样做存在风险。
package a;

import java.util.*;

public class Container {
public static void main(String [] args) {
List list = new ArrayList();
list.add(“Hello World!”);
list.add(1);
// 程序运行到这里是不会报错的,但是执行下面这一步的时候,就会出现异常了
for(Object o : list) {
// 这一步会出现异常,因为 List 中存放的不仅仅是 String 类型,还有 Integer 类型,向下转型出现异常
String a = (String) o;
System.out.println(a);
}
}
}
那么用什么方式使容器记住这些对象究竟使什么类型呢?解决方案称为参数化类型,在 Java 中也称为泛型。表示方法为一对尖括号,中间包含类型信息。
List<String> list = new ArrayList<String>();
这样一来,就限定了 List 中只能存放 String 类型的对象啦!当然,我们还是能够通过反射绕过这层验证,毕竟在编译后运行时,是去泛型的。
对象的创建和生命周期
在使用对象时,最关键的问题之一便是他们的生成和销毁方式。
对象的创建
new Constructor();:通过 new 关键词向堆中申请内存,通过 Constructor 来说明类的创建方式。
生命周期
Java 采用动态内存分配的方式。动态方式有个一般性假设:对象趋于变得复杂,所以查找和释放内存空间的开销不会对对象的创建造成重大冲击。动态方式所带来的更大的灵活性是解决一般化编程问题的要点。
Java 提供了被称为垃圾收集器的机制,用来处理内存释放问题。垃圾收集器的运行基础是单根继承结构和只能在堆上创建对象的特性。
异常处理
错误处理始终是编程的难题之一。
什么是异常
异常是一种对象,其从出错点被抛出,并被特定类型的异常处理器所捕获。异常处理就像与程序正常执行路径并行的、在错误发生时执行的另一条路径。
异常的处理
异常不能被忽略,它保证一定会在某处得到处理。异常提供了一种从错误状态进行可靠恢复的途径。Java 一开始就内置了异常处理,并强制你必须使用它。它是唯一可接受的错误报告方式。
并发编程
在计算机编程中,存在着在同一时刻处理多个任务的思想。这些彼此独立运行的部分称为线程,同一时刻处理多个任务称为并发。
在单一处理器中,线程只是一种为单一处理器分配执行时间的手段,换而言之,如果只有一个处理器,那么多线程程序的运行不过是多个任务竞争使用处理器的性能。在多处理器的情况下,实现的才是真正意义上的并发,多处理器并行计算。
多线程同时存在一个隐患,在存在共享资源的时候,可能会造成资源之间的竞争,进而造成死锁。所以在多线程修改共享资源时,必然在共享资源使用期进行锁定。
在 Java 中,JDK1.5 后提供了 concurrent 包支持更好的并发特性。
结语
以上是对象导论的一些基本概念,是继续阅读后面章节的非必要补充性材料。对于文中的一些代码段或概念,暂时不理解的,可以先放一放,等后面看完了,再回过来看就恍然大悟了。
下一节将讲解对象并写第一个 Java 程序。如果你对这些内容不感兴趣,想看点更高难度的,欢迎关注我的博客:https://www.cnblogs.com/lurke…;欢迎关注我的微信公众号 JavaCorner

正文完
 0