本文通过概念+代码的方式,来帮助读者了解面向对象程序设计的全貌。
抽象过程
概念
机器模型:位于解空间
内,是对问题建模的地方;可以这样理解,汇编语言和命令式语言,在解决问题时要基于计算机的架构;因此架构限定了解决方案,所以说机器模型是解空间。
实际待解决问题:问题空间
,是问题存在的地方
如何抽象
抽象的类型和质量,决定了人们所能够解决的问题的复杂性。抽象的类型指的是“所抽象的是什么”。一种是在机器模型和实际待解决问题的模型之间建立联系的抽象;另一种是只针对待解决问题建模。而面向对象则是向程序员提供表示问题空间中元素的工具,我们将问题空间中的元素及其在解空间中的表示称为“对象”。
面向对象程序设计的特性:
- 万物皆是对象;
- 程序是对象的集合,对象间方法的调用是程序运行的基本表现;
- 对象可以包含其他对象;
- 每个对象都拥有其特定的类型;
- 某一特定类型的所有对象都可以接收同样的方法调用;
每个对象都应该都归属于一个类或接口
名词解释
对象:具有状态、行为和标识的实体。如银行存款账户是一个类,那么具体的每个人的银行存款账户就是这个类目下的对象。
类:可以看作类型来考虑。比如说鸟类,是动物中的其中一种类型。
类和对象
类在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的访问权限控制关键字
访问权限关键字的修饰范围
- public:可以修饰外部类、属性、方法;
- protected:只能修饰属性和方法;
- private:只能修饰属性、方法、内部类;
复用具体实现
代码复用的基本方式:
- 直接使用该类的一个对象;
- 可以将某个类的一个对象置于一个新类中,作为新类的成员出现;
类之家的关系
在两个类之间存在有关系和没关系两种情况,在有关系的情况下,其关系包括以下六种类型
继承关系
相关名词定义
父类:又称源类、基类、超类;
子类:又称导出类、继承类;
父类和子类
父类和子类之间的类型层次结构同时体现了他们之间的相似性和差异性。当继承现有类型时,也就创造了新的类型,同时子类又归属于父类的类型。这个新的类型不仅包括现有类型的所有成员,而且更重要的是它复制了父类的接口,这意味着所有对父类对象的调用同时可可以对子类对象发起,这遵循了编程原则之一的里氏替换原则。
如果只是简单地继承一个类而不做其他任何事情,那么在父类接口中的方法将会直接继承到子类中。当需要使父类和子类产生差异时,有以下两种方式:
- 直接在子类中添加新的方法;在采用该种方案时需要仔细考虑是否存在父类也需要这些额外方法的可能性。
- 覆写父类中的某个方法;该种方案需要在子类中定义与父类需覆写方法同名、同返回值类型、同方法参数类型的方法。
那么继承是否应该只覆写父类的方法呢?
如果继承只覆写了父类的方法,那么子类对象可以完全替代父类对象,这通常称之为替代原则
,在这种情况下的类关系称为is-a
;但有时又的确需要在子类中添加新的接口,这种情况下父类无法访问新添加的接口,这种情况下类关系为is-like-a
,这时这种父类与子类之间的关系,被视为非存粹替代
多态
相关概念
前期绑定:编译器将产生对一个具体函数名字的调用,而在运行时需要将这个调用解析到将要被执行的代码的绝对地址(意味着运行前就需要知道具体代码的位置)。
后期绑定:编译器只确保调用的方法存在,而且调用参数和返回值类型正确;在运行时,通过特殊代码,解析具体将要执行的代码的具体位置。
多态的实现理念
通过导出新的子类而轻松扩展设计的能力,是对改动进行封装的基本方式之一。
在试图将子类对象当作其基类对象来看待时,需要解决的一个问题是:编译器无法精确地了解哪一段代码将会被执行。在OOP程序设计中,程序直到运行时才能够确定代码的位置。
OOP程序设计语言使用了后期绑定的概念:编译器确保调用方法的存在,并对调用参数和返回值执行类型检查,但并不知道将被执行的确切代码。Java使用一小段特殊的代码来替代绝对地址调用,这段特殊代码
用来计算方法体的具体位置。Java默认是动态绑定的。
向上转型
把子类对象看作父类对象的过程,称作向上转型
。原因是在类图中,父类总是位于类图的顶部,把子类对象视为父类对象,即将子类类型向上推导。
单根继承
单根继承结构保证所有对象都具备某些功能。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程序。欢迎关注我的微信公众号,可以更方便的获取每日推送