第五章 类与面向对象初步
在本章中,我们将研究Java编程中非常重要的概念 - 面向对象编程。我们将学习什么是面向对象编程,以及如何编写我们自己的类并从中创建对象。
5.1 面向对象编程
面向对象编程(OOP)是如今多种编程语言所实现的一种编程范式,包括 Java、C++。下面我们将介绍两个概念,面向对象,面向过程。
面向过程(Procedure Oriented 简称PO :如C语言):
从名字可以看出它是注重过程的。当解决一个问题的时候,面向过程会把事情拆分成: 一个个函数和数据(用于方法的参数) 。然后按照一定的顺序,执行完这些方法(每个方法看作一个过程),等方法执行完了,事情就搞定了。
面向对象(Object Oriented简称OO :如C++,JAVA等语言):
看名字它是注重对象的。当解决一个问题的时候,面向对象会把事物抽象成对象的概念,就是说这个问题里面有哪些对象,然后给对象赋一些属性和方法,然后让每个对象去执行自己的方法,问题得到解决。
面向对象编程将一个系统抽象为许多对象的集合,每一个对象代表了这个系统的特定方面。对象包括函数(方法)和数据。一个对象可以向其他部分的代码提供一个公共接口,而其他部分的代码可以通过公共接口执行该对象的特定操作,系统的其他部分不需要关心对象内部是如何完成任务的,这样保持了对象自己内部状态的私有性。下面我们用几个示例来了解面向对象:
例子一:
问题: 洗衣机里面放有脏衣服,怎么洗干净?
面向过程的解决方法:
1、执行加洗衣粉方法;
2、执行加水方法;
3、执行洗衣服方法;
4、执行清洗方法;
5、 执行烘干方法;
以上就是将解决这个问题的过程拆成一个个方法(是没有对象去调用的),通过一个个方法的执行来解决问题。
面向对象的解决方法:
1、我先弄出两个对象:“洗衣机”对象和“人”对象
2、针对对象“洗衣机”加入一些属性和方法:“洗衣服方法”“清洗方法”、“烘干方法”
3、针对对象“人”加入属性和方法:“加洗衣粉方法”、“加水方法”
4、然后执行
人.加洗衣粉
人.加水
洗衣机.洗衣服
洗衣机.清洗
洗衣机.烘干
解决同一个问题 ,面向对象编程就是先抽象出对象,然后用对象执行方法的方式解决问题。
例子二 : 打麻将例子 你 我 他
面向过程: 打麻将 (你,我,他)
---------解决问题 拆成一个动作,把数据丢进去
面向对象: 我.打麻将(你,他) or 你.打麻将(我,他) or 他.打麻将(我,你)
---------解决问题 抽象出对象,对象执行动作 。
例子三:
最后在用一个网上常见的五子棋例子来说明一下:
面向过程的设计思路就是首先分析问题的步骤:
1、开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。把上面每个步骤用不同的方法来实现。
面向对象的设计则是从另外的思路来解决问题。
整个五子棋可以分为1、黑白双方,这两方的行为是一模一样的,2、棋盘系统,负责绘制画面,3、规则系统,负责判定诸如犯规、输赢等。第一类对象(玩家对象)负责接受用户输入,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到了棋子的变化就要负责在屏幕上面显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。
可以明显地看出,面向对象是以功能来划分问题,而不是步骤。同样是绘制棋局,这样的行为在面向过程的设计中分散在了多个步骤中,很可能出现不同的绘制版本,因为通常设计人员会考虑到实际情况进行各种各样的简化。而面向对象的设计中,绘图只可能在棋盘对象中出现,从而保证了绘图的统一。
5.2 类
类是Java的核心。整个Java语言建立在其上形成自己的逻辑结构,因为它定义了对象的形态和性质。因此,类形成了Java面向对象编程的基础。你希望在Java程序中实现的任何目的都必须封装在一个类中。 由于类对Java如此重要,本章和接下来的几章将专门介绍它。在这里,您将了解类的基本元素,并学习如何使用类创建对象。您还将了解方法、构造函数和关键字“this”。
从本书开始就使用了类。然而,直到现在,只展示了最基本形式的类。在前面的章节中创建的类主要存在于封装main()方法中,用于演示Java语法的基础知识。然而Java中的类比迄今为止所展示的受限制的类要强大得多。 也许最重要的是要理解一个类定义了一个新的数据类型。一旦定义,这个新类型可以用来创建该类型的对象。因此,类是一个对象的模板,而对象是类的一个实例。因为对象是类的一个实例,您经常会看到这两个词——对象和实例被互换使用。
5.2.1 类的一般形式
当您定义一个类时,您声明了它的确切形式和性质。您通过指定它包含的数据和操作这些数据的代码来实现这一点。一个类的代码定义了其数据的接口和实现其中数据操作的方法。 我们将使用class关键字声明一个类。类可以(而且通常会)变得更加复杂。这里先展示了一个简化的一般类定义形式:
class 类名{
类型 变量名;
类型 变量名;
...
类型 方法名(参数){
方法的内容
}
...
}
类中定义的数据或变量称为实例变量。所有需要执行的代码都被包含在类中的方法里。在一个类中定义的方法和变量统称为该类的成员。在大多数类中,实例变量都会被类中的方法访问,操作。换句话说,一般情况下,方法决定了如何使用类的数据。
在类中定义的变量称为实例变量,因为该类的每个实例(即该类的每个对象)都包含类的所有变量的副本。因此,一个对象的数据与另一个对象的数据是分离和唯一的。我们很快就会体会这一点,这是一个早期学习的重要概念。
所有方法都具有与main()方法相同的一般形式,我们迄今为止一直在使用它。但是,大多数方法不会被指定为public static。请注意,类的一般形式并没有特定需要main()方法。Java类不需要有main()方法。只有当该类是程序的起始点时才指定一个。
5.2.2 简单的类的示例
类可以看成是创建 Java 对象的模板。
通过上图创建一个简单的类来理解下 Java 中类的定义:
public class Dog {
String breed;
int size;
String colour;
int age;
void eat() {
}
void run() {
}
void sleep(){
}
void name(){
}
}
一个类可以包含以下类型变量:
- 局部变量:在方法、构造方法或者语句块中定义的变量被称为局部变量。变量声明和初始化都是在方法中,方法结束后,变量就会自动销毁。
- 成员变量:成员变量是定义在类中,方法体之外的变量。这种变量在创建对象的时候实例化。成员变量可以被类中方法、构造方法和特定类的语句块访问。
- 类变量:类变量也声明在类中,方法体之外,但必须声明为 static 类型。
一个类可以拥有多个方法,在上面的例子中:eat()、run()、sleep() 和 name() 都是 Dog 类的方法。
一个类其实是声明了一种新的数据类型,上面的例子里,这种新的数据类型的名称叫做Dog
。请务必记住,一个类的声明仅仅是声明了一个模板,它并没有创建对象(或实例)。所以上面的代码并没有创建任何的Dog
对象的实例。
为了创建一个Dog
对象,我们可以使用如下的语句:
Dog myDog=new Dog();//创建一个名字叫做myDog的对象
执行这条语句后,myDog
将引用Dog
类的一个实例。因此,它实际存在于物理内存中。暂时不用担心这个语句的细节。 正如之前提到的,每次创建一个类的实例时,都会创建类中所有变量的副本。因此,每个Dog
对象都将包含breed,size,colour,age
的实例变量副本。要访问这些变量,我们将使用点(.)运算符。点运算符将对象的名称与实例变量的名称链接在一起。例如,要将myDog
的年龄变量赋值为100,我们将使用以下语句:
myDog.age=5;
这个语句告诉编译器将包含在myDog
对象内的age
的副本赋值为5。通常情况下,我们使用点运算符来访问对象内的实例变量和方法。虽然我们把它通常称为点运算符,但Java的正式规范将“.”称为分隔符。然而,由于“点运算符”一词的使用非常广泛,因此本书中仍然使用该术语。
接下来我们创建一个新的类Box
,并且写一个程序来展示类的用法:
/*
一个使用了Box类的程序
将这个文件命名为 BoxDemo.java
*/
package com.mycompany.boxdemo;
class Box{
double width;
double height;
double depth;
}
//在这个类当中我们声明了Box类的对象
public class BoxDemo {
public static void main(String[] args) {
Box mybox=new Box();
double vol;
//给mybox对象赋值
mybox.width=10;
mybox.height=20;
mybox.depth=15;
//计算盒子的体积
vol=mybox.width*mybox.height*mybox.depth;
System.out.println("体积为"+vol);
}
}
我们应该将这个程序的文件命名为BoxDemo.java,因为main()
方法位于BoxDemo
类中,而不是Box
的类中。当你编译这个程序时,你会发现生成了两个.class文件,一个是Box类的,另一个是BoxDemo类的。Java编译器会自动将每个类放入自己的.class文件中。实际上,我们可以不把Box
和BoxDemo
类写在一个Java程序文件里。你可以分别将每个类放入它自己的文件中,分别命名为Box.java和BoxDemo.java,并且在BoxDemo.java开头声明调用Box.java,具体的细节留到后面在讲。
要运行这个程序,你必须执行BoxDemo.class。当你执行时,你会看到以下输出:
体积为3000.0
正如之前所述,每个对象的变量都是类中变量的副本。这意味着如果你有两个Box对象,每个对象都有自己的depth、width和height的副本,对一个对象的变量所做的更改对另一个对象的变量没有影响。例如,以下程序声明了两个Box对象:
/*
一个使用了Box类的程序
将这个文件命名为 BoxDemo2.java
*/
package com.mycompany.boxdemo2;
class Box{
double width;
double height;
double depth;
}
//在这个类当中我们声明了两个Box类的对象
public class BoxDemo2 {
public static void main(String[] args) {
Box mybox1=new Box();
Box mybox2=new Box();
double vol;
//给mybox对象赋值
mybox1.width=10;
mybox1.height=20;
mybox1.depth=15;
//给mybox2赋不同的值
mybox2.width=3;
mybox2.height=6;
mybox2.depth=9;
//计算第一个盒子的体积
vol=mybox1.width*mybox1.height*mybox1.depth;
System.out.println("体积为"+vol);
//计算第二个盒子的体积
vol=mybox2.width*mybox2.height*mybox2.depth;
System.out.println("体积为"+vol);
}
}
它的输出为:
体积为3000.0
体积为162.0
可见mybox1
的数据是完全独立于mybox
的。
5.3 声明对象
在上一节中我们提到,当我们创建一个类时,我们实际上是在创建一个新的数据类型。我们可以使用这个类型来声明该类型的对象。然而,获取一个类的对象是一个两步过程。首先,你必须声明一个类类型的变量。这个变量并没有定义一个对象,它只是一个可以引用对象的变量。第二,你必须获取一个在物理内存中实际存在的对象副本并将其赋值给该变量。你可以使用new
运算符来完成这个操作。new
运算符在运行时分配一个对象的内存,并返回一个对它的引用。这个引用实质上是由new
分配的对象在内存中的地址。然后将这个引用存储在变量中。因此,在Java中,所有的类对象都必须是动态分配的。现在让我们来看看这个过程的细节。在前面的示例程序中,我们使用以下的方式声明一个对象:
Box mybox = new Box();
刚才提到过,这个语句实际上是包含了两步,它可以重写为以下的形式:
Box mybox; // 声明对象的引用
mybox = new Box(); // 分配一个实际存在的对象
第一行声明mybox是指向一个Box类型对象的引用。此时,mybox还没有指向一个实际的对象。下一行分配一个实际的对象,并将一个引用赋给mybox。在第二行执行后,你可以像使用Box对象一样使用mybox。但实际上,mybox只是保存了实际的Box对象的内存地址。这两行代码的效果如下图所示。
5.3.1 new
正如刚才所解释的,new运算符动态地为对象分配内存。它具有以下一般形式:
class-var = new classname();
这里,class-var是正在创建的类类型的变量。classname是被实例化的类的名称。类名后面跟着括号指定了类的构造函数。构造函数定义了创建类的对象时程序所需要做的事情。构造函数是所有类的重要部分,并具有许多重要属性。大多数真实世界的类都在其类定义内明确定义了自己的构造函数。然而,如果没有明确的构造函数,那么Java将自动提供一个默认构造函数。我们之前的示例Box
就是如此。
此时,你可能会想知道为什么不需要使用new来处理整数或字符等原始变量类型。这是因为Java的原始类型并不是类。相反,它们被实现为“普通”变量。这样做是出于效率的考虑。类具有许多特征和属性,Java需要将它们与原始类型区分对待,通过不对原始类型施加与类相同的开销,Java可以更有效地实现原始类型。稍后,你将看到原始类型的对象版本,可用于那些需要完整对象的情况。
理解new在运行时为对象分配内存是很重要的。这种方法的优点是,你的程序可以在执行期间创建所需数量的对象。然而,由于内存是有限的,如果没有足够的内存,new将无法为对象分配内存。如果这种情况发生,将会发生运行时异常。(你将在第10章学习如何处理异常。)对于本书中的示例程序,你不需要担心内存用尽的问题,但你需要在编写实际应用程序时考虑这种可能性。
5.4 对象的赋值
当进行赋值操作时,对象引用变量的行为与您的预期可能不同。例如,您认为以下片段会做什么?
Box b1 = new Box();
Box b2 = b1;
您可能认为b2被分配了一个对b1所引用对象的副本的引用。即b1和b2引用了不同的独立对象。但是,这是错误的。实际上,在此片段执行后,b1和b2都将引用同一个对象。将b1赋值给b2不会分配任何新的内存或复制原始对象的任何部分。它只是使b2引用与b1相同的对象。因此,通过b2对对象进行的任何更改都会影响b1所引用的对象,因为它们是同一个对象。 这种情况如下图所示:
尽管b1和b2都引用同一个对象,但它们在任何其他方面都没有联系。例如,对b1的后续的赋值将仅从原始对象中取消b1的引用,而不会影响对象或影响b2。例如:
Box b1 = new Box();
Box b2 = b1;
// ...
b1 = null;
在这里,b1被设置为null,但b2仍然指向原始对象。
记住当你将一个对象的引用赋值给另一个对象的时候,我们并没有将整个对象复制,而是仅仅复制了这个对象的引用。
5.5 方法初步
正如本章开头提到的,类通常由两部分组成:实例变量和方法。方法是一个很大的主题,因为Java为它们提供了很多的功能和灵活性。下一章的很多内容都是关于方法的。然而,现在您需要学习一些基础知识,以便开始向您的类中添加方法。下面是方法的一般形式:
类型 名称(参数列表) {
// 方法体
}
在这里,类型指定方法返回的数据类型。这可以是任何有效类型,包括您创建的类类型。如果您想让该方法不返回任何值,则其返回类型必须为void。方法的名称可以是任何合法的标识符,同时这个标识符不能再在方法的作用域内作其他用途使用。参数列表是由逗号分隔的类型和标识符对组成的序列。参数本质上是在调用方法时接收传递给该方法的参数值的变量。如果该方法没有参数,则参数列表将为空。 具有返回类型的方法将使用以下形式的return语句将值返回给调用程序:
return value;
在这里,value是返回的值。 在接下来的几节中,您将看到如何创建各种类型的方法,包括那些带有参数和返回值的方法。
5.5.1 向Box类中添加一个方法
虽然创建一个仅包含数据的类是完全可以的,但这种情况很少发生。大多数情况下,您将使用方法来访问类定义的实例变量。实际上,方法定义了大多数类的接口。这使得类的实现者可以隐藏内部数据结构的具体布局,从而实现更清晰的方法抽象。除了定义提供数据访问的方法之外,您还可以定义被类自身内部使用的方法。
让我们首先在Box类中添加一个方法。在查看前面的程序时,您可能已经意识到,计算一个盒子的体积最好由Box类而不是BoxDemo类处理。毕竟,由于盒子的体积取决于盒子的尺寸,让Box类来计算是有意义的。下面我们将在Box类中添加方法:
package com.mycompany.boxdemo3;
class Box{
double width;
double height;
double depth;
//添加一个显示体积的方法
void volume(){
System.out.print("体积为");
System.out.println(width*height*depth);
}
}
public class BoxDemo3 {
public static void main(String[] args) {
Box mybox1=new Box();
Box mybox2=new Box();
//给mybox1的示例变量赋值
mybox1.width=10;
mybox1.height=20;
mybox1.depth=15;
/*
给mybox2的示例变量赋不同的值
*/
mybox2.width=3;
mybox2.height=6;
mybox2.depth=9;
//显示第一个盒子的体积
mybox1.volume();
//显示第二个盒子的体积
mybox2.volume();
}
}
它的输出为:
体积为3000.0
体积为162.0
请仔细观察以下两行代码:
mybox1.volume();
mybox2.volume();
这里的第一行调用了mybox1里的volume()方法。它使用对象的名称后紧跟的点运算符,调用mybox1.volume()会显示mybox1定义的盒子的体积,而调用mybox2.volume()会显示mybox2定义的盒子的体积。每次调用volume()方法时,它会显示指定盒子的体积。
下面我们来讨论一下Java方法的执行过程。当执行mybox1.volume()时,Java运行时系统将控制权转移到volume()方法内部定义的代码。在volume()方法内部的语句执行完毕后,控制权返回到调用volume()方法结束的地方,然后继续执行调用后的代码。从最一般的意义上讲,方法是Java实现子程序的方式。
在volume()方法内部,volume()方法直接引用实例变量width、height和depth,而不使用对象名称或点运算符作为前缀。当一个方法使用它所定义的类的实例变量时,无需显式引用对象或使用点运算符。如果您仔细思考一下,这很容易理解。方法总是相对于其类的某个对象进行调用。一旦发生了这次调用,方法都能读到对象的变量。因此,在一个方法内部,不需要再次指定对象。这意味着volume()方法内部的width、height和depth隐含地引用调用volume()方法的对象中找到的这些变量的副本。
让我们总结一下:当由不属于定义该实例变量的类的代码访问实例变量时,必须通过对象通过点运算符进行访问。然而,当由与实例变量所在的类相同的类的代码访问实例变量时,可以直接引用该变量。同样的规则也适用于方法。
5.5.2 返回一个值
尽管volume()方法的实现使得计算Box体积的过程移动到了Box类内部,但这并不是最佳的方法。例如,如果程序的其他部分想要知道一个Box的体积,但不显示体积的数值,那么更好的方法是使volume()方法计算盒子的体积并将结果返回给调用者。以下示例是前面程序的改进版本:
package com.mycompany.boxdemo4;
//更改volume()函数
class Box{
double width;
double height;
double depth;
//计算并返回体积,并不显示体积的值
double volume(){
return width*height*depth;
}
}
public class BoxDemo4 {
public static void main(String[] args) {
Box mybox1=new Box();
Box mybox2=new Box();
double vol;
//给mybox1的示例变量赋值
mybox1.width=10;
mybox1.height=20;
mybox1.depth=15;
/*
给mybox2的示例变量赋不同的值
*/
mybox2.width=3;
mybox2.height=6;
mybox2.depth=9;
//获取第mybox1的体积值
vol=mybox1.volume();
System.out.println("体积为"+vol);
//获取mybox2的体积值
vol=mybox2.volume();
System.out.println("体积为"+vol);
}
}
当调用 volume() 函数时,它在赋值语句的右侧。左侧是一个变量vol,它将接收 volume() 函数返回的值。因此,在执行 vol = mybox1.volume();
后,mybox1.volume()
的计算结果为 3,000,这个值将传递给变量vol。
关于方法的返回值,有两个重要的要点:
• 方法返回的数据类型必须与方法指定的返回类型兼容。例如,如果某个方法的返回类型是布尔型,就不能返回整数。
• 接收方法返回值的变量(在上面的例子中为 vol)也必须与方法指定的返回类型兼容。
其实上面的程序可以更加高效地编写,因为实际上并不需要 vol 变量。可以直接在 println() 语句中使用 volume() 函数的调用,如下所示:
System.out.println("体积为" + mybox1.volume());
在这种情况下,当执行 println()
时,mybox1.volume()
会被自动调用,并将其值传递给 println()
。
5.5.3 带有参数的方法
大多数的方法都需要参数,参数使得方法具有通用性。带参数的方法可以处理多种类型的数据,或在多种不同的需求下执行。为了说明这一点,让我们使用一个非常简单的例子:下面是一个返回数字10的平方的方法:
int square()
{
return 10*10;
}
尽管这个方法确实返回10的平方值,但它的使用非常有限。然而,如果你修改该方法,使其接受一个参数,如下所示,那么你可以使square()
方法变得更加有用。
int square(int i)
{
return i*i;
}
现在,square()
方法将返回调用时传入的参数i
的平方。也就是说,square()
现在是一个更加通用的方法,可以计算任何整数值的平方,而不仅仅是10。 以下是一个例子:
int x, y;
x = square(5); // x 等于 25
x = square(9); // x 等于 81
y = 2;
x = square(y); // x 等于 4
在第一次调用square()
时,值5将被传递给参数i
。在第二次调用中,i
将接收值9。第三次调用传递了y
的值,在这个例子中是2。正如这些例子所示,square()
能够返回传入的任何数据的平方。
参数和参数值的含义是不同的。参数是在方法被调用时,方法接收的一个值,它的定义在整个方法内部都是有效的。例如,在square()
方法中,i
就是一个参数。参数值是在调用方法时人为传递给方法的值。例如,square(100)
将100作为参数值传递。在square()
方法内部,参数i接收该值。 您可以使用带参数的方法来改进Box类。在前面的例子中,每个盒子的尺寸必须通过一系列语句单独设置,例如:
mybox1.width = 10;
mybox1.height = 20;
mybox1.depth = 15;
这段代码存在两个问题。首先,它很笨拙且容易出错。例如,程序员可能很容易忘记设置尺寸。其次,在设计良好的Java程序中,实例变量应该只能通过类定义的方法进行访问。我们应该通过方法去改变示例变量的值。因此,更好的设置盒子尺寸的方法是创建一个方法,该方法通过参数传递盒子的尺寸并适当设置每个实例变量。以下程序实现了这个想法:
package com.mycompany.boxdemo5;
class Box{
double width;
double height;
double depth;
//计算并返回体积的大小
double volume(){
return width*height*depth;
}
//设置盒子的参数
void setDim(double w,double h,double d){
width=w;
height=h;
depth=d;
}
}
public class BoxDemo5 {
public static void main(String[] args) {
Box mybox1=new Box();
Box mybox2=new Box();
double vol;
//初始化盒子的参数
mybox1.setDim(10,20,15);
mybox2.setDim(3, 6, 9);
//通过方法得到体积的值
vol=mybox1.volume();
System.out.println("体积为"+vol);
vol=mybox2.volume();
System.out.println("体积为"+vol);
}
}
setDim()
方法用于设置每个盒子的尺寸。例如,当执行mybox1.setDim(10, 20, 15)
时,10被传递到参数w,20传递到h,15传递到d。而在setDim()
方法内部,参数w、h和d的值分别赋值给width、height和depth。
对于许多读者来说,前面几节中介绍的概念可能很熟悉。然而,如果方法调用和参数对您来说是新的概念,那么在继续之前,您可能需要花一些时间进行学习。方法调用、参数和返回值的概念是Java编程的基础。
5.6 构造函数
每次创建一个实例时,初始化类中的所有变量可能会很繁琐。即使您添加了像之前setDim()
这样的便利方法,我们可以把初始化变量这个过程设置在构造函数时就实现。由于类的初始化要求非常常见,Java允许对象在创建时自行初始化。这种自动初始化是通过使用构造函数来实现的。 构造函数在对象创建时立即初始化。它与其所在的类具有相同的名称,并且在语法上类似于方法。一旦定义了构造函数,它将在对象创建时自动调用。构造函数看起来有点奇怪,因为它们没有返回类型,甚至不是void类型。这是因为类的构造函数的隐式返回类型是类类型本身。构造函数的工作是初始化对象的内部状态,以便创建实例的代码将立即拥有一个完全初始化且可用的对象。 让我们重构Box示例,以便在构造对象时自动初始化盒子的尺寸。为此,我们将用构造函数替换setDim()
。让我们先定义一个简单的构造函数,将每个盒子的尺寸设置为相同的值,如下所示:
package com.mycompany.boxdemo6;
//我们用一个构造函数去初始化盒子的参数
class Box{
double width;
double height;
double depth;
//Box的构造函数
Box(){
System.out.println("构造Box类");
width=10;
height=10;
depth=10;
}
//计算体积
double volume(){
return width*height*depth;
}
}
public class BoxDemo6 {
public static void main(String[] args) {
//声明并构造Box类
Box mybox1=new Box();
Box mybox2=new Box();
double vol;
//通过方法得到体积的值
vol=mybox1.volume();
System.out.println("体积为"+vol);
vol=mybox2.volume();
System.out.println("体积为"+vol);
}
}
它的输出结果为:
构造Box类
构造Box类
体积为1000.0
体积为1000.0
创建mybox1和mybox2时,它们都是通过Box()构造函数进行初始化的。由于构造函数为所有的Box对象提供了相同的尺寸,即10×10×10,所以mybox1和mybox2的体积相同。Box()
内的println()
语句仅用于说明目的,大多数构造函数不会显示任何内容,它们只会简单地初始化一个对象。
在继续之前,让我们重新审视一下new运算符。new运算符的一般形式为:
class-var = new classname();
现在你可以理解为什么需要在类名后面加上括号了。实际上我们调用了该类的构造函数。因此,在这行代码中:
Box mybox1 = new Box();
new Box()
操作调用了 Box()
构造函数。当你没有显式地为一个类定义构造函数时,Java会为该类创建一个默认构造函数。这就是为什么在之前版本的Box中没有定义构造函数时,上述代码仍然有效。当使用默认构造函数时,所有未初始化的实例变量将具有它们的默认值,对于数值类型是0,对于引用类型是null,对于布尔类型是false。
默认构造函数通常对于简单的类来说已经足够了,但对于更复杂的类来说通常不够用。一旦你定义了自己的构造函数,就不再使用默认构造函数了。
5.6.1 带参数的构造函数
在前面的示例中,Box()
构造函数初始化了一个 Box 对象,但它并不是非常实用的 — 因为所有的盒子都具有相同的尺寸。更实用的做法是设置一种构造具有不同尺寸的 Box 对象的方法。简单的解决方案是向构造函数添加参数。例如,下面这个版本的 Box 定义了一个带参数的构造函数,根据这些参数设置了一个盒子的尺寸。
package com.mycompany.boxdemo7;
//构造一个带参数的构造函数
class Box{
double width;
double height;
double depth;
//这个是构造函数
Box(double w,double h,double d){
width=w;
height=h;
depth=d;
}
//计算体积并返回体积的大小
double volume(){
return width*height*depth;
}
}
public class BoxDemo7 {
public static void main(String[] args) {
Box mybox1=new Box(10,20,15);
Box mybox2=new Box(3,6,9);
double vol;
//计算第一个盒子的体积
vol=mybox1.volume();
System.out.println("体积为"+vol);
//计算第二个盒子的体积
vol=mybox2.volume();
System.out.println("体积为"+vol);
}
}
它的输出为:
体积为3000.0
体积为162.0
每个对象都根据其构造函数中的参数进行初始化。例如,在下面这行代码中:
Box mybox1 = new Box(10, 20, 15);
在使用new创建对象时,值10、20和15会传递给Box()构造函数。因此,mybox1的width、height和depth的副本将分别为10、20和15。
5.7 this关键字
在Java中,this关键字用于引用当前对象。它可以在一个对象的方法中使用,指代调用该方法的对象本身。你可以将this理解为指向自己的指针。它的作用是在方法内部区分当前对象的成员变量和方法参数,以及访问自己的方法和属性。使用this关键字可以避免命名冲突,并明确指出要操作的是当前对象的成员。
为了更好地理解this关键字的引用对象,思考下面这个Box()方法:
Box(double w,double h,double d){
this.width=w;
this.height=h;
this.depth=d;
}
这个版本的Box()
方法与之前的版本完全相同。使用this是多余的,但完全正确。在Box()
方法内部,this始终指向调用该方法的对象。虽然在这种情况下多余,但在其他情况下this是有用的,下面我们将阐述this在其他情况下的用途。
5.7.1 实例变量隐藏
在Java中,相同的作用域内声明两个同名的局部变量是不合法的。然而,Java允许你有与类的实例变量同名的局部变量。当局部变量与实例变量同名时,局部变量会隐藏实例变量。这就是为什么在Box类的Box( )
构造函数内部,width、height和depth不被用作参数的名称。如果使用了这些名称,例如width,它将引用形式参数,从而隐藏实例变量width。虽然我们可以使用不同的名称,但我们依然可以用this关键字来解决这个问题。因为this可以直接引用对象,你可以使用this来解决实例变量和局部变量之间可能发生的命名空间冲突。例如,下面是另一个版本的Box( )方法,它使用width、height和depth作为参数名称,并使用this关键字来访问具有相同名称的实例变量:
//用this关键字解决命名冲突
Box(double width,double height,double width){
this.width=width;
this.height=height;
this.depth=depth;
}
在这种情况下使用this有时可能会令人困惑,一些程序员小心地避免使用隐藏实例变量的局部变量和形式参数名称。当然,其他程序员持相反的观点,他们认为使用相同的名称可以增加清晰度,并使用this来解决实例变量隐藏的问题是一个好的方法。选择采用哪种方法是一个个人喜好的问题。
5.8 垃圾回收
由于对象是通过使用new运算符动态分配的,你可能会想知道这些对象如何销毁并释放它们的内存以供后续重新分配使用。在某些语言中,如传统的C++,动态分配的对象必须通过使用delete运算符手动释放。Java采用了不同的方法;它会自动处理释放操作。实现这一点的技术称为垃圾回收。其工作方式如下:当没有对象引用存在时,该对象被认为不再需要,并且所占用的内存可以被回收。不需要显式地销毁对象。垃圾回收仅在程序执行过程中偶尔进行(如果有的话)。它不会仅仅因为存在一个或多个不再使用的对象而发生。此外,不同的Java运行时的实现采用不同的垃圾回收方法,但在编写程序时,大部分时间你不需要考虑它。
5.9 栈类
为了展示类的真正威力,本章将以一个更复杂的示例结束。OOP面向对象编程最重要的好处之一就是数据封装和操作数据的代码的封装。类是Java中实现封装的机制。通过创建一个类,你创建了一系列的数据类型和操作这些数据的方法。此外,方法还定义了类数据的接口。因此,你可以通过类的方法使用类,而不必担心其实现细节或数据在类内部的实际管理方式。从某种意义上说,类就像一个“数据引擎”。不我们需要了解引擎内部的运行情况就可以通过引擎控制这些数据。由于细节被隐藏起来,我们可以根据需要更改其内部工作方式。只要你的代码通过类的方法使用类,内部细节的变化就不会对类外部产生副作用。
下面我们将开发封装的典型示例之一:栈。栈使用先进后出的顺序存储数据。也就是说,栈就像桌子上的一叠盘子,放在桌子上的第一个盘子是最后一个使用的。栈通过两个传统称为push和pop的操作进行控制。使用push将一个数据放在栈的顶部,使用pop将一个数据从栈中取出。
下面是一个名为Stack的类,它实现了一个最多可以存储十个整数的栈:
//这个类定义了一个可以容纳10个整形的栈
class Stack{
int stck[]=new int[10];
int tos;
//初始化栈顶
Stack(){
tos=-1;
}
//将一个数据压入栈中
void push(int item){
if(tos==9)
System.out.println("栈满了");
else
stck[++tos]=item;
}
//将一个数据从栈中弹出
int pop(){
if(tos<0){
System.out.print("栈下溢");
return 0;
}
else
{
return stck[tos--];
}
}
}
Stack类定义了两个数据项、两个方法和一个构造函数。整数栈由数组stck
保存。这个数组由变量tos
索引,它始终指向栈顶。Stack( )
构造函数将tos
初始化为-1,表示一个空栈。push( )
方法将一个数据项放入栈中。要弹出一个项,我们调用pop( )
。对栈的访问是通过push( )
和pop( )
进行的。栈可以保存在更复杂的数据结构中,比如链表,但push( )
和pop( )
定义的接口基本思想仍然是相同的。
下面我们将展示一个TestStack类,用来演示了Stack类。它创建了两个整数栈,向每个栈中推入一些值,然后再将它们弹出。
package com.mycompany.teststack;
//这个类定义了一个可以容纳10个整形的栈
class Stack{
int stck[]=new int[10];
int tos;
//初始化栈顶
Stack(){
tos=-1;
}
//将一个数据压入栈中
void push(int item){
if(tos==9)
System.out.println("栈满了");
else
stck[++tos]=item;
}
//将一个数据从栈中弹出
int pop(){
if(tos<0){
System.out.print("栈下溢");
return 0;
}
else
{
return stck[tos--];
}
}
}
public class TestStack {
public static void main(String[] args) {
Stack mystack1=new Stack();
Stack mystack2=new Stack();
//将一些数值压入栈中
for(int i=0;i<10;i++) mystack1.push(i);
for(int i=10;i<20;i++)mystack2.push(i);
//将这些数值从栈中弹出
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
转载自:https://juejin.cn/post/7235820837005934650