本笔记参考自: 《On Java 中文版》
目录
写在第一行
Java的迭代与发展
Java的迭代
Java的参考文档
对象的概念
抽象
接口
访问权限
复用实现
继承
基类和子类
A是B和A像B
多态
单根层次结构
集合
参数化类型
对象的创建和生命周期
写在第一行
作为一门派生语言,Java的诞生离不开C++。也正因如此,Java继承了来自于C++“面向对象”(Small-Talk)的概念。而后来,这种面向对象的概念被证明是有些激进的。而正如每一门语言都有的一样,Java也存在着自身的局限,但知晓一门语言的不足也同样重要。
(例如Java中那些设计存在缺陷的库及语言。)
很显然,将一切都封装成对象过于激进,但完全抛弃它也不免浪费。因此在Java一些面向初学者的书中,对于对象这一概念的态度是折中的(笔者:合理地利用)。本笔记参考的《On Java 中文版》既是如此。
Java的迭代与发展
Java的迭代
发展至今,Java已经迭代了很久。除去早期版本的版本号,现在的Java一般每6个月发布一个新版本,并且使用整数作为其版本号。值得注意的是,Java在新发布的版本中往往会包含一些试用功能,但难以保证这些功能长期存在的,所有写代码时最好别依赖它。
Java提供了两种不同的支持:短期支持(STS)与长期支持(LTS,常见的Java 8就是该版本)。但同样地,它们都会提供不同的功能试用。
在每一版的Java中,都会包含不同的功能试用,可以分为三类:实验(Experimental)、预览(Preview)、孵化中(Incubating)。
Java的参考文档
Java的开发工具集(即JDK)带有电子在线文档,该文档对于每个类的描述都很详细。在进行Java开发的时候可以参考该文档。
对象的概念
抽象
如果说计算机是一种表达的媒介,编程语言是一种思维模式(就好比不同的语言最终影响了我们的衣食住行一样),那么面向对象编程,就是对使用计算机这一媒介的一次尝试。
要解释上面这句话,就要提到抽象,任何编程语言都是一种抽象,有的是对于计算机结构的抽象,有的是对特定问题的抽象。这些不同的语言会在不同的问题上展现自己的优势,但同样的,它们往往都存在无能为力的地方。因此,面向对象编程应运而生。
在面向对象编程中,对象可以是某种生物,某个建筑或者某种服务,而我们进行抽象的对象不再是计算机,而是实际的问题。通过这种方式,我们将程序转变为一段用来描述问题的文字。(注:对象与计算机之间的联系可以通过“状态”进行体现,因为每一个对象都有自己的行为和特征)
Alan Kay曾经总结过SmallTalk(一种面向对象语言)的基本特征,其同样适用于其他面向对象的编程:
- 万物皆对象。
- 一段程序实际上就是多个对象通过发送消息来通知彼此要干什么。
- 从内存角度而言,每一个对象都是由其他更为基础的对象组成的。
- 每一个对象都有类型。
- 同一类型的对象可以接收相同的信息。
接口
在拥有了对象这一概念后,我们将对象进行了分类,因此就诞生了类型。我们把状态不同而结构相同的对象分为“同一类对象”,而反过来,每一个对象所归属的类就决定了对象具有的行为特征。
在很多面向对象的编程语言中,存在着class(即:类)这一关键字。可以说,这一关键字就代表着type(即:类型)。而事实上,由于类描述了对象的相同特征和行为,这和数据类型很相似,所以也有这样的说法:类就是数据类型。
但此时依旧存在着一个麻烦的问题,那就是对象能够接受什么请求。这就是由“接口”决定的,对象归属的类定义了这些接口。在面向对象的操作中,我们会向对象发出特定的请求,而如何处理请求是由对象来决定的。例如,下方图片展示了一个名为Light的类型:
Light A = new Light(); //A是一个引用,使用new关键字新建一个对象
A.on(); //调用这一对象的接口
||| “实现”的概念: 用于响应请求的代码,以及与之相关的数据,它们的总和被称为“实现”。
在开发面向对象的程序时,我们应该将对象想象为一个“服务的提供者”,对象应该能够,且只能够做好一件事(每个对象都只提供一种服务)。
访问权限
一般,可以将程序员分为两类,分别是“类的创建者”和“客户程序员”,一方负责创建数据类型,而另一方则使用各种类。这意味着,“类的创建者”只为“客户程序员”提供必要的类的接口。这种做法当然是有意义的:
一方面,这种做法可以提高安全性,因为这可以使得“客户程序员”无法解除那些本不允许他们接触的内容;而另一方面,当创建者想要修改类的内容时,由于修改的内容处于内部,因此不会影响使用该类的“客户程序员”。
Java为了实现这种对访问权限的控制,设置了3个显式关键字,分别是public、private和protected,不同的修饰有不同的权限:
关键字 | 访问权限 |
---|---|
public | 可以被所有人访问 |
private | 只能被类的创建者通过该类自身的方法访问(其余人不可访问) |
protected | 类似于private,但被protected修饰的类,其子类可以访问protected的成员 |
若不使用上述任意一种访问修饰符,Java也有默认的访问权限,即“包访问”,这一权限运行一个类访问其同一个包内的其他类,但不影响外部访问。 |
复用实现
在理想的情况下,我们希望我们创建的对象能够有效,且应该是可以复用的。但现实情况是,这种可复用的对象设计更为少见。
复用一个类有不同的方式,如果是利用已有的类组合成一个新的类,这种方式被称为“组合”(若组合是动态的,则被称为“聚合”)。组合代表的通常是一种“有”的关系,例如:“汽车有发动机”。
组合是一种很灵活的复用方式,这是因为通过组合形成的新类,其内部的对象通常具有private属性,因此它即安全,又方便创建者修改这些内部对象。也因此,在创建新类时可以优先考虑组合。
继承
基类和子类
在实际面对问题时,我们有时会发现:可能需要两个功能及其相似的类。如果因此大费周章,就太麻烦了。而继承(复制现有的类,在复制类上进行增补)就可以对此进行处理。通常,我们把被复制的类称为“基类”(“超类”或“父类”),而把被修改的“复制”类称为“子类”(“派生类”或“继承类”)。
继承的缺点是:若基类发生变化,则子类也会跟着变化。
继承的这种思路使得我们可以有基类来阐述核心的思想,而众多的子类则是这一核心思想的不同实现方式。例如:如果现在需要绘制一个图形,把“形状”(Shape)作为基类,每个具体的形状都有具体的信息(大小、颜色)和行为(绘制、上色),那么圆形(Circle)、矩形(Square)、三角形(Triangle)等就是被派生出的子类。
由继承产生的新类不仅会继承基类的所有成员(private成员除外),还会继承基类的接口。也就是说,基类对象能够接收的消息,子类也一样可以接收。从这一点上看,子类和基类拥有相同的类型。
子类和基类会因为两种不同的方式产生区别:第一种方式是为子类添加新的方法。
而另一种方式则是修改基类的已有方法,即“重写”。想要重写一个方法,只需在子类中对该方法进行重新定义即可。
A是B和A像B
若子类和基类的接口一模一样,这也就意味着子类和基类的类型是完全相同的。在这种情况下,我们说基类和子类之间存在的关系是“A是B”,就比如“圆是一个形状”。此时,可以直接用子类的对象替代基类的对象。
但还有另一类“A像B”的情况,在这种情况下,子类除了拥有基类接口外,还有一些独立的接口。这时无法通过基类的接口获取子类的新方法。这时之前提到的那种替换通用性就会有所下降,显得并不这么合适。此时如果需要迎合子类,比较好的方法是从设计层面进行更改(增添功能)。
多态
在涉及类型层次(即子类和基类结构)时,通常会将对象视为其基类的一个实例。通过派生子类的方式,可以轻松扩展程序设计。在这种方式中,我们并不需要关心具体执行的代码,因为对象能够根据类型执行对应正确的代码。
这里存在着这样的一个问题:既然我们并不知道对象的具体类型,那么应该怎么为这一对象匹配正确的处理呢(毕竟它仅仅调用了接口)?
答案来自于继承机制的一种重要技巧:编译器不是通过传统方式来调用方法。这种机制被称为“后期绑定”,这种机制要求程序直到运行时才确定代码的地址。而在Java中,方法都默认具有后期绑定特性,这使得我们不需要使用额外代码来实现多态。
与“后期绑定”对应的就是“前期绑定”,对于非面向对象编译器而言,其生成的函数调用会触发“前期绑定”,此时编译器会生成一个具体方法名的调用,再通过该方法决定被执行代码的绝对地址。
除此之外,这里还会涉及一个“向上转型”的概念,即将子类视为基类。如果我们为基类编写一个方法do(),那么这个方法也将适用于该基类的任意的一个子类,而do()发送给基类的信息也可以发送给子类。例如,还是以Shape为例:
单根层次结构
在Java中,面向对象中的所有类都默认继承自一个终极基类Object。这种“单根层次结构”的使得所有对象都具有了共同的接口,这就提高了Java的兼容性。单根层次结构的另一个好处就是垃圾收集器,因为所有对象都有类型信息(Object),所以在对这些信息处理时,不需要费尽心思去考虑处理对象的类型。
集合
有时,在面对一些问题时,我们会遇到对象个数不明,数量不明,存在时间不明的情况。在程序开始执行前,我们无法得知上述情报。很显然,这对开辟空间是一个巨大的阻力。
在面向对象设计中,为了解决上述情况,通常都会用到集合(其实也可以使用数组)。集合是一种对象,这种对象通过保存其他对象的引用来解决问题,并且会根据放入的内容来调整空间。Java提供了一些集合,例如List类、Map类、Set类等,还有一些队列、栈和树。
当然,无论集合的类型多么丰富,我们需要的都是能够解决问题的集合。比如List有两种基础的集合:ArrayList和LinkedList。二者会因为结构的不同而在执行效率上有所区别。(不过由于两者都是基于List接口的子类,因此可以通过改动代码进行切换)
参数化类型
在Java 5之前的版本中,集合只支持通用类型Object。这意味着,如果我们将对象添加到集合中,那么我们使用的对象将向上转型为Object,从而失去自身的特征。这无疑加大了从集合中提取对象的难度。
与“向上转型”相反,存在着“向下转型”的概念。当然,这种转型其实不太安全,因为一个基类可以有多个子类,若不声明确切的对象类型,那么在选择子类上就会可能出现安全问题。
“参数化类型”的概念,为创建的集合明确其中包含的对象类型,成为了上述问题的解决方案。一个被参数化的类型是一个特殊的类,可以让编译器自动为其适配类型。例如:一个被定义为存放Shape类型的集合,也只能从中取出Shape类型。
Java 5新增了一个特性用于支持参数化类型,或者说“泛型”。如果需要创建一个Shape对象的ArrayList,只需要在下方尖括号内加上类名,就可以定义泛型:
ArrayList<Shape> shapes = new ArrayList<>();
对象的创建和生命周期
每个对象的创建都要消耗内存资源,当不需要使用该对象时,回收其占据的资源无疑是必要的行为。但随着问题复杂度的上升,当系统的某一部分不再需要一个对象时,该系统的其他部分可能还在使用该对象。这就使得一些使用显式删除对象的编程语言(例如C++)的程序员要耗费精力去解决这一问题。
C++将对生命周期的处理交给了程序员,通过对栈区和堆区的选择比较效率和灵活性。而Java只运行动态分配内存,通过new操作符来创建一个对象的动态实例(不过也有特例,就是基本类型)。当然,Java也不需要手动释放内存,因为通过垃圾收集器机制,Java能够自动销毁无用对象。