面向对象语言基础 一
学习如何定义类和创建对象
摘要:
杰福·弗里森(Jeff·Friesen)以Java面向对象的特性连载了他的Java旅程系列。在这期,他介绍了面向对象编程以及如何定义类和从这些类创建对象。此外,Friesen成功的兑现了他对javadoc研究的许诺。参见:“Java文档化”。
名词术语,提示和告戒,作业以及此专栏的其他信息,参看相关的“学习指南”。(3500字)
回顾80年代中期,我使用当时流行的软件开发方法论——结构化编程――来辅助我编写计算机程序。结构化编程的着重在将程序的数据和他的功能分开。正如我发现的,将数据从功能中分离的典型困难是难以维护和理解――特别是在大的程序里。
90年代早期,我学到了一种新的开发理论――面向对象编程(OOP).OOP在源代码以及可执行文件代码中创建了与现实世界十分逼真的软件模型――如车、银行帐号与狗等。结构化编程着重在创建实体的描述以适应编程语言的限制;面向对象编程则着重在创建编程语言以适应描述的限制。举例来说,在结构化编程语言中,一辆车被描述为一组函数(代码段名)以用来启动、刹车、停车、加速等等。一组独立的变量用来定义车的颜色、门的号码、制造、型号等。你需要初始化这些变量并调用一些函数来操作这些变量――哇,你有了一辆车!
而OOP语言则将车看成由他的行为――功能,和保持车状态的变量――数据值所构成的一个整体。将状态值和行为整合的结果引发了对象的产生。你可以简单的创建那个对象,同时初始化那些变量。在任何时候,面向对象编程都标识了使用和调用的方法工作的对象。本质上,面向对象程序是以一组对象(如:车),而不是将功能(如:停车)和变量(如:门的数目)分开来考虑的。
将状态变量和行为整合进对象的技术称为“封装“。封装促进了信息的屏蔽――通过隐藏状态变量和行为使程序更容易维护而不需要进入其内部――这是OOP三大基本概念之一(我将在接下来的文章中探讨其他两个:继承和多态)。
OOP已经在开发者中广泛应用了将近15年了,这归功于C++语言的普及。然而,由于它C语言的遗留特性,C++并不是完全面向对象的;你可以用C++写程序而不使用面向对象的特性。相反的,JAVA是完全面向对象的:每一个Java程序要求至少一个类(一个面向对象的特性)。而且,Java的官方语言定义包含了面向对象条款(见“资源”)。
本文将以Java的视角对OOP进行深入的探索。我将从审视类和对象开始;也会讨论javadoc这个工具使包、类、字段、方法等文档化。更多内容请见附录:“Java 文档化”。
整个面向对象语言基础系列:
一:如何定义类和创建对象
二:声明、访问字段和方法
三:结构:从其他对象构建对象
四:继承:层层构建对象
五:基类
六:使用接口来实现更多安全的多重继承
七:学习Java的更多特性并在你的类体系结构中调整框架
俗话说:巧妇难为无米之炊。正如一个建筑承包商需要根据建筑师的篮图来建造高楼大厦一样,一个运行中的程序在访问一个类之前必须事先创建基于该类的对象。
定义类:
“类”是对象的源代码蓝图。这个蓝图定义了每一个对象的行为和状态变量。使用下面的语法来声明一个类:
[ 'public' ] [ ( 'abstract' | 'final' ) ] 'class' class_name
'{'
// 行为和状态变量在 '{' 和 '}' 之间定义
'}'
在介绍类声明前,先详细介绍关键字:class以及它后面的标识符――以class_name描诉的――类的名字。标识符不能是保留字。举例来说:类 Account描述了一个类,它使用Account来做为这个类的名字。(习惯上,类名使用大写。)关键字public和/或abstract或final可以随意地先于class之前出现――但不能同时有abstract和final,这将导致该类无意义。
使用public关键字来联系包。将class_name标记为public以允许其为所有包中的所有类所访问。而如果不是为public所标记的类则只能为与class_name同名包中的类所访问。(我将在以后的文章中探索包。)
在定义一个public类时请记住以下规则:在源文件中只能有一个public类。而且,这个源文件的文件名必须和class_name一致。如下例:
// MyClass.java
public class MyClass
{
}
Java要求MyClass类必须定义在MyClass.java的源文件中。在别的文件中声明该类――如Fred.java,或者即使是myclass.java――都是非法的。对类名使用正确的标识符,如大小写的敏感。(如MyClass,你必须注意到M和C是大写的,而其他都是小写的.)
你可以将非public类定义在以任何名字命名的源文件中,但文件的后缀仍然一定要是.java。如下例:
//Fred.java
class MyClass
{
}
上面的例子定义了一个非公共类:MyClass,并将该类的实现存储在文件Fred.java中。
使用关键字abstract将class_name定义为一个抽象类。对象不能从抽象类中被创建。(我将在以后的系列中讲述抽象类的概念。)
使用关键字final将class_name定义为一个final类。你不能从一个final类中继承行为和状态变量。(我将在以后系列中探讨继承的概念。)关键字:public,abstract,和final可以用任何顺序来声明。例如:public abstract class fred,final public fred,abstract fred等等。
每一个类都有一段正文来定义行为(叫做方法)和成员变量(叫做字段)。正文开始于:’{’结束于’}’。虽然你可以不在{,}之间定义任何方法和字段(如上述MyClass的例子),但没有定义方法和字段的类几乎是没有必要的。因此,你将经常在类中定义字段,方法,或者皆有两者。(我将在以后对字段和方法进行详细的研究。)
类和Java程序
每一个Java程序都有一个或多个类组成。这些类中的一个被做为“启动”类,因为它是JVM装载的第一个类。实际上,类文件实现了这个启动类――通过提供代码来实现行为和保持状态的变量――由JVM装载。
正如你在先前的Java 101文章中看到的,一个Java应用程序包含一个启动类:main()方法。请查看清单一中的CODemo1程序来回忆一个简单程序的概况:
清单一 CODemo1.java
// CODemo1.java
class CODemo1
{
public static void main (String [] args)
{
System.out.println ("Hello");
}
}
CODemo1—类/对象演示1——是一个简单的Java程序。CODemo1类的正文是一个单独的方法:main()。在你编译这段代码并用:java CODemo1命令来运行它后,JVM装载启动类文件――CODemo1.class――并且执行由main()构成的字节码。执行的结果是在标准输出设备上出现:Hello信息。
创建对象
一个对象是一个类的实例。使用下面的语法来创建一个对象:
'new' constructor
“new”关键字,通常被称为构造符,给一个对象分配内存空间并使用默认值进行初始化。对象的字段值被存放在内存中。由于“new”是个操作符,所以它取一个运算域:构建器,构建对象的特殊方法。当“new”完成内存分配和初始化后,它就调用构建器来完成对象的初始化工作。
清单一展示了一个最简单的也许有用的应用。虽然清单一清楚的展示了一个程序必须的类的定义,但却没有对象从该类中创建。你将怎样从执行码的main()方法中创建对象并通过对其字段的访问以及调用其方法来操作这个对象呢?清单二给出了这个答案:
清单二.CODem2.java
// CODemo2.java
class CODemo2
{
int i = 3;
public static void main (String [] args)
{
CODemo2 obj1 = new CODemo2 ();
System.out.println ("obj1.i = " + obj1.i);
obj1.printHello ();
CODemo2 obj2 = new CODemo2 ();
obj1.i = 5;
System.out.println ("obj1.i = " + obj1.i);
obj1.printHello ();
System.out.println ("obj2.i = " + obj2.i);
obj2.printHello ();
}
void printHello ()
{
System.out.println ("Hello! i = " + i + "/n");
}
}
字段定义语句:int i = 3;定义了一个整型(使用int关键字)字段:i,并且将它初始化为3。
CODemo2 obj1 = new CODemo2();则是从CODemo2类创建了一个对象。
CODemo2 obj2 = new CODemo2();则创建了第二个对象。由于每一个对象都是类的一个实例,所以“类实例”通常被用作对象的同义词。
CODemo2 obj1 和 CODemo2 obj2使用相近的方式定义变量:int count 或double balanceOwing。数据类型关键字:int 和double定义了名为:count 和balanceOwing的存储位置来分别存放基本类型数据:整型和双精度浮点值。另一方面,CODemo2定义了一个储存单元以存放一个引用--或者说地址--的数据类型的值。换句话说,因为obj1和obj2拥有CODemo2的数据类型而CODemo2是一个引用数据类型,任何赋值给obj1和obj2的值都指向由CODemo2类创建的对象(或者说地址)。
CODemo2的 new 操作符调用CODemo2()来创建对象。且慢!在CODemo2中并没有声明CODemo2()这个构造函数。将会发生什么呢?请在本系列的下一篇文章中寻找答案。
当构造函数创建完毕对象后,构造符new返回这个对象的地址。在清单2中,第一个返回的CODemo2对象地址赋给了obj1变量;第二个则给了obj2。图一描述了这两个为各自的变量所引用的对象。
|
图一.两个CODemo2对象以及他们各自的引用变量的概念
我自己把对象想象成由一个或者多个箭头指向的圆。每一个箭头都阐明了一个对象引用,而箭头起始于一个储存对象地址的矩形。
请看清单2中对以下方法的调用:
System.out.println(“obj1.i = “ + obj1.i);
obj1.i是什么意思呢?i 是一个被初始化为3的整型变量。从CODemo2创建的对象有能力拥有自己的一份那个变量的拷贝(不同的值),所以我们必须区别多个变量的拷贝。因此,我们必须通知程序该使用哪一个拷贝。如前面的标识符i是属于名为obj1的对象引用的(而不是属于obj2的)。前缀符一般使用点操作符。(我将在下个月的专栏中介绍这方面知识)
obj1.printHello();调用了printHello()方法,如在obj1.i中一样,printHello()的前缀是obj1.,因为在清单2中还有obj2.printHello();,所以你可能认为有两个printHello()方法的拷贝——每一个对象拥有一个。实际上,只存在一个。在CODemo2类文件加载时就已经生成了字节吗。那么这唯一的一个printHello()拷贝又如何区分这个i到底是obj1的还是obj2的呢?答案在于obj1.和obj2.前缀上。出现在obj1和printHello()之间的点操作符通知printHello()方法使用的i属于obj1(而不是obj2的)。对obj2也是如此。(我将在下月的专栏中介绍更多的方法)
为什么在printHello()中不使用obj1或obj2前缀呢?如果你有疑惑,请看下月的专题。
现在你已经看到了一点对象的内在本质,而且知道它是如何引用的,你可能对内部结构有疑惑。图2展示了一点结构。
|
图2.一个CODemo2对象的内部
图2展示了两个变量——obj1和obj2——引用了两个CODemo2对象。实际上,每个变量都是包含了一个包含对象信息的对象描述符的地址。其中,描述符保存了一块包含对象字段以及每个CODemo2方法字节码的内存地址。
仔细看图2,Java似乎提供了指针—指向其他变量地址的变量。但在语言级别,Java并没有提供指针。取而代之的是Java使用“引用”——抽象对象标识符。在底层,由JVM确定如何实现一个引用。例如:某种JVM可能把一个数字赋值给一个指向对象表的索引的引用变量,然而另一种JVM可能将对象的内存地址赋给引用变量。(在这个例子中,引用变量基本上就是一个指向对象的指针。)因为你不知道(也不需要知道)怎么赋值给一个引用变量,只需要知道引用变量是引用对象的方式而已—忘掉指针吧!
释放对象
C++要求所有创建堆的对象都必须被明确释放——使用C++关键字:delete。然而,Java承担了释放创建的对象的责任。JVM垃圾收集器,以间歇性周期运作以检查每个对象是否被至少一个变量所引用。如果不是,则垃圾收集器将销毁这个对象并同时释放这片内存。
在内部,JVM为每一个对象提供一个引用计数器。当构造操作第一次创建一个对象时(例如:new CODemo2();),并且当赋值操作将这个引用指向一个引用变量(如:CODemo2 obj1 = new CODemo2();)时,引用计数器初始化为一(在后台)。每当这个引用赋给另外同数据类型的变量时,这个计数器就加一。当这个计数器为零时——接下来垃圾收集器运行——JVM就释放了这个对象。请看下面的例子:
//创建一个对象并将他赋给 myObjectReference.
CODemo2 myObjectReference = new CODemo2 ();
// 内部引用计数器为1
//将 myObjectReference的引用赋给anotherObjectReference.
CODemo2 anotherObjectReference = myObjectReference;
// 内部引用计数器为2
// 释放其中的一个
myObjectReference = null;
//内部引用计数器为1
// 释放另外一个
anotherObjectReference = null;
// //内部引用计数器为0
// 在这里,在垃圾收集器运作是对象被释放并回收
关于作者:
Jeff Friesen使用复杂的计算机已经超过20年。他拥有计算机科学学位并使用多种计算机语言工作。并且Jeff在学院教授Java程序。另外他为JavaWorld撰写文章。他为初学者写过一本Java的书——《Java 2 By Example 》(QUE, 2000)——并且在其基础上撰写了第二本Java书——《Special Edition Using Java 2 Platform 》(QUE, 2001)。Jeff有个“JavaJeff”的昵称。欲知道其最近的工作,请参看他的网站:
资源:略
Java文档化:略