面向对象编程(Object Oriented Programming)简称OOP,就是使我们分析、设计和实现一个系统的方法尽可能地接近我们认识客观世界的方法。
对象(Object)代表现实世界中可以明确标识的一个实体。每个对象都有自己独特的标识、状态和行为。
一个对象的状态(State)也称为特征(Property)或属性(Attribute),是指那些具有它们当前值的数据域(Data Field),用变量(Variable)描述。
一个对象的行为(Behavior)也称为动作(Action),是由方法(Method)定义的。调用对象的一个方法就是要求对象完成一个动作。
同一类的对象具有相同的类别的属性和行为。面向对象方法把同一类对象划归一个一个类(Class),用类来描述同一类对象所共同具有的那些类别的属性和行为。类作为创建对象的模板使用。
从程序组成的角度看,类是一种数据类型,而对象则是相应类(数据类型)的一个具体“值”。通常也把对象成为它所属类的实例(Instance)。而有类创建一个具体对象的过程则称为实例化(Instantiation)。
Java类使用变量定义数据域,使用方法定义动作。此外,Java类还提供一种称为构造方法(Constructor)的特殊类型的方法用于初始化对象。所谓初始化对象就是给对象的每个数据域都赋一个初值,以确定对象的初试状态。
对象的状态是由对象持有的所有数据域的取值决定的。如果把对象的所有数据域的值看作是一个向量的话,那么这个向量在某个时刻的值就代表了对象的当前状态。
面向对象程序设计的基本原则和特征
1. 抽象
抽象是指从事物中舍弃个别的、非本质的特征,而抽取共同的本质特征的做法。
2. 分类
分类是按照某种原则划分事物的类别。在面向对象的方法中,分类就是把具有相同属性和相同操作的对象划分为一类,用类作为这些对象所具有的结构和操作的抽象描述。
3. 封装
面向对象中的封装就是用对象把属性(数据)和对这些属性的操作包装起来,形成一个独立的实体单元。通过封装,可以把数据之间的关系和对数据处理的细节隐藏在封装体的内部。从外部看一个封装体,仅仅能够看到一些接口。外部对对象的操作通过调用对象的接口来实现。封装原则使对象能够集中而完整的描述并对应一个具体的事物,体现了事物的相对独立性。
4. 继承
继承是指特殊类自动地拥有或隐含地复制其一般类的全部非私有属性与操作。
继承提供了两点好处:
一是通过继承的方式定义新的类,可以减少编写代码的工作量。原有类中的代码就不用重新写了。
二是可以更好的模拟现实系统,可以更自然的构造系统模型。事实上,继承机制不仅仅是为了少写一些代码,更重要的是可以让我们更合理的构建系统模型。
5. 多态
多态是指在同一个类内或具有继承关系的类之间可以定义同名的操作或属性,但这些操作或属性具有不同的含义,即具有不同的数据类型或表现出不同的行为。这样相应的对象可以按不同的行为响应同一消息。
其实这个现实世界中事物的多态是类似的。简单的说就是对同一消息,事物可以有多种不同的表现形态。现实中,对同一类的不同事物做相同的操作,事物的行为可能是不同的。比如汽车以60mph的速度正常行驶,我们踩刹车。不同的汽车刹车距离是不同的。这就是对同一操作的多种不同的表现形态。
同继承一样,多态也让我们可以减少代码编写工作量,以及更合理地构建系统模型。
6. 消息通信
从面向对象的观点来看,所有的面向对象的程序都是由对象来组成的。这些对象是自治的,对象之间可以互相通信、协调和配合,共同完成整个程序的功能和任务。
对象之间通过消息进行通信,实现对象之间的动态联系。具体地说,在面向对象的方法中,对象发出的操作请求称为消息。
类的声明
[public] [abstract|final] class ClassName [extends SuperClassName] [implements InterfaceNameList]
{classbody}
其中,class是定义类的关键字,ClassName是类名。用户定义的类名必须符合标示符的命名规定。
由花括号扩起来的语句是类体。在定义一个类的时候,这三部分是必不可少的。其余的由方括号扩起来的部分则是可选的。
所谓“可选”的意思就是在定义一个类是,可以加上这些部分,也可以不加这些部分。
在第二个方括号中,是两个修饰符,abstract和final,它们用竖线“|”分隔,意思是二者只能选一。
(1) 关键字public
public是类声明中的修饰符,它的作用是把类声明为公共类,或叫做公有类。在没有这个修饰符的情况下,类只能被同一个源程序文件或同一个包中的其它类使用。注意:在同一个源程序文件中,不能出现两个以上的public类。
(2) 关键字abstract
abstract说明一个类为抽象类,不能用它实例化一个对象,它只能被继承。
(3) 关键字final
final说明一个类为最终类,最终类不能有子类。换句话说,最终类不能被继承。注意:final和abstract不能同时修饰一个类。
(4) extends SuperClassName
extends是Java的关键字,它表示类继承类某一个父类。SuperClassName是父类的类名。
(5) implements InterfaceNameList
implements是Java的关键字,它表示类实现了某些接口。InterfaceNameList是接口的列表。
成员变量的声明
成员变量的声明必须放在类体中,通常是在成员方法之前。
在方法中声明的变量不是成员变量,而是方法的局部变量,二者是有区别的。
成员变量的作用域是整个类,类中所有方法均可访问成员变量;而局部变量的作用域只是方法内部,一旦从方法中返回,该局部变量就消失。
完整的成员变量声明格式如下:
[public|private|protected] [package] // 访问控制修饰符
[static] [final] [transient] [volatile]类型名称;
在成员变量声明格式中,前四个修饰符用于设定该成员变量的访问权限,限制其它对象对它的访问,所以叫做“访问控制修饰符”。
修饰符 | 类 | 子类 | 包 | 所有类和包 | 备注 |
---|---|---|---|---|---|
public | √ | √ | √ | √ | 可以被任何包中的任何类访问。由于公共变量对任何类都是可见的,所以它不具有数据保护功能。 |
private | √ | 只能被声明它的类所使用,拒绝任何外部类的访问。私有变量是不公开的,所以它的得到了最好的保护。 | |||
protected | √ | √﹡ | √ | 可以被声明它的类和该类的子类,以及同一个包中的类访问。 | |
package | √ | √ | 可以被声明它的类和同一包中的其它类所访问。关键字package常常被省略,也就是说,没有访问控制修饰符的成员变量被自动视为package变量。 |
static静态变量
用static修饰符修饰的变量是静态变量。静态变量属于这个类,被属于这个类的所有对象共同拥有。
final最终变量
当使用关键字final修饰变量的时候,该变量的值在程序中将不能再被改变。这样的变量称为最终变量。最终变量也叫做标示符常量。
transient过渡变量
用transient修饰符修饰的变量叫做过渡变量。过渡变量不允许被序列化。
volatile易失变量
用volatile修饰符修饰的变量称为易失变量。volatile的作用是防止编译器对该成员变量进行某种优化。
成员方法的声明
对象的行为是通过方法实现的,其它对象可以调用一个对象的方法,通过消息的传递实现对该对象的控制。
[public|private|protected] [package] // 访问控制修饰符
[static] [final|abstract] [native] [synchronized]
returnType methodName( [paramList] ) [throws exceptionList]
(1) final 最终方法
(2) abstract 抽象方法
用abstratct修饰符修饰的抽象方法只能用在抽象类中。
在对方法进行修饰的时候,final和abstract只能二选其一,不能同时使用final和abstract修饰同一个成员方法。
(3) native 本地方法
修饰符native用来定义本地方法。本地方法用来把Java代码和其它语言的代码集成在一起。
JDK提供了Java本地接口JNI(Java Native Interface),使得Java虚拟机能运行嵌入在Java程序中的其它语言的代码。这些语言包括C/C++、FORTARN、汇编语言等等。
(4) synchronized 同步方法
同步方法用于多线程编程。多线程在运行时可能会同时存取一个数据。为了避免数据的不一致性,应将方法声明为同步方法,对数据进行加锁,以保证线程的安全。
(5) throws exceptionList
当方法内的程序代码可能发生异常,而且在方法中又没有捕获这些异常时,就要使用短语throws exceptionList来从方法中抛出异常,以便让调用这个方法的程序来处理被抛出的异常。
(6) returnType 返回值类型
Java要求一个方法必须声明它的返回值类型。如果方法没有返回值,就用关键字void作为返回值类型。
(7) methodName 方法名
(8) paramList 参数表
在参数表中要声明参数的类型。当有多个参数时,各参数之间要用逗号分隔。在声明成员方法时使用的参数,称为方法的形式参数,简称形参。在调用方法时使用的参数称为方法的实际参数,简称实参。
(9) 方法体
方法体式对方法的实现。它包括局部变量的声明以及所有合法的Java指令。
在方法中声明的变量称为局部变量。局部变量的作用域就是声明它的块,出了这个块,变量就消失了。
另外要注意,方法体包含在一对花括号中,即使方法体中没有语句,一对花括号也是不可少的。
构造方法的声明
构造方法是类的一种特殊的方法。
Java中的每个类都有构造方法,构造方法的功能是为类的实例(对象)定义初始化状态。
由于类是非原始数据类型,所以在创建属于某个类的实例(对象)的时候,要使用new运算符给对象分配内存。在new运算符为对象分配内存之后,Java自动调用类的构造方法,来确定对象的初始状态。
构造方法也有名称、参数、和方法体,并且也有访问权限的限制。但是,作为一种特殊的方法,它与其它方法还是有很大区别的。构造方法与其它方法的区别如下:
① 构造方法的名称必须与类名相同。
② 构造方法不能有返回值。
③ 用户能直接调用构造方法,必须通过关键字new自动调用它。
④ 输出数据
当类中没有定义任何构造方法的时候,系统隐含提供默认构造方法。默认构造方法是一个没有参数的特殊的构造方法。
用默认构造方法初始化对象时,由系统用默认值初始化对象的成员变量。
注意:只要类中定义了构造方法,系统就不会再提供默认构造方法。
声明与创建对象
由类创建对象的过程,称为类的实例化。
由类创建的这个对象,也称为类的一个实例。
一个对象的生命周期包括三个阶段:创建、使用和清除。
要创建属于某个类的对象,可以通过下面两个步骤来完成:
type objectName;
其中,type是复合类型(包括类和接口),而objectName是对象变量名。
对象变量名可以使用任何合法的标示符。
我们声明的对象变量,不是基本数据类型的变量,它所存放的并不是对象的实体。当声明一个对象变量之后,编译器仅仅分配一块内存给它,用来保存指向对象实体的一个地址。
这个地址,在Java中称为引用或参考(reference)。对象的引用实际上类似于C\C++中的一个指针,它指向对象所在的内存地址。但是在Java中,这个地址是不能更改的,所以Java把它叫做引用或参考(reference)。
在Java中,每当创建对象时,系统都会从它管理的内存中分配一定的空间给这个对象。创建对象的方法是使用new运算符。使用new运算符创建新对象,并把新对象的引用赋给对象变量的格式如下:
objectName = new classConstructor();
objectName.memberVariableName
objectName.memberMethodName
创建一个对象之后,它的数据和方法可以使用圆点运算符(.)来访问和调用,该运算符也称为对象成员访问运算符(Object member access operator)。Java通过成员访问运算符访问对象的数据和方法,其语法格式如下:
对象的清除是指释放对象占用的内存空间。很多OOP语言要求程序员跟踪所创建的对象,当不再使用这些对象的时候,由程序员负责清除它们,收回所占用的内存。
但在Java语言中,对象使用完后的释放工作是由系统自动完成的,不需要程序员编程时关注。
Java引入了新的内存管理机制,由Java虚拟机担当垃圾收集器的工作。程序员可以创建对象而不用担心如何清除它们,垃圾收集器会自动清除它们。
使用new操作符创建对象之后,Java虚拟机自动为该对象分配内存并保持跟踪。Java虚拟机能判断出对象是否还被引用,对不再被引用的对象,释放它所占用的内存。
这种定期寻找不再使用的对象并自动释放对象所占用内存的过程就称为垃圾收集。Java虚拟机实际上是利用变量生存期来管理内存的,对象的引用被保存在变量中,当程序跳出变量做在的区域后,它会被自动清除。
我们也可以自行清除一个对象,只要把一个空值null赋给这个对象引用即可。
方法的签名
为了描述方法,我们需要详细说明方法名和方法的参数类型。方法名和方法的参数类型称为方法的签名。
void addAnEmployee(String n, double s)
{
for (int i = 0; i <staff.length; i++)
{
if (null == staff[i])
{
staff[i] = new Employee(n, s);
break;
}
}
}
其签名是 addAnEmployee(String, double)
方法的签名惟一标识了一个方法。换句话说就是,方法名和方法参数类型一起惟一标识了一个方法。
方法的调用
需要调用方法才能使用方法提供的功能。
逻辑上看,方法是对象的一个组成部分,因而方法调用通常是通过对象的引用调用的。调用方法的语句格式是:
这里 reference 表示某个对象的引用,method(arguments) 是对象的某个方法。需要注意的是 method(arguments) 是在对象所属的类中定义的。 arguments 是方法的参数列表。参数列表中可以包含多个参数,每个参数的声明包含参数类型和参数名两个部分,参数与参数之间用逗号隔开。
方法的重载
方法重载可以实现Java的编译时多态。
重载的英文名称是overloading,它是在相同类内,定义名称相同,但参数个数或参数类型不同的方法。(仅仅有返回值数据类型不同是不行的)。
编译器会根据实参的个数和类型,确定调用哪个方法。所谓编译时多态,是指程序会根据参数的不同来调用相应的方法。具体调用哪个被重载的方法,是由编译器在编译阶段决定的。所以编译时多态也叫做静态多态性。
关于重载
• 重载的关键是方法具有不同的“签名”,如此编译器才能区分它们的不同
• 不但可以重载成员方法,也可以重载构造方法。
内部类
Java允许将一个类的定义放在另一个类的内部。定义在另一个类内部的类就是内部类。内部类允许我们把一些逻辑相关的类组织在一起,并控制在内部的类的访问特性。
内部类的声明和使用
内部类的声明格式如下:
class 外部类名称
{
//外部类成员
class 内部类名称
{
//内部类成员
}
}
内部类的对象持有一个隐式引用,它引用了实例化该内部对象的外围类对象。通过这个引用,可以访问外围类对象的全部状态。
注释:
内部类是一种编译器现象,与虚拟机无关。当使用内部类时,编译器会从外部类中把内嵌的类分离出来并调整编译结果。使用内部类的目的只是是为了使用更合适的方式组织代码。
匿名内部类
有些情况下我们创建的对象仅仅在程序中使用一次就不会再用了。这个时候就没有必要给这个对象命名。匿名内部类提供了创建不具名称的对象的简便地编写代码的方法。
创建匿名内部类并访问成员的语法:
(
new ClassName()
{
type variableName;
returnType methodName([paramList])
{ methodBody; }
}
).methodName(param1,param2,...,param n)
分成两步来看:
第一步:
第二步:
成员变量和成员方法是类的基本组成部分,它们分别用于描述该类的实例具有的属性和功能。不管成员变量也好,还是成员方法也好,都有类成员和实例成员之分。
类和对象的逻辑结构与内存区域
类的代码和对象都占有一定的内存区域。在类的内存区域中,存储了类的成员变量的定义信息和成员方法代码。
对象也会占有自己的内存区域。图3.7表明了类和对象的内存区域。
由 Employee 类创建了两个对象,一个是 e1,一个是e2。
按照前面讲的,每个对象都有自己的内存区域,在每个对象的存储区域中都会为变量 name 和变量 salary 分配存储空间,这样 e1 的 name 和 salary 与 e2 的 name 和 salary 在存储上是独立的,存储的实际的值是互不影响的。
但是要注意,成员方法的代码只在类的内存区域存储,在每个对象的内存区域是不会再分配的,因为所有对象的成员方法的代码都是相同的,没有必要在每个对象内再重复存储方法的代码。
值得注意的是,虽然方法的代码只在类的存储区域存储,不在每个对象的内存区域重复存储,但是从逻辑上看,我们仍然认为对象持有成员方法。
类变量和实例变量
通过前面的介绍,我们知道了类怎样定义对象的成员变量和成员方法,而且我们知道,在每个对象的内存区域中都单独存储自己拥有的成员变量,而成员方法只在类的内存区域中存储一份。这是我们通常所需要的。但是,有时我们会希望某个变量被类的所有实例共享。例如,在薪酬管理系统中,每个雇员会有一个惟一的雇员号,为了这个属性,我们给 Employee 类增加一个用于记录雇员号的成员变量 id。我们希望雇员号是递增的,即新增加雇员的时候新雇员的雇员号是原有最大雇员号加一。那么我们还需要一个表示当前最大雇员号的变量,比如叫 maxId。显然,这个 maxId 应该是 Employee 类的所有对象共有的,而不是某个对象特有的。如果仍然按照前面的方法去处理变量 maxId 显然不合理,那么我们该怎么办呢?答案是我们在声明变量 maxId 的时候用 static 修饰符修饰它,把它声明为类变量。
在声明成员变量的时候,用关键字static修饰的变量就是类变量,类变量也称为静态变量。没有被关键字static修饰的变量就是实例变量。两种成员变量区别在于实例变量属于个别对象所有,每个对象都有自己的一份实例变量;而类变量属于类本身,属于类的所有实例,它在内存中只存储一份,该类的所有实例访问的都是类变量的同一个拷贝。
类方法和实例方法
成员方法也有类方法和实例方法之分。在声明成员方法的时候,用关键字static修饰的方法就是类方法,没有被关键字static修饰的方法就是实例方法。类方法也称为静态方法。
类方法和实例方法的区别在于类方法不针对特定的对象,所以在没有创建对象之前可以执行,而且在类方法中只能访问类变量和类方法,而不能访问实例变量和实例方法。实例方法则只能针对特定的对象执行,因此,如果任何对象都不存在,则也就没有可以执行的实例方法。调用类方法有两种情况,一种通过类名调用类方法。