面向对象语言基础二(1)<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
摘要:
Jeff Friesen在他的面向对象语言基础系列的第二部分中对字段和方法进行了探索。在本文中,你将理解字段,参数,局部变量以及学会怎样定义和访问字段及方法。
名词术语,提示和告戒,作业以及此专栏的其他信息,参看相关的“学习指南”。(6400字)
俗话说:一叶障目、不见森林。这句话指对总体了解的过分详细的。那么在介绍Java类和对象时,还有什么比过细的研究字段和方法更能让人迷惑呢?这就是为什么在本系列第一部分中,我详细的介绍总体:类和对象,而略带的介绍字段和方法的原因。然而,正如你最终还是要进入森林并并将注意力放在树上一样,这个月的专栏将非常详细的讲述字段和方法。
整个面向对象语言基础系列:
Java的变量可以分为三类:字段,参数以及局部变量。我将先介绍字段,在稍后讨论方法时介绍参数和局部变量。
定义字段
字段是定义在类中的变量,其用来保存一个对象的状态(实例字段)或者类状态(类字段)。使用如下的Java语法来定义一个变量:
[(‘public’|’private’|’protected’)]
[(‘final’|volatile’)]
[‘static’][‘transient’]
data_type field_name [ ‘=’ expression] ‘;’
一个字段的定义规定了字段的名字、数据类型、表达式(初始化字段值)、存取限定以及修改标识等。选取一个不是保留字的标识符作为字段的数据类型和名字,例如:
class Employee
{
String name; //员工名
double salary; //薪水
int jobID; //工作表识号(如:会计,考勤员,经理等等)
}
Employee类定义了三个字段:name,salary,以及jobID。当创建一个Employee对象时,name最终保持一个String对象的引用以保存员工的名字。salary字段将保存员工的薪水,而jobID则保存了一个整形变量以标识员工的工种。
存取限定符
你可以任意地使用以下三个存取限定关键字来定义一个字段:public,private或者protected。存取限定符确定了其他类中的代码对这个字段的存取权限。存取范围从完全访问到完全不可存取。
如果你在定义变量时没有使用存取限定符,Java将赋给这个字段一个默认的存取级别,从本身类中的代码到同一个包中的所有类都能访问该字段。(将包想象成一个类库——在以后的文章中我将讲述这个概念。)任何不在同一个包中定义的类都无法存取该字段,例如:
class MyClass
{
int fred;
}
只有MyClass类和定义在MyClass包中的其他类的代码才能存取fred。
如果你将一个字段定义为private,则只有他自己的类中的代码能存取这个字段。任何其他类,无论在哪个包中的,都不能存取该字段。如:
class Employee
{
private double salary;
}
只有Employee类才能存取salary;
如果你将一个字段定义为public,则不仅他自己的类中的代码,而且所有其他包中的类也都能存取该字段,如:
public class Employee
{
public String name;
}
Employee类中的代码和其他所有包中的类的代码都能存取name(Employee必须定义为public才能使其他包中的类能存取name)。
将所有字段定义为public会违反数据的封装原则。试想你创建了一个Body类来模仿人的身体,Eye,Heart以及Lung类来模仿眼睛,心脏和肺。Body类中定义了Eye,Heart及Lung等的引用字段,如下例:
public class Body
{
public Eye leftEye,rightEye;
private Heart heart;
private Lung leftLung,rightLung;
}
leftEye和rightEye字段声明为public是因为人的眼睛对观察者来说是可见的。然而,heart,leftLung和rightLung声明为private是因为这些器官是隐藏在人的身体里面的。试想如果heart,leftLung和rightLung被声明为public时,一个暴露了心和肺的身体还能象以前一样工作吗?
最后,一个字段被定义为protected时与默认的存取级别类似。两者仅有的区别是任何包中的该类的子类都能访问protected字段。例如:
public class Employee
{
protected String name;
}
只有Employee类中的代码以及在Employee包中的所有其他类,以及Employee类的子类(定义在任何包中)才能够存取name。
修改标识
你可以任意使用如下的修改限定关键字来定义一个字段:final或者volatile和/或者static和/或者transient。
如果你将一个字段定义为final,编译器将确保字段当成一个常量——只读变量来初始化和处理。因为编译器知道常量是不变的,所以在程序的字节码中对其进行了内部优化。如下例:
class Employee
{
final int ACCOUNTANT = 1;
final int PAYROLL_CLERK = 2;
final int MANAGER = 3;
int jobID = ACCOUNTANT;
}
上例中定义了三个final int字段:ACCOUNTANT,PAYROLL_CLERK和MANAGER.
注意:在常量的声明中习惯上将所有字符大写以及使用下划线来分隔多个单词。这将有助于在分析源代码时区分常量和可写读变量。
如果你将一个字段声明为volatile,则多线程将能访问此字段,而特定的编译器将防止最优化以使该字段能被适当的访问。(你将在我以后的专栏中关于讨论线程时学习volatile字段。)
如果你将一个字段定义为static,则所有对象都将共享此字段的一份拷贝。当你将一个新值赋给这个字段时,所有对象都将得到这个新值。如果没有指定为static,则这个字段将是一个实例字段,每个对象都使用他们自己的一份拷贝。
最后,定义为transient的字段值在对象串行化过程中将不被保存。(我将在以后专栏中研究这个主题。)
实例字段
“实例字段”就是没有使用static修改标识符定义的字段。实例字段和对象紧密相连——而不是和类。当在一个对象代码里修改时,仅仅这个相关的类实例——对象——可以得到这个改变。实例字段随对象的创建而创建,随对象的释放而释放。
下例示范了一个实例字段:
class SomeClass1
{
int i = 5;
void print()
{
System.out.println(i);
}
public static void main(String[] args)
{
SomeClass1 sc1 = new SomeClass1();
System.out.println(sc1.i);
}
}
SomeClass1定义了一个名为i的实例字段,并演示了两种用相同方式访问该实例字段的方法—实例方法和类方法。两个方法和实例字段都在同一个类中。
在同一类中通过实例方法来存取实例字段,你仅需要使用该字段名。而其他类中的实例方法访问该实例字段时,你必须有一个你要存取的实例字段的对象的引用变量,该变量存放了从类创建的对象的地址。将这个对象的引用变量——和点操作符一起——做为这个实例字段名的前缀。(实例方法你可以在本文稍后看到。)
在同一类中通过类方法来存取实例字段,从这个类中创建一个对象,将其赋给一个引用变量,并将这变量做为这个实例字段的前缀。其他类的类方法访问实例字段时,和上述在同一类中访问该字段的步骤相同。(本文稍后讲解类方法。)
当JVM创建一个对象时,为每一个实例字段分配内存空间并在接下来将这些字段的内存清零。以给实例字段赋以默认值。默认值依赖于其数据类型。引用字段的默认值是null,数字字段是0或0.0,布尔值是false,字符是/u0000.
类字段
类字段是用static关键字定义的字段。类字段和类联系——而不是对象。当在一个类代码中修改时,这个类(以及所有创建的对象)都能感知这个变化。类字段随类的加载而创建,随类的卸载而释放。(我相信有些JVM卸载了类而其他有的JVM则没有。)
下例说明了类字段:
class SomeClass2
{
static int i = 5;
void print()
{
System.out.println(i);
}
public static void main(String[] args)
{
System.out.println(i);
}
}
SomeClass2定义了一个名为i的类字段,并演示了两种用相同方式访问该i的方法——实例方法和类方法(两个方法都和类字段在同一个类中)。
在同类中通过实例方法来存取类字段,你仅需要使用这个类字段的名字。而其他类的实例方法访问该类字段时将定义这个类字段的类名做为该类字段的前缀即可。例如:使用SomeClass2.i以使其他类的实例方法能存取i—这些类必须是在SomeClass2包中的,因为SomeClass2没有声明为public。
同一类中的类方法访问类字段时,你也仅需使用该字段名。其他类的类方法访问这个类字段时,和其他类中的实例方法访问类字段过程是相同的。
当类一加载,JVM就给每一个类字段分配内存并且赋给类字段以默认值。类字段的默认值和实例字段的默认值相同。
在Java中类字段就如全局变量。如下例:
class Global
{
static String name;
}
class UseGlobal
{
public static void main(String[] args)
{
Global.name = “UseGlobal”;
System.out.println(Global.name);
}
}
上面的代码在同一个文件中定义里两个类:Global和UseGlobal。如果你编译和运行这个程序,JVM加载UseGlobal并且开始执行main()的字节码。当发现Global.name时,JVM查找、加载、并检验Global类。一当Global类通过检查,JVM就为name分配内存并将其初始化为null。在后台,JVM创建了一个String对象并将其初始化为双引号中的字符——UseGlobal。并将此引用赋给name。然后,程序获取Global.name所指向的String对象的引用,然后将这传给System.out.println()。最后,String对象的内容将显示在标准输出设备上。
因为无论Global还是UseGlobal都明确的定义为public,所以你可以选择任何一个名字做为源文件的名字。编译此文件的结果是两个类文件:Global.class和UseGlobal.class。因为UseGlobal中有main()方法,所以用这个类来运行此程序。在命令行键入:java UseGlobal来运行这个程序。
如果你键入的是:java Global,你将得到如下错误:
Exception in thread “main” java.lang.NoSuchMethodError: main
错误信息行的最后一个词main表示java编译器在类Global中没有发现main()方法。
常量
“常量”是一种只读变量;当JVM初始化这种变量后,变量的值就不能改变了。
使用final关键字来定义常量。正如有两种字段——实例和类字段,常量也有两种——实例常量和类常量。为了提高效率,应当创建类常量,或者说是final static字段。如:
class Constants
{
final int FIRST = 1;
final static int SECOND = 2;
public static void main(String[] args)
{
int iteration = SECOND;
if (iteration == FIRST)//编译错误
System.out.println(“first iteration”);
else
if (iteration == SECOND)
System.out.println(“second iteration”);
}
}
上例中的Constants类定义了一对常量——FIRST和SECOND。FIRST是实例常量,因为JVM给每个Constants对象分配一份FIRST的拷贝。相反的,因为JVM在加载Constants类后只创建了一份SECOND拷贝,所以SECOND是类常量。
注意:当你尝试在main()中直接访问FIRST时会导致一个编译错误。常量FIRST直到一个对象创建时才存在,所以FIRST仅仅只能被这个对象所访问——而不是类。
在以下两种情况下使用常量:
1. 在源码中,修改常量的初始值比替换所有晦涩的数字更容易和少出错。
2. 常量能使源代码更具可读性和更容易理解。例如:常量NUM_ARRAY_ELEMENTS比数字12更具意义。
枚举类型
试想你正写一个Java程序,Zoo1,来模仿一群马戏团的动物:
清单一,Zoo1.java
//Zoo1.java
class CircusAnimal
{
final static int TIGER = 1;
final static int LION = 2;
final static int ELEPHANT = 3;
final static int MONKEY = 4;
}
class Zoo1
{
private int animal;
public static void main(String[] args)
{
Zoo1 z1 = new Zoo1();
Z1.animal = CircusAnimal.TIGER;
//稍后………
if ( z1.animal == CircusAnimal.TEGER)
System.out.println(“This circus animal is a tiger!”);
else
if ( z1.animal == CircusAnimal.MONKEY)
System.out.println(“This circus animal is a monkey!”);
else
System.out.println(“Don’t know what this circus animal is!”);
}
}
Zoo1.java定义了两个类:CircusAnimal和Zoo1,类CircusAnimal定义了一些方便的整型常量来区分不同的动物。而Zoo1是静态类——他的main()方法运行Zoo1程序。
main()方法创建了Zoo1对象,将这个对象的引用赋给z1,并初始化这个对象的animal实例字段给CircusAnimal.TIGER。虽然animal是private,但main()方法还是能够访问那些变量――通过z1这个对象的引用――因为main()是定义animal的类的一部分。接着,main()检查animal的当前值并根据所检查到的输出一个适当的消息。
Zoo1有个特殊问题:animal的int数据类型关键字。假设我们将987324赋给z1.animal――那是合法的,因为animal和987324都是整型。但这赋值的结果却是无意义的。987324是什么动物?把整型做为animal的数据类型可能会产生一些不合理的代码,而这往往容易产生bugs。这个问题的解决办法是给animal一个很适当的数据类型并能限制赋给animal的值的范围。这就是使用枚举数据类型的基本原理。
枚举数据类型就是一组限制值的引用数据类型。每个值都是由枚举数据类型创建的对象,如下:
清单2:Zoo2.java
//Zoo2.java
class CircusAnimal
{
static final CircusAnimal TIGER = new CircusAnimal ("Tiger");
static final CircusAnimal LION = new CircusAnimal ("Lion");
static final CircusAnimal ELEPHANT = new CircusAnimal ("Elephant");
static final CircusAnimal MONKEY = new CircusAnimal ("Monkey");
private String animalName;
private CircusAnimal (String name)
{
animalName = name;
}
public String toString ()
{
return animalName;
}
}
class Zoo2
{
private CircusAnimal animal;
public static void main (String [] args)
{
Zoo2 z2 = new Zoo2 ();
z2.animal = CircusAnimal.TIGER;
// Some time later ...
if (z2.animal == CircusAnimal.TIGER)
System.out.println ("This circus animal is a tiger!");
else
if (z2.animal == CircusAnimal.MONKEY)
System.out.println ("This circus animal is a monkey!");
else
System.out.println ("Don't know what this circus animal is!");
}
}
CircusAnimal类定义了四个常量:TIGER,LION,ELEPHANT及MONKEY。每个常量初始化为一个CircusAnimal对象。
定义了一个特殊的CircusAnimal构造函数,其只带一个String变量。这个字符串将一个private animalName字段传给了构造函数。(我将在本文稍后介绍构造函数。)
构造函数被声明为private是为了保证所创建的CircusAnimal对象不超出那被定义成常量的四个对象的范围。你不想有创建一个无意义的987324的CircusAnimal对象吧。在Zoo2中仅这四个CircusAnimal常量是有效的。
为什么在CircusAnimal中定义了一个toStiring()方法呢?这方法返回一个String对象,它引用的是animalName的值。假设你在初始化z2.animal为CircusAnimal.TIGER后调用System.out.println(z2.animal)时,结果是Tiger将被输出,这个值是在CircusAnimal的构造函数中创建TIGER常量时赋予的。System.out.println()在后台调用toString()方法以获得Tiger。(在后续专栏中我将更多的讲到toString()方法。)
现在你知道该怎么定义何访问字段了,接下来你需要学习怎样定义和访问方法。
下节:定义方法
资源:略