1.1 抽象过程
Java是一门面向对象的语言,它的一个优点在于只针对待解问题抽象,而不用为具体的计算机结构而烦心,这使得Java有完美的移植性,也即Java的口号"Write Once, Run Anywhere"。
所谓的抽象过程,可以理解为对待解问题建模。比如待解问题是一个人,那么我们可以对人进行建模,它的类型是人,有属性姓名、性别、年龄,还有行为吃饭、走路。Java能直接完全据此建模编码,而无需考虑具体的计算机结构。所以当我们阅读Java程序时,正如书上说的"当你在阅读描述解决方案的代码的同时,也是在阅读问题的表述"。
1.2 对象的属性
正如我们所观察到的,人有属性姓名和年龄,圆有属性半径,三角形有属性边长和角度,每个对象都有一定的属性,也可以称属性是对象的成员。
1.3 每个对象都提供服务
每个对象都提供服务也是面向对象的一个重要的思想。
就像在人类世界里,造房子是为了住宿,造车子是为了出行,造学校是为了教育,造医院是为了医治。同样的,在Java中造对象当然是为了给用户使用。如果所造的对象没有一点用处,不能提供任何有用的服务,那简直成了垃圾对象。
既然每个对象都能提供服务,就意味着对象能做一些事,有一些公共方法可以被其他对象使用,这些公共方法也称为接口。
1.4 每个对象都有一个接口
面向对象有一个最重要的思想——物以类聚,各从其类。大千世界,无论何物,它都被视为一个实例化的对象,并且任何一个实例化的对象必定是按照一个模板创建出来的,其中此模板就是类。读过生物的应该都了解任何生物都可以根据"界、门、纲、目、科、属、种"进行分类,如果是非生物的,比如病毒、石头、沙子等,可当我们叫出它们的名字时,我们也找到了其所属的类。
科普一下接口的定义:简单来说,接口指的是“我能做什么”。(不妨打开JAVA的API,看看java.lang包,里面的接口如Appendable,Cloneable,Runnable,后缀都是-able,这样的命名也体现了接口的含义)
问题来了,为什么要用接口?为什么每个对象都有一个接口?我们不妨看看电视机这个实例化的对象,我们按打开按钮时,它就收到"打开"的请求,然后内部就做了一些工作,最后屏幕就亮了,我们按下一个频道时,它就收到"下一个频道"的请求,然后内部就做了一些工作,最后显示下一个频道。其实每个对象都应有相同的工作机制,比如人吃饭,人吃一口饭,内部也就收到"吃"的请求,然后就是一系列的消化的工作,最后会化作每个细胞工作需要的能量。我们对这种工作机制抽象,因而有了接口和实现的概念。接口,就像暴露在电视机外面的按钮一样,有打开,上/下一个频道,音量+/-等功能按钮;实现,就像隐藏在电视机里面复杂的内部工作一样,它最好是隐藏起来的。之所以说每个对象都有一个接口,就是避免把类设计成黑盒子,如果电视机一个接口也没有,那么我们即使用遥控器也没能操作它,这样的电视机有什么用处?因此这就告诫每个程序员,设计类的同时必须至少定义一个公共方法。
1.5 被隐藏的具体实现
为什么要把具体实现隐藏起来?除了 1.4 节所述原因,还有另一个重要的原因。要知道这编程世界有两种程序员,一者曰类创建者(那些创建新数据类型的程序员),二者曰客户端程序员(那些在其应用中使用数据类型的类消费者)。最普遍的,假设称Java类库的创建者为A1(即类创建者),称我们这些使用Java类库的程序员为A2(即客户端程序员)。A1与A2形成一种关系,A1负责构建类,而A2使用A1提供的类来做开发。假设类创建者开发了一个计算组合数的类Combination,数学上组合数的公式为C(n,m)=m!/n!(m-n)!,我们可以定义两个方法,一个是计算阶乘,另一个是计算组合数,我们不希望计算阶乘的方法成为接口暴露出去,因为一旦暴露了,客户端程序员就可以随意设置参数,修改内部实现,因此应当把这类具体实现隐藏起来,这样才能避免被毁坏,减少程序的bug,此外,这样做对客户端程序员而言也是一种服务,因为这样他们就知道哪些方法是可用的,哪些方法是不可用的,还有一个原因是,当我需要修改隐藏的实现时,不会影响到客户端程序员,比如我原来是使用循环来实现阶乘的计算,但后来我发现用递归更加好,我这么一改,甚至连方法名都改了,但是这个类的接口依然没变,所以没影响到客户端程序员。
其实不仅要隐藏具体实现,还应当隐藏对象的属性。这些属性和具体实现的隐藏在面向对象中被称为封装。封装的意义在于保护了,比如人,他有属性年龄,我们都知道年龄必须是非负数的,如果没有封装起来,一些调皮的程序员就可以随意篡改了,比如改成 -1 岁,年龄哪能是负数?所以为了防止篡改,保护对象的属性,就引入了封装。
1.6 复用具体实现
复用具体实现是Java的一大特色了。这里涉及了类之间的关系:聚合、组合、继承、关联、依赖。
简单来讲,聚合就是拥有关系,但是两者生命周期不一致,比如人拥有一台电脑就是聚合关系,其中电脑的寿命到了,但人的寿命还没到,人的寿命到了,但电脑的寿命还没到;
组合也是拥有关系,更是一种强聚合关系,两者的生命周期是一致的。比如人和自己的大脑,两者谁也不能离开谁;
继承不用多说了,比如苹果继承自水果,苹果是水果;
关联也是拥有关系,不同的是,关联是一对多的关系,比如一个订单只能有一个客户,而一个客户拥有多个订单;
依赖就是使用关系,比如一个人要过河,他就需要使用一下船只,在代码里体现在一个类的方法定义了被依赖类的局部变量,或者被依赖类作为此类方法的参数。
1.7 继承
继承是复用代码的一种方式,它是指在现有的一个类的基础上创建一个新的类,被继承的类称为基类(或父类、超类),继承出的类成为导出类(或子类、次类)。
继承有三种情形,1.导出类什么都没做,因此导出类与基类完全一样;2.导出类覆盖(修改)了基类的方法,即导出类纯替代了基类;3.导出类添加了新的方法,还可能覆盖了基类的方法,此时导出类扩展了基类,实际上我们在继承时往往都是扩展基类。
在继承上Java与C++最大的不同就是单根继承了,每个类都只能继承自一个类,并且,Java中所有的类都直接或间接地继承自根类Object,如下图所示
1.8 伴随多态的可互换对象
如 1.7 节所示,我们可以说直角三角形是一个三角形,等边三角形是一个三角形,但三角形不一定是直角三角形或等边三角形。假设我们已经设计了三角形的继承体系,这时我们再设计一个操作三角形的类,类中有一个方法用来填充三角形,但已知有三种三角形,即直角三角形、等腰三角形以及等边三角形,难道要分别为这三种三角形重载三种填充方法吗?不必,多态使得只要在参数上接受三角形这种泛化的类型即可,也就是说,无论是哪种三角形,在多态上都视为三角形,因此能被方法的参数所接受,故也都能被填充。比如下面的程序:
// 不使用多态 void fill(直角 e) { print("填充直角三角形"); } void fill(等腰 e) { print("填充等腰三角形"); } void fill(等边 e) { print("填充等边三角形"); } // 使用多态 void fill(三角形 e) { print("填充" + e); }
可见,因多态机制,代码更加简洁。
到此,补充一句面试爱问的问题:封装、继承和多态是面向对象的三个特征。
1.9 单根继承结构
如 1.7 节所示,Java中所有的类最终都继承自单一的基类,就是Object。这样的单根继承结构有三个优点:
- 所有对象都具有一个共用接口,这样它们归根到底都是相同的基本类型,在设计时还可以利用适配器模式解决接口不兼容问题;
- 保证所有对象都具备某些功能,比如都有toString( )、equal( )方法;
- 使垃圾回收器的实现变得更加容易,因为能保证每个对象具有其类型信息。
1.10 容器
我们都知道在声明数组时必须要指定其大小,而一指定大小就再也不能改了,那问题来了,如果数组的大小仍然不够那怎么办?也只能重新再定义一个数组了,这样子就很不方便了。为解决这个问题Java提供了容器,容器能根据自己所需的元素个数动态地调整容器的大小。容器有Set,Map,List以及栈、队列、树等,其中面试最爱问的就是ArrayList了。
1.11 对象的创建和生命周期
在Java中,创建对象只要使用new关键字即可,然后对象就被创建在堆中,如果对象是创建在堆栈上,编译器就能确定它的生命周期,还可以销毁它,但对象是存在于堆中,编译器就无法确定对象的存活时间了。为了销毁对象,释放内存,Java提供了垃圾回收机制,程序员就不再需要手动销毁对象了,这对程序的编写的确是方便了许多。
当然Java这样做也是有弊端的,因为对象是存在于堆的,因此对对象的创建和操作会慢一些,此外把销毁对象的工作交给垃圾回收器,这样子有点不放心,如果程序退出了,但垃圾回收器还没有销毁对象呢,因此Java程序占用内存高就闻名世界了。
1.12 异常处理:处理错误
举个例子,我们设计一个简单除法的方法divide
int divide(int x, int y) { return x / y; }
这时如果对 y 赋值为0,程序就运行异常,然后二话不说直接退出,很明显程序体验太差,其实这情况还不算太差,如果是为银行、军事、航空设计程序,但没有处理好异常,那样子该有多危险。所以为了确保程序的健壮性,必须要能处理好异常。
1.13 并发编程
许多程序设计问题都要求,程序能够停下正在做的工作,转而解决其他问题。有的程序还要求能够同时做多件事情。比如下载,我们希望同时下载多个资源,支持断点续传,可以暂停下载;比如QQ在线状态,可以一边聊天一边查看好友的状态。对这些问题,只能使用多线程来解决。