`
fulerbakesi
  • 浏览: 561575 次
文章分类
社区版块
存档分类
最新评论

java基础教程-类

 
阅读更多

6

类是Java的核心和本质。它是Java语言的基础,因为类定义了对象的本性。既然类是面向对象程序设计Java语言的基础,因此,你想要在Java程序中实现的每一个概念都必须封装在类以内。

因为类是Java的基础,所以在本章和以后几章中对其进行介绍。本章将介绍类的基本元素,并学习如何运用类来创建对象。同时也将学习方法、构造函数及this这个关键字。

6.1

从本书的开始我们就使用类了。当然,使用的都是非常简单的类。在前面几章中创造的类主要都包含在main( )方法中,用它来表明Java句法的基础。你将看到,类的功能实质上比你到目前为止看到的要强大得多。

也许理解类的最重要的事情就是它定义了一种新的数据类型。一旦定义后,就可以用这种新类型来创建该类型的对象。这样,类就是对象的模板(template),而对象就是类的一个实例(instance)。既然一个对象就是一个类的实例,所以你经常看到objectinstance这两个词可以互换使用。

<chsdate w:st="on" year="1899" month="12" day="30" islunardate="False" isrocdate="False"><span style="mso-bookmark: _Toc528746826"><span lang="EN-US">6.1.1</span></span></chsdate> 类的通用格式

当你定义一个类时,你要声明它准确的格式和属性。你可以通过指定它包含的数据和操作数据的代码来定义类。尽管非常简单的类可能只包含代码或者只包含数据,但绝大多数实际的类都包含上述两者。你将看到,类的代码定义了该类数据的接口。

使用关键字class来创建类。在这一点上类实际上被限制在它的完全格式中。类可以(并且常常)是一个组合体。类定义的通用格式如下所示:

class classname {

type instance-variable1;

type instance-variable2;

// ...

type instance-variableN;

type methodname1(parameter-list) {

// body of method

}

type methodname2(parameter-list) {

// body of method

}

// ...

type methodnameN(parameter-list) {

// body of method

}

}

在类中,数据或变量被称为实例变量(instance variables),代码包含在方法(methods)内。定义在类中的方法和实例变量被称为类的成员(members)。在大多数类中,实例变量被定义在该类中的方法操作和存取。这样,方法决定该类中的数据如何使用。

定义在类中的变量被称为实例变量,这是因为类中的每个实例(也就是类的每个对象)都包含它自己对这些变量的拷贝。这样,一个对象的数据是独立的且是惟一的。关于这一点我们将马上讨论,但是这是一个重要的概念,因此要早一点学习。

所有的方法和我们到目前为止用过的方法main()的形式一样。但是,以后讲到的方法将不仅仅是被指

定为staticpublic。注意类的通用格式中并没有指定main()方法。Java类不需要main( )方法。main()方法只是在定义您程序的起点时用到。而且,Java小应用程序也不要求main( )方法。

注意:如果你有C++编程经验请注意,类的声明和方法的实现要存储在同一个地方并且不能被单独定义。由于所有类的定义必须全部定义在一个单个的源文件中,这有时会生成很大的.java文件。在Java中设计这个特征是因为从长远来说,在一个地方指明,定义以及实现将使代码更易于维护。

<chsdate w:st="on" year="1899" month="12" day="30" islunardate="False" isrocdate="False"><span style="mso-bookmark: _Toc528746827"><span lang="EN-US">6.1.2</span></span></chsdate> 一个简单的类

让我们先从一个简单的例子来开始对类的研究。下面定义了一个名为box的类,它定义了3个实例变量:widthheightdepth。当前,box类不包含任何方法(但是随后将增加一些)。

class Box {

double width;

double height;

double depth;

}

前面已经说过,一个类定义一个新的数据类型。在本例中,新的数据类型名为Box。你可以使用这个名字来声明Box类型的对象。记住类声明只是创建一个模板(或类型描述),它并不会创建一个实际的对象。因此,上述代码不会生成任何Box类型的对象实体。

要真正创建一个Box对象,你必须使用下面的语句:

Box mybox = new Box(); // create a Box object called mybox

这个语句执行后,mybox就是Box的一个实例了。因此,它将具有“物理的”真实性。现在,先不必考虑这个语句的一些细节问题。

每次你创建类的一个实例时,你是在创建一个对象,该对象包含它自己的由类定义的每个实例变量的拷贝。因此,每个Box对象都将包含它自己的实例变量拷贝,这些变量即widthheight,和 depth。要访问这些变量,你要使用点号“.”运算符。点号运算符(dot operator)将对象名和成员名连接起来。例如,要将myboxwidth变量赋值为100,使用下面的语句:

mybox.width = 100;

该语句告诉编译器对mybox对象内包含的width变量拷贝的值赋为100。通常情况下,你可以使用点号运算符来访问一个对象内的实例变量和方法。

下面是使用Box类的完整程序:

/* A program that uses the Box class.

Call this file BoxDemo.java

*/

class Box {

double width;

double height;

double depth;

}

// This class declares an object of type Box.

class BoxDemo {

public static void main(String args[]) {

Box mybox = new Box();

double vol;

// assign values to mybox's instance variables

mybox.width = 10;

mybox.height = 20;

mybox.depth = 15;

// compute volume of box

vol = mybox.width * mybox.height * mybox.depth;

System.out.println("Volume is " + vol);

}

}

你应该把包含该程序的的文件命名为BoxDemo.java,因为main( )方法在名为 BoxDemo 的类中,而不是名为Box的类中。当你编译这个程序时,你会发现生成了两个“.class”文件,一个属于box,另一个属于BoxDemoJava编译器自动将每个类保存在它自己的“ .class ”文件中。没有必要分别将Box类和Boxdemo类放在同一个源文件中。你可以分别将它们放在各自的文件中,并分别命名为Box.Java BoxDemo.java

要运行这个程序,你必须执行BoxDemo.class。运行该程序后,你会看见如下输出:

Volume is 3000.0

前面已经讲过,每个对象都含有它自己的、由它的类定义的实例变量的拷贝。因此,假设你有两个Box对象,每个对象都有其自己的depthwidthheight拷贝。改变一个对象的实例变量对另外一个对象的实例变量没有任何影响,理解这一点是很重要的。例如,下面的程序定义了两个Box对象:

// This program declares two Box objects.

class Box {

double width;

double height;

double depth;

}

class BoxDemo2 {

public static void main(String args[]) {

<street w:st="on"><span lang="EN-US">Box</span></street> mybox1
= new Box();

<street w:st="on"><span lang="EN-US">Box</span></street> mybox2
= new Box();

double vol;

// assign values to mybox1's instance variables

mybox1.width = 10;

mybox1.height = 20;

mybox1.depth = 15;

/* assign different values to mybox2's

instance variables */

mybox2.width = 3;

mybox2.height = 6;

mybox2.depth = 9;

// compute volume of first box

vol = mybox1.width * mybox1.height * mybox1.depth;

System.out.println("Volume is " + vol);

// compute volume of second box

vol = mybox2.width * mybox2.height * mybox2.depth;

System.out.println("Volume is " + vol);

}

}

该程序产生的输出如下所示:

Volume is 3000.0

Volume is 162.0

你可以看到,mybox1的数据与mybox2的数据完全分离。

6.2

正如刚才讲过的,当你创建一个类时,你创建了一种新的数据类型。你可以使用这种类型来声明该种类型的对象。然而,要获得一个类的对象需要两步。第一步,你必须声明该类类型的一个变量,这个变量没有定义一个对象。实际上,它只是一个能够引用对象的简单变量。第二步,该声明要创建一个对象的实际的物理拷贝,并把对于该对象的引用赋给该变量。这是通过使用new运算符实现的。new运算符为对象动态分配(即在运行时分配)内存空间,并返回对它的一个引用。这个引用或多或少的是new分配给对象的内存地址。然后这个引用被存储在该变量中。这样,在Java中,所有的类对象都必须动态分配。让我们详细看一下该过程。

在前面的例子中,用下面的语句来声明一个Box类型的对象:

Box mybox = new Box();

本例将上面讲到的两步组合到了一起,可以将该语句改写为下面的形式,以便将每一步讲的更清楚:

Box mybox; // declare reference to object

mybox = new Box(); // allocate a Box object

第一行声明了mybox,把它作为对于Box类型的对象的引用。当本句执行后,mybox 包含的值为null,表示它没有引用对象。这时任何引用mybox的尝试都将导致一个编译错误。第二行创建了一个实际的对象,并把对于它的引用赋给mybox 。现在,你可以把mybox作为Box的对象来使用。但实际上,mybox仅仅保存实际的Box对象的内存地址。这两行语句的效果如图6-1所示。

<shapetype id="_x0000_t75" stroked="f" filled="f" path="m@4@5l@4@11@9@11@9@5xe" o:preferrelative="t" o:spt="75" coordsize="21600,21600"><stroke joinstyle="miter"></stroke><formulas><f eqn="if lineDrawn pixelLineWidth 0"></f><f eqn="sum @0 1 0"></f><f eqn="sum 0 0 @1"></f><f eqn="prod @2 1 2"></f><f eqn="prod @3 21600 pixelWidth"></f><f eqn="prod @3 21600 pixelHeight"></f><f eqn="sum @0 0 1"></f><f eqn="prod @6 1 2"></f><f eqn="prod @7 21600 pixelWidth"></f><f eqn="sum @8 21600 0"></f><f eqn="prod @7 21600 pixelHeight"></f><f eqn="sum @10 21600 0"></f></formulas><path o:connecttype="rect" gradientshapeok="t" o:extrusionok="f"></path><lock aspectratio="t" v:ext="edit"></lock></shapetype><shape id="_x0000_i1025" style="WIDTH: 296.25pt; HEIGHT: 133.5pt" o:allowoverlap="f" o:allowincell="f" type="#_x0000_t75"><imagedata cropbottom="9510f" o:title="图6-1" src="file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtml1/01/clip_image001.jpg"></imagedata></shape>

6-1 声明Box类型的对象

注意:那些熟悉C/C++语言的读者,可能已经注意到了对象的引用看起来和指针类似。这种怀疑实质上是正确的。一个对象引用和内存指针类似。主要的差别(也就是Java安全的关键)是你不能像实际的指针那样来操作它。这样,对于对象引用,你就不能像指针那样任意分配内存地址,或像整数一样操作它

<chsdate w:st="on" year="1899" month="12" day="30" islunardate="False" isrocdate="False"><span style="mso-bookmark: _Toc528746829"><span lang="EN-US">6.2.1</span></span></chsdate> 深入研究new的运算符

刚才已经解释过,new运算符动态地为一个对象分配地址。它的通用格式如下:

class-var = new classname();

其中,class-var 是所创建类类型的变量。classname 是被实例化的类的名字。类的后面跟的圆括号指定了类的构造函数。构造函数定义当创建一个类的对象时将发生什么。构造函数是所有类的重要组成部分,并有许多重要的属性。大多数类在他们自己的内部显式地定义构造函数。如果一个类没有显式的定义它自己的构造函数,那么Java将自动地提供一个默认的构造函数。对类Box的定义就是这种情况。现在,我们将使用默认的构造函数。不久,你将看到如何定义自己的构造函数。

这时,你可能想知道为什么对整数或字符这样的简单变量不使用new运算符。答案是Java的简单类型不是作为对象实现的。出于效率的考虑,它们是作为“常规”变量实现的。你将看到,对象有许多特性和属性,使Java对对象的处理不同于简单类型。由于对处理对象和处理简单类型的开销不同,Java能更高效地实现简单类型。后面,你将看见,对于那些需要完全对象类型的情况下,简单类型的对象版本也是可用的。

理解new运算符是在运行期间为对象分配内存的是很重要的。这样做的好处是你的程序在运行期间可以创建它所需要的内存。但是,内存是有限的,因此new有可能由于内存不足而无法给一个对象分配内存。如果出现这种情况,就会发生运行时异常(你将在第10章学习如何处理这种异常以及其他异常情况)。对于本书中的示例程序,你不必担心内存不足的情况,但是在实际的编程中你必须考虑这种可能性。

让我们再次复习类和对象之间的区别。类创建一种新的数据类型,该种类型能被用来创建对象。也就是,类创建了一个逻辑的框架,该框架定义了它的成员之间的关系。当你声明类的对象时,你正在创造该类的实例。因此,类是一个逻辑构造,对象有物理的真实性(也就是对象占用内存空间)。弄清楚这个区别是很重要的。

6.3 给对象引用变量赋值

对象变量的赋值和你直觉期望的不同。例如,你认为下面的程序段是做什么呢?

<street w:st="on"><span lang="EN-US">Box</span></street> b1
= new Box();

<street w:st="on"><span lang="EN-US">Box</span></street> b2
= b1;

你可能认为,变量b2被赋值为变量b1对象引用的一个拷贝。也就是,你可能认为b1b2引用的是不同的对象,但实际情况却相反,b1b2将引用同样的对象。将b1赋值给b2并没有分配任何内存或对原对象做任何部分的拷贝。由于它们是同一个对象,因此通过变量b2对对象的改变也将影响b1所对应的对象。

这种情况描绘如下:

<shape id="_x0000_i1026" style="WIDTH: 192.75pt; HEIGHT: 1in" o:allowoverlap="f" type="#_x0000_t75" o:ole=""><imagedata o:title="" src="file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtml1/01/clip_image003.png"><font size="3"></font></imagedata></shape>

尽管b1b2都引用同一个对象,但是他们之间没有任何其他的关系。例如,接下来对b1的赋值仅仅使b1脱离unhook)初始对象,而没有影响对象或影响b2

<street w:st="on"><span lang="EN-US">Box</span></street> b1
= new Box();

<street w:st="on"><span lang="EN-US">Box</span></street> b2
= b1;

// ...

b1 = null;

这里,b1被设置为空,但是b2仍然指向原来的对象。

注意:当你将一个对象引用赋值给另一个对象引用时,你并没有创建该对象的一个拷贝,而是仅仅对引用的一个拷贝。

6.4

在本章的开始提到,类通常由两个要素组成:实例变量和方法。方法是个很大的话题,因为Java给他们如此大的功能和灵活性。事实上,下一章的大部分都用来介绍方法。然而,你现在需要学习一些基础以便你能开始把方法加到你的类中。

这是方法一般的形式:

type name(parameter-list) {

// body of method

}

其中,type指定了方法返回的数据类型。这可以是任何合法有效的类型,包括你创建的类的类型。如果该方法不返回任何值,则它的返回值type必须为void 。方法名由name指定。除了被当前作用域中的其他项使用的标识符以外,方法名可以是任何合法的标识符。parameter-list (自变量列表)是一系列类型和标识符对,用逗号分开。自变量本质上是变量,它接收方法被调用时传递给方法的参数值。如果方法没有自变量,那么自变量列表就为空。

对于不返回void类型的方法,使用下面格式的return语句,方法返回值到它的调用程序:

return value;

其中,value是返回的值。

接下来,你将看到怎样创建多种类型的方法,包括带参数的和那些返回值的方法。

<chsdate w:st="on" year="1899" month="12" day="30" islunardate="False" isrocdate="False"><span style="mso-bookmark: _Toc528746832"><span lang="EN-US">6.4.1</span></span></chsdate> Box类添加一个方法

尽管创建一个仅包含数据的类是相当不错的事情,但这样的情况很少发生。大部分情况是你将使用方法存取由类定义的实例变量。事实上,方法定义大多数类的接口。这允许类实现函数可以把内部数据结构的特定布局隐蔽到方法抽象后面。除了定义提供数据的存取的方法,你也可以定义被类的内部自己所使用的方法。

让我们由对Box类增加一个方法开始。回顾一下前面计算盒子体积的例子,你会发现用Box类有时会比使用BoxDemo类能更好地处理这个问题。不管怎么说,一个盒子的体积依赖于盒子的大小,这就是我们想到用Box类来计算盒子的体积。为了做到这一点,你必须对Box类增加一个方法,示例如下:

// This program includes a method inside the box class.

class Box {

double width;

double height;

double depth;

// display volume of a box

void volume() {

System.out.print("Volume is ");

System.out.println(width * height * depth);

}

}

class BoxDemo3 {

public static void main(String args[]) {

<street w:st="on"><span lang="EN-US">Box</span></street> mybox1
= new Box();

<street w:st="on"><span lang="EN-US">Box</span></street> mybox2
= new Box();

// assign values to mybox1's instance variables

mybox1.width = 10;

mybox1.height = 20;

mybox1.depth = 15;

/* assign different values to mybox2's

instance variables */

mybox2.width = 3;

mybox2.height = 6;

mybox2.depth = 9;

// display volume of first box

mybox1.volume();

// display volume of second box

mybox2.volume();

}

}

该程序产生的输出如下,与先前版本程序的输出一样。

Volume is 3000.0

Volume is 162.0

注意看下面两行程序:

mybox1.volume ();

mybox2.volume ();

该例的第一行调用mybox1volume()方法。也就是,它使用对象名加点号运算符调用mybox1对象的volume()方法。这样,调用mybox1.volume()显示mybox1 定义的盒子的体积,调用mybox2.volume()将显示mybox2 定义的盒子的体积。每次调用volume(),它都会显示指定对象的体积。

如果你对方法调用的概念比较陌生,下列的讨论将有助于澄清该概念。当mybox1.volume ( )被执行时,Java运行系统将程序控制转移到volume( )定义内的代码。当volume( )内的语句执行后,程序控制返回调用者,然后执行程序调用的下一行语句。Java执行方法的过程类似于子程序的运行。

volume()方法中有一些需要注意的地方:实例变量widthheightdepth被直接引用,

并没有在它们前面加对象名或点号运算符。当一个方法使用由它的类定义的实例变量时,它可以直接这样做,而不必使用显式的对象引用和使用点号运算符。这是很容易理解的。一个方法总是被它的类的对象调用。只要这个调用过程一发生,对象就是可见的。因此,在方法中就没有必要二次指定对象了。这意味着,volume()中的widthheightdepth已经隐含地引用了调用volume()方法中的这些变量的拷贝。

让我们复习一下:当一个实例变量不是被该实例变量所在类的部分代码访问时,它必须通过该对象加点运算符来访问。但是当一个实例变量被定义该变量的类的代码访问时,该变量可以被直接引用。同样的规则也适用于方法。

<chsdate w:st="on" year="1899" month="12" day="30" islunardate="False" isrocdate="False"><span style="mso-bookmark: _Toc528746833"><span lang="EN-US">6.4.2</span></span></chsdate> 返回值

执行volume()方法确实将计算盒子体积的值返回到Box类,但这并不是最好的方法。例如,你的程序的其他部分如何知道一个盒子的体积,而不显示它的值?一个更好地实现volume()的方法是将它计算的盒子体积的结果返回给它的调用者。下面的例子是对前面程序的改进,它正是这样做的:

// Nowvolume() returns the volume of a box.

class Box {

double width;

double height;

double depth;

// compute and return volume

double volume() {

return width * height * depth;

}

}

class BoxDemo4 {

public static void main(String args[]) {

<street w:st="on"><span lang="EN-US">Box</span></street> mybox1
= new Box();

<street w:st="on"><span lang="EN-US">Box</span></street> mybox2
= new Box();

double vol;

// assign values to mybox1's instance variables

mybox1.width = 10;

mybox1.height = 20;

mybox1.depth = 15;

/* assign different values to mybox2's

instance variables */

mybox2.width = 3;

mybox2.height = 6;

mybox2.depth = 9;

// get volume of first box

vol = mybox1.volume();

System.out.println("Volume is " + vol);

// get volume of second box

vol = mybox2.volume();

System.out.println("Volume is " + vol);

}

}

在这个程序中,当volume()被调用时,它被放在赋值语句的右边。左边是接收volume()返回值的变量。因此,当下面的语句执行后,

vol = mybox1.volume();

变量mybox1.volume ( )的值是 3000,且该值被保存在vol中。

对于返回值的理解,要注意下面两件重要的事情:

· 方法返回的数据类型必须与该方法指定的返回类型相兼容。例如,如果一个方法的返回值是布尔型,就不可能返回整数。

· 接收方法返回值的变量 (例如本例中的变量 vol)也必须与指定方法返回值的类型相兼容。

另外一点:因为实际上不需要vol变量,前面的程序可以被写得更高效一些。对volume( )方法的调用可以直接用在 println ( )语句中,如下面的语句:

System.out.println("Volume is " + mybox1.volume());

在本例中,当println ( )被执行时,mybox1.volume ( )将自动地被调用,而且它的值会被传递给println ( )

<chsdate w:st="on" year="1899" month="12" day="30" islunardate="False" isrocdate="False"><span style="mso-bookmark: _Toc528746834"><span lang="EN-US">6.4.3</span></span></chsdate> 加入带自变量的方法

大多数方法不需要自变量。自变量对方法没有特殊要求。也就是说,带自变量的方法,可以完成各种数据操作,它还可以用在很多有微妙差别的情况。为了说明这一点,让我们举一个非常简单的例子。下面的方法返回数字10的平方:

int square()

{

return 10 * 10;

}

运行该方法,确实返回了10 的平方的值,但它的使用是很有限的。然而,如下所示,如果你修改该方法,以便它带一个自变量,这样square( )就更有用了。

int square(int i)

{

return i * i;

}

现在,square()可以返回任何调用它的值的平方。也就是说,square()现在是可以计算任何整数值的平方的一个通用方法,而不单纯是数字10

下面是一个例子:

int xy;

x = square(5); // x equals 25

x = square(9); // x equals 81

y = 2;

x = square(y); // x equals 4

在第一次调用square()时,值5被传递给自变量i。在第二次调用时,i接收到值9。第三次调用时,将值传递给y,在本例中是 2 。如这些例子所示,square ( )可以返回传递给它的任何数据的平方。

区分自变量(parameter)和参数(argument)这两个术语是很重要的。自变量是方法定义的一个变量,当方法被调用时,它接收一个值。例如在square()中,i就是一个自变量。参数是当一个方法被调用时,传递给该方法的值。例如,square(100)100作为参数传递。在square()中,自变量i接收该值。

可以使用一个带自变量的方法来改进Box类。在前面的例子中,每个盒子的尺寸不得不用单独的语句顺序的来设置,例如:

mybox1.width = 10;

mybox1.height = 20;

mybox1.depth = 15;

本例中的代码在执行时,它在两个方面比较麻烦。首先,它笨拙且容易发生错误。例如,很容易忘记设置其中的一个尺寸。其次,在设计的很好的Java程序中,实例变量应该仅仅由定义类的方法来存取。在后面,你可以改变一个方法的行为,但是你不能改变一个暴露的实例变量的行为。

这样,设置一个盒子尺寸的更好的途径是创建一个自变量代表盒子尺寸的方法,而且适当地设置每个实例变量。下面的例子实现了这个想法。

// This program uses a parameterized method.

class Box {

double width;

double height;

double depth;

// compute and return volume

double volume() {

return width * height * depth;

}

// sets dimensions of box

void setDim(double wdouble hdouble d) {

width = w;

height = h;

depth = d;

}

}

class BoxDemo5 {

public static void main(String args[]) {

<street w:st="on"><span lang="EN-US">Box</span></street> mybox1
= new Box();

<street w:st="on"><span lang="EN-US">Box</span></street> mybox2
= new Box();

double vol;

// initialize each box

mybox1.setDim(102015);

mybox2.setDim(369);

// get volume of first box

vol = mybox1.volume();

System.out.println("Volume is " + vol);

// get volume of second box

vol = mybox2.volume();

System.out.println("Volume is " + vol);

}

}

正如你看到的,setDim方法用来设置每个盒子的尺寸,例如,当下面的语句执行后:

mybox1.setDim(102015);

10被拷贝进参数w20被拷贝进h15被拷贝进d。在setDim( )内的whd的值分别赋给widthheightdepth

许多读者,特别是那些有C/C++经验的读者,对前面章节中的概念会比较熟悉。但是,如果像方法调用、参数、自变量这些概念对你来说比较新的话,在继续学习以前,你要花些时间来练习。方法调用,自变量,返回值这些概念是Java编程的基础。

6.5

每次在创建实例变量,对类中的所有变量都要初始化是很乏味的。即使你对setDim ( )这样的方法增加有用的功能时,你也不得不这样做。如果在一个对象最初被创建时就把对它的设置做好,那样的话,程序将更简单并且更简明。因为对初始化的要求是共同的,Java允许对象在他们被创造时初始化自己。这种自动的初始化是通过使用构造函数来完成的。

构造函数(constructor)在对象创建时初始化。它与它的类同名,它的语法与方法类似。一旦定义了构造函数,在对象创建后,在new运算符完成前,构造函数立即自动调用。构造函数看起来有点奇怪,因为它没有任何返回值,即使是void型的值也不返回。这是因为一个类的构造函数内隐藏的类型是它自己类的类型。构造函数的任务就是初始化一个对象的内部状态,以便使创建的实例变量能够完全初始化,可以被对象马上使用。

你可以重写Box例子程序,以便当对象创建时盒子的尺寸能被自动地初始化。为了达到这个目的,用构造函数代替setDim。让我们由定义仅仅将每个盒子的尺寸设置为同样值的一个简单的构造函数开始。示例如下:

/* HereBox uses a constructor to initialize the

dimensions of a box.

*/

class Box {

double width;

double height;

double depth;

// This is the constructor for Box.

Box() {

System.out.println("Constructing Box");

width = 10;

height = 10;

depth = 10;

}

// compute and return volume

double volume() {

return width * height * depth;

}

}

class BoxDemo6 {

public static void main(String args[]) {

// declareallocateand initialize Box objects

<street w:st="on"><span lang="EN-US">Box</span></street> mybox1
= new Box();

<street w:st="on"><span lang="EN-US">Box</span></street> mybox2
= new Box();

double vol;

// get volume of first box

vol = mybox1.volume();

System.out.println("Volume is " + vol);

// get volume of second box

vol = mybox2.volume();

System.out.println("Volume is " + vol);

}

}

运行该程序,产生如下的结果:

Constructing Box

Constructing Box

Volume is 1000.0

Volume is 1000.0

正如你能看到的一样,当mybox1mybox2被创建时,它们两个都被Box构造函数初始化。因为构造函数将所有的盒子赋为一样的尺寸,长、宽、高都是10mybox1 mybox2 将有一样的体积。在Box( )内的println( )语句仅仅是为说明的缘故。大多数构造函数的功能不显示任何东西,他们仅简单地初始化一个对象。

在继续学习前,让我们再考察new运算符。你已经知道,当分配一个对象时,使用下

面的通用格式:

class-var = new classname();

现在你可以理解为什么在类的名字后面需要圆括号。圆括号的作用是调用该类的构造函数。这样,在下面的这行中

<street w:st="on"><span lang="EN-US">Box</span></street> mybox1
= new Box();

new Box()调用Box()构造函数。如果你不显式为类定义一个构造函数,Java将为该类创建一个默认的构造函数。这就是本行程序在Box早期版本没有定义构造函数工作的原因。默认构造函数自动地将所有的实例变量初始化为零。默认构造函数对简单的类是足够的,但是对更复杂的类它就不能满足要求了。一旦你定义了你自己的构造函数,默认构造函数将不再被使用。

<chsdate w:st="on" year="1899" month="12" day="30" islunardate="False" isrocdate="False"><span lang="EN-US">6.5.1</span></chsdate> 带自变量的构造函数

虽然在前面的例子中,Box构造函数确实初始化了Box对象,但它不是很有用,因为所有的盒子都是一样的尺寸。我们所需要的是一种能够构造各种各样尺寸盒子对象的方法。比较容易的解决办法是对构造函数增加自变量。你可能已经猜到,这将使他们更有用。例如,下面版本的Box程序定义了一个自变量构造函数,它根据自变量设置每个指定盒子的尺寸。特别注意Box对象是如何被创建的。

/* HereBox uses a parameterized constructor to

initialize the dimensions of a box.

*/

class Box {

double width;

double height;

double depth;

// This is the constructor for Box.

Box(double wdouble hdouble d) {

width = w;

height = h;

depth = d;

}

// compute and return volume

double volume() {

return width * height * depth;

}

}

class BoxDemo7 {

public static void main(String args[]) {

// declareallocateand initialize Box objects

Box mybox1 = new Box(102015);

Box mybox2 = new Box(369);

double vol;

// get volume of first box

vol = mybox1.volume();

System.out.println("Volume is " + vol);

// get volume of second box

vol = mybox2.volume();

System.out.println("Volume is " + vol);

}

}

该程序的输出如下:

Volume is 3000.0

Volume is 162.0

正如你看到的,每个对象被它的构造函数指定的参数初始化。例如,在下行中,

Box mybox1 = new Box(102015);

new创建对象时,值102015传递到Box()构造函数。这样,mybox1 的拷贝width heightdepth将分别包含值102015

6.6 this关键字

有时一个方法需要引用调用它的对象。为此,Java定义了this这个关键字。this可以在引用当前对象的所有方法内使用。也就是,this总是调用该方法对象的一个引用。你可以在当前类的类型所允许对象的任何地方将this作为一个引用。

为了更好理解this引用什么,考虑下面版本的Box()

// A redundant use of this.

Box(double wdouble hdouble d) {

this.width = w;

this.height = h;

this.depth = d;

}

本例中的box( )和它的更早版本完成同样的操作。使用this是冗余的,但是完全正确。在Box( )内,this总是引用调用的对象。虽然在本例中它是冗余的,但在另外的环境中,它是有用的,其中的一种用法在下一小节解释。

<chsdate w:st="on" year="1899" month="12" day="30" islunardate="False" isrocdate="False"><span style="mso-bookmark: _Toc528746838"><span lang="EN-US">6.6.1</span></span></chsdate> 隐藏的实例变量

你知道,在同一个范围或一个封装范围内,定义二个重名的局部变量在Java中是不合法的。有趣的是,局部变量,包括传递到方法的正式的自变量,可以与类的实例变量的名字重叠。在这种情况下,局部变量名就隐藏(hide)了实例变量名。这就是在Box类中,widthheightdepth没有作为Box()构造函数自变量名字的原因。如果它们是,那么width将正式的

引用自变量,而隐蔽实例变量width。由于通常简单地使用不同的名字更容易,对这种状况还有其他的解决办法。因为this可以使你直接引用对象,你能用它来解决可能在实例变量和局部变量之间发生的任何同名的冲突。例如,下面的例子是另外一个版本的Box()程序,它用widthheightdepth作为自变量的名字,然后使用this关键字来存取同名的实例变量:

// Use this to resolve name-space collisions.

Box(double widthdouble heightdouble depth) {

this.width = width;

this.height = height;

this.depth = depth;

}

注意,在这样的环境下使用this有时会引起混淆。有些程序员比较小心,不使用和局部变量、正式的自变量同名的隐藏的实例变量。当然,另外的程序员则相反,相信用this来“揭开”与局部变量、自变量同名的实例变量是一个好习惯。这取决于你的爱好。

尽管在上面的例子中,this没有有什么意义,但它在某种状况下是很有用的。

6.7

由于使用new运算符来为对象动态地分配内存,你可能想知道这些对象是如何撤消的以及他们的内存在以后的重新分配时是如何被释放的。在一些语言,例如C++中,用delete运算符来手工地释放动态分配的对象的内存。Java使用一种不同的、自动地处理重新分配内存的办法:垃圾回收( garbage collection)技术,它是这样工作的:当一个对象的引用不存在时,则该对象被认为是不再需要的,它所占用的内存就

被释放掉。它不像C++那样需要显式撤消对象。垃圾回收只在你的程序执行过程中偶尔发生。它不会因为一个或几个存在的对象不再被使用而发生。况且,Java不同的运行时刻会产生各种不同的垃圾回收办法,但是对你编写的大多数程序,你不必须考虑垃圾回收问题。

6.8 finalize()方法

有时当撤消一个对象时,需要完成一些操作。例如,如果一个对象正在处理的是非Java资源,如文件句柄或window字符字体,这时你要确认在一个对象被撤消以前要保证这些资源被释放。为处理这样的状况,Java提供了被称为收尾(finalization)的机制。使用该机制你可以定义一些特殊的操作,这些操作在一个对象将要被垃圾回收程序释放时执行。

要给一个类增加收尾(finalizer),你只要定义finalize ( )方法即可。Java回收该类的一个对象时,就会调用这个方法。在finalize ( )方法中,你要指定在一个对象被撤消前必须执行的操作。垃圾回收周期性地运行,检查对象不再被运行状态引用或间接地通过其他对象引用。就在对象被释放之前,Java运行系统调用该对象的finalize( )方法。

finalize()方法的通用格式如下:

protected void finalize()

{

// finalization code here

}

其中,关键字protected是防止在该类之外定义的代码访问finalize()标识符。该标识符和其他标识符将在第7章中解释。

理解finalize()正好在垃圾回收以前被调用非常重要。例如当一个对象超出了它的作用域时,finalize()并不被调用。这意味着你不可能知道何时——甚至是否——finalize()被调用。因此,你的程序应该提供其他的方法来释放由对象使用的系统资源而不能依靠finalize()来完成程序的正常操作。

注意:如果你熟悉C++,那你知道C++允许你为一个类定义一个撤消函数(destructor),它在对象正好出作用域之前被调用。Java不支持这个想法也不提供撤消函数。finalize()方法只和撤消函数的功能接近。当你对Java有丰富经验时,你将看到因为Java使用垃圾回收子系统,几乎没有必要使用撤消函数。

6.9 一个堆栈类

尽管Box类在说明一个类的必要的元素时是有用的,但它实际应用的价值并不大。为了显示出类的真实的功能,本章将用一个更复杂的例子来说明类的强大功能。如果你回忆起在第2章中讲过的面向对象编程的讨论,你就会想起对象编程的最重要的好处之一是对数据和操作该数据的代码的封装。你已经知道,在Java中,就是通过类这样的机制来完成封装性。在创建一个类时,你正在创建一种新的数据类型,不但要定义数据的属性,也要定义操作数据的代码。进一步,方法定义了对该类数据相一致的控制接口。因此,你可以通过类的方法来使用类,而没有必要担心它的实现细节或在类的内部数据实际上是如何被管理的。在某种意义上,一个类像“一台数据引擎”。你可以通过操纵杆来控制使用引擎,而不需要知道引擎内是如何工作的。事实上,既然细节被隐蔽,当需要时,它的内部工作可以被改变。只要你的代码通过类的方法来使用它,内部的细节可以改变而不会对类的外部带来负面影响。

为了看看前面讨论概念的一个实际的应用,让我们开发一个封装的典型例子:堆栈(stack)。堆栈用先进后出的顺序存储数据。堆栈通过两个传统的操作来控制:压栈(push)和出栈(pop)。在堆栈的上面加入一项,用压栈,从堆栈中取出一项,用出栈。你将看到,将整个堆栈机制封装是很容易的。

下面是一个叫做Stack的类,实现整数的堆栈。

// This class defines an integer stack that can hold 10 values.

class Stack {

int stck[] = new int[10];

int tos;

// Initialize top-of-stack

Stack() {

tos = -1;

}

// Push an item onto the stack

void push(int item) {

if(tos==9)

System.out.println("Stack is full.");

else

stck[++tos] = item;

}

// Pop an item from the stack

int pop() {

if(tos < 0) {

System.out.println("Stack underflow.");

return 0;

}

else

return stck[tos--];

}

}

正如你看到的,Stack类定义了两个数据项、三个方法。整数堆栈由数组stck存储。该数组的下标由变量tos控制,该变量总是包含堆栈顶层的下标。Stack()构造函数将tos初始化为-1,它指向一个空堆栈。方法push()将一个项目压入堆栈。为了重新取回压入堆栈的项目,调用pop()。既然存取数据通过push()pop(),数组中存储堆栈的事实实际上和使用的堆栈不相关。例如,堆栈可以被存储在一个更复杂的数据结构中,例如一个链表,但push()pop()定义的接口仍然是一样的。

下面示例的类TestStack,验证了Stack类。该类产生两个整数堆栈,将一些值存入,然后将它们取出。

class TestStack {

public static void main(String args[]) {

Stack mystack1 = new Stack();

Stack mystack2 = new Stack();

// push some numbers onto the stack

for(int i=0; i<10; i++) mystack1.push(i);

for(int i=10; i<20; i++) mystack2.push(i);

// pop those numbers off the stack

System.out.println("Stack in mystack1:");

for(int i=0; i<10; i++)

System.out.println(mystack1.pop());

System.out.println("Stack in mystack2:");

for(int i=0; i<10; i++)

System.out.println(mystack2.pop());

}

}

该程序产生的输出如下:

Stack in mystack1:

9

8

7

6

5

4

3

2

1

0

Stack in mystack2:

19

18

17

16

15

14

13

12

11

10

你已经看到,每个堆栈中的内容是分离的。

关于Stack类的最后一点。正如它现在执行的一样,通过Stack类外面的代码可以改变保存堆栈的数组stck。这样的Stack是开放的,容易误用或损坏。在下一章中,你将会看到如何补救这种情况。


7 进一步研究方法和类

在本章中我们接着上一章继续研究方法和类。我们先研究几个有关方法的主题,包括方法重载、参数传递和递归。然后研究类,讨论存取控制,关键字static的用法,以及Java最重要的内置类之一:String

7.1

Java中,同一个类中的2个或2个以上的方法可以有同一个名字,只要它们的参数声明不同即可。在这种情况下,该方法就被称为重载(overloaded),这个过程称为方法重载(method overloading)。方法重载是Java实现多态性的一种方式。如果你以前从来没有使用过一种允许方法重载的语言,这个概念最初可能有点奇怪。但是你将看到,方法重载是Java最激动人心和最有用的特性之一。

当一个重载方法被调用时,Java用参数的类型和(或)数量来表明实际调用的重载方法的版本。因此,每个重载方法的参数的类型和(或)数量必须是不同的。虽然每个重载方法可以有不同的返回类型,但返回类型并不足以区分所使用的是哪个方法。当Java调用一个重载方法时,参数与调用参数匹配的方法被执行。

下面是一个说明方法重载的简单例子:

// Demonstrate method overloading.

class OverloadDemo {

void test() {

System.out.println("No parameters");

}

// Overload test for one integer parameter.

void test(int a) {

System.out.println("a: " + a);

}

// Overload test for two integer parameters.

void test(int aint b) {

System.out.println("a and b: " + a + " " + b);

}

// overload test for a double parameter

double test(double a) {

System.out.println("double a: " + a);

return a*a;

}

}

class Overload {

public static void main(String args[]) {

OverloadDemo ob = new OverloadDemo();

double result;

// call all versions of test()

ob.test();

ob.test(10);

ob.test(1020);

result = ob.test(123.25);

System.out.println("Result of ob.test(123.25): " + result);

}

}

该程序产生如下输出:

No parameters

a: 10

a and b: 10 20

double a: 123.25

Result of ob.test(123.25): 15190.5625

从上述程序可见,test()被重载了四次。第一个版本没有参数,第二个版本有一个整型参数,第三个版本有两个整型参数,第四个版本有一个double型参数。由于重载不受方法的返回类型的影响,test()第四个版本也返回了一个和重载没有因果关系的值

当一个重载的方法被调用时,Java在调用方法的参数和方法的自变量之间寻找匹配。但是,这种匹配并不总是精确的。在一些情况下,Java的自动类型转换也适用于重载方法的自变量。例如,看下面的程序:

// Automatic type conversions apply to overloading.

class OverloadDemo {

void test() {

System.out.println("No parameters");

}

// Overload test for two integer parameters.

void test(int aint b) {

System.out.println("a and b: " + a + " " + b);

}

// overload test for a double parameter

void test(double a) {

System.out.println("Inside test(double) a: " + a);

}

}

class Overload {

public static void main(String args[]) {

OverloadDemo ob = new OverloadDemo();

int i = 88;

ob.test();

ob.test(1020);

ob.test(i); // this will invoke test(double)

ob.test(123.2); // this will invoke test(double)

}

}

该程序产生如下输出:

No parameters

a and b: 10 20

Inside test(double) a: 88

Inside test(double) a: 123.2

在本例中,OverloadDemo 的这个版本没有定义test(int)。因此当在Overload内带整数参数调用test()时,找不到和它匹配的方法。但是,Java可以自动地将整数转换为double型,这种转换就可以解决这个问题。因此,在test(int)找不到以后,Javai扩大到double型,然后调用test(double)。当然,如果定义了test(int),当然先调用test(int)而不会调用test(double)。只有在找不到精确匹配时,Java的自动转换才会起作用。

方法重载支持多态性,因为它是Java实现 “一个接口,多个方法”范型的一种方式。要理解这一点,考虑下面这段话:在不支持方法重载的语言中,每个方法必须有一个惟一的名字。但是,你经常希望实现数据类型不同但本质上相同的方法。可以参考绝对值函数的例子。在不支持重载的语言中,通常会含

有这个函数的三个及三个以上的版本,每个版本都有一个差别甚微的名字。例如,在C语言中 ,函数abs( )返回整数的绝对值,labs( )返回long型整数的绝对值( ),而fabs( )返回浮点值的绝对值。尽管这三个函数的功能实质上是一样的,但是因为C语言不支持重载,每个函数都要有它自己的名字。这样就使得概念情况复杂许多。尽管每一个函数潜在的概念是相同的,你仍然不得不记住这三个名字。在Java中就不会发生这种情况,因为所有的绝对值函数可以使用同一个名字。确实,Java的标准的类库包含一个绝对值方法,叫做abs ( )。这个方法被Javamath类重载,用于处理数字类型。Java根据参数类型决定调用的abs()的版本。

重载的价值在于它允许相关的方法可以使用同一个名字来访问。因此,abs这个名字代表了它执行的通用动作(general action)。为特定环境选择正确的指定(specific)版本是编译器要做的事情。作为程序员的你,只需要记住执行的通用操作就行了。通过多态性的应用,几个名字减少为一个。尽管这个例子相当简单,但如果你将这个概念扩展一下,你就会理解重载能够帮助你解决更复杂的问题。

当你重载一个方法时,该方法的每个版本都能够执行你想要的任何动作。没有什么规定要求重载方法之间必须互相关联。但是,从风格上来说,方法重载还是暗示了一种关系。这就是当你能够使用同一个名字重载无关的方法时,你不应该这么做。例如,你可以使用sqr这个名字来创建一种方法,该方法返回一个整数的平方和一个浮点数值的平方根。但是这两种操作在功能上是不同的。按照这种方式应用方法就违背了它的初衷。在实际的编程中,你应该只重载相互之间关系紧密的操作。

<chsdate w:st="on" year="1899" month="12" day="30" islunardate="False" isrocdate="False"><span style="mso-bookmark: _Toc528746947"><span lang="EN-US">7.1.1</span></span></chsdate> 构造函数重载

除了重载正常的方法外,构造函数也能够重载。实际上,对于大多数你创建的现实的类,重载构造函数是很常见的,并不是什么例外。为了理解为什么会这样,让我们回想上一章中举过的Box类例子。下面是最新版本的Box类的例子:

class Box {

double width;

double height;

double depth;

// This is the constructor for Box.

Box(double wdouble hdouble d) {

width = w;

height = h;

depth = d;

}

// compute and return volume

double volume() {

return width * height * depth;

}

}

在本例中,Box()构造函数需要三个自变量,这意味着定义的所有Box对象必须给Box()构造函数传递三个参数。例如,下面的语句在当前情况下是无效的:

Box ob = new Box();

因为Box( )要求有三个参数,因此如果不带参数的调用它则是一个错误。这会引起一些重要的问题。如果你只想要一个盒子而不在乎 (或知道)它的原始的尺寸该怎么办?或,如果你想用仅仅一个值来初始化一个立方体,而该值可以被用作它的所有的三个尺寸又该怎么办?如果Box类是像现在这样写的,与此类似的其他问题你都没有办法解决,因为你只能带三个参数而没有别的选择权。

幸好,解决这些问题的方案是相当容易的:重载Box构造函数,使它能处理刚才描述的情况。下面程序是Box的一个改进版本,它就是运用对Box构造函数的重载来解决这些问题的:

/* HereBox defines three constructors to initialize

the dimensions of a box various ways.

*/

class Box {

double width;

double height;

double depth;

// constructor used when all dimensions specified

Box(double wdouble hdouble d) {

width = w;

height = h;

depth = d;

}

// constructor used when no dimensions specified

Box() {

width = -1; // use -1 to indicate

height = -1; // an uninitialized

depth = -1; // box

}

// constructor used when cube is created

Box(double len) {

width = height = depth = len;

}

// compute and return volume

double volume() {

return width * height * depth;

}

}

class OverloadCons {

public static void main(String args[]) {

// create boxes using the various constructors

Box mybox1 = new Box(102015);

<street w:st="on"><span lang="EN-US">Box</span></street> mybox2
= new Box();

Box mycube = new

<street w:st="on">Box</street>(7)
;

double vol;

// get volume of first box

vol = mybox1.volume();

System.out.println("Volume of mybox1 is " + vol);

// get volume of second box

vol = mybox2.volume();

System.out.println("Volume of mybox2 is " + vol);

// get volume of cube

vol = mycube.volume();

System.out.println("Volume of mycube is " + vol);

}

}

该程序产生的输出如下所示:

Volume of mybox1 is 3000.0

Volume of mybox2 is -1.0

Volume of mycube is 343.0

在本例中,当new执行时,根据指定的自变量调用适当的构造函数。

7.2 把对象作为参数

到目前为止,我们都使用简单类型作为方法的参数。但是,给方法传递对象是正确的,也是常用的。例如,考虑下面的简单程序:

// Objects may be passed to methods.

class Test {

int ab;

Test(int iint j) {

a = i;

b = j;

}

// return true if o is equal to the invoking object

boolean equals(Test o) {

if(o.a == a && o.b == b) return true;

else return false;

}

}

class PassOb {

public static void main(String args[]) {

Test ob1 = new Test(10022);

Test ob2 = new Test(10022);

Test ob3 = new Test(-1-1);

System.out.println("ob1 == ob2: " + ob1.equals(ob2));

System.out.println("ob1 == ob3: " + ob1.equals(ob3));

}

}

该程序产生如下输出:

ob1 == ob2: true

ob1 == ob3: false

在本程序中,在Test中的equals()方法比较两个对象的相等性,并返回比较的结果。也就是,它把调用的对象与被传递的对象作比较。如果它们包含相同的值,则该方法返回值为真,否则返回值为假。注意equals中的自变量o指定Test作为它的类型。尽管Test是程序中创建的类的类型,但是它的使用与Java的内置类型相同。

对象参数的最普通的使用涉及到构造函数。你经常想要构造一个新对象,并且使它的初始状态与一些已经存在的对象一样。为了做到这一点,你必须定义一个构造函数,该构造函数将一个对象作为它的类的一个参数。例如,下面版本的Box允许一个对象初始化另外一个对象:

// HereBox allows one object to initialize another.

class Box {

double width;

double height;

double depth;

// construct clone of an object

Box(Box ob) { // pass object to constructor

width = ob.width;

height = ob.height;

depth = ob.depth;

}

// constructor used when all dimensions specified

Box(double wdouble hdouble d) {

width = w;

height = h;

depth = d;

}

// constructor used when no dimensions specified

Box() {

width = -1; // use -1 to indicate

height = -1; // an uninitialized

depth = -1; // box

}

// constructor used when cube is created

Box(double len) {

width = height = depth = len;

}

// compute and return volume

double volume() {

return width * height * depth;

}

}

class OverloadCons2 {

public static void main(String args[]) {

// create boxes using the various constructors

Box mybox1 = new Box(102015);

<street w:st="on"><span lang="EN-US">Box</span></street> mybox2
= new Box();

Box mycube = new

<street w:st="on">Box</street>(7)
;

Box myclone = new Box(mybox1);

double vol;

// get volume of first box

vol = mybox1.volume();

System.out.println("Volume of mybox1 is " + vol);

// get volume of second box

vol = mybox2.volume();

System.out.println("Volume of mybox2 is " + vol);

// get volume of cube

vol = mycube.volume();

System.out.println("Volume of cube is " + vol);

// get volume of clone

vol = myclone.volume();

System.out.println("Volume of clone is " + vol);

}

}

在本程序中你能看到,当你开始创建你自己的类的时候,为了方便高效的构造对象,必须为同一构造函数方法提供多种形式。

7.3 参数是如何传递的

总的来说,计算机语言给子程序传递参数的方法有两种。第一种方法是按值传递(call-by-value)。这种方法将一个参数值(value)复制成为子程序的正式参数。这样,对子程序的参数的改变不影响调用

它的参数。第二种传递参数的方法是引用调用(call-by-reference)。在这种方法中,参数的引用(而不是参数值)被传递给子程序参数。在子程序中,该引用用来访问调用中指定的实际参数。这样,对子程序参数的改变将会影响调用子程序的参数。你将看到,根据传递的对象不同,Java将使用这两种不同的方法。

Java中,当你给方法传递一个简单类型时,它是按值传递的。因此,接收参数的子程序参数的改变不会影响到该方法之外。例如,看下面的程序:

// Simple types are passed by value.

class Test {

void meth(int iint j) {

i *= 2;

j /= 2;

}

}

class CallByValue {

public static void main(String args[]) {

Test ob = new Test();

int a = 15b = 20;

System.out.println("a and b before call: " +

a + " " + b);

ob.meth(ab);

System.out.println("a and b after call: " +

a + " " + b);

}

}

该程序的输出如下所示:

a and b before call: 15 20

a and b after call: 15 20

可以看出,在meth( )内部发生的操作不影响调用中ab的值。它们的值没在本例中没有变为3010

当你给方法传递一个对象时,这种情形就会发生戏剧性的变化,因为对象是通过引用传递的。记住,当你创建一个类类型的变量时,你仅仅创建了一个类的引用。因此,当你将这个引用传递给一个方法时,接收它的参数将会指向该参数指向的同一个对象。这有力地证明了对象是通过引用调用传递给方法的。该方法中对象的改变确实影响了作为参数的对象。例如,考虑下面的程序:

// Objects are passed by reference.

class Test {

int ab;

Test(int iint j) {

a = i;

b = j;

}

// pass an object

void meth(Test o) {

o.a *= 2;

o.b /= 2;

}

}

class CallByRef {

public static void main(String args[]) {

Test ob = new Test(1520);

System.out.println("ob.a and ob.b before call: " +

ob.a + " " + ob.b);

ob.meth(ob);

System.out.println("ob.a and ob.b after call: " +

ob.a + " " + ob.b);

}

}

该程序产生下面的输出:

ob.a and ob.b before call: 15 20

ob.a and ob.b after call: 30 10

正如你所看到的,在这个例子中,在 meth ( )中的操作影响了作为参数的对象。

有趣的一点是,当一个对象引用被传递给方法时,引用本身使用按值调用被传递。但是,因为被传递的值指向一个对象,该值的拷贝仍然指向它相应的参数所指向的同一个对象。

注意:当一个简单类型传递给一个方法时,使用按值传递。对象传递则按引用传递。

7.4

方法能够返回任何类型的数据,包括你创建的类的类型。例如,在下面的程序中,incrByTen()方法返回一个对象,在该对象中的值a比调用对象中的值a10

// Returning an object.

class Test {

int a;

Test(int i) {

a = i;

}

Test incrByTen() {

Test temp = new Test(a+10);

return temp;

}

}

class RetOb {

public static void main(String args[]) {

Test ob1 = new Test(2);

Test ob2;

ob2 = ob1.incrByTen();

System.out.println("ob1.a: " + ob1.a);

System.out.println("ob2.a: " + ob2.a);

ob2 = ob2.incrByTen();

System.out.println("ob2.a after second increase: "

+ ob2.a);

}

}

该程序产生的输出如下所示:

ob1.a: 2

ob2.a: 12

ob2.a after second increase: 22

正如你看到的,每次调用incrByTen(),就产生一个新对象,同时将它的引用返回到调用子程序。

上面的程序还有另外重要的:既然所有的对象用关键字new动态地分配内存,你不必担心一个对象会出范围,因为它被其创建的方法终止。只要你程序中有它的一个引用,该对象将会继续存在。当没有该对象的引用时,在下一次垃圾回收发生时该对象将被回收。

7.5

Java支持递归(recursion)。递归就是依照自身定义事物的过程。在Java编程中,递归是允许方法调用自身调用的属性。调用自身的方法称为是递归的(recursive)。

递归的典型例子是数字的阶乘。数字N的阶乘是1N之间所有整数的乘积。例如3的阶乘就是1´ 2´3,或者是6。下面的程序使用递归来计算数字的阶乘。

// A simple example of recursion.

class Factorial {

// this is a recursive function

int fact(int n) {

int result;

if(n==1) return 1;

result = fact(n-1) * n;

return result;

}

}

class Recursion {

public static void main(String args[]) {

Factorial f = new Factorial();

System.out.println("Factorial of 3 is " + f.fact(3));

System.out.println("Factorial of 4 is " + f.fact(4));

System.out.println("Factorial of 5 is " + f.fact(5));

}

}

该程序产生的输出如下所示:

Factorial of 3 is 6

Factorial of 4 is 24

Factorial of 5 is 120

如果你对递归的方法比较陌生,那么fact( )的操作可能看起来似乎有点糊涂。它是这样工作的:当fact( )带着参数1被调用时,该方法返回1;否则它返回fact( n-1 )n的乘积。为了对这个表达式求值,fact()带着参数n-1被调用。重复这个过程直到 n 等于 1,且对该方法的调用开始返回。

为了更好地理解fact( )方法是如何工作的,让我们通过一个短例子来说明。例如当计算 3 的阶乘时,对fact()的第一次调用引起参数2的第二次调用。这个调用将引起fact以参数1的第三次调用,这个调用返回1,这个值接着与2(第二次调用时n的值)相乘。然后该结果(现为2)返回到fact()的最初的调用,并将该结果与3n的初始值)相乘。这时得到答案,6。如果你在fact()中插入println()语句,显示每次调用的阶数以及中间结果,你会觉得很有意思。

当一个方法调用它自身的时候,堆栈就会给新的局部变量和自变量分配内存,方法代码就带着这些新的变量从头执行。递归调用并不产生方法新的拷贝。只有参数是新的。每当递归调用返回时,旧的局部变量和自变量就从堆栈中清除,运行从方法中的调用点重新开始。递归方法可以说是像“望远镜”一样,可以自由伸缩。

许多子程序的递归版本执行时会比它们的迭代版本要慢一点,因为它们增加了额外的方法调用的消

耗。对一个方法太多的递归调用会引起堆栈崩溃。因为自变量和局部变量的存储都在堆栈中,每次调用都创建这些变量新的拷贝,堆栈有可能被耗尽。如果发生这种情况,Java的运行时系统就会产生异常。但是,除非递归子程序疯狂运行,否则你大概不会担心这种情况。

递归的主要优点在于:某些类型的算法采用递归比采用迭代算法要更加清晰和简单。例如快速排序算法按照迭代方法是很难实现的。还有其他一些问题,特别是人工智能问题,就依赖于递归提供解决方案。最后,有些人认为递归要比迭代简单。

当编写递归方法时,你必须使用if条件语句在递归调用不执行时来强制方法返回。如果你不这么做,一旦你调用方法,它将永远不会返回。这类错误在使用递归时是很常见的。尽量多地使用println()语句,使你可以了解程序的进程;如果发现错误,立即中止程序运行。

下面是递归的又一个例子。递归方法 printArray ( )打印数组values中的前i个元素。

// Another example that uses recursion.

class RecTest {

int values[];

RecTest(int i) {

values = new int[i];

}

// display array – recursively

void printArray(int i) {

if(i==0) return;

else printArray(i-1);

System.out.println("[" + (i-1) + "] " + values[i-1]);

}

}

class Recursion2 {

public static void main(String args[]) {

RecTest ob = new RecTest(10);

int i;

for(i=0; i<10; i++) ob.values[i] = i;

ob.printArray(10);

}

}

该程序产生如下的输出:

[0] 0

[1] 1

[2] 2

[3] 3

[4] 4

[5] 5

[6] 6

[7] 7

[8] 8

[9] 9

7.6 介绍访问控制

我们知道,封装将数据和处理数据的代码连接起来。同时,封装也提供另一个重要属性:访问控制(access control)。通过封装你可以控制程序的哪一部分可以访问类的成员。通过控制访问,可以阻止对

象的滥用。例如,通过只允许适当定义的一套方法来访问数据,你能阻止该数据的误用。因此,如果使用得当,可以把类创建一个“黑盒子”,虽然可以使用该类,但是它的内部机制是不公开的,不能修改。但是,本书前面创建的类可能不会完全适合这个目标。例如,考虑在第6章末尾示例的Stack类。方法push( )pop()确实为堆栈提供一个可控制的接口,这是事实,但这个接口并没被强制执行。也就是说,程序的其他部分可以绕过这些方法而直接存取堆栈,这是可能的。当然,如果使用不当,这可能导致麻烦。本节将介绍能精确控制一个类各种各样成员的访问的机制。

一个成员如何被访问取决于修改它的声明的访问指示符(access specifier)。Java提供一套丰富的访问指示符。存取控制的某些方面主要和继承或包联系在一起(包,package,本质上是一组类)。Java的这些访问控制机制将在以后讨论。现在,让我们从访问控制一个简单的类开始。一旦你理解了访问控制的基本原理,其他部分就比较容易了。

Java的访问指示符有public(公共的,全局的)、private(私有的,局部的)、和protected(受保护的)。Java也定义了一个默认访问级别。指示符protected仅用于继承情况中。下面我们描述其他两个访问指示符。

让我们从定义publicprivate开始。当一个类成员被public指示符修饰时,该成员可以被你的程序中的任何其他代码访问。当一个类成员被指定为private时,该成员只能被它的类中的其他成员访问。现在你能理解为什么main( )总是被public指示符修饰。它被在程序外面的代码调用,也就是由Java运行系统调用。如果不使用访问指示符,该类成员的默认访问设置为在它自己的包内为public,但是在它的包以外不能被存取(包将在以后的章节中讨论)。

到目前为止,我们开发的类的所有成员都使用了默认访问模式,它实质上是public。然而,这并不是你想要的典型的方式。通常,你想要对类数据成员的访问加以限制,只允许通过方法来访问它。另外,有时你想把一个方法定义为类的一个私有的方法。

访问指示符位于成员类型的其他说明的前面。也就是说,成员声明语句必须以访问指示符开头。下面是一个例子:

public int i;

private double j;

private int myMethod(int achar b) { // ...

要理解publicprivate对访问的作用,看下面的程序:

/* This program demonstrates the difference between

public and private.

*/

class Test {

int a; // default access

public int b; // public access

private int c; // private access

// methods to access c

void setc(int i) { // set c's value

c = i;

}

int getc() { // get c's value

return c;

}

}

class AccessTest {

public static void main(String args[]) {

Test ob = new Test();

// These are OKa and b may be accessed directly

ob.a = 10;

ob.b = 20;

// This is not OK and will cause an error

// ob.c = 100; // Error!

// You must access c through its methods

ob.setc(100); // OK

System.out.println("aband c: " + ob.a + " " +

ob.b + " " + ob.getc());

}

}

可以看出,在Test类中,a使用默认访问指示符,在本例中与public相同。b被显式地指定为public。成员c被指定为private,因此它不能被它的类之外的代码访问。所以,在AccessTest类中不能直接使用c。对它的访问只能通过它的public方法:setc()getc()。如果你将下面语句开头的注释符号去掉,

// ob.c = 100; // Error!

则由于违规,你不能编译这个程序。

为了理解访问控制在实际中的应用,我们来看在第6章末尾所示的Stack类的改进版本。

// This class defines an integer stack that can hold 10 values.

class Stack {

/* Nowboth stck and tos are private. This means

that they cannot be accidentally or maliciously

altered in a way that would be harmful to the stack.

*/

private int stck[] = new int[10];

private int tos;

// Initialize top-of-stack

Stack() {

tos = -1;

}

// Push an item onto the stack

void push(int item) {

if(tos==9)

System.out.println("Stack is full.");

else

stck[++tos] = item;

}

// Pop an item from the stack

int pop() {

if(tos < 0) {

System.out.println("Stack underflow.");

return 0;

}

else

return stck[tos--];

}

}

在本例中,现在存储堆栈的stck和指向堆栈顶部的下标tos,都被指定为private。这意味着除了通过push()pop(),它们不能够被访问或改变。例如,将tos指定为private,阻止你程序的其他部分无意中将它的值设置为超过stck 数组下标界的值。

下面的程序表明了改进的Stack类。试着删去注释前面的线条来证明stcktos成员确实是不能访问的。

class TestStack {

public static void main(String args[]) {

Stack mystack1 = new Stack();

Stack mystack2 = new Stack();

// push some numbers onto the stack

for(int i=0; i<10; i++) mystack1.push(i);

for(int i=10; i<20; i++) mystack2.push(i);

// pop those numbers off the stack

System.out.println("Stack in mystack1:");

for(int i=0; i<10; i++)

System.out.println(mystack1.pop());

System.out.println("Stack in mystack2:");

for(int i=0; i<10; i++)

System.out.println(mystack2.pop());

// these statements are not legal

// mystack1.tos = -2;

// mystack2.stck[3] = 100;

}

}

尽管由类定义的方法通常提供对数据的访问,但情况并不总是这样。当需要时允许一个实例变量为public是完全合适的。例如,为简单起见,本书中大多数的简单类在创建时不关心实例变量的存取。然而,在大多数实际应用的类中,你将有必要仅仅允许通过方法来对数据操作。下一章将回到访问控制的话题。你将看到,在继承中访问控制是至关重要的。

7.7 理解static

有时你希望定义一个类成员,使它的使用完全独立于该类的任何对象。通常情况下,类成员必须通过它的类的对象访问,但是可以创建这样一个成员,它能够被它自己使用,而不必引用特定的实例。在成员的声明前面加上关键字static(静态的)就能创建这样的成员。如果一个成员被声明为static,它就能够在它的类的任何对象创建之前被访问,而不必引用任何对象。你可以将方法和变量都声明为staticstatic成员的最常见的例子是main( )。因为在程序开始执行时必须调用main(),所以它被声明为static

声明为static的变量实质上就是全局变量。当声明一个对象时,并不产生static变量的拷贝,而是该类所有的实例变量共用同一个static变量。

声明为static的方法有以下几条限制:

· 它们仅能调用其他的static方法。

· 它们只能访问static数据。

· 它们不能以任何方式引用thissuper(关键字super与继承有关,在下一章中描述)。

如果你需要通过计算来初始化你的static变量,你可以声明一个static块,Static块仅在该类被加载时执行一次。下面的例子显示的类有一个static方法,一些static变量,以及一个static初始化块:

// Demonstrate static variablesmethodsand blocks.

class UseStatic {

static int a = 3;

static int b;

static void meth(int x) {

System.out.println("x = " + x);

System.out.println("a = " + a);

System.out.println("b = " + b);

}

static {

System.out.println("Static block initialized.");

b = a * 4;

}

public static void main(String args[]) {

meth(42);

}

}

一旦UseStatic类被装载,所有的static语句被运行。首先,a被设置为3,接着static块执行(打印一条消息),最后,b被初始化为a*412。然后调用main()main()调用meth(),把值42传递给x3println ( )语句引用两个static变量ab,以及局部变量x

注意:在一个static方法中引用任何实例变量都是非法的。

下面是该程序的输出:

Static block initialized.

x = 42

a = 3

b = 12

在定义它们的类的外面,static方法和变量能独立于任何对象而被使用。这样,你只要在类的名字后面加点号运算符即可。例如,如果你希望从类外面调用一个static方法,你可以使用下面通用的格式:

classname.method()

这里,classname 是类的名字,在该类中定义static方法。可以看到,这种格式与通过对象引用变量调用非static方法的格式类似。一个static变量可以以同样的格式来访问——类名加点号运算符。这就是Java如何实现全局功能和全局变量的一个控制版本。

下面是一个例子。在main()中,static方法callme()static变量b在它们的类之外被访问。

class StaticDemo {

static int a = 42;

static int b = 99;

static void callme() {

System.out.println("a = " + a);

}

}

class StaticByName {

public static void main(String args[]) {

StaticDemo.callme();

System.out.println("b = " + StaticDemo.b);

}

}

下面是该程序的输出:

a = 42

b = 99

7.8 介绍final

一个变量可以声明为final,这样做的目的是阻止它的内容被修改。这意味着在声明final变量的时候,你必须初始化它(在这种用法上,final类似于C/C++中的const)。例如:

final int FILE_NEW = 1;

final int FILE_OPEN = 2;

final int FILE_SAVE = 3;

final int FILE_SAVEAS = 4;

final int FILE_QUIT = 5;

你的程序的随后部分现在可以使用 FILE_OPEN等等,就好像它们是常数,不必担心它们的值会被改变。

final变量的所有的字符选择大写是一个普遍的编码约定。声明为final的变量在实例中不占用内存。这样,一个final变量实质上是一个常数。

关键字final也可以被应用于方法,但是它的意思和它被用于变量实质上是不同的。final的第二种用法将在下一章描述继承时解释。

7.9 重新温习数组

在此之前已经在本书中介绍过数组了。现在既然你已了解了类,可以介绍关于数组的重要的一点:数组是作为对象来实现的。因此,你可能想要利用数组的一种特别的属性,具体地说,就是一个数组的大小——也就是,一个数组能保存的元素的数目——可以在它的length实例变量中找到。所有的数组都有这个变量,并且它总是保存数组的大小。下面的程序示例了这个性质:

// This program demonstrates the length array member.

class Length {

public static void main(String args[]) {

int a1[] = new int[10];

int a2[] = {357189944-10};

int a3[] = {4321};

System.out.println("length of a1 is " + a1.length);

System.out.println("length of a2 is " + a2.length);

System.out.println("length of a3 is " + a3.length);

}

}

该程序显示如下输出:

length of a1 is 10

length of a2 is 8

length of a3 is 4

可以看出,每个数组的大小都被显示。要记住length的值和数组实际使用的元素的个数没有关系。length仅反映了数组能够包含的元素的数目。

在许多情况下,你可以好好利用length。例如,下面的程序是Stack类的改进版本。你可能回忆起,该类的早期的版本总是要产生一个10个元素的堆栈。下面的版本可以让你产生任意长度的堆栈。stck.length的值用来防止堆栈溢出。

// Improved Stack class that uses the length array member.

class Stack {

private int stck[];

private int tos;

// allocate and initialize stack

Stack(int size) {

stck = new int[size];

tos = -1;

}

// Push an item onto the stack

void push(int item) {

if(tos==stck.length-1) // use length member

System.out.println("Stack is full.");

else

stck[++tos] = item;

}

// Pop an item from the stack

int pop() {

if(tos < 0) {

System.out.println("Stack underflow.");

return 0;

}

else

return stck[tos--];

}

}

class TestStack2 {

public static void main(String args[]) {

Stack mystack1 = new Stack(5);

Stack mystack2 = new Stack(8);

// push some numbers onto the stack

for(int i=0; i<5; i++) mystack1.push(i);

for(int i=0; i<8; i++) mystack2.push(i);

// pop those numbers off the stack

System.out.println("Stack in mystack1:");

for(int i=0; i<5; i++)

System.out.println(mystack1.pop());

System.out.println("Stack in mystack2:");

for(int i=0; i<8; i++)

System.out.println(mystack2.pop());

}

}

注意,该程序创建了两个堆栈:一个有5个元素,另一个有8个元素。可以看出,数组保持它们自己长度信息的事实使创建任何大小的堆栈很容易。

7.10 介绍嵌套类和内部类

在另一个类中定义的类就是嵌套类(nested classes)。嵌套类的范围由装入它的类的范围限制。这样,如果类B被定义在类A之内,那么BA所知,然而不被A的外面所知。嵌套类可以访问嵌套它的类的成员,包括private成员。但是,包围类不能访问嵌套类的成员。

嵌套类一般有2种类型:前面加static标识符的和不加static标识符的。一个static的嵌套类有static修饰符。因为它是static,所以只能通过对象来访问它包围类的成员。也就是说,它不能直接引用它包围类的成员。因为有这个限制,所以static嵌套类很少使用。

嵌套类最重要的类型是内部类(inner class)。内部类是非static的嵌套类。它可以访问它的外部类的所有变量和方法,它可以直接引用它们,就像外部类中的其他非static成员的功能一样。这样,一个内部类完全在它的包围类的范围之内。

下面的程序示例了如何定义和使用一个内部类。名为Outer的类有一个名为outer_x的示例变量,一个名为test()的实例方法,并且定义了一个名为Inner的内部类。

// Demonstrate an inner class.

class Outer {

int outer_x = 100;

void test() {

Inner inner = new Inner();

inner.display();

}

// this is an inner class

class Inner {

void display() {

System.out.println("display: outer_x = " + outer_x);

}

}

}

class InnerClassDemo {

public static void main(String args[]) {

Outer outer = new Outer();

outer.test();

}

}

该程序的输出如下所示:

display: outer_x = 100

在本程序中,内部类Inner定义在Outer类的范围之内。因此,在Inner类之内的任何代码可以直接访问变量outer_x。实例方法display()定义在Inner的内部,该方法以标准的输出流显示 outer_xInnerClassDemomain( )方法创建类Outer的一个实例并调用它的test( )方法。创建类Innerdisplay()方法的一个实例的方法被调用。

认识到Inner类只有在类Outer的范围内才是可知的是很重要的。如果在类Outer之外的任何代码试图实例化Inner类,Java编译器会产生一条错误消息。总体来说,一个嵌套类和其他任何另外的编程元素没有什么不同:它仅仅在它的包围范围内是可知的。

我们解释过,一个内部类可以访问它的包围类的成员,但是反过来就不成立了。内部类的成员只有在内部类的范围之内是可知的,而且不能被外部类使用。例如:

// This program will not compile.

class Outer {

int outer_x = 100;

void test() {

Inner inner = new Inner();

inner.display();

}

// this is an inner class

class Inner {

int y = 10; // y is local to Inner

void display() {

System.out.println("display: outer_x = " + outer_x);

}

}

void showy() {

System.out.println(y); // errory not known here!

}

}

class InnerClassDemo {

public static void main(String args[]) {

Outer outer = new Outer();

outer.test();

}

}

这里,y是作为Inner的一个实例变量来声明的。这样对于该类的外部它就是不可知的,因此不能被showy()使用。

尽管我们强调嵌套类在它的外部类的范围之内声明,但在几个程序块的范围之内定义内部类是可能的。例如,在由方法定义的块中,或甚至在for循环体内部,你也可以定义嵌套类,如下面的程序所示:

// Define an inner class within a for loop.

class Outer {

int outer_x = 100;

void test() {

for(int i=0; i<10; i++) {

class Inner {

void display() {

System.out.println("display: outer_x = " + outer_x);

}

}

Inner inner = new Inner();

inner.display();

}

}

}

class InnerClassDemo {

public static void main(String args[]) {

Outer outer = new Outer();

outer.test();

}

}

该程序的这个版本的输出如下所示。

display: outer_x = 100

display: outer_x = 100

display: outer_x = 100

display: outer_x = 100

display: outer_x = 100

display: outer_x = 100

display: outer_x = 100

display: outer_x = 100

display: outer_x = 100

display: outer_x = 100

尽管嵌套类在日常的大多数编程中不使用,但当处理applet(小应用程序)时是特别有帮助的。在第20章中我们将继续嵌套类的话题。在那里你将看到对于某些类型的事件内部类如何被用来简化代码。你也将了解匿名内部类(anonymous inner classes,它是一个没有名字的内部类。

最后一点:嵌套类在Java的最初的1.0版本中是不允许的。直到Java 1.1中才添加了嵌套类。

7.11 探索String

尽管String类将在本书的第2部分深入地研究,但因为我们将在第1部分末尾的一些例子程序中使用字符串,因此,现在应该对它做一个简单的探索。String类是Java类库中最常用的类,其中最明显的原因是字符串在编程语言中是很重要的部分。

有关字符串的最重要一点是,你创建的每一个字符串实际上都是String类型的一个对象,即使是字符串常量实际上也是String对象。

System.out.println("This is a Stringtoo");

字符串“This is a Stringtoo”是一个字符串常数。幸好,Java处理字符串常数和其他计算机语言处理“正常”的字符串的方法一样,因此你不必担心这个。

字符串的另一个特点是,String类型的对象是不可改变的;一旦创建了一个字符串对象,它的内容是不能被改变的。这看起来是一个严格的限制,但实际上不是,因为这有两个原因:

· 如果你需要改变一个字符串,你可以创建一个新的字符串,其中包含修改后的字符串即可。

· Java定义了一个和String类同等的类叫StringBuffer,它允许字符串改变,因此所有正常的字符串操作在Java中还是可用的(StringBuffer在本书的第2部分描述)。

字符串可以通过多种方法构造。最容易的一种用如下的语句:

String myString = "this is a test";

一旦你创建了一个字符串对象,你可以在任何允许字符串的地方使用它,例如下面这条语句显示myString

System.out.println(myString);

Java定义了一个String对象的运算符:“+”。它用来连接两个字符串。例如,下面这条语句

String myString = "I" + " like " + "Java.";

的结果是myString包含“I like Java.

下面的程序表明了前面的概念:

// Demonstrating Strings.

class StringDemo {

public static void main(String args[]) {

String strOb1 = "First String";

String strOb2 = "Second String";

String strOb3 = strOb1 + " and " + strOb2;

System.out.println(strOb1);

System.out.println(strOb2);

System.out.println(strOb3);

}

}

该程序产生的输出如下所示:

First String

Second String

First String and Second String

String类包含许多操作字符串的方法。例如下面就是其中一些。你可以用equals()来检验两个字符串是否相等。你可以调用方法length()来获得一个字符串的长度。你可以调用charAt()来获得一个字符串指定索引的字符。这三个方法的通用格式如下所示:

boolean equals(String object)

int length()

char charAt(int index)

下面的程序示例了这些方法:

// Demonstrating some String methods.

class StringDemo2 {

public static void main(String args[]) {

String strOb1 = "First String";

String strOb2 = "Second String";

String strOb3 = strOb1;

System.out.println("Length of strOb1: " +

strOb1.length());

System.out.println("Char at index <chmetcnv w:st="on" unitname="in" sourcevalue="3" hasspace="True" negative="False" numbertype="1" tcsc="0">3 in</chmetcnv> strOb1: " +

strOb1.charAt(3));

if(strOb1.equals(strOb2))

System.out.println("strOb1 == strOb2");

else

System.out.println("strOb1 != strOb2");

if(strOb1.equals(strOb3))

System.out.println("strOb1 == strOb3");

else

System.out.println("strOb1 != strOb3");

}

}

该程序产生如下的输出:

Length of strOb1: 12

Char at index <chmetcnv w:st="on" unitname="in" sourcevalue="3" hasspace="True" negative="False" numbertype="1" tcsc="0">3 in</chmetcnv> strOb1: s

strOb1 != strOb2

strOb1 == strOb3

当然,与其他对象类型一样,strings也可以组成数组,例如:

// Demonstrate String arrays.

class StringDemo3 {

public static void main(String args[]) {

String str[] = { "one""two""three" };

for(int i=0; i<str.length; i++)

System.out.println("str[" + i + "]: " +

str[i]);

}

}

下面是该程序产生的输出:

str[0]: one

str[1]: two

str[2]: three

在下节里你将看到,字符串数组在许多Java程序中起重要的作用。

7.12 使用命令行参数

有时你想在运行程序时将信息传递到一个程序中。这通过将命令行参数(command-line arguments)传递给main()来实现。命令行参数是程序执行时在命令行中紧跟在程序名后的信息。在Java程序中访问命令行参数是相当容易的——它们作为字符串存储在传递给main()String数组中。例如,下面的程序显示了调用的所有的命令行参数:

// Display all command-line arguments.

class CommandLine {

public static void main(String args[]) {

for(int i=0; i<args.length; i++)

System.out.println("args[" + i + "]: " +

args[i]);

}

}

尝试执行这个程序,命令如下所示:

java Commandline this is a test 100 -1

执行后,你会看到如下输出:

args[0]: this

args[1]: is

args[2]: a

args[3]: test

args[4]: 100

args[5]: -1

注意:所有的命令行参数都是以字符串的形式传递的。你必须手工把数字值变换到它们的内部形式,这将在第14章解释。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics