Beginng_Java7(译):发现类和对象(第二章1.2节)(完+1)

第1章通过主要关注从注释到语句的基本语言特性,向您简要介绍了Java语言。 仅使用这些功能,您可以创建简单的应用程序(例如HelloWorld和本章练习中提到的应用程序),这些应用程序让人想起用C等结构化编程语言编写的应用程序。


■注意结构化编程是一种编程范例,它通过数据结构(命名为数据项的聚合),函数(命名的代码块,将代码块返回到调用[将程序执行传递给它们])强制执行程序的逻辑结构,以及 过程(命名的代码块,不向其调用者返回值)。 结构化程序使用序列(一个语句遵循另一个语句),选择/选择(if / switch)和重复/迭代(for / while / do)编程构造; 不鼓励使用可能有害的GOTO声明(请参阅http://en.wikipedia.org/wiki/GOTO)。


结构化程序将数据与行为分开。 这种分离使得对现实世界实体(例如银行账户和员工)进行建模变得困难,并且在程序变得复杂时经常导致维护问题。 相反,类和对象将数据和行为组合到程序实体中; 基于类和对象的程序通常更容易理解和维护。

第2章通过关注对类和对象的支持,深入介绍Java语言。 您首先要学习如何从这些类声明类和创建对象,然后学习如何通过字段和方法将状态和行为封装到这些程序实体中。 在了解了类和对象初始化之后,您将超越这个基于对象的编程模型,并通过探索Java的面向继承和多态的语言特性,深入研究面向对象的编程。

在这一点上,本章介绍了Java更令人困惑的语言特性之一:接口。 您将了解接口是什么,它们与类的关系以及它们如此有用的原因。

Java程序创建占用内存的对象。 为了减少内存不足的可能性,Java虚拟机(JVM)的垃圾收集器偶尔会通过查找不再使用的对象来执行垃圾收集,并删除这些垃圾以释放内存。 第2章最后介绍了垃圾收集过程。

声明类和创建对象

结构化程序创建数据结构,组织和存储数据项,并通过函数和过程操纵存储在这些数据结构中的数据。 结构化程序的基本单元是其数据结构和操作它们的功能或过程。 虽然Java允许您以类似的方式创建应用程序,但这种语言实际上是关于声明类和从这些类创建对象。 这些程序实体是Java程序的基本单元。

本节首先向您展示如何声明一个类,然后向您展示如何在new运算符和构造函数的帮助下从该类创建对象。 然后,该部分将向您展示如何指定构造函数参数和局部变量。 最后,您将学习如何使用用于从类创建对象的相同new运算符创建数组。

声明类

类是制造对象的模板(命名为代码和数据的聚合),也称为类实例,或简称为实例。 类概括了现实世界的实体,而对象是程序级这些实体的特定表现形式。 您可能会认为类是饼干模具和对象,因为饼干模具创建的饼干对象。

因为您无法从不存在的类中实例化对象,所以必须先声明该类。 声明包含一个标题后跟一个主体。 至少,标题包含保留字类,后跟一个标识类的名称(以便可以从源代码中的其他地方引用它)。 主体以一个开放的大括号字符({)开头,以一个紧密的大括号(}结束。 夹在这些分隔符之间的是各种声明。 考虑清单2-1。

清单2-1。 声明模板Image类

class Image
{
// various member declarations
}

清单2-1声明了一个名为Image的类,它可能描述了某种用于在屏幕上显示的图像。 按照惯例,类的名称以大写字母开头。 此外,多字类名称中每个后续单词的首字母大写。 这被称为camelcasing(骆峰表示法)。

使用new 操作符和一个构造函数创建对象

Image是用户定义类型的示例,可以从中创建对象。 您可以通过将new运算符与构造函数一起使用来创建这些对象,如下所示:

Image image = new Image();

new运算符分配内存来存储其类型由new的单独操作数指定的对象,在本例中恰好是Image()。 该对象存储在称为堆的内存区域中。

跟随Image的括号(圆括号)表示构造函数,它是用于通过以某种方式初始化对象来构造对象的代码块。 新的运算符在分配内存以存储对象后立即调用(调用)构造函数。

当构造函数结束时,new返回对象的引用(内存地址或其他标识符),以便可以在程序的其他位置访问它。 关于新创建的Image对象,其引用存储在名为image的变量中,其类型指定为Image。 (通常将变量称为对象,就像在图像对象中一样,尽管它只存储对象的引用而不存储对象本身。)


■注意new的返回引用在源代码中用关键字this表示。 无论出现在何处,它都代表当前对象。 此外,存储引用的变量称为引用变量。


Image没有显式声明构造函数。 当一个类没有声明构造函数时,Java会隐式地为该类创建一个构造函数。 创建的构造函数称为默认的无参数构造函数,因为在调用构造函数时,它的(和)字符之间不会出现任何参数(稍后讨论)。


■注意当声明至少一个构造函数时,Java不会创建默认的无参数构造函数。


指定构造函数参数和局部变量

通过指定类的名称,后跟参数列表,显式声明类的主体内的构造函数,参数列表是由零括号分隔的逗号分隔的零个或多个参数声明列表。 参数是一个构造函数或方法变量,它接收在调用时传递给构造函数或方法的表达式值。 此表达式值称为参数。

清单2-2通过声明三个带有参数列表的构造函数来增强清单2-1的Image类,这些参数列表声明了零个,一个或两个参数; 以及用于测试此类的main()方法。

清单2-2。 使用三个构造函数和一个main()方法声明一个Image类

class Image
{
Image()
{
System.out.println("Image() called");
}
Image(String filename)
{
this(filename, null);
System.out.println("Image(String filename) called");
}
Image(String filename, String imageType)
{
System.out.println("Image(String filename, String imageType) called");
if (filename != null)
{
System.out.println("reading "+filename);
if (imageType != null)
System.out.println("interpreting "+filename+" as storing a "+
imageType+" image");
}
// Perform other initialization here.
}
public static void main(String[] args)
{
Image image = new Image();
System.out.println();
image = new Image("image.png");
System.out.println();
image = new Image("image.png", "PNG");
}
}

清单2-2的Image类首先声明一个无参数构造函数,用于将Image对象初始化为默认值(无论它们是什么)。 此构造函数通过调用System.out.println()来模拟默认初始化,以输出表示已调用它的消息。

Image next声明一个Image(String filename)构造函数,其参数列表由单个参数声明组成 - 参数声明由变量的类型后跟变量的名称组成。 java.lang.String参数名为filename,表示此构造函数从文件中获取图像内容。


■注意在本书的章节中,我通常首先使用预定义类型(如String)作为存储类型的包层次结构的前缀。 例如,String存储在java包的lang子包中。 我这样做是为了帮助您了解存储类型的位置,以便您可以更轻松地指定导入语句以将这些类型导入(无需首先搜索类型的包)到源代码中 - 您不必导入类型 存储在java.lang包中,但我仍然将java.lang包作为完整性的类型名称加前缀。 我将在第3章中详细介绍包和import语句。


一些构造函数依赖于其他构造函数来帮助它们初始化对象。 这样做是为了避免冗余代码,这会增加对象的大小,并且不必要地将内存从可用于其他目的的堆中取出。 例如,Image(String filename)依赖于Image(String filename,String imageType)将文件的图像内容读入内存。

虽然看起来不是这样,但构造函数没有名称(尽管通常通过指定类名和参数列表来引用构造函数)。 构造函数通过使用关键字this和圆括号分隔和逗号分隔的参数列表来调用另一个构造函数。 例如,Image(String filename)执行此操作(filename,null); 执行Image(String filename,String imageType)。


■注意您必须使用它来调用另一个构造函数 - 您不能使用类的名称,如Image()中所示。 this()构造函数调用(如果存在)必须是在构造函数中执行的第一个代码。 此规则阻止您在同一构造函数中指定多个this()构造函数调用。 最后,您不能在方法中指定this() - 构造函数只能由其他构造函数和对象创建期间调用。 (我将在本章后面讨论方法。)


如果存在,构造函数调用必须是构造函数中指定的第一个代码; 否则,编译器报告错误。 因此,调用另一个构造函数的构造函数只能在其他构造函数完成后执行其他工作。 例如,Image(String filename)执行System.out.println(“Image(String filename)”); 在调用的Image(String filename,String imageType)构造函数完成之后。

Image(String filename,String imageType)构造函数声明一个imageType参数,该参数表示存储在文件中的图像类型 - 例如,便携式网络图形(PNG)图像。 据推测,构造函数使用imageType来加速处理,方法是不检查文件的内容以学习图像格式。 当null传递给imageType时,与Image(String filename)构造函数一样,Image(String filename,String imageType)检查文件内容以了解格式。 如果null也传递给filename,Image(String filename,String imageType)将不会读取文件,但可能会通知尝试创建错误条件的Image对象的代码

在声明构造函数之后,清单2-2声明了一个main()方法,该方法允许您创建Image对象并查看输出消息。 main()创建三个Image对象,调用第一个没有参数的构造函数,第二个构造函数使用参数“image.png”,第三个构造函数使用参数“image.png”和“PNG”。


■注意传递给构造函数或方法的参数数量或运算符操作数的数量称为构造函数,方法或运算符的arity。


每个对象的引用都分配给名为image的引用变量,替换先前存储的第二个和第三个对象赋值的引用。 (每次出现System.out.println();都会输出一个空行以使输出更易于阅读。)

main()的存在将Image从一个类更改为一个应用程序。 您通常将main()放在用于创建对象的类中,以便测试这些类。 构建供其他人使用的应用程序时,通常在一个类中声明main(),其中的目的是运行一个应用程序而不是从该类创建一个对象 - 然后该应用程序只从该类运行。 有关示例,请参见第1章的HelloWorld类

将清单2-2保存到Image.java后,通过在命令行执行javac Image.java来编译此文件。 假设没有错误消息,请通过指定java Image来执行应用程序。 您应该观察以下输出:

Image() called
Image(String filename, String imageType) called
reading image.png
Image(String filename) called
Image(String filename, String imageType) called
reading image.png
interpreting image.png as storing a PNG image

第一个输出行表示已调用无参数构造函数。 后续输出行表示已调用第二个和第三个构造函数。

除了声明参数之外,构造函数还可以在其主体内声明变量,以帮助它执行各种任务。 例如,先前呈现的Image(String filename,String imageType)构造函数可能从(假设的)File类创建一个对象,该类提供读取文件内容的方法。 在某些时候,构造函数实例化此类并将实例的引用分配给变量,如下所示:

Image(String filename, String imageType)
{
System.out.println("Image(String filename, String imageType) called");
if (filename != null)
{
System.out.println("reading "+filename);
File file = new File(filename);
// Read file contents into object.
if (imageType != null)
System.out.println("interpreting "+filename+" as storing a "+
imageType+" image");
else
// Inspect image contents to learn image type.
; // Empty statement is used to make if-else syntactically valid.
}
// Perform other initialization here.
}

与filename和imageType参数一样,file是构造函数的本地变量,称为局部变量,以将其与参数区分开来。 尽管所有三个变量都是构造函数的局部变量,但参数和局部变量之间存在两个关键差异:

•filename和imageType参数在构造函数开始执行并存在之前就存在,直到执行离开构造函数。 相反,文件在声明时就存在,并且一直存在,直到声明它的块被终止(通过右括号字符)。 参数或局部变量的此属性称为生存期。

•可以从构造函数中的任何位置访问filename和imageType参数。 相反,只能从声明点到声明它的块的末尾访问文件。 它在声明之前或声明块之后无法访问,但嵌套的子块可以访问局部变量。 参数或局部变量的此属性称为范围。


■注意生命周期和范围(也称为可见性)属性也适用于类,对象和字段(稍后讨论)。 类在加载到内存时就会存在,并且在从内存中卸载时停止存在,通常是在应用程序退出时。 此外,加载的类通常对其他类可见,但情况并非总是如此 - 附录C在介绍类加载器时会有更多关于此问题的说法。


对象的生命周期范围从创建new操作符到垃圾收集器从内存中删除。 其范围取决于各种因素,例如何时将其引用分配给局部变量或字段。 我将在本章后面讨论这些领域。

字段的生命周期取决于它是实例字段还是类字段。 如果该字段属于一个对象,它将在创建对象时生成,并在对象从内存中消失时消失。 如果该字段属于某个类,则该字段在加载类时开始存在,并在从内存中删除该类时消失。 与对象一样,字段的范围取决于各种因素,例如是否声明字段具有私有访问权限 - 您将在本章后面了解私有访问。


局部变量不能与参数具有相同的名称,因为参数始终与局部变量具有相同的范围。 但是,局部变量可以与另一个局部变量具有相同的名称,前提是两个变量都位于不同的范围内(即,在不同的块内)。 例如,您可以指定int x = 1; 在if-else语句的if块中并指定double x = 2.0; 在语句的相应else块中,每个局部变量都是不同的。


■注意构造函数参数,参数和局部变量的讨论也适用于方法参数,参数和局部变量 - 我将在本章后面讨论方法。


使用new运算符创建数组

new运算符还用于在堆中创建对象数组,并且是第1章中介绍的数组初始值设定项的替代方法。


■注意数组是作为特殊Java对象实现的,其只读长度字段包含数组的大小(元素数)。 您将在本章后面了解字段。


创建数组时,请指定new,后跟一个名称,该名称标识存储在数组中的值的类型,后跟一对或多对方括号,表示数组占用的维数。 最左边的方括号必须包含一个指定数组大小的整数表达式(元素数),而其余对包含整数表达式或为空。

例如,您可以使用new来创建一维对象引用数组,如以下示例所示,该示例创建一个可存储十个Image对象引用的一维数组:

Image[] imArray = new Image[10];

当您创建一维数组时,新的零会将每个数组元素的存储位置中的位置零,您在源代码级别将其解释为文字值false,’\ u0000’,0,0L,0.0,0.0F或null( 取决于元素类型)。 在前面的示例中,每个imArray的元素都初始化为null,表示空引用(对无对象的引用)。

创建数组后,需要为其元素分配对象引用。 以下示例通过创建Image对象并将其引用分配给imArray元素来演示此任务:

for (int i = 0; i < imArray.length; i++)
imArray[i] = new Image("image"+i+".png"); // image0.png, image1.png, and so on

“image”+ i +“。png”表达式使用字符串连接运算符(+)将图像与存储在变量i中的整数值的字符串(.png)组合在一起。 生成的字符串将传递给Image的Image(String filename)构造函数。


■注意在循环上下文中使用字符串连接运算符可能会导致创建大量不必要的String对象,具体取决于循环的长度。 当我向您介绍String类时,我将在第4章讨论这个主题。


您还可以使用new来创建基本类型值的数组(例如整数或双精度浮点数)。 例如,假设您要创建一个双精度浮点温度值的二维三行二列数组。 以下示例完成此任务:

double[][] temperatures = new double[3][2];

创建二维数组后,您将需要使用合适的值填充其元素。 下面的示例通过Math.random()将每个温度元素(可以作为temperatures [row] [col]访问)初始化为随机生成的温度值,我将在第4章中解释:

for (int row = 0; row < temperatures.length; row++)
for (int col = 0; col < temperatures[row].length; col++)
temperatures[row][col] = Math.random()*100;

您可以随后使用for循环以表格格式输出这些值,如以下示例所示 - 代码不会尝试将温度值完美对齐
列:

for (int row = 0; row < temperatures.length; row++)
{
for (int col = 0; col < temperatures[row].length; col++)
System.out.print(temperatures[row][col]+" ");
System.out.println();
}

Java提供了一种创建多维数组的替代方法,您可以在其中单独创建每个维度。 例如,要以这种方式通过new创建二维数组,首先创建一维行数组(外部数组),然后创建一维列数组(内部数组),如下所示:

// Create the row array.
double[][] temperatures = new double[3][]; // Note the extra empty pair of brackets.
// Create a column array for each row.
for (int row = 0; row < temperatures.length; row++)
temperatures[row] = new double[2]; // 2 columns per row

这种数组称为不规则数组,因为每行可以有不同数量的列; 数组不是矩形,但是是粗糙的。


■注意创建行数组时,必须在new之后指定一对额外的空括号作为表达式的一部分。 (对于三维数组 - 一维数组表,其中此数组的元素引用行数组 - 您必须指定两对空括号作为新表达式之后的表达式的一部分。)


如果需要,您可以将new与第1章的数组初始化语法结合使用。 例如,Image [] imArray = new Image [] {new Image(“image0.png”),new Image(“image1.png”)}; 创建一对Image对象和一个初始化为Image对象引用的双元素Image数组对象,并将数组的引用赋给imArray。 以这种方式创建数组时,不允许在方括号之间指定整数表达式。 例如,编译器在遇到Image [] imArray = new Image [2] {new Image(“image0.png”),new Image(“image1.png”)};时报告错误。 要更正此错误,请从方括号中删除2。

封装状态和行为

类从模板的角度模拟真实世界的实体; 例如,汽车和储蓄账户。 对象代表特定实体; 例如,John的红色丰田凯美瑞(汽车实例)和Cuifen的储蓄账户余额为2万美元(储蓄账户实例)。

实体有属性,如红色,制造丰田,模型凯美瑞,和平衡两万美元。 实体的属性集合称为其状态。 实体还具有行为,例如打开车门,驾驶汽车,显示燃料消耗,存款,取款和显示帐户余额。

类及其对象通过将状态与行为组合成单个单元来对实体进行建模 - 类抽象状态,而其对象提供具体的状态值。 将状态和行为结合在一起被称为封装。 与结构化编程不同,开发人员专注于通过结构化代码对行为进行建模,并通过存储数据项以便结构化代码进行操作的数据结构来建模状态,使用类和对象的开发人员通过声明封装状态和类的类来专注于模板化实体。 行为,从这些类中实例化具有特定状态值的对象以表示特定实体,并通过其行为与对象进行交互。

本节首先介绍Java用于表示状态的语言功能,然后介绍其用于表示行为的语言功能。 因为某些状态和行为支持类的内部体系结构,并且对于那些想要使用该类的人来说不应该是可见的,所以本节最后介绍了信息隐藏的重要概念。

通过字段表示状态

Java允许您通过字段表示状态,字段是在类的主体内声明的变量。 实体属性通过实例字段描述。 因为Java还支持与类关联而不与对象关联的状态,所以Java提供了类字段来描述此类状态。

首先,您将学习如何声明和访问实例字段,然后学习如何声明和访问类字段。 在发现如何声明只读实例和类字段之后,您将查看从不同上下文访问字段的规则。

声明和访问实例字段

您可以通过最小化指定类型名称,后跟标识字段的标识符,后跟分号字符(;)来声明实例字段。 清单2-3给出了一个Car类,它有三个实例字段声明。

清单2-3。 使用make,model和numDoors实例字段声明Car类

class Car
{
String make;
String model;
int numDoors;
}

清单2-3声明了两个名为make和model的String实例字段。 它还声明了一个名为numDoors的int实例字段。 按照惯例,字段名称以小写字母开头,并且多字段字段名称中每个后续单词的第一个字母大写。

创建对象时,实例字段初始化为默认零值,您在源代码级别将其解释为文字值false,’\ u0000’,0,0L,0.0,0.0F或null(取决于元素类型)。 例如,如果要执行Car car = new Car();,则make和model将初始化为null,numDoors将初始化为0。

您可以使用成员访问运算符(。)为对象的实例字段赋值或读取值; 左操作数指定对象的引用,右操作数指定要访问的实例字段。
清单2-4使用此运算符初始化Car对象的make,model和numDoors实例字段。

class Car
{
String make;
String model;
int numDoors;
public static void main(String[] args)
{
Car car = new Car();
car.make = "Toyota";
car.model = "Camry";
car.numDoors = 4;
}
}

清单2-4给出了一个实例化Car的main()方法。 汽车实例的make实例字段分配了“Toyota”字符串,其模型实例字段分配了“Camry”字符串,其numDoors实例字段分配了整数文字4.(字符串的双引号分隔字符串的字符序列但是 不属于字符串。)

在声明该字段以提供非零默认值时,可以显式初始化实例字段,该值将覆盖默认的零值。 清单2-5演示了这一点。

清单2-5 将Car的numDoors实例字段初始化为默认的非零值:

class Car
{
String make;
String model;
int numDoors = 4;
Car()
{
}
public static void main(String[] args)
{
Car johnDoeCar = new Car();
johnDoeCar.make = "Chevrolet";
johnDoeCar.model = "Volt";
}
}

清单2-5明确地将numDoors初始化为4,因为开发人员已经假设大多数由此类建模的汽车都有四个门。 当Car通过Car()构造函数初始化时,开发人员只需要为那些有四扇门的汽车初始化make和model实例字段。

直接初始化对象的实例字段通常不是一个好主意,您将在我讨论信息隐藏时学习原因(本章稍后部分)。 相反,您应该在类的构造函数中执行此初始化 - 参见清单2-6。

清单2-6 通过构造函数初始化Car的实例字段

class Car
{
String make;
String model;
int numDoors;
Car(String make, String model)
{
this(make, model, 4);
}
Car(String make, String model, int nDoors)
{
this.make = make;
this.model = model;
numDoors = nDoors;
}
public static void main(String[] args)
{
Car myCar = new Car("Toyota", "Camry");
Car yourCar = new Car("Mazda", "RX-8", 2);
}
}

清单2-6的Car类声明了Car(String make,String model)和Car(String make,String model,int nDoors)构造函数。 第一个构造函数允许您指定make和model,而第二个构造函数允许您指定三个实例字段的值。

第一个构造函数执行它(make,model,4); 将其make和model参数的值以及默认值4传递给第二个构造函数。 这样做演示了显式初始化实例字段的替代方法,从代码维护的角度来看,这是优选的。

Car(String make,String model,int numDoors)构造函数演示了关键字this的另一种用法。 具体来说,它演示了一个场景,其中构造函数参数与类的实例字段具有相同的名称。 使用“this。”前缀变量名称会导致Java编译器创建访问实例字段的字节码。 例如,this.make = make; 将make参数的String对象引用赋给此(当前)Car对象的make实例字段。 如果make = make; 相反,它被指定,它将通过赋予自己的make值来完成任何事情; Java编译器可能无法生成代码来执行不必要的分配。 相比之下,numDoors = nDoors不需要“this。”; 赋值,从nDoors参数值初始化numDoors字段。

声明和访问类字段

在许多情况下,实例字段都是您需要的。 但是,无论创建了多少对象,您可能会遇到需要单个字段副本的情况。

例如,假设您要跟踪已创建的Car对象的数量,并将计数器实例字段(初始化为0)引入此类。 您还将代码放在类的构造函数中,在创建对象时将计数器的值增加1。 但是,因为每个对象都有自己的计数器实例字段副本,所以该字段的值永远不会超过1.清单2-7通过在字段声明前加上static关键字来声明计数器是一个类字段来解决这个问题。

清单2-7 将计数器类字段添加到Car

class Car
{
String make;
String model;
int numDoors;
static int counter;
Car(String make, String model)
{
this(make, model, 4);
}
Car(String make, String model, int numDoors)
{
this.make = make;
this.model = model;
this.numDoors = numDoors;
counter++;
}
public static void main(String[] args)
{
Car myCar = new Car("Toyota", "Camry");
Car yourCar = new Car("Mazda", "RX-8", 2);
System.out.println(Car.counter);
}
}

清单2-7的静态前缀意味着只有一个计数器字段的副本,而不是每个对象一个副本。 将类加载到内存中时,类字段将初始化为默认的零值。 例如,计数器初始化为0.(与实例字段一样,您也可以在其声明中为类字段赋值。)每次创建对象时,由于Car中的计数器++表达式,计数器将增加1 (String make,String model,int numDoors)构造函数。

与实例字段不同,类字段通常通过成员访问运算符直接访问。 虽然您可以通过对象引用访问类字段(如在myCar.counter中),但通常使用类的名称来访问类字段,如Car.counter中所示。 (也可以更容易地判断代码正在访问类字段。)


■注意因为main()方法是清单2-7的Car类的成员,所以您可以直接访问计数器,如System.out.println(counter);. 但是,要在另一个类的main()方法的上下文中访问计数器,您必须指定Car.counter。


如果运行清单2-7,您会注意到它输出2,因为已经创建了两个Car对象。

声明只读实例和类字段

之前声明的字段可以写入也可以读取。 但是,您可能希望声明一个只读字段; 例如,一个命名常量值的字段,例如pi(3.14159 …)。 Java允许您通过提供保留字final来完成此任务。

每个对象都接收自己的只读实例字段的副本。 必须初始化此字段,作为字段声明的一部分或在类的构造函数中。 如果在构造函数中初始化,则只读实例字段称为空白final,因为它在构造函数中为其分配了一个值之前没有值。 因为构造函数可能会为每个对象的空白final指定不同的值,所以这些只读变量不是真正的常量。

如果需要一个真常量,它是一个可供所有对象使用的单个只读值,则需要创建一个只读类字段。 您可以通过在该字段的声明中包含保留字static和final来完成此任务。

清单2-8显示了如何声明只读类字段。

清单2-8 在Employee类中声明一个真常量

class Employee
{
final static int RETIREMENT_AGE = 65;
}

清单2-8的RETIREMENT_AGE声明是编译时常量的一个示例。 因为它的值只有一个副本(感谢static关键字),并且因为这个值永远不会改变(多亏了final关键字),编译器可以通过将常量值插入到所有计算中来自由优化编译代码 它被使用了。 代码运行得更快,因为它不必访问只读类字段。

查看字段访问规则

之前的字段访问示例可能看起来令人困惑,因为您有时可以直接指定字段的名称,而您需要在字段名称前加上对象引用或类名称,并在其他时间使用成员访问运算符。 以下规则通过为您提供有关如何从各种上下文访问字段的指导来消除这种混淆:
•从类字段声明的同一类中的任何位置指定类字段的名称。 示例:计数器

•指定类字段的类的名称,后跟成员访问运算符,后跟类外部的类字段的名称。 示例:Car.counter

•在与实例字段声明相同的类中,从任何实例方法,构造函数或实例初始值设定项(稍后讨论)中指定实例字段的名称。 示例:numDoors

•指定一个对象引用,后跟成员访问运算符,后跟任何类方法或类初始化程序(稍后讨论)中与实例字段声明相同的类中的实例字段的名称,或者从类外部指定。 示例:Car car = new Car(); car.numDoors = 2;

虽然后一条规则似乎暗示您可以从类上下文访问实例字段,但事实并非如此。 而是从对象上下文访问该字段。

之前的访问规则并非详尽无遗,因为还有两个需要考虑的字段访问方案:声明一个与实例字段或类字段同名的局部变量(甚至是一个参数)。 在任一情况下,局部变量/参数都被称为阴影(隐藏或掩盖)字段。

如果您发现已声明局部变量或影响字段的参数,则可以重命名局部变量/参数,或者可以使用成员访问运算符和保留字this(实例字段)或类名(类字段) 明确地识别该字段。 例如,清单2-6的Car(String make,String model,int nDoors)构造函数通过指定诸如this.make = make之类的语句来演示后一种解决方案。 区分实例字段和相同的命名参数。

通过方法表示行为

Java允许您通过方法表示行为,这些方法是在类的主体中声明的命名代码块。 实体行为通过实例方法描述。 由于Java还支持与类关联而不与对象关联的行为,因此Java提供了类方法来描述这些类行为。

首先学习如何声明和调用实例方法,然后学习如何创建实例方法调用链。 接下来,您将了解如何声明和调用类方法,遇到有关将参数传递给方法的其他详细信息,以及探索Java的return语句。 在学习如何递归调用方法作为迭代的替代方法以及如何重载方法之后,您将查看从不同上下文调用方法的规则。

声明和调用实例方法

您可以通过最小化指定返回类型名称,后跟命名方法的标识符,后跟参数列表,后跟大括号分隔的主体来声明实例方法。 清单2-9显示了一个带有printDetails()实例方法的Car类。

清单2-9 在Car类中声明printDetails()实例方法

class Car
{
String make;
String model;
int numDoors;
Car(String make, String model)
{
this(make, model, 4);
}
Car(String make, String model, int numDoors)
{
this.make = make;
this.model = model;
this.numDoors = numDoors;
}
void printDetails()
{
System.out.println("Make = "+make);
System.out.println("Model = "+model);
System.out.println("Number of doors = "+numDoors);
System.out.println();
}
public static void main(String[] args)
{
Car myCar = new Car("Toyota", "Camry");
myCar.printDetails();
Car yourCar = new Car("Mazda", "RX-8", 2);
yourCar.printDetails();
}
}

清单2-9声明了一个名为printDetails()的实例方法。 按照惯例,方法的名称以小写字母开头,并且多字方法名称中每个后续单词的第一个字母大写。

方法类似于构造函数,因为它们具有参数列表。 您在调用方法时将参数传递给这些参数。 因为printDetails()不接受参数,所以它的参数列表为空。


■注意方法的名称及其参数的数量,类型和顺序称为其签名。


调用方法时,将执行其正文中的代码。 对于printDetails(),此方法的主体执行一系列System.out.println()方法调用,以输出其make,model和numDoors实例字段的值。

与构造函数不同,方法声明具有返回类型。 返回类型标识方法返回的值的类型(例如,int count()返回32位整数)。 如果方法没有返回值(并且printDetails()没有返回值),则其返回类型将替换为关键字void,如void printDetails()中所示。


■注意构造函数没有返回类型,因为它们无法返回值。 如果构造函数可以返回任意值,那么该值将如何返回? 毕竟,new运算符返回对象的引用,new如何返回构造函数值?


使用成员访问运算符调用方法; 左操作数指定对象的引用,右操作数指定要调用的方法。 例如,myCar.printDetails()和yourCar.printDetails()表达式调用myCar和yourCar对象上的printDetails()实例方法。

编译清单2-9(javac Car.java)并运行此应用程序(java Car)。 您应该观察以下输出,其不同的实例字段值证明printDetails()与对象关联:

Make = Toyota
Model = Camry
Number of doors = 4
Make = Mazda
Model = RX-8
Number of doors = 2

调用实例方法时,Java会将隐藏参数传递给方法(作为参数列表中最左边的参数)。 此参数是对调用该方法的对象的引用,并通过保留字this在源代码级别表示。 每当您尝试访问不是参数名称的实例字段名称时,您不需要在方法中使用“this。”作为实例字段名称的前缀,因为在这种情况下会假定“this。”。

方法 - 调用堆栈

方法调用需要方法调用堆栈(也称为方法调用堆栈)来跟踪执行必须返回的语句。 将方法调用堆栈想象为自助餐厅中一堆干净托盘的模拟 - 从堆叠顶部弹出(移除)干净的托盘,洗碗机将下一个清洁托盘推入(插入)到顶部 堆。
调用方法时,JVM将其参数和第一个语句的地址按照调用的方法执行到方法调用堆栈上。 JVM还为方法的局部变量分配堆栈空间。 当方法返回时,JVM删除局部变量空间,弹出堆栈中的地址和参数,并将执行转移到该地址的语句。

将实例方法调用链接在一起(键式访问)

可以通过成员访问运算符将两个或多个实例方法调用链接在一起,从而产生更紧凑的代码。 要完成实例方法调用链接,您需要以不同的方式重新构建实例方法,如清单2-10所示。

清单2-10。 实现实例方法,以便可以将对这些方法的调用链接在一起

class SavingsAccount
{
int balance;
SavingsAccount deposit(int amount)
{
balance += amount;
return this;
}
SavingsAccount printBalance()
{
System.out.println(balance);
return this;
}
public static void main(String[] args)
{
new SavingsAccount().deposit(1000).printBalance();
}
}

清单2-10显示您必须将类的名称指定为实例方法的返回类型。 deposit()和printBalance()中的每一个都必须将SavingsAccount指定为返回类型。 此外,您必须指定return this; (返回当前对象的引用)作为最后一个语句 - 我稍后讨论return语句。

例如,新的SavingsAccount()。deposit(1000).printBalance(;创建一个SavingsAccount对象,使用返回的SavingsAccount引用来调用SavingsAccount的deposit()实例方法,为储蓄账户增加一千美元(我忽略了分钱) 为方便起见),最后使用deposit()返回的SavingsAccount引用(与SavingsAccount实例相同)来调用SavingsAccount的printBalance()实例方法来输出帐户余额。

声明和调用类方法

在许多情况下,实例方法就是您所需要的。 但是,您可能会遇到需要描述独立于任何对象的行为的情况。

例如,假设您要引入一个实用程序类(由静态[类]方法组成的类),其方法执行各种转换(例如从摄氏度转换为华氏度)。 您不希望从此类创建对象以执行转换。 相反,您只需要调用方法并获取其结果。 清单2-11通过使用一对类方法呈现Conversions类来解决此要求。 无需创建Conversions对象即可调用这些方法。

清单2-11。 具有一对类方法的Conversions实用程序类

class Conversions
{
static double c2f(double degrees)
{
return degrees*9.0/5.0+32;
}
static double f2c(double degrees)
{
return (degrees-32)*5.0/9.0;
}
}

清单2-11的Conversions类声明了c2f()和f2c()方法,用于将摄氏度转换为华氏度,反之亦然,并返回这些转换的结果。 每个方法头(方法签名和其他信息)都以关键字static为前缀,将方法转换为类方法。

要执行类方法,通常在其名称前加上类名。 例如,您可以执行Conversions.c2f(100.0); 找出相当于100摄氏度的华氏温度,和Conversions.f2c(98.6); 发现相当于正常体温的摄氏度。 您不需要实例化Conversions,然后通过该实例调用这些方法,尽管您可以这样做(但这不是好的形式)。


■注意每个应用程序至少有一个类方法。 具体来说,应用程序必须指定public static void main(String [] args)作为应用程序的入口点。 静态保留字使该方法成为类方法。 (我将在本章后面解释保留字。)


因为类方法不是使用引用当前对象的隐藏参数调用的,所以c2f(),f2c()和main()不能访问对象的实例字段或调用其实例方法。 这些类方法只能访问类字段和调用类方法。

将参数传递给方法

方法调用包括传递给方法的(零个或多个)参数列表。 Java通过称为pass-by-value的参数传递方式将参数传递给方法,以下示例演示:

Employee emp = new Employee("John ");
int recommendedAnnualSalaryIncrease = 1000;
printReport(emp, recommendAnnualSalaryIncrease);
printReport(new Employee("Cuifen"), 1500);

按值传递传递变量的值(例如,存储在emp中的参考值或存储在recommendedAnnualSalaryIncrease中的1000值)或某个其他表达式(例如新员工(“Cuifen”)或1500)的值 方法。

由于传值,您不能通过此参数的printReport()参数从printReport()内部为emp分配不同的Employee对象。 毕竟,您只将emp值的副本传递给方法。
许多方法(和构造函数)要求您在调用时传递固定数量的参数。 但是,Java也可以传递可变数量的参数 - 这样的方法/构造函数通常被称为varargs方法/构造函数。 要声明一个采用可变数量参数的方法(或构造函数),请在方法的/ constructor最右边的参数的类型名称后面指定三个连续的句点。 以下示例显示了一个sum()方法,该方法接受可变数量的参数:

double sum(double... values)
{
int total = 0;
for (int i = 0; i < values.length; i++)
total += values[i];
return total;
}

sum()的实现总计传递给此方法的参数数量; 例如,sum(10.0,20.0)或sum(30.0,40.0,50.0)(在幕后,这些参数存储在一维数组中,由values.length和values [i]证明。)在这些值之后 总计,此总数通过return语句返回。

通过返回值声明从方法返回

在不返回值的方法中执行语句(其返回类型设置为void)从第一个语句流向最后一个语句。 但是,Java的return语句允许方法(或构造函数)在到达最后一个语句之前退出。 如清单2 12所示,return语句的这种形式包括保留字返回后跟分号。

清单2-12 使用return语句从方法中提前返回

class Employee
{
String name;
Employee(String name)
{
setName(name);
}
void setName(String name)
{
if (name == null)
{
System.out.println("name cannot be null");
return;
}
else
this.name = name;
}
public static void main(String[] args)
{
Employee john = new Employee(null);
}
}

清单2-12的Employee(String name)构造函数调用setName()实例方法来初始化名称实例字段。 为此目的提供单独的方法是个好主意,因为它允许您在构造时以及稍后的时间初始化实例字段。 (也许员工改变了他或她的名字。)


■注意从同一类中的构造函数或方法调用类的实例或类方法时,仅指定方法的名称。 您不必使用成员访问运算符和对象引用或类名称为方法调用添加前缀。


setName()使用if语句来检测尝试为名称字段分配空引用。 检测到此类尝试时,它会输出“name not not null”错误消息并从方法中提前返回,以便无法分配空值(并替换以前指定的名称)。


■注意事项使用return语句时,可能会遇到编译器报告“无法访问的代码”错误消息的情况。 当它检测到永远不会执行的代码并且不必要地占用内存时,它会这样做。 您可能遇到此问题的一个方面是switch语句。 例如,假设您指定大小写“-v”:printUsageInstructions(); 返回; 打破; 作为本声明的一部分。 编译器在return语句后面检测到break语句时报告错误,因为break语句不可达; 它永远不会被执行。


返回语句的前一种形式在返回值的方法中是不合法的。 对于此类方法,Java提供了返回的备用版本,该方法允许该方法返回一个值(其类型必须与方法的返回类型匹配)。 以下示例演示了此版本:

double divide(double dividend, double divisor)
{
if (divisor == 0.0)
{
System.out.println("cannot divide by zero");
return 0.0;
}
return dividend/divisor;
}

divide()使用if语句检测尝试将其第一个参数除以0.0,并在检测到此尝试时输出错误消息。 此外,它返回0.0表示此尝试。 如果没有问题,则执行除法并返回结果。


■警告您不能在构造函数中使用此形式的return语句,因为构造函数没有返回类型。


递归调用方法

方法通常执行可能包含对其他方法的调用的语句,例如调用System.out.println()的printDetails()。 但是,有时方法调用本身也很方便。 此方案称为递归。

例如,假设您需要编写一个返回阶乘的方法(所有正整数的乘积,包括特定的整数)。 例如,3! (!是阶乘的数学符号)等于3×2×1或6。

编写此方法的第一种方法可能包括以下示例中提供的代码:

int factorial(int n)
{
int product = 1;
for (int i = 2; i <= n; i++)
product *= i;
return product;
}

虽然此代码完成其任务(通过迭代),但也可以根据以下示例的递归样式编写factorial()。

int factorial(int n)
{
if (n == 1)
return 1; // base problem
else
return n*factorial(n-1);
}

递归方法利用能够以更简单的方式表达问题。 根据这个例子,最简单的问题,也称为基本问题,是1!(1)。

当大于1的参数传递给factorial()时,此方法通过使用下一个较小的参数值调用自身将问题分解为更简单的问题。 最终,将达到基本问题。

例如,调用factorial(4)会产生以下表达式堆栈:

4*factorial(3)
3*factorial(2)
2*factorial(1)

最后一个表达式位于堆栈的顶部。 当factorial(1)返回1时,这些表达式在堆栈开始展开时被计算:

• 2*factorial(1) now becomes 2*1 (2)
• 3*factorial(2) now becomes 3*2 (6)
• 4*factorial(3) now becomes 4*6 (24)

递归提供了表达许多问题的优雅方式。 其他示例包括搜索特定值的基于树的数据结构,并且在分层文件系统中,查找并输出包含特定文本的所有文件的名称。


■注意递归会占用堆栈空间,因此请确保递归最终以基本问题结束; 否则,您将耗尽堆栈空间,您的应用程序将被强制终止。


重载方法

Java允许您将具有相同名称但不同参数列表的方法引入同一个类。此功能称为方法重载。 当编译器遇到方法调用表达式时,它会将调用方法的参数列表与每个重载方法的参数列表进行比较,因为它会查找要调用的正确方法。

当参数列表的数量或参数顺序不同时,两个同名的方法会被重载。 例如,Java的String类提供了重载的public int indexOf(int ch)和public int indexOf(int ch,int fromIndex)方法。 这些方法的参数计数不同。 (我在第4章探索String)

当至少一个参数类型不同时,两个同名方法会重载。 例如,Java的java.lang.Math类提供了重载的public static double abs(double a)和public static int abs(int a)方法。 一个方法的参数是double; 另一个方法的参数是一个int。 (我在第4章探索数学)

您不能通过仅更改返回类型来重载方法。 例如,double sum(double … values)和int sum(double … values)不会重载。 这些方法没有重载,因为编译器没有足够的信息来选择在源代码中遇到sum(1.0,2.0)时要调用的方法。

查看方法调用规则

之前的方法调用示例可能看起来很混乱,因为您有时可以直接指定方法的名称,而您需要在方法名称前加上对象引用或类名称,并在其他时间使用成员访问运算符。 以下规则通过为您提供有关如何从各种上下文调用方法的指导来消除这种混淆:

•从类方法的同一类中的任何位置指定类方法的名称。 示例:c2f(37.0);

•指定类方法的类的名称,后跟成员访问运算符,后跟类外部的类方法的名称。 示例:Conversions.c2f(37.0); (您也可以通过对象实例调用类方法,但这被认为是不好的形式,因为它隐藏了随意观察调用类方法的事实。)

•从与实例方法相同的类中的任何实例方法,构造函数或实例初始值设定项中指定实例方法的名称。 示例:setName(name);

•指定对象引用,后跟成员访问运算符,后跟实例方法中与实例方法相同的类中的任何类方法或类初始值设定项的实例方法的名称,或者来自类外部。 示例:Car car = new Car(“Toyota”,“Camry”);car.printDetails();

虽然后一条规则似乎暗示您可以从类上下文中调用实例方法,但事实并非如此。 而是从对象上下文中调用该方法。

此外,不要忘记确保传递给方法的参数数量,以及传递这些参数的顺序,以及这些参数的类型与调用方法中的参数对应项一致。


■注意字段访问和方法调用规则组合在表达式System.out.println();中,其中最左边的成员访问运算符访问java.lang.System类中的out类字段(类型为java.io.PrintStream), 最右边的成员访问操作符调用此字段的println()方法。 您将在第8章和第4章中的系统中了解PrintStream。


隐藏信息

每个类X都公开一个接口(一个由构造函数,方法和[可能]字段组成的协议,这些字段可用于从其他类创建的对象,用于创建和与X的对象通信)。

接口充当类及其客户端之间的单向契约,它是外部构造函数,方法和其他(面向初始化的)类实体(本章稍后讨论),它们通过调用构造函数与类的实例进行通信。 方法,以及访问字段(通常是公共静态最终字段或常量)。 合同是这样的,类承诺不会改变它的接口,这将破坏依赖于接口的客户端。

X还提供了一个实现(公开方法中的代码以及可选的辅助方法和不应公开的可选支持字段),用于编码接口。 辅助方法是辅助暴露方法的方法,不应暴露。

在设计类时,您的目标是在隐藏该接口实现的细节的同时公开有用的接口。 您隐藏实现以防止开发人员意外访问不属于该类接口的类的部分,以便您可以在不破坏客户端代码的情况下自由更改实现。 隐藏实现通常被称为信息隐藏。 此外,许多开发人员认为实现隐藏是封装的一部分。

Java通过提供四级访问控制来支持实现隐藏,其中三个级别通过保留字指示。 您可以使用以下访问控制级别来控制对字段,方法和构造函数的访问,以及其中两个级别来控制对类的访问:

•Public:可以从任何地方访问声明为public的字段,方法或构造函数。 类也可以声明为public。

• Protected:声明受保护的字段,方法或构造函数可以从与成员的类相同的包中的所有类访问,也可以从该类的子类访问,而不管包是什么。 (我将在第3章讨论包。)

•Private:声明为private的字段,方法或构造函数不能从声明它的类之外访问。

•Package-private:如果没有访问控制保留字,则只能在与成员类相同的包中的类访问字段,方法或构造函数。 非公开课也是如此。
缺乏公共,受保护或私有意味着包私有。


■注意声明为public的类必须存储在具有相同名称的文件中。 例如,公共Image类必须存储在Image.java中。 源文件只能声明一个公共类。


您通常会将类的实例字段声明为私有,并提供用于设置和获取其值的特殊公共实例方法。 按照惯例,设置字段值的方法的名称以set开头,称为setter。 类似地,获取字段值的方法的名称带有get(或者,对于布尔字段)前缀,并且称为getter。 清单2-13在Employee类声明的上下文中演示了这种模式。

清单2-13。 从实现中分离接口

public class Employee
{
private String name;
public Employee(String name)
{
setName(name);
}
public void setName(String empName)
{
name = empName; // Assign the empName argument to the name field.
}
public String getName()
{
return name;
}
}

清单2-13显示了一个由公共Employee类,它的公共构造函数和它的公共setter / getter方法组成的接口。 可以从任何地方访问此类和这些成员。 实现由私有名称字段和构造函数/方法代码组成,只能在Employee类中访问。

当你可以简单地省略private并直接访问name字段时,去解决所有这些问题似乎毫无意义。 但是,假设您被告知要引入一个新的构造函数,该构造函数采用单独的名字和姓氏参数以及将该员工的名字和姓氏设置/获取到此类中的新方法。 此外,假设已经确定将比整个名称更频繁地访问名字和姓氏。 清单2-14显示了这些更改。

清单2-14。 在不影响现有接口的情况下修改实现

public class Employee
{
private String firstName;
private String lastName;
public Employee(String name)
{
setName(name);
}
public Employee(String firstName, String lastName)
{
setName(firstName+" "+lastName);
}
public void setName(String name)
{
// Assume that the first and last names are separated by a
// single space character. indexOf() locates a character in a
// string; substring() returns a portion of a string.
setFirstName(name.substring(0, name.indexOf(' ')));
setLastName(name.substring(name.indexOf(' ')+1));
}
public String getName()

{
return getFirstName()+" "+getLastName();
}
public void setFirstName(String empFirstName)
{
firstName = empFirstName;
}
public String getFirstName()
{
return firstName;
}
public void setLastName(String empLastName)
{
lastName = empLastName;
}
public String getLastName()
{
return lastName;
}
}

清单2-14显示名称字段已被删除,有利于新的firstName和lastName字段,这些字段是为了提高性能而添加的。因为setFirstName()和setLastName()将比setName()更频繁地被调用,并且因为getFirstName()和getLastName()将比getName()更频繁地被调用,所以它具有更高性能(在每种情况下)具有第一个两个方法设置/获取firstName和lastName的值,而不是将任何值合并到/从名称的值中提取此值。

清单2-14还显示了setName()调用setFirstName()和setLastName(),以及getName()调用getFirstName()和getLastName(),而不是直接访问firstName和lastName字段。虽然在此示例中不需要避免直接访问这些字段,但想象另一个实现更改,它将更多代码添加到setFirstName(),setLastName(),getFirstName()和getLastName();不调用这些方法将导致新代码不执行。
当Employee的实现从清单2-13所示的更改变为清单2-14所示的时,客户端代码(实例化并使用类的代码,例如Employee)不会中断,因为原始接口保持不变,尽管接口有被延长了。隐藏清单2-13的实现,特别是名称字段,导致这种缺乏破坏。


■注意setName()调用String类的indexOf()和substring()方法。 您将在第4章中了解这些和其他String方法。


Java提供了一些鲜为人知的信息隐藏相关语言特性,它允许一个对象(或类方法/初始化器)访问另一个对象的私有字段或调用其私有方法。 清单2- 15提供了一个演示。

清单2-15。 一个对象访问另一个对象的私有字段

class PrivateAccess
{
private int x;
PrivateAccess(int x)
{
this.x = x;
}
boolean equalTo(PrivateAccess pa)
{
return pa.x == x;
}
public static void main(String[] args)
{
PrivateAccess pa1 = new PrivateAccess(10);
PrivateAccess pa2 = new PrivateAccess(20);
PrivateAccess pa3 = new PrivateAccess(10);
System.out.println("pa1 equal to pa2: "+pa1.equalTo(pa2));
System.out.println("pa2 equal to pa3: "+pa2.equalTo(pa3));
System.out.println("pa1 equal to pa3: "+pa1.equalTo(pa3));
System.out.println(pa2.x);
}
}

清单2-15的PrivateAccess类声明了一个名为x的私有int字段。 它还声明了一个带有PrivateAccess参数的equalTo()方法。 我们的想法是将参数对象与当前对象进行比较,以确定它们是否相等。

通过使用==运算符将参数对象的x实例字段的值与当前对象的x实例字段的值进行比较来进行相等性确定,当它们相同时返回布尔值true。 看起来令人费解的是,Java允许您指定pa.x来访问参数对象的私有实例字段。 此外,main()能够通过pa2对象直接访问x。

我之前介绍了Java的四个访问控制级别,并提供了有关私有访问控制级别的以下声明:“声明为私有的字段,方法或构造函数无法从声明它的类之外访问。”当您仔细阅读时考虑这个语句并检查清单2-15,您将意识到x不是从声明它的PrivateAccess类之外访问的。因此,不会违反专用访问控制级别。

唯一可以访问此私有实例字段的代码是位于PrivateAccess类中的代码。如果您尝试通过在另一个类的上下文中创建的PrivateAccess对象访问x,则编译器将报告错误。

能够从PrivateAccess中直接访问x是一种性能增强;直接访问此实现细节比调用返回其值的方法更快。

编译PrivateAccess.java(javac PrivateAccess.java)并运行应用程序(java PrivateAccess)。您应该观察以下输出:

pa1 equal to pa2: false
pa2 equal to pa3: false
pa1 equal to pa3: true
20

■提示养成在隐藏实现时开发有用接口的习惯,因为它可以在维护类时节省很多麻烦。


猜你喜欢

转载自blog.csdn.net/m0_37696990/article/details/83215469
今日推荐