《Java编程思想》是一本厚厚的理论书,我不建议新学Java的人去阅读它,至少不应该从头到尾的阅读。这个学习记录我打算写成一个系列,每个系列都以章节记录,在学习过程中不断理解Java这种面向对象语言的编程思想。在我已有Java基础的前提下,再次重温Java黑皮书《Java编程思想》,将我所学所想分享给你们。
抽象过程
编程语言提供抽象机制,这种机制使得编程语言能够对现实中复杂问题进行 “解答”。
程序员必须建立起在机器模型(位于“解空间”内,这是对问题建模的地方,例如计算机)和实际待解问题的模型(位于“问题空间”,这是问题存在的地方,例如一项业务)之间的关联。即一项实际业务一定能在语言建模中有相应的对应。
“解空间”是用来解决“问题空间”的。
Java是一种面向对象编程语言,只是用来表示“问题空间”中元素的工具之一。Java将“问题空间”中的元素及其在“解空间”中的表示称为 “对象(Object)” 。
Java的特性:
- 万物皆为对象:理论上将,可以抽取待求解问题的任何概念化构件(狗,建筑物等),将其表示为程序中的对象。
- 对象的集合构成了程序,对象需要对外提供方法:想请求一个对象,就必须对该对象发送一条消息,这种消息可以理解为对某个特定对象的方法的调用请求。
- 每个对象都有自己的属性或者包含其它的对象构建而成:一个人有姓名,年龄,这是个人的属性,他可能还有一只狗。
- 每个对象都有特定的类型:每个对象都是某个类的实例,这个类需要有特定的区别于其它类的类型,例如String类,类型是String。
- 某一特定类型的所有对象都可以接收相同的消息:其实这是一种向上转型的体现,例如
Person Chengyunlai = new Chengyunlai
,我是属于特定类型:人,所以发送给人这个类处理的消息,我是肯定可以处理的。
每个对象都会有接口
对“问题空间”使用Java来建设“解空间”,要知道对象是类的实例,所以对问题进行语言建模的时候,要先确定类,一旦类被建立,就可以随心所欲地创建类的对象,通过对象去对实际问题的解答和对应。比如说现实中有个电灯,我们要编程对灯进行控制,我们肯定得建立一个类型灯,它得有开灯和关灯的方法,就像现实中开关等一样,这种方法的设计(开灯,关灯)也就是类的接口,接口确定了对某一特定对象所能发出的请求。
Light lt = new Light();
lt.on()
每个对象都提供服务(设计思想)
这个思想是:将对象想象成“服务提供者”。在设计过程中非常有用,它有助于思考我们如何去解决一个问题,它应该是什么样子,需要提供哪些服务,需要哪些对象才能履行这个对象的义务。每个对象都能够很好的完成一项任务。
被隐藏的具体实现
程序开发人员按照角色分为:
- 类创建者(那些创建新数据类型的程序员)
- 客户端程序员(那些在其应用中使用数据类型的类消费者)
客户端程序员调用已经写好的类(类创建者构建的),是不用关心类其内部是怎么样的结构,是一种开箱即用的做法。
这样的好处有:
- 让客户端程序员无法触及他们不应该触及的部分——这些部分对数据类型的内部操作来说是必需的,但并不是用户解决特定问题所需的接口的一部分。
- 允许库设计者可以改变类内部的工作方式,而不用担心会影响到客户端程序员。即最终输出或者完成的功能不变,内部实现进行了升级而客户端程序员感受不到这一变化,即客户端程序员写的代码不用变动。
- 客户端程序员不能访问内部一些代码,意味着类创建者不用担心被粗心或者不知情的客户端程序员所修改毁坏。
这种“边界”的设计在Java的体现是三个关键字:public
,private
,protected
。
名称 | 类本身 | 同个包 | 继承类 | 所有类 |
public | √ | √ | √ | √ |
private | √ | × | × | × |
protected | √ | √ | √ | × |
no modifier | √ | √ | × | × |
Ps:这个形容是在其他地方创建这个类之后,能不能访问此类内部属性
复用具体实现
复用体现的是一种类的组合关系,这表示一个类被创建并且被测试完,那么它就应该(在理想情况下)代表一个有用的代码单元,被复用起来。
例子:一个写好的红旗轮子类。和红旗车架类进行组合。即红旗车架类中包含红旗轮子类。这体现了 红旗轮子类 复用的体现。
继承
通过继承可以实现,将一些类的公共部分抽取出来,以这个抽取的类为基础,其他的类(子类)可以选择继承这个基础类,那么就会得到这个基础类的已经定义好的方法和属性,可以选择在这个基础上新增方法,或者选择覆盖方法,使得子类的功能变得更加强大。
- 纯粹的继承+修改方法的方式,将子类和父类之间的关系称为:is-a,例如三角形(子类)
is-a(是一个)
几何形状(父类) - 继承+新增方法的方式,将子类和父类之间的关系称为:is-like-a,例如热力泵(子类)可以加热也可以加冷,父类是一个制冷系统,方法只有加冷。很明显子类是继承父类之后新增了一个加热方法。称热力泵
is-like-a
制冷系统
Java中使用extends
关键字
向上转型
在处理类型的层次结构时,经常想把一个对象不当作它属的特定类型来对待,而是将其当做基类的对象来对待,这使得人们可以编写出不依赖于特定类型的代码。
这一个思想非常重要。
比如说:一个控制类是控制人行走的,人作为一个基类,它有一个行走的方法;程云来是人这个类的一个具体对象,那么语句可以这样写:
Class PersonController
{
public PersonController();
public void walk(Person person){
person.walk();
}
}
观察这个代码,这个代码是不受新增人物的影响的(增加新类)。这种能力可以极大地改善我们的设计,同时降低软件维护的代价。
在OPP中,程序直到运行时才能够确定代码的地址,为了解决这个问题,面向对象程序设计语言使用了后期绑定的概念,被调用代码直到运行时才能确定,编译器确保被调用方法的存在,并对调用参数和返回值执行类型检查,但不知道将被执行的确切代码。为执行后期绑定,Java使用一小段特殊的代码来替代绝对地址调用。这段代码使用在对象中存储的信息来计算按方法体的地址。
单根继承
在OPP中,所有的类最终都继承自单一的基类。
这样做的好处:
- 所有对象都可以很容易在堆上创建。
- 垃圾回收器的实现变得容易很多。
容器
当出现这种情况:解决某一特定问题的时候,对象的数量以及存活时间是无法知道的。
我们是不可能知道如何事先存储这些对象的,或者说我们需要开辟多少空间,因为具体信息只有在运行的时候才知道。在面向对象设计中,解决思路是这样的,创建一个数据类型,这个类型负责的是持有对其他对象的引用。好在,我们不用自己去实现这个‘数据类型’。
它们通常被称为数组,或者容器。它在任何时候都可以扩充,容纳新的对象,做到了不需要知道对象的数量及存储空间。我们只需要创建一个容器对象,任何让它去处理所有细节。
Java在这方面提供了:
- List(用于存储序列)
- Map(建立对象之间的关联)
- Set(每种对象类型只持有一个)
Java为什么要提供这么多类型的容器呢?一个数组List不行吗?
原因:
- 不同容器提供了不同类型的接口和外部行为。堆栈相比队列就具备不同的接口和行为,也不同于集合和列表的接口和行为。
- 不同的容器对于某些操作具有不同的效率,例子:
ArrayList
和LinkedList
。在ArrayList
中访问是便捷的,都是固定时间,而修改的开销却巨大。LinkedList
相反,访问的代价越靠近队尾开销越大,而修改的开销却比ArrayList
远远要小。
参数化类型-泛型
SE5之前,在容器中存储的都是通用类型Object,而且只能存Object类型数据,这表示着所有被存入容器的对象都被向上转型了。向上转型是一个安全操作,但同时它也造成了一个现象:对象丢失了身份。当你再次从容器中拿出对象的时候,你就需要强制转成自己所需要用到的类型。这个转成更加具体的类型称为向下转型,而这一操作是不安全的。这是因为很有可能会发生:我是个男生,可你却把我当成女生这种荒谬的类型错误,是不是很难受。
那么有没有一种可能,我创建的容器,这个容器它知道自己所保存对象的类型,从而不再需要向下转型以及消除犯错误的可能。这种解决方案称为参数化类型机制。参数化类型就是一个编译器可以自动定制作用于特定类型上的类。在Java中称为泛型。
例子:Array<Person> persons = new ArrayList<Person>()
创建了一个存储Person
类型的Arraylist。
对象的创建和生命周期
Java管理对象的方式是:在被称为堆的内存池中动态地创建对象。在这种方式中,直到运行时才知道需要多少对象,它们的生命周期如何,以及它们的具体类型是什么。因为存储空间是在运行时被动态管理的,所以需要大量的时间在堆中分配存储口空间,这要远远大于在堆栈中创建存储空间的时间。
在堆栈中创建存储空间和释放存储空间通常各需要一条汇编指令即可,分别对应将栈顶指针向下移动和将栈顶指针向上移动。创建堆存储空间的时间依赖于存储机制的设计。这种动态的方式,也正是Java使用的方式:动态内存分配方式。每当想创建新对象时,就要使用new关键字来构建此对象的动态实例。
这样做的好处是:在堆栈中创建的对象,编译器可以确定其存活时间,并自动销毁它。例如基本类型变量。但是在堆中创建的对象,Java提供了“垃圾回收器”的机制,它可以自动发现对象何时不再被使用,并继而销毁它,有效的避免了内存泄漏的问题。
异常处理
异常是一种对象。Java内置了异常处理,并且强制使用。当没有编写处理异常的代码,那么就会得到一条编译时出错消息。
它的思想是:从错误地点被“抛出”,并被专门设计用来处理特定类型错误的异常处理处理器“捕获”。
try{
}catch(Exception e){
}
并发编程
在计算机变成中有一个基本概念,就是在同一时刻处理多个任务的思想。有时对于大量问题,我们只是想把问题切分成多个可独立运行的部分(任务),从而提高程序的响应能力。在程序中,这些彼此独立运行的部分称为线程,上述概念称为“并发”。
并发带来的隐患:资源共享。如果有多个并行任务都要访问同一项资源,那么就会出现问题。为了解决这个问题,就有锁的概念。这个过程可以概括为:某个任务锁定某项资源,完成其任务,释放资源锁,其他任务才可以使用这项资源。