Java编程实战宝典(光盘内容另行下载,地址见书封底)(txt+pdf+epub+mobi电子书下载)


发布时间:2020-05-18 03:39:21

点击下载

作者:刘新,管磊

出版社:清华大学出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

Java编程实战宝典(光盘内容另行下载,地址见书封底)

Java编程实战宝典(光盘内容另行下载,地址见书封底)试读:

前言

Java是目前最为流行的程序开发语言。市面上介绍Java的书籍很多,既包括国外的经典名著,也包括国内各种各样的教学书籍。国外名著由于知识背景的差异,作者的思维方式总是和中国读者有一定的距离,因此刚入门的读者无法领略其中的精妙。大多数国外书籍,则将Java当作纯粹的语言来介绍,忽视了它作为一个应用平台的强大威力,读者看了之后,难免会误会Java不过是一个精简版的C++。

笔者在多年的教学和开发实践中,深感需要编写一本既能让初学者快速入门,又能真正利用Java进行软件开发的指导性书籍。几年前笔者就萌生了一个想法:亲自编写一本既适合读者自学,又可供教学参考的Java图书。而真正付诸实施,这本书花了笔者近一年的时间。笔者在自己平时所用课件的基础上,进行了大量增改,终于编写出了本书。本书以J2SE为平台,以最新的JDK 1.7技术规范为切入点,由浅入深、循序渐进地介绍了有关J2SE平台下的大部分常用开发技术。书中的每个知识点和技术都采用了实例讲解为主、理论分析为辅的方式进行介绍。

本书假设读者没有任何编程经验,举例时也尽量避免复杂的数据结构和算法设计。每个例子都着重于Java知识点本身,尽量浅显易懂,不涉及其他知识。对于初学者易犯的错误,都有明确的提示。为了让读者养成良好的编程习惯,本书的程序代码均按照软件工程的规范来编写。全书讲解时配合了大量的程序示例、实用程序、图例及代码说明,所有程序代码笔者均仔细调试过,确保准确无误。

本书特色

本书是根据笔者多年的教学和软件开发经验总结出来的,将知识范围锁定在了适合初、中级读者阅读的部分。本书以大量的实例进行示范和解说,其特点主要体现在以下几个方面。

□ 内容全面,涵盖广泛:本书全面涵盖了Java的基础语法、面向对象编程、Java高级技术中的多线程、集合、泛型和RTTI等,而且系统介绍了GUI程序设计、多媒体程序设计、数据库程序设计和网络程序设计等。

□ 技术最新,紧跟趋势:本书以最新的JDK 1.7技术规范为切入点进行讲解,详细介绍了新版本的各种新技术和新功能,让读者了解和掌握最新的Java技术。

□ 由浅入深,循序渐进:本书的编排采用了由浅入深、循序渐进的方式,使得初、中级读者都可以容易地掌握复杂的编程技术。

□ 实例丰富,讲解详细:本书提供了大量的示例和实例,并按照“知识点→例或实例→示例或实例解析→运行效果→贴心提示”的模式讲解,理解起来非常容易。书中给出了这些例子的详细源代码,并对代码进行了详细注释,还对例子的重点和难点进行了详细的讲解和分析。书中的例子简洁规范,能让读者专心于知识点,而不被其他事情所干扰。它们大多具有实际意义,着重于解决工作中的实际问题,可帮助读者理解和上机模拟实践。

□ 案例精讲,注重实战:本书最后用3章的篇幅详细介绍了一个完整的即时通信软件项目案例的设计和实现过程,让读者体验实际的项目开发,提升开发水平。

□ 实践练习,巩固提高:本书各章都提供了实践练习题,读者每阅读完一章,可以通过完成这些练习题来检测自己的学习效果,从而达到巩固和提高的目的。

□ 视频教学,光盘超值:笔者专门录制了大量的配套多媒体教学视频,便于读者更加高效、直观地学习。另外,配书光盘中还赠送了大量的Java开发范例、模块和案例的源程序及教学视频库,并提供了一部《Java程序员面试宝典》电子书。

本书内容安排

本书共22章,分为8篇,不仅包含了Java的基础知识,也对它的高级技术和实用技术做了详细介绍。

第1篇 Java基础知识入门(第1、2章)

本篇首先全面介绍了Java的运行开发环境。其中详细讲解了JDK的安装和配置,如何使用UltraEdit来编辑一个Java源程序,以及如何编译和运行Java程序。第2章介绍了Java的基础知识,包括数据类型、运算符与表达式、流程控制等。最后以几个实例来引导读者步入程序设计的大门。这一篇是整个Java程序设计的基础。

第2篇 对象和类(第3、4章)

本篇介绍了如何使用Java来进行面向对象的程序设计。包括对象和类的成员定义与使用、单继承和多重继承、运行时多态、接口、内部类、包等。本篇是Java的精华,也是学习Java面向对象技术必备的知识。

第3篇 数据处理(第5~7章)

本篇介绍了Java中的数据处理。首先介绍了Java中的两个特殊类:数组和字符串。然后介绍了Java中的异常处理机制。最后介绍了输入和输出,包括标准设备的输入和输出以及文件的处理,还对新版JDK 1.7中有关Java输入与输出新增技术进行了说明。学完本篇,已经可以编写一些实用程序了。

第4篇 Java中的高级技术(第8~13章)

本篇介绍了Java中的高级技术,包括多线程、RTTI、泛型、集合、类型包装以及实用工具类等。这些内容是编写复杂实用程序的基础。使用这些高级技术,可以大大降低编程的烦琐程度和难度。

第5篇 GUI程序设计(第14、15章)

本篇介绍了普通窗口程序和多媒体程序的编写。GUI是目前最为流行的程序界面,但这类程序的编制比普通控制台程序要复杂一些。本篇详细介绍了和GUI有关的事件、布局管理以及各种组件的使用。并通过大量的实例来介绍如何编写一个实用的桌面程序,以及编程中的常用技巧和应该注意的问题。在多媒体程序设计中,则着重介绍了各种文字、图像、声音和视频的处理。在编程中,应尽量使用Java自己提供的类,以降低编程的难度。

第6篇 数据库程序设计(第16、17章)

本篇介绍了数据库程序设计。数据库编程是Java的一个重要应用方面。本篇先介绍一般性的数据库理论,主要是SQL语句的使用。然后详细介绍了如何使用Java中的各种类来处理数据库,并提供了一个实例来说明编写数据库程序与普通程序的一些差别。

第7篇 Java网络程序开发(第18、19章)

本篇介绍了网络程序设计。首先介绍一般的C/S模式的网络程序设计,主要是利用Socket进行网络通信。随后介绍了JSP程序设计,这是Java应用的又一重要领域。另外,本篇提供了5个实例来说明JSP程序设计中应该注意的一些问题。

第8篇 即时通信系统开发项目实战(第20~22章)

本篇讲解了一个以QQ为原型的Java版即时通信系统的应用开发案例,综合使用了Java中的桌面程序设计、图像处理、数据库处理以及网络通信中的各种技术,以及软件工程的思想,对Java应用系统从架构设计、数据设计到编码开发都进行了细致的讲解。最后两章是对Java技术的一个全面应用综合演练。通过这个软件,读者可以领略到Java的强大实用编程能力。因篇幅所限,本篇内容以PDF电子文档的格式收录于本书的配套光盘中。

本书光盘内容

□ 本书各章涉及的实例源文件;

□ 18小时本书配套教学视频;

□ 23小时Java开发实例教学视频;

□ 4个Java项目案例源程序及3小时教学视频;

□ 100页本书第8篇内容的电子书;

□ 355页《Java程序员面试宝典》电子书。

适合阅读本书的读者

□ 想全面学习Java开发技术的人员;

□ 没有任何编程基础的计算机专业的学生;

□ 具备一定自学能力的Java编程爱好者;

□ 利用Swing开发桌面程序的Java程序员;

□ 进行JSP网站开发的人员;

□ 使用C/S模式设计网络程序的Java程序员;

□ 想了解Java中、高级技术的编程人员;

□ 使用Java做开发的工程技术人员和科研人员;

□ 大中专院校Java语言的教学人员;

□ 需要案头必备手册的Java程序员。

本书作者

本书由刘新和管磊主笔编写。其他参与编写的人员有陈小云、陈晓梅、陈欣波、陈智敏、崔杰、戴晟晖、邓福金、董改香、董加强、杜磊、杜友丽、范祥、方家娣、房健、付青、傅志辉、高德明、高雁翔、宫虎波、古超、桂颖、郭刚、郭立峰、郭秋滟、韩德、韩花、韩加国、韩静、韩伟、何海讯、衡友跃、李宁、李锡江、李晓峰、刘建准。

本书的编写对笔者而言是一个“浩大的工程”。虽然笔者投入了大量的精力和时间,但只怕百密难免一疏。若读者在阅读本书时发现任何疏漏,希望能及时反馈给我们,以便及时更正。联系我们请发邮件至bookservice2008@163.com。

最后祝各位读者读书快乐,学习进步!编者第1篇 Java基础知识入门◆ 第1章 Java的开发运行环境◆ 第2章 Java语言基础第1章 Java的开发运行环境

学好Java最重要的一个步骤就是上机编程,熟悉Java的开发运行环境是成为Java程序员的第一步。本章将详细介绍如何安装并配置Oracle公司最新的JDK 1.7 for Windows开发平台,如何编写一个简单的Java程序,如何基于JDK环境编译Java源程序,如何运行编译好的class文件,以及如何避免初学者常犯的错误。通过本章的学习,将轻松地迈入Java的殿堂。

本章的内容要点如下:□ Java运行原理与Java虚拟机;□ Java开发环境;□ Java应用程序的编写;□ 一个简单的Java Applet小程序。1.1 Java运行原理与Java虚拟机

任何一个可执行文件,都必须在某个平台上才能运行。例如,Windows下的exe文件,必须在Windows环境下、X86硬件平台上才能运行。而Java程序也必须在特定的平台上才能运行,这是由Java程序运行的原理及Java虚拟机的本质特性决定的。在Java的世界里,其独特的编译和解释过程,使得Java语言具有了平台无关性,而这些特性的关键在于Java字节码的设计以及运行该字节码的Java虚拟机。本节将带领大家认识一下Java运行的内部原理及支撑其运行的虚拟机平台。1.1.1 Java运行原理简述

在计算机编程领域,几乎所有的编程语言都需要通过编译或者解释才可以通过计算机硬件执行。可是Java与众不同,它同时需要这两个过程。当编写好一个完整的Java源程序后,Java编译程序先将Java源程序翻译为Java 虚拟机可以执行的一种叫做字节码(byte code)的中间代码。然后再由Java平台的解释器将这种字节码文件翻译成本地的机器指令来执行。Java运行原理如图1.1所示。

由图1.1可以看出,Java程序的运行包括源代码编译和字节码解释两个大的环节,Java从源程序到字节码的编译过程与其他程序设计语言存在很大的不同。例如,像C++这样的语言在编译的时候,是与机器的硬件平台信息密不可分的,编译程序通过查表将所有对符号的引用转换为特定的内存偏移量以保证程序运行,并且编译结果是可执行的代码。而Java编译器却不将对变量和方法的引用编译为数值引用,也不确定程序执行过程中的内存布局,而是将这些符号引用信息保留在一种扩展名为.class的字节码文件中。这种文件的最大特点就是不包含硬件的信息,因此这种字节码文件还不能在机器上执行,如果需要执行,还要再由Java的解释器在解释执行字节码的过程中创立内存布局,然后再通过查表来确定每一条指令所在的具体地址。图1.1 Java运行原理图

Java的解释过程也很特别,传统的解释性语言如BASIC在解释执行的时候,是直接将源程序一条一条地通过解释器进行词法分析、语法分析等最终翻译为本地的机器指令,并在CPU上执行。而Java的解释过程是先通过Java虚拟机读取Java字节码文件,Java字节码是一套用来在Java系统下运行时执行的高度优化的指令集,执行该指令集的系统是Java的虚拟机,通过Java虚拟机执行字节码并将其转换成和本地系统硬件相关的本地指令集,并最终在CPU上执行。

以上所描述的就是Java程序运行的基本原理,这种特殊的编译和解释过程,才使得Java语言具有了平台无关性,也正是Java的特色所在。1.1.2 Java虚拟机

在Java代码的执行过程中,Java虚拟机是整个Java平台的核心。Java虚拟机(Java Virtual Machine)简称JVM,是一个想象中的机器,是运行所有Java程序的抽象计算机,是Java语言的运行环境,在实际的计算机上通过软件模拟来实现。Java虚拟机本身是一种用于计算机设备的规范,可用不同的方式(软件或硬件)加以实现。只要根据JVM规格描述将解释器移植到特定的计算机上,就能保证经过编译的任何Java代码能够在该系统上运行。Java虚拟机有自己想象中的硬件,如处理器、堆栈、寄存器等,还具有相应的指令系统。为了让编译产生的字节码可以更好地解释与执行,通常把JVM分成6个功能模块:JVM解释器、指令系统、寄存器、栈、存储区和碎片回收区。□ JVM解释器:JVM解释器负责将字节码转换成为CPU能执行的机器指令。□ 指令系统:指令系统同硬件计算机很相似。一条指令分成操作码和操作数两部分。操作码为8位二进制数,操作数可以根据需要而定。操作码是为了说明一条指令的功能,所以JVM可以有多达256种不同的操作指令。□ 寄存器:JVM有自己的虚拟寄存器,这样就可以快速地和JVM的解释器进行数据交换。为了实现必需的功能,JVM设置了4个常用的32位寄存器:pc(程序计数器)、optop(操作数栈顶指针)、frame(当前执行环境指针)和vars(指向当前执行环境中第一个局部变量的指针)。□ 栈:JVM栈是指令执行时数据和信息存储的场所和控制中心,它提供给JVM解释器运算时所需要的信息。□ 存储区:JVM存储区用于存储编译后的字节码等信息。□ 碎片回收区:JVM碎片回收,是指将那些使用后的Java类的具体实例从内存中进行回收。因此,可以避免开发人员自己编程控制内存的麻烦。随着JVM的不断升级,其碎片回收技术和算法也更加合理。比较经典的算法有引用计数、复制、标记-清除和标记-整理。在JVM 1.4.1版以后,产生了一种代收集技术。简单地说,就是利用对象在程序中生存的时间划分成代,以这个代为标准进行碎片回收。☆说明:JVM的运用,真正让Java实现了“一次编译,处处运行”,它是整个运行系统的核心。1.2 Java的开发环境

要开发Java程序,必须要有一个开发环境。而一提到开发环境,读者可能首先想到的就是那些大名鼎鼎的集成开发工具:Eclipse、JBuilder、Microsoft Visual J++、JCreator Pro、Net Beans、Sun Java Studio、Visual Age for Java、WebLogic Workshop、Visual Cafe for Java和IntelliJ等。但实际上,如此众多的开发工具中,除了Microsoft Visual J++是使用自己的编译器外,其余的大多是使用Sun公司提供的免费的JDK作为编译器,只不过是开发了一个集成环境套在外面,方便程序员编程而已。

这些集成开发工具,虽然方便了程序员开发大型软件,但是它们封装了很多有关JDK(Java Development Kit)的基本使用方法,在某些方面又过于复杂,并不太适合初学者使用。因此,在本书中,将首先介绍最基本的JDK的安装和使用。1.2.1 JDK的安装

JDK,就是Java开发工具包,里面是Java类库和Java的语言规范,同时Java语言的任何改进都会加到其中作为后续版本发布。JDK本身并不是一个像jbuilder这样的开发软件,它不提供具体的开发平台,它所提供的是无论你用何种开发软件写Java程序都必须用到的类库和Java语言规范,没有JDK,你的Java程序根本就不能用。

最主流的JDK是Sun公司发布的JDK,除了Sun之外,还有很多公司和组织都开发了自己的JDK,例如IBM公司开发的JDK,BEA公司的Jrocket,还有GNU组织开发的JDK等等。其中IBM的JDK包含的JVM(Java Virtual Machine)运行效率要比Sun JDK包含的JVM高出许多。而专门运行在x86平台的Jrocket在服务器端运行效率也要比Sun JDK好很多。但不管怎么说,进行基础性的Java开发,先把Sun JDK掌握好就可以了。

JDK最初由Sun公司负责发布,Oracle收购SUN后,由Oracle负责JDK具体业务。它从Alpha 1.0开始,先后经历了JDK 1.0、JDK 1.1……多次升级,目前最新版本是2011年7月份发布的JDK 1.7,当时发布的版本中仅包含Windows、Linux 和Solaris下32位和64位版本。

按照应用平台进行划分,JDK有3个主要成员:可扩展的企业级应用Java 2平台的 J2EE(Java 2 Enterprise Edition)、用于工作站和PC机的Java标准平台的J2SE(Java 2 Standard Edition),以及用于嵌入式消费电子平台的J2ME(Java 2 Micro Edition)。

按照运行的操作系统进行划分,JDK分别有for Windows、for Linux、for Solaris和for MacOS等不同版本。☆说明:本书中使用的是J2SE平台的JDK 1.7 for Windows。由于JDK是向下兼容的,后继版本也可以编译运行本书中的示例程序。

各个版本的JDK安装和配置过程并无多大的差异。下面就以JDK 1.7为例,来介绍它的安装和配置过程。

开始安装前,读者需要到Oracle公司官网或者是其他相关网站下载JDK 1.7 for Windows。JDK 1.7自推出后已有多次升级,目前最新的版本是JDK-7u10,本书使用的就是最新的Update 10版本,在联网的主机上访问http://www.oracle.com,通过页面导航即可定位到JavaSE的下载界面,如图1.2所示。图1.2 JDK网络下载界面

在图1.2所示的界面中,列出的是当前最新的JDK版本,读者也可以根据需要选择其它的版本下载。由于JDK是向后兼容的,为了保证本书的代码都能正确运行,建议下载最新的版本。在图1.2中,选择Windows x86操作系统平台对应的JDK文件,即可将其下载到本地。JDK下载到本地后的文件名为jdk-7u10-windows-i586.exe,这是一个普通的Windows下的可执行文件,可以安装在Windows 2000及其以后所有版本的Windows平台上。本书选择Windows XP SP3作为系统平台,以下所述的就是在Windows XP上安装JDK7的过程。

双击下载到本地的jdk-7u10-windows-i586.exe文件,就可以开始安装了。首先是自解压过程,用户不必干涉,稍作等待,当自解压过程完成后,将出现安装向导界面及具体的安装步骤指引。(1)进入安装向导界面

双击.exe安装文件后,即进入安装向导界面,如图1.3所示,在此界面中,直接单击“下一步”按钮。(2)选择要安装的模块和路径

这是安装中最重要的一步,如图1.4所示。图1.3 JDK 1.7安装引导界面图1.4 选择要安装的模块和路径

其中,“开发工具”是必选的,“源代码”是给开发者做参考的,除非硬盘空间非常紧张,否则最好安装。“公共JRE”是一个独立的Java运行时环境(Java Runtime Environment,JRE),任何应用程序均可使用此JRE。它会向浏览器和系统注册Java插件和Java Web Start。如果不选择此项,IE浏览器可能会无法运行Java编写的Applet程序。

安装路径是默认安装到C:\program files\java\jdk1.7.0_10目录下。如果需要更改此路径,单击“更改”按钮,选择你想要安装的盘符,并填入文件夹名称。本书使用系统默认的安装路径,不做更改。对于更改的安装路径一定要记录好具体的文件位置,因为稍后对JDK进行环境变量配置时,将用到此路径。(3)查看安装进度

在图1.4中配置好后,再单击“下一步”按钮,出现如图1.5所示的对话框,用户可以看到安装的进度。

这个过程不需要用户干涉。直到安装快完毕时,如果用户前面选择了安装公共JRE,则会进入到第(4)步,要求用户选择安装JRE的模块和安装路径。否则,将直接进入第(5)步。(4)选择JRE的安装模块和路径

图1.6显示了安装JRE时的模块选项。其作用主要是用于对欧洲语言的支持。通常情况下,不需要用户修改这些默认选项。默认的安装路径是C:\program files\java\jre7,选择默认,不用修改。直接单击“下一步”按钮即可。图1.5 JDK安装进度图1.6 选择安装JRE的模块和路径(5)结束安装

如果前面一切正常,将出现如图1.7所示的对话框,表示JDK 1.7已经正确安装到机器上。读者只要单击“完成”按钮,则安装过程到此结束。

JDK安装完毕后,在安装路径下有以下几个文件夹,如图1.8所示。图1.7 安装完成图1.8 JDK安装完成后目录结构□ bin文件夹:存放编程所要用到的开发工具,包括编译器、解释执行程序、小应用程序浏览器、调试器、文档生成工具和反编译器等。□ db文件夹:存放JDK自带的小型数据库文件。□ include文件夹:存放本地文件(Native means)。□ jre文件夹:Java运行时环境的根目录,存放JVM所需的各种文件。□ lib文件夹:存放库文件。□ src.zip文件:是JDK的源文件压缩包,在实际开发过程中,可以将系统诸多类库与源代码进行关联,对于理解程序内部原理,尤其是代码深度调试非常有用。☆注意:和一般的Windows程序不同,JDK安装成功后,不会在“开始”菜单和桌面生成快捷方式。这是因为bin文件夹下面的可执行程序都不是图形界面的,它们必须在控制台中以命令行方式运行。另外,还需要用户手工配置一些环境变量才能正常使用JDK。1.2.2 如何设置系统环境变量

环境变量是包含关于系统及当前登录用户的环境信息的字符串,一些程序使用此信息确定在何处放置和搜索文件。和JDK相关的环境变量有3个:Java_home、path和classpath。其中,Java_home是JDK安装的目录路径,用来定义path和classpath的相关位置,path环境变量告诉操作系统到哪里去查找JDK工具,classpath环境变量则告诉JDK工具到哪里去查找类文件(.class文件)。下面分别介绍在Windows 2003之前和之后版本的操作系统,其环境变量的配置方式。

1. 在Windows 2003版本之前的操作系统中设置环境变量

所有的操作系统设置环境变量的内容和原理都是一样的,只是不同的操作系统操作方式略有区别。Windows 2003以前的操作系统主要有Windows 2000和Windows XP(包含Windows 2003),在这些系统中设置环境变量的方法是完全一样的,过程如下。(1)右击“我的电脑”,在弹出的下拉列表中选择“属性”,接着在弹出的对话框中选择“高级”标签,然后在此界面中单击“环境变量”按钮,就会弹出如图1.9所示的环境变量设置对话框。

环境变量的设置过程中,有3个参数需要配置,分别为:CLASSPATH、Java_HOME和Path,Path变量是Windows系统本来就有的,无需新建,只需添加相应的值即可。而CLASSPATH和Java_HOME变量,则需新建变量名后再设置相应的值。(2)在图1.9所示的环境变量设置界面中,上半部分显示是用户变量,以当前登录主机的用户名来标识,只对当前用户有效,下半部分是系统变量,所设置的环境变量值对所有用户都有效。如果你希望所有用户都能使用,就在系统变量下单击“新建”按钮,在变量名中填入Java_HOME,变量值填入“JDK的安装目录”,如图1.10所示。图1.9 Windows下变量设置界面图1.10 Java_HOME的变量值设置界面☆注意:Java_HOME是JDK的安装目录,许多依赖JDK的开发环境都靠它来定位JDK,所以必须保证正确无误。本书中JDK的安装目录就是系统默认的目录,即C:\Program Files\Java\jdk1.7.0_10。(3)设置完Java_HOME变量的值以后,接着设置Path变量的值,找到系统变量Path,单击“编辑”按钮,在显示的“变量值”输入框中,不要改动原有的设置值,只是在值的最后附加上JDK和JRE的可执行文件的所在目录即可,附加的变量值为:%Java_HOME%\bin;%Java_HOME%\jre\bin;

将此值附加到Path中的时候,多个值之间要以分号“;”隔开,而且分号要在英文状态下输入,如图1.11所示。☆注意:如果系统安装了多个Java虚拟机,比如安装了Oracle数据库就有自带的JDK 1.4,此时就必须把当前的JDK 1.7的路径放在其他JVM的前面,否则JDK在通过环境变量去寻找类路径时,默认选择最前面的路径,从而分引发一些版本错误。(4)最后一个需要设置的环境变量是CLASSPATH,Java虚拟机在运行的时候,会根据CLASSPATH的设定的值来搜索class字节码文件所在目录,但这不是必须的,可以在运行Java程序时显式地指定CLASSPATH。比如在Eclipse中运行写好的Java程序时,它会自动设定CLASSPATH,但是为了在控制台能方便地运行Java程序,建议最好还是设置一个CLASSPATH。

设置CLASSPATH,需要在系统变量里新建一个名为CLASSPATH的变量名,再将JDK的一些常用包、类库所在的路径设置为它的值即可。根据本书JDK的安装路径,CLASSPATH的值为:.;%Java_HOME%\lib;%Java_HOME%\lib\tools.jar;%Java_HOME%\lib\dt.jar;

设置方法如图1.12所示。图1.11 PATH变量的设置界面图1.12 CLASSPATH变量的设置界面☆注意:CLASSPATH值中,有多个值的要以分号“;”隔开,其中有一个值为“.”,它是一个点“.”代表当前目录的意思。用惯了Windows的用户可能会以为Java虚拟机在搜索时会搜索当前目录,其实不会,这是UNIX中的习惯,出于安全考虑。许多初学Java的朋友兴冲冲地照着书上写好了Helloworld程序,运行时却弹出java.lang.NoClassDefFoundError,其实就是没有设置好CLASSPATH,只要添加一个当前目录“.”就可以了。

按以上的步骤操作完毕后,整个Java的环境变量也就设置完成了。

2. 在Windows 2003版本以后的操作系统中设置环境变量

Windows 2003以后的操作系统,主要是Windows 7、Windows 2008及最新的Windows 8系统,这些操作系统中配置环境变量的方式与Window XP下类似,只是选择的界面有所不同。具体操作方式是:在桌面右击“计算机”,选择“属性”命令,弹出如图1.13所示的窗体。图1.13 Windows 7系统中环境变配置

通过选择“开始”→“控制面板”→“系统”命令,也可以进入到如图1.13所示的界面。在界面中,左侧单击“高级系统设置”选项,此时将弹出“系统属性”窗体,选择“高级”选项卡,单击“环境变量(N)…”按钮,弹出环境变量设置窗体。具体的配置过程与上述Windows XP系统中配置的过程完全一样,这里不再赘述。☆注意:对于较老式的操作系统,如Windows 9.X系列,没有可视化的配置界面,设置环境变量要修改autoexec.bat文件。但是笔者不建议读者在此类操作系统上进行Java的学习和开发。1.2.3 JDK安装后的测试与验证

JDK安装完成后,还需要对其进行简单的测试,以验证本机的JDK是否正确地安装、环境变量配置是否正确。具体操作方法是:在主机窗口中,单击“开始”→“运行”命令,键入cmd,打开命令行界面。输入java-version,如能出现Java版本的提示信息,如图1.14所示的界面,说明安装配置正确。

只有正确地安装JDK并正确地配置系统环境变量,才能正常使用JDK。图1.14 JDK的测试界面1.2.4 编译命令的使用

JDK中所有的命令都集中在安装目录的bin文件夹下面,而且都是控制台程序,要以命令行的方式运行。JDK所提供的开发工具主要有编译程序、解释执行程序、调试程序、Applet执行程序、文档管理程序和包管理程序等。JDK工具包中,最常用的两个命令是javac命令和java命令。

javac.exe是JDK的编译程序,在命令行上执行javac命令可以将Java源程序编译成字节码,生成与类同名但后缀名为.class的文件。编译器会把.class文件放在和Java源文件相同的一个文件夹里,除非用了-d选项。如果引用到某些自己定义的类,必须指明它们的存放路径,这就需要利用环境变量参数classpath。注意,它总是将系统类的目录默认地加在classpath后面,除非用-classpath选项来编译。javac的一般用法如下:javac[-选项]file.java...

在主机窗口中,单击“开始”→“运行”命令,键入cmd,打开命令行界面。输入javac命令,出现如图1.15所示的命令提示信息。图1.15 Javac命令参数表

图1.15中列出了javac命令的参数列表及用法说明,javac中的编译选项及其含义如表1.1所示。表1.1 javac命令中的编译选项及其含义

虽然javac的选项众多,但对于初学者而言,并不需要一开始就掌握这些选项的用法,只需要掌握一个最简单的用法就可以了。比如生成一个hello.java文件,要执行它,只需在命令行输入:c:\>javac hello.java

这里的hello.java是准备编译的文件名,这里必须将文件名完整输入,不能省略后缀名。如果编译成功,它不会有任何提示,因为Java遵循的原则是“没有消息就是好消息”,并且会在hello.java所在的同一文件夹下生成一个或多个.class文件。☆注意:这个class文件的主文件名并不一定和hello.java的主文件同名,它的名称会和源程序中定义的类的名字相同。

编译成功后,下一步就是运行这个class文件,这需要用到解释执行命令。1.2.5 解释执行命令的使用

JDK的解释执行程序是java.exe,该程序将编译好的class加载到内存,然后调用JVM来执行它。它有两种用法:执行一个class文件: java[-选项]class[参数...]执行一个jar文件: java[-选项]-jar jarfile[参数...]

关于jar文件,将在4.8节介绍。注意上面命令中的[参数...],表示要传递给执行文件的参数,称为“命令行参数”,它的详细用法将在3.8节介绍。同样在命令行界面中输入java命令,出现如图1.16所示的命令提示信息。图1.16 java命令参数表

图1.16中列出了java命令的参数列表及用法说明,java命令中的选项及其含义如表1.2所示。表1.2 java命令中的选项及其含义

与javac相同,初学者只要掌握最简单的用法就可以了。比如上例中,生成了一个hello.class的文件。要执行它,只需要在命令行输入:c:\>java hello☆注意:java命令是区分大小写的。大小写不同,表示不同的文件。所以文件名hello一定不能写成Hello或HELLO等。还有,文件的后缀.class也不能要,只要主文件名就可以了。具体原因,将在4.7节中说明。

限于篇幅,不能一一介绍JDK中所有的命令。读者如果想要详细了解这些命令的使用,可以查阅Sun公司发布的JDK 5.0 Documentations中的JDK Tools and Utilities部分。也可以直接在命令行输入想要执行的程序名,可以看到一个简要的帮助。1.2.6 UltraEdit的使用

Java的源程序必须以纯文本文件的形式编辑和保存。而在JDK中,并没有提供文本编辑器。用户编辑源程序时,需要自行选择文本编辑器。最简单的纯文本编辑器是Windows自带的记事本。但是记事本不仅功能太弱,而且在作为Java的源程序编辑器时,存盘时特别容易出错,如图1.17所示。图1.17 错误地用记事本保存源文件☆注意:记事本的默认类型是文本文档。即使用户像图1.17中那样输入文件名为first.java,则保存之后,它的文件名仍然会变成first.java.txt,编译时将找不到源文件。这里必须在“保存类型”下拉列表框中选择“所有文件”,如图1.18所示。图1.18 正确地用记事本保存源文件

正因为记事本存在诸多不足,所以笔者推荐使用功能更为方便的文本编辑器—— UltraEdit作为学习Java过程中的编辑工具。UltraEdit是Windows下功能最强大的纯文本编辑器,读者可以通过网络下载得到,有英文版和汉化版两个版本。作为源程序编辑器时,它有3个很有特色的功能:□ 支持语法高亮度显示(也就是Java等语言的关键字用不同的颜色显示出来)。□ 可以执行DOS命令。□ 可同时编辑多个文件,且每个文件大小不限。但在某一时刻,只有最前面的文件为活动文件。

有了这几个功能,用户可以将UltraEdit搭建为一个简单的集成编程环境,所以很多程序员将它作为小程序开发工具。当然也可以将UltraEdit作为开发Java程序的集成环境,下面来看看一些使用UltraEdit的关键步骤。

1. 新建和编辑源程序

启动UltraEdit后,它会自动建立一个空白文档,也可以选择菜单中的File→New命令来新建一个空白文档。用户可以在此编辑自己的Java程序,编辑时的各种操作和记事本的使用完全相同,如图1.19所示。图1.19 在空白文档中编辑一个源程序☆注意:图1.19所示文档上部的标签,显示为“Edit1*”,说明该文件还从来没有命名保存过。

2. 保存源程序

文档编辑后,需要保存。这要选择菜单中的File→Save命令,将出现如图1.20所示的对话框。☆注意:图1.20中,选择的保存类型是Java Files,这样文件名可以只要主文件名,而无需扩展名。如果没有选择保存类型为Java Files,而是其他任意类型,则文件名必须是“主文件名.java”的形式。

保存成功后,就可以看到源程序中各种关键字、数字和字符串等都以不同的颜色显示,这就是UltraEdit的“语法高亮度”功能。图1.20 保存源程序

3. 编译源程序

要编译源程序,不必再运行cmd转到DOS窗口,可以直接在UltraEdit中编译。方法是选择菜单中的Advance→Dos Command命令,出现如图1.21所示的对话框,在其中填入所需要的编译命令。图1.21 编译源程序

注意,图1.21中的Command下拉列表框中,填入的是javac%f。javac是前面介绍过的编译命令;%f是UltraEdit自己定义的一个宏代换变量,表示当前正在编辑的文件全名。例如,前面保存的文件名为HelloWorldApp.java,那么UltraEdit就会把javac%f代换为javac HelloWorldApp.java,这正是JDK的编译命令。Working Directory下拉列表框中,填入的是D:\javabook\example\chapter,这是当前文件存放的位置。笔者建议读者在学习过程中,将编制的所有Java源程序都集中存放在一个文件夹下,这样便于管理。

填好这两个下拉列表框之后,单击OK按钮,UltraEdit就会执行Command框中的命令。而且会自动保存这些命令,以后用户无需再重新填写,只要调出该对话框就可以重复执行上面的命令。

执行编译命令后,UltraEdit会接收javac返回的信息,并显示在一个名为Command Output×的文档中供用户查看。如果该文档为空,表示编译已经成功,用户可以按Ctrl+F4组合键将该文档关闭。否则就需要修改源程序,直到编译通过为止。

4. 解释执行程序

源程序编译通过后,就可以执行编译好的class文件,这同样可以利用UltraEdit来执行。方法是单击菜单中的Advance→Dos Command命令,出现如图1.22所示的对话框,在其中填入所需要的执行命令。图1.22 执行程序

图1.22中的Command下拉列表框中,填入的是java%n。java是前面介绍过的解释执行命令;%n是UltraEdit定义的另一个宏代换变量,表示当前正在编辑的主文件名。比如,前面保存的文件名为HelloWorldApp.java,编译生成后的class文件为HelloWorldApp.class,那么UltraEdit就会把java%n代换为java HelloWorldApp,这正是JDK的解释执行命令。另外一个关于Working Directory下拉列表框中的内容无需改变。单击OK按钮,程序就会被执行。

程序执行后输出的结果,UltraEdit同样会将它接收后显示在一个名为Command Output×的文档中供用户查看。☆注意:上面所述的第(3)、(4)步操作中,准备编译或执行的文件必须是当前活动文件。如果不是,只要简单地单击文件对应的标签,就可将它设置为活动文件。以上讲解的时候,是以英文版的UltraEdit为标准。如果读者下载的版本是汉化版,操作步骤也是完全相同的,只是上述这些图片中显示的信息为对应的汉语。1.3 Java应用程序示例

Java的程序有两类:一类是只能嵌入在网页文件中,通过浏览器运行的程序,被称为Applet,译为小程序;除此之外的Java程序,都被称为Application,译为应用程序。有了前面的基础,就可以开始编制自己的第一个程序了。本节介绍一个最简单的应用程序的编制。【例1.1】 编程输出字符串:Hello World!

//----------文件名HelloWorldApp.java,程序编号1.1-------------□ 程序中,首先用保留字class来声明一个新的类,其类名为HelloWorldApp,该类是一个公共类(public)。整个类的定义由大括号 {}括起来。□ 在该类中定义了一个main()方法,其中,public表示访问权限,表明所有的类都可以使用本方法。□ static指明该方法是一个类方法(又称静态方法),它可以通过类名直接调用。□ void指明main()方法不返回任何值。对于一个应用程序来说,main()方法是必需的,而且必须按照如上的格式来定义。Jave解释器以main()为入口来执行程序,main()方法定义中,括号中的String args[]是传递给main()方法的参数。□ 在main()方法的实现(大括号)中,只有一条语句:System.out.println ("Hello World!");,它用来实现字符串“Hello World!”的输出。□ System.out.println()方法是最常用的输出方法,括号内的参数一般是一个字符串,也可以是后面要介绍的各种数据类型。如果有多个输出项目,用“+”将它们连接起来。

该源程序可以用前面介绍的记事本或UltraEdit来编辑。☆注意:Java源程序是区分大小写的,例如该程序中的String和System两个单词都必须以大写字母开头,请不要输错,否则编译将无法通过。存盘的时候,文件名也是区分大小写的。Java规定,如果类前面用public来修饰,那么文件名必须和类名完全相同。笔者建议无论类前面是否有public修饰,文件名也应与类名相同,而且在一个源程序中,只定义一个类。这么做不仅便于编程时使用UltraEdit来编译运行,也便于以后对源程序进行修改和维护。

像上面这个程序1.1,它的类名为HelloWorldApp,笔者就将源文件命名为HelloWorldApp.java,并保存在D:\javabook\example\chapter1下面(后面如果不做特殊说明,所有Java源程序和生成的class文件都存放example目录对应章节的文件夹下)。

存盘之后,就可以编译运行程序了。如果使用记事本,则需要经过如下几个步骤。(1)运行cmd命令,进入到DOS窗口。(2)执行DOS命令,进入D盘。C:\Documents and Settings>D:(3)进入到D:\javabook\example\chapter1文件夹下面。D:\>cd javabook\example\chapter1(4)使用编译命令编译源程序。D:\javabook\example\chapter1>javac HelloWorldApp.java

如果有错误提示,请仔细检查源程序编辑是否正确。初学者很容易犯一些极不起眼的小错误。例如,漏写一个分号,写错一个字母,都会导致编译失败。如果没有提示,则表示编译成功。编译生成的是一个名为HelloWorldApp.class的文件。这里的主文件名HelloWorldApp,是根据源程序中类的名字HelloWorldApp生成的,与HelloWorldApp.java的文件名完全没有关系,读者可以用dir命令或是资源管理器来查看是否生成了该文件。(5)执行java命令,运行程序。D:\javabook\example\chapter1>java HelloWorldApp☆注意:HelloWorldApp的大小写不能错。(6)如果一切顺利,将在屏幕上显示一行字符串:Hello World!(7)如果没有上面这行字符串,而是这样一行提示:Exception in thread "main" java.lang.NoClass-DefFoundError:HelloWorldApp

通常是因为环境变量设置错误,或者是输入java命令时大小写弄错了,也有少部分是由于源程序中的main()方法的名字或者是参数写错了,请读者仔细检查。

以上所述的步骤,就是编制一个程序的一般过程。其中有多步可能会出错,需要反复修改源程序。越是复杂的程序,修改源程序的次数就越多,这也是程序员积累编程经验的过程。整个程序编制的过程如图1.23所示。

如果使用UltraEdit,则编译和运行步骤不必使用命令行方式,而可以像1.2节中图1.21和图1.22介绍的那样,通过对话框来编译和运行,而且不用考虑文件名的大小写问题,这样可以大大提高编程的效率。1.4 Java Applet程序示例

Applet程序只能嵌入HTML网页中通过浏览器来运行。HTML是Hyper Text Markup Language的缩写,它是浏览器的通用语言。HTML是由纯文本字符组成的,其中有各种各样预定义的标签以及用于图1.23 Java程序编写流程显示的文本。浏览器根据这些标签的意义,将文本按照一定的格式显示出来,这就是平常看到的网页。一个HTML结构一般具有如下形式:

可以看到,HTML标签多数是成对出现的,如…。Applet程序就嵌入在这些标签中间。当然,它要用到自己特制的标签。下面来编制一个简单的Applet程序。【例1.2】 第一个Java Applet程序。

//----------文件名firstApplet.java,程序编号1.2-------------□ 这个程序中没有main()方法,取而代之的是paint()方法,这个方法会被浏览器自动调用。这是Applet和Application程序的根本区别。□ drawString()方法用于输出信息。□ import关键字表示要引入某个包。□ extends Applet表示本类是Applet的子类。

源程序编制完成后,要保存为firstApplet.java文件,并按照1.3小节介绍的方法编译。然后再编写下面这个HTML文件。

//----------文件名firstApplet.htm----------------------

注意,其中引号中的内容就是刚才生成的class文件名,这里就是firstApplet.class。将这段HTML代码文件保存为firstApplet.htm,把它和firstApplet.class文件保存在同一个文件夹下,代码的编写过程就完成了。

现在可以用IE浏览器打开firstApplet.htm文件,如果浏览器安装了JVM,就可以看到显示的效果。如果没有安装JVM,则可以使用JDK提供的工具AppletViewer。在DOS窗口中输入:D:\javabook\example\chapter1>javac firstApplet.javaD:\javabook\example\chapter1>appletviewerfirstApplet.htm

运行结果如图1.24所示。图1.24 Applet运行结果

最初SUN公司设计Applet是为了增强网页的表现能力和与用户的交互能力。早期的Web网页几乎全是静止的图片和文字,也无法和用户交互。而Applet中可以显示动画、播放声音,拥有各种控件,可以接收用户输入的数据,执行用户的指令。Applet一经推出,凭借Sun公司网站上一杯热气腾腾的咖啡,一夜之间名声大噪,Java也随即走红。

不过随着技术的进步,浏览器中很快就出现了JavaScript/VBScript这样既易于编写,功能也不错的脚本语言,它们完全可以和用户进行交互。微软也很快在万维网浏览器中实现了ActiveX技术,它几乎拥有Applet所有的功能。凭借万维网浏览器在市场上的垄断地位,ActiveX迅速占领了绝大多数市场。微软决定从IE 5.0起,不再捆绑JVM,如果用户需要,必须另外下载JVM,这极大地限制了Applet的应用。目前已经很少有网页中再嵌入Applet,它的用途已经不大,所以本书中的绝大多数例子都以Application程序形式提供。1.5 本章小结

本章介绍了Java的入门知识,主要是编辑器和编译器的使用。这里使用的是命令行方式来编译程序,更有助于程序员对于编译器工作状态的掌握。由于这里没有介绍集成开发环境,所以读者可能会觉得有点难。特别是在刚刚开始编译运行的时候,可能会多次出现错误,这多数是由环境变量配置错误造成的,需要读者耐心地找出错误。1.6 实战习题1. Java语言的执行过程是什么?2. Java虚拟机的特点是什么,它是如何执行Java字节码程序的?3. 说说开发与运行Java程序的基本步骤。4. Java源程序的命名规则是什么?5. 如何区分Java应用程序和Java Applet小程序?6. 熟悉UltraEdit的安装和使用方法,试着用UltraEdit编写一个可运

行的Java程序,并通过命令行的方式执行。第2章 Java语言基础

本章主要介绍Java语言的基础知识,包括Java语言的特点、程序结构、数据类型和流程控制语句等。熟练掌握这些基础知识,是运用Java语言编写程序的前提条件。

曾经学习过C语言的读者会发现,Java语言的数据类型及流程控制语句和C语言非常相像。对于这部分读者,本章只需要简单浏览一遍,重点注意Java语言与C语言的区别。如果读者没有任何程序设计的经验,则需要仔细阅读本章,最好能将本章结尾部分的程序全部上机调试出来,通过上机编程来掌握Java语言的基本的语法知识。为了让读者能够集中精力学习基本语法,本章所有的例子尽量浅显易懂,没有涉及任何复杂的算法。本章将从以下几个方面来讲解Java语言的基础知识:□ Java语言的特点;□ Java的程序构成;□ Java数据类型;□ 运算符与表达式;□ 流程控制;□ Java编码风格;□ Java实例练习。2.1 Java语言的关键特性

Java语言在1995年正式诞生,不到10年时间,就一举超越C/C++,成为使用者最多的编程语言,这样的发展速度,令人惊叹不已。

Java能取得这样的成功,并不是偶然的。它的设计者充分吸取了现存语言的优点,将各种语言的长处集于一身。有人评价说,Java没有哪一项技术是自己独创的,但它的设计却是最为先进的。具体来说,Java有这样一些特点:平台无关性、面向对象、分布式、健壮性、安全、高性能、多线程以及动态性等。

1. 平台无关性(可移植性)

平台无关性是指用Java写的应用程序不用修改就可在不同的软硬件平台上运行。平台无关有两种:源代码级和目标代码级。C和C++具有一定程度的源代码级平台无关,用C或C++写的应用程序不用修改,只需重新编译就可以在不同的平台上运行。Java的跨平台性是目标代码级的,它通过JVM实现了“一次编译,处处运行”。例如,在Windows下编译生成的目标代码可以毫无阻碍地运行在Linux/Unix平台上。

Java的平台无关性具有深远的意义。它使得编程人员所梦寐以求的事情(开发一次软件在任意平台上运行)变成事实,这大大加快和促进了软件产品的开发。平台无关性使Java程序可以方便地被移植到网络上的不同机器,使网络计算成为了现实。

2. 面向对象

Java是一种完全面向对象的程序设计语言。它除了数值、布尔和字符3种基本类型之外,其他类型都是对象,完全摒弃了非面向对象的特性。Java语言的设计集中于对象及其接口,它提供了简单的类机制以及动态的接口模型。对象中封装了它的状态变量以及相应的方法,实现了模块化和信息隐藏。而类则提供了一类对象的原型,并且通过继承机制,子类可以使用父类所提供的方法,实现了代码的复用。因此,大大提高了程序开发的效率。

3. 分布性

分布式包括数据分布和操作分布。数据分布是指数据可以分散在网络的不同主机上,操作分布是指把一个计算分散在不同主机上处理。

Java支持WWW客户机/服务器计算模式,因此,它支持这两种分布性。对于数据分布,Java提供了一个叫作URL的对象,利用这个对象,可以打开并访问具有相同URL地址上的对象,访问方式与访问本地文件系统相同;对于操作分布,Java的Applet小程序可以从服务器下载到客户端,即部分计算在客户端进行,提高系统执行效率。

Java提供了一整套网络类库,封装了Internet上的各种协议,开发人员可以利用类库进行网络程序设计,方便地实现Java的分布式特性。

4. 健壮性

Java最初设计的目的是应用于电子类消费产品,因此要求其具有较高的可靠性。Java虽然源于C++,但它消除了许多C++的不可靠因素,可以避免许多编程错误。

Java是强类型的语言,要求用显式的方法声明,这保证了编译器可以发现方法调用错误,使程序更加可靠。

Java不支持指针,这杜绝了内存的非法访问。

Java解释器在运行时实施检查,可以发现数组和字符串访问的越界,解决了令C/C++程序员极为头痛的越界问题。

Java提供自动垃圾收集来进行内存管理,避免程序员在管理内存时容易产生的错误。Java提供集成的面向对象的异常处理机制。在编译时,Java提示可能出现但未被处理的异常,帮助程序员正确地进行选择以防止系统的崩溃。

5. 安全性

由于Java主要用于网络应用程序开发,因此对安全性有较高的要求。如果没有安全保证,用户从网络下载程序并执行就非常危险。Java通过自己的安全机制防止了病毒程序的产生和下载程序对本地系统的威胁破坏。当Java字节码进入解释器时,首先必须经过字节码校验器的检查。然后,Java解释器将决定程序中类的内存布局。随后,类装载器负责把来自网络的类装载到单独的内存区域,避免应用程序之间的相互干扰破坏。最后,客户端用户还可以限制从网络上装载的类只能访问某些文件系统。上述几种机制结合起来,使得Java成为安全的编程语言。

6. 简单性

Java语言学习起来很简单。它的设计思想是通过提供最基本的方法来完成指定的任务,只需理解一些基本的概念,就可以用它编写出适合于各种情况的应用程序。Java省略了运算符重载、多重继承等模糊的概念,并且通过实现自动垃圾收集,大大简化了程序设计者的内存管理工作。

另外,Java也适合于在低档机器上运行。它的基本解释器及类的支持只有40KB左右,加上标准类库和线程的支持也只有215KB左右。因此,Java适合用于各种嵌入式设备。

7. 高性能

和其他解释执行的语言(如BASIC、VBScript、JavaScript和PERL等)不同,Java字节码的设计使之能很容易地直接转换成对应于特定CPU的机器码,从而得到较高的性能。2004年,美国宇航局用来操纵火星车的“科学活动计划者”装备,使用的编程语言就是Java,充分证明了它的高性能。

8. 多线程

多线程机制使应用程序能够并行执行,而且同步机制保证了对共享数据的正确操作。通过使用多线程,程序设计者可以分别用不同的线程完成特定的行为,而不需要采用全局的事件循环机制。因此,很容易地实现网络上的实时交互行为。

Java在两方面支持多线程:一方面,Java环境本身就是多线程的。若干个系统线程运行负责必要的无用单元的回收、系统维护等系统级操作;另一方面,Java语言内置了多线程控制,可以大大简化多线程应用程序的开发。Java提供了一个Thread类,由它负责启动运行和终止线程,并可检查线程状态。Java的线程还包括一组同步原语。这些原语负责对线程实行并发控制。利用Java的多线程编程接口,开发人员可以方便地写出支持多线程的应用程序,提高程序的执行效率。必须注意的是,Java的多线程支持,在一定程度上受运行时支持平台的限制。例如,如果操作系统本身不支持多线程,Java的多线程特性可能就表现不出来。

9. 动态性

Java的设计使它适合于一个不断发展的环境。在类库中可以自由地加入新的方法和实例变量,而不会影响用户程序的执行。并且Java通过接口来支持多重继承,使之比严格的类继承具有更灵活的方式和扩展性。Java在运行时采用动态装载技术,类装入器的灵活性甚至允许动态地重新装入已修改的代码,同时应用程序继续执行。2.2 Java程序的构成及文本风格

任何一种编程语言,都有自己的程序结构和编码规范,这类结构和规范是与编程语言的语法及语义无关的,它是程序设计语言的一种外在表现特征,也是代码的一种自组织方式。一个好的程序,不仅能够正确运行出结果,而且要具有易读性,就是要求编写的程序不仅编程者自己看得懂,而且也要让别人能看懂。程序文本的风格如何,直接反映了编码者的训练素质。就像优美的身材能让人赏心悦目一样,一个好的程序文本也能给人以美的享受。程序文本的风格主要体现在3个方面:符号既要规范又能表达语义;注释要简明扼要、位置合理;程序文本编排的格式清晰易读。2.2.1 Java程序的构成

本小节中再次以例1.1为例,详细分析一个Java程序的构成。为了便于说明,笔者给源程序加上行号。读者在上机编程时,注意不要加行号,这里所加的行号,是为了后文对程序结构讲解需要。

Java是完全面向对象的语言。任何一个程序,都必须以类的形式来组织。

第1行是对类的声明。关键字class用来声明一个类,紧跟在它后面的HelloWorldApp就是类名。这个名字可以由程序员随便取——前提是要遵循Java的命名规则。这两个词都不可缺少。关键字public表示本类是一个公共类,它是可以省略的。建议读者使用public来修饰类,当类名和保存的文件名不同的时候,编译器会报错,这样可以减少错误的发生。

第2行的“ {”和第7行的“}”是成对出现的。它是类体的界定符号,在这对括号之间的东西都属于类HelloWorldApp。一个类体中可以什么都没有,也就是说,第3~6行都是可以省略的。不过这样,该类就什么事也不能做。但即便是一个空类,界定符号也不能省略。

第3行在该类中定义了一个main()方法。main是Java规定的名字,不能更改。Jave解释器以main()为入口来执行程序。main后面的“()”是方法的参数列表括号,不能省略。

括号中的String args[]是传递给main()方法的参数。String是系统预定义的字符串类。args[]表示它是一个数组,其中args是用户取的名字,可以更改。

main前面的关键字public表示访问权限,表明所有的类都可以使用本方法。

static指明该方法是一个类方法(又称静态方法),它可以通过类名直接调用,而无需创建对象。

void指明main()方法不返回任何值。

对于一个应用程序来说,main()方法是必需的,而且必须按照如上的格式来声明。

第4行的“ {”和第6行的“}”是成对出现的。它是方法体的界定符号,在这对括号之间的东西都属于方法main(),被称为方法体。即使方法体内为空,这对符号也不能少。

第5行是main()方法体中的执行语句。它用来实现字符串“Hello world!”的输出。其中,System.ou.println()是系统类中预定义好的一个静态方法,专门用来输出信息。后面将反复用到它,读者需要牢牢掌握它的使用。“Hello world!”是用户自己定义的字符串,可以改成想要的任何信息,也可以是中文,比如“世界,你好!”。

第5行的最末尾是一个“;”,它是Java规定的语句结束符,任何一条可执行语句的末位都必须有这样一个分号。

Java规定,一行可以写多个语句,一个语句也可写成多行。各个单词之间,可以用空格、TAB和回车来分隔,也可以由“(”、“)”、“ {”、“}”、“[”、“]”以及各种运算符来分隔。

上面关于程序的分析,都是以单词为单位的。细心的读者可能已经注意到,这些单词分为两类:一类像public、class和static,它们都有固定的含义,是系统预定义的符号,都是不可更改的。这些符号被称为关键字。Java中有50多个关键字,后面会详细介绍。

另一类单词如HelloWorldApp、args是可以由用户自行定义并更改的,被称为用户标识符。标识符是用来给类、对象、方法、变量、接口和自定义数据类型命名的。Java语言中,对于变量、常量、函数和语句块也有名字,统统称之为Java标识符,用户标识符必须遵循一定的命名规则。Java规定,标识符是由字母、下划线(“_”)或美元符(“$”)开头,后面跟0个或多个字母、下划线(“_”)、美元符(“$”)或数字组成的符号序列。根据此定义,下列单词都是合法的标识符:i count num_day Scoll_Lock $a789 a89 _Java Int

而下列标识符是不合法的:abc&# a3*4 int b-c #ab class☆注意:Java的关键字不能用作Java的标识符,但Java是区分大小写的。int是关键字,而Int则是合法的用户标识符。2.2.2 Java的代码结构</p><p>上文说明了一个Java程序的基本构成,要保证一段Java代码可以正常地编译并在JVM中运行,上述的几个构成部分是必不可少的。但从工程编码的角度来看,一个规范的Java类或接口程序文件的代码结构应如图2.1所示。图2.1 一个规范的Java类或接口程序文件的代码结构</p><p>在图2.1所示的代码结构中,除了相关的声明信息外,还包括Java程序所涵盖的常量、变量、构造器、方法和内部类等信息。按此结构编写的代码不仅结构清晰、功能分明,而且具备良好的可读性。良好的程序结构和代码规范,应该从学习编程的第一步学起。2.2.3 Java程序的格式编排</p><p>程序的格式编排就是通过使用缩进、空格和空行等方法对程序文本的外观作必要的处理,以提高程序的可读性。具体来说,要达到两个目的:一是用程序文本结构反映算法的逻辑结构;二是增强程序的视觉效果,使阅读者眼睛不易疲劳。</p><p>例如,下面这段Java程序:</p><p>如果不作处理,相信读者很难看懂。同样一个程序,当使用缩进、空格和空行对它稍作整理后,无论是语句之间的逻辑关系,还是视觉效果会完全不同。其实上面这个程序,就是2.5.11小节的程序2.46。</p><p>比较两个程序,最明显的区别是,程序2.46根据语句间的逻辑关系采用了缩进对齐。通常采取的做法是,块语句“ {}”中的语句应该要比它的控制语句缩进几格。如果里面有嵌套的块语句要作同样的处理。所有处在同一层次的语句应该对齐。这种格式称为“犬齿格式”。</p><p>另一个区别是,程序2.46在某些运算符的两边和括号的内侧加了空格。这样当代码很长时,它可以帮助阅读者耐心地读下去。如果一眼看去符号和字母集成一堆,恐怕阅读者很难有兴趣坚持下去。2.2.4 Java代码的注释风格</p><p>所谓注释,是指程序中的解释性文字。这些文字供程序的阅读者查看,编译器将不对其进行编译。注释能够帮助读者理解程序,并为后续进行测试和维护提供明确的指导信息。注释是说明代码做些什么,而不是怎么做的。注释要简明,恰到好处,没有注释的晦涩代码是糟糕编程习惯的显著标志。</p><p>从用途上分,注释可以分为序言性注释和功能性注释。□ 序言性注释:通常位于程序或者模块的开始部分。它给出了程序或模块的整体说明,这种描述不应该包括执行过程细节(它是怎么做的),它可能会导致不必要的注释维护工作。因为随着调试或者其他原因,方法的具体实现可能会被更新。□ 功能性注释:一般嵌入在源程序体之中。其主要描述某个语句或程序段做什么,执行该语句或程序段会怎么样,不是解释怎么做。只有复杂的执行细节才需要嵌入注释,描述其实现方法。为了避免注释与代码本身重复,不要用注释的形式把语句翻译成自然语言。</p><p>根据注释符的不同,在Java程序中有3种注释。</p><p>1. 行注释符“//”</p><p>编译器会认为以“//”开头的字符直至本行末尾都是注释,所以此种注释方法又称为“行注释”。如果注释的文字有多行,需要在每一行的开头都写上“//”。本章前面所有的例子,都是使用的行注释。它一般用于对某条语句或是某个变量的注释,以及一些文字不多的注释。</p><p>大多数的程序员在编写注释时,会将对代码的注释放在其上方或右边相邻位置,不会放在下面。而对数据结构的注释放在其上方相邻位置,不会放在下面。变量和常量的注释也一般放在其上方相邻位置或右方。本书中所有的注释都遵循这个习惯。</p><p>2. 块注释符“/*”和“*/”“/*”和“*/”是成对出现的,它们之间的文字都是注释。这些注释可以分成多行,不必再添加行注释符。相对于行注释,块注释显得有些麻烦(要多打两个字符“*”),而且它的功能基本上能被下面将介绍的文档注释所实现,所以现在很少单独使用块注释。</p><p>3. 文档注释“/**”和“*/”</p><p>文档注释符也是一种块注释,它是用来生成帮助文档的。当程序员编写完程序以后,可以通过JDK提供的javadoc命令,生成所编程序的API文档,而该文档中的内容主要就是从文档注释中提取的。该API文档以HTML文件的形式出现,与Java帮助文档的风格及形式完全一致。凡是在“/**”和“*/”之间的内容都是文档注释。下面是使用文档注释的一个简单例子。【例2.1】 Java代码文档注释示例</p><p>//-------------文件名DocTest.java,程序编号2.1------------</p><p>只要在命令行方式下输入:javadoc -d.DocTest.java</p><p>就会自动生成介绍类DocTest的index.html文件,文档注释中的内容都会出现在index.html中。为了便于javadoc生成帮助文档,一般还应遵循下列规则。(1)类注释</p><p>类注释必须放在import语句之后、类定义之前。由于Java的一个源程序通常只有一个公共类,所以类注释也可以看作是程序文件注释。它一般应包括:文件名、版本号、作者、生成日期、模块功能描述(如功能、主要算法、内部各部分之间的关系、该类与其他类之间的关系等)、主要方法的清单及本文件历史修改记录等。例如:</p><p>如果要手工排列上面每一行开头的“*”,是件比较麻烦的事情。不过,很多编辑器会自动为程序员完成这一工作。如果你的编辑器没有这个功能,建议不要在每一行开头加上“*”。(2)方法注释</p><p>在每个方法的前面要有必要的注释信息,其主要包括:方法名称、功能描述、输入和输出及返回值说明、调用关系及被调用关系说明等。例如:/**(3)通用注释</p><p>在(2)所述的例子中,使用了一个标记“@”,这是Java中的通用注释符。它可以是以下几种:@author name</p><p>这个标记产生一个作者条目,可以使用多个@author标记,每个标记对应一个作者。@version text</p><p>这个标记产生一个版本条目,text是对版本信息的描述。@since text</p><p>这个标记产生一个“始于”条目,text是对版本修改历史的描述。@deprecated text</p><p>这个标记对类、方法或变量添加一个不再使用的注释,建议程序员不要再使用它。@see reference</p><p>这个标记产生一个超链接,链接到javadoc文档的其他相关部分或是外部文档。关于文档注释的更多信息,请参阅本书第12.3节。2.3 数据类型</p><p>一个程序,应当包含两个方面的内容:□ 数据的描述。□ 操作步骤(算法),即动作的描述。</p><p>数据是操作的对象,操作的结果会改变数据的状况。作为程序设计人员,必须认真考虑和设计数据结构和操作步骤。因此,著名计算机科学家沃思(Nikiklaus Wirth)提出一个著名的公式:程序=数据结构+算法</p><p>实际上,一个程序除了以上两个主要要素之外,还应当采用适当的程序设计方法,并且用一种计算机语言来表示。因此程序可以这样来表示:程序=算法+数据结构+程序设计方法+语言工具和开发环境</p><p>本书主要介绍Java语言及其开发环境,而不会深入介绍数据结构和算法等方面的内容。Java提供的数据结构是以基本数据类型和复合数据类型的形式出现的。2.3.1 基本数据类型</p><p>在Java语言中,为解决具体问题,要采用各种类型的数据。数据类型不同,它所表示的数据范围、精度和所占据的存储空间均不相同。Java中的数据类型可分为两大类。□ 基本类型:包括整型、浮点型、布尔型和字符型。□ 复合类型:包括数组类型、类和接口。</p><p>Java的基本数据类型可以分为三大类,分别为字符类型char、布尔类型boolean以及数值类型byte、short、int、long、float和double,而数值类型又可以分为整数类型byte、short、int、long和浮点数类型float、double。Java中的数值类型不存在无符号的,它们的取值范围是固定的,不会随着机器硬件环境或者操作系统的改变而改变。本小节分4类简要介绍Java的基本数据类型。☆注意:Java中还存在另外一种基本类型void,它也有对应的包装类java.lang.Void,不过我们无法直接对它进行操作。Java中的整型和浮点型,也可以归为一类,统称为数值型。</p><p>1. 整型</p><p>Java中的整型数据也可分为4种:□ 基本型,以int表示;□ 短整型,以short表示;□ 长整型,以long表示;□ 字节型,以byte表示。</p><p>各种类型数据所占空间位数和数的范围如表2.1所示。表2.1 各种整型数据所占空间及数的范围</p><p>Java中的整型数据,是以补码的形式存放在内存中的。以short类型为例,它有16位,能存储的最小的数是:16</p><p>这个数是–2,换算成十进制数是–32 768。</p><p>它能存储的最大的数是:16</p><p>这个数是2–1,换算成十进制数是32767。其他类型数的范围,读者可以用同样的方法来验证。如果对补码不熟悉,可以参阅有关计算机基础的书籍。☆注意:与C/C++不同,Java中没有无符号型整数,而且明确规定了各种整型数据所占的内存字节数,这样就保证了平台无关性。 </p><p>2. 浮点型</p><p>Java中用浮点类型来表示实数。浮点型也有两种:单精度数和双精度数,分别以float和double表示。浮点类型的有关参数见表2.2。表2.2 浮点类型所占位数及数值范围</p><p>Java中的浮点数,是按照IEEE-754标准来存放的。有兴趣的读者可以查阅相关资料。</p><p>3. 字符型</p><p>Java中的字符型用char来表示。和C/C++不同,它用两个字节(16个位)来存放一个字符。而且存放的并不是ASCII码,而是Unicode码。</p><p>Unicode码是一种在计算机上使用的字符编码。它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换及处理的需求。无论是英文字符还是中文汉字,都可以在其中找到唯一的编码。而且它和ASCII码是兼容的,所有的ASCII码字符,都会在高字节位置添上0,成为Unicode编码。例如,字母a的ASCII码是0x61,在Unicode中,编码是0x0061。</p><p>4. 布尔型</p><p>布尔类型用boolean表示。它是用来处理逻辑值的,所以布尔类型又被称为逻辑类型。布尔类型只有两个取值:true和false,分别表示条件成立或不成立。☆注意:Java中不再像C/C++那样,能用整型值来表示逻辑结果,它只能用布尔型表示。</p><p>Java中的数据有常量和变量之分,它们分别属于上面4种类型。本章将分别以常量和变量为例来详细介绍这些基本的数据类型。2.3.2 常量</p><p>常量是指在程序运行过程中,其值不能被改变的量。常量包括布尔常量、整型常量、浮点类型常量、字符型常量和字符串常量。例如,12是整型常量,–1.2是浮点型常量,“a”是字符型常量,“hello”是字符串常量。这些常量,可以直接从字面上看出它的值和数据类型,所以又称为字面常量。</p><p>字面常量虽然使用很简单,但却存在很大的隐患。首先,程序的阅读者可能只知道它的值却不能明白这个值所代表的实际意义。比如,某程序中,用1来表示男性,用2来表示女性。那么别人看到这个1的时候,很难弄明白它到底是代表男性还是女性。甚至隔了一段时间之后,连程序的作者也可能忘了1和2的具体意义。</p><p>另外一个问题是,常量的修改和维护不方便。如果在程序中大量使用某个常量,比如3.14,后来又需要修改这个值,改成3.1416。这就需要程序员在每一个使用该常量的地方去修改,只要有一个地方被遗漏,就很有可能导致整个程序运行错误。</p><p>正由于常量存在这样的问题,所以又被称为“神仙数”,意为只有神仙才能看懂的数。为了解决这个问题,Java又提供了符号常量,即用标识符来表示一个常量。由于标识符是有意义的字符串,所以阅读者很容易从字面上了解这个常量的实际意义。定义常量的方法如下:final int MALE=1;final int FEMALE=2;</p><p>关键字final表示定义一个常量,int表示它是一个整型值,MALE是常量名,1是它具体的值。常量只在定义的时候被赋值,以后它的值再也不能被改变。</p><p>程序后面再用到“男性”或“女性”的时候,使用MALE或FEMALE就可以了。而且如果要修改它的具体值,只需要在定义的位置修改,免去了到处修改的麻烦。</p><p>在很多情况下,程序员在使用符号常量的时候,只关心它的实际意义,并不关心它的具体值。比如想使用红色,就用常量RED,至于RED是等于1还是等于2,并没有必要知道。JDK中定义了很多符号常量,以方便程序员编程使用。☆提示:习惯上,符号常量名用大写,普通变量名用小写,以示区别。建议读者使用符号常量以提高程序的可读性和可维护性。2.3.3 变量</p><p>在程序运行过程中,其值可以改变的量称为变量。一个变量会有一个名字,在内存中占据一定的存储单元。在该存储单元中存放变量的值。请注意区分变量名和变量值这两个不同的概念。</p><p>Java和其他高级语言一样,用来标识变量名、常量名、方法名和类名等有效字符序列都被称为用户标识符,简称标识符。前面第2.2节已经介绍过标识符的命名规则。变量名作为标识符的一种,也要遵循这些规则。</p><p>定义变量的一般格式是:类型名 标识符1[=初始值1,标识符2=[初始值2,[...]]]</p><p>在Java中,所有用到的变量都要“先定义,后使用”。这样规定的目的如下所述。(1)凡是未被事先定义的,不作为变量名,这可以保证程序中变量名正确地使用。例如,如果定义了变量:int student;</p><p>而在使用时错写成了statent,如:statent=0;</p><p>在编译时就会发现statent未经定义,不能作为变量名,会输出相应的错误信息,便于程序员查错。(2)每一个变量被指定为一个确定类型,在编译时就能为其分配相应的存储单元。例如,指定i为int型,那么就会为它分配4个字节的空间。(3)每一个变量属于一个类型,便于编译时据此检查该变量进行的运算是否合法。例如,指定f为float类型,如果使用f来做位运算,编译器就会报错。</p><p>读者在给变量命名时,除了要遵循命名规则外,最好要选择相应的一个或多个英文单词作为它的名称,这样可以“见名知意”,增强程序的可读性。</p><p>为了和其他的程序员交流,读者也应当学习他人的命名方法。目前比较流行的命名方法有两种:一种是微软推行的匈牙利命名法,另一种是基于Unix/linux的命名法。Sun公司在随JDK发布的例子程序中,变量使用的是一种简化的匈牙利命名法,被称为驼峰命名法,它只有两条规则:□ 如果只有一个单词,则整个单词小写。□ 如果有两个以上的单词,则第一个单词全部小写,其余各单词的首字母大写。</p><p>比如,customField、jpgFilter、previewer和chooser等都是按照这个规则来命名的。Java的标识符可以是任意长度,但建议不要取得太长,最好不要超过4个单词。因为太长的名字容易输入错误,降低编程效率。</p><p>在Java编程中,针对类、接口、方法名、变量名和常量名等还有一些通用的命名约定,遵循这类约定,可以增强程序的可读性,便于代码的规范。□ 类和接口名:每个字的首字母大写,含有大小写。例如,MyClass、HelloWorld、Time等。□ 方法名:采用上述的驼峰命名法,首字的首字母小写,其余的首字母大写,含大小写。尽量少用下划线。例如,myName、setTime等。□ 常量名:基本数据类型的常量名使用全部大写字母,字与字之间用下划线分隔。对象常量可大小混写。例如,SIZE_NAME。□ 变量名:可大小写混写,首字符小写,字间分隔符用字的首字母大写。不用下划线,少用美元符号。给变量命名是尽量做到见名知义。</p><p>另外,虽然JDK 1.5及其以后的版本中也支持用中文给变量命名,不过很少有人采用中文变量名。一个直观的原因就是在中文输入法下,标点符号也是中文的,但Java只支持西文的标点符号,这样输入容易出错。☆说明:在本书中,多数采用的是驼峰命名法,只有简单循环变量可能会采用i、j、k之类的单字符。2.3.4 整型数据</p><p>整型数据用于表示整数,可分别用常量和变量来表示。</p><p>1. 整型常量</p><p>整型常量即整常数。Java中的整常数,由一个或多个数字组成,可以带正负号。根据进制的不同,又可分为十进制数、八进制数和十六进制数,分别用下面的形式表示:□ 十进制整数。如123、–456、0。□ 八进制整数。规定以0开头的都是八进制数。如,0123表示八进制数123,等于十进制数的83。–011等于十进制数的–9。□ 十六进制数。以0X或0x开头的都是十六进制数。如0x123和0X123都是十六进制数123,等于十进制数的291。–0x12和–0X12都等于十进制数的–18。</p><p>默认情况下,整型常数是基本类型,占4个字节。当整型常数后面跟有字母l或L时,表示该数是长整型常量,如4987L和0X4987L。虽然这两个数本来用4个字节足够存放,但加上后缀L后,强迫以计算机用8个字节来存放。由于小写字母l容易和数字1混淆,所以建议读者使用时用大写的L。</p><p>2. 整型变量</p><p>整型变量按照占用的内存空间不同,可以分成4种:字节型、短整型、基本型和长整型,分别用byte、short、int和long来定义。和其他所有类型的变量一样,整型变量在使用之前必须要先定义。下面来看一个简单的例子。【例2.2】 整型变量使用示例。</p><p>//--------------文件名integerExample.java,程序编号2.2------------</p><p>程序中的“//”符号表示注释,它后面的语句是写给程序阅读者看的,编译器无视它们的存在。去掉之后不会影响程序的运行。</p><p>–12345689987654L后面的L不能省略。因为默认常量是int型,这个值已经超出了int的范围。</p><p>Systm.out.println表示输出后面括号中的信息,然后换行。这是它和Systm.out.print的区别。</p><p>在Systm.out.println的括号中,"字节型变量byteVariable="+byteVariable表示将byteVariable的值转换成对应字符串,然后加在前面字符串的末尾输出。这种输出方式,后面会经常用到。</p><p>程序本身的逻辑很简单,不过是先定义,后赋值,然后输出。但它的输出结果可能会令读者感到有些意外。下面是它的运行结果:字节型变量byteVariable=127短整型变量shortVariable=64基本型变量byteVariable=4660长整型变量byteVariable=-12345689987654</p><p>注意它的两个变量值“shortVariable=64”和“byteVariable=4660”,而不是程序先前赋给它们的0100和0x1234。当然,读者可能会意识到,这是这两个八进制数和十六进制数转换成十进制数后的结果。☆说明:这正是整型变量和整型常量的一个区别。整型变量是无所谓八进制、十进制和十六进制这些概念的,因为无论什么进制的数,存放在计算机内部,都是二进制数。而这些八进制、十进制和十六进制只是方便给用户看的。</p><p>给变量byteVariable赋的值127,是它能够接受的最大值。如果把它改成128,编译器会报告:</p><p>这是使用整型变量时最容易犯的一个错误:数据超出了它所能容纳的范围,被称为“溢出”。这里编译器为程序员做了一个错误检查,但程序员不能完全依靠编译器的检查能力,编译器不可能将所有这种溢出错误都检查出来,请看下面这个例子。【例2.3】 字节型变量溢出示例。</p><p>//--------------文件名overflowExample.java,程序编号2.3------------</p><p>这个例子也很简单,先给byteVariable赋值127,然后让它的值加1,变成128。编译的时候没有任何错误。但是运行的结果却是:字节型变量byteVariable=-128</p><p>这是因为byteVariable只占8个位(1个字节),127存储在它里面的时候是下面这个样子的:</p><p>加了1之后,变成了这个样子:</p><p>在Java中,这个数是一个补码,而这个补码所表示的值就是–128。</p><p>这里虽然是以byte类型为例来讲解的,但实际上,任何整型变量都存在这个问题。所以在设计程序的时候,选用哪一种整型变量,需要根据程序运行时所能容纳数值的大小来确定。2.3.5 浮点型数据</p><p>浮点型数据是用来表示实数的,由于采用了浮点表示法,所以它比占同样空间的整型数据的表示范围要大得多。与整型数据相同,浮点型数据也分为常量和变量两种。</p><p>1. 浮点型常量</p><p>浮点型常量有两种形式:□ 普通的十进制数形式。它由数字和小数点组成。比如:0.123、123.0、.123、123.、0.0都是合法的实数常量。□ 指数形式。指数形式类似于科学计数法,比如1.5E5表示1.5×105;2.9E–7表示2.9×10–7。注意字母E(也可以是小写的e)之前必须有数字,且E后面的指数必须是整数。如E3、2E1.7都不是合法的指数形式。</p><p>Java还规定,浮点常量默认为双精度数,如果需要指定为单精度数,需要在末尾加上F或f。比如12.5F、12E5f。</p><p>2. 浮点型变量</p><p>浮点型变量分为单精度和双精度两类,分别用float和double来定义。</p><p>浮点数的有效位数是有限的,float型只有7个有效位,double型只有15~16个有效位,超过部分会自动做四舍五入处理。请看下例。【例2.4】 浮点数有效位数示例。</p><p>//--------------文件名realExample.java,程序编号2.4------------</p><p>它的输出结果是:单精度变量floatVariable=1234567.5双精度变量doubleVariable=1.234567891234568</p><p>由于浮点数是采用二进制存储的原因,浮点数往往不能精确表示一个十进制小数,即使这个小数是个有限小数。比如1.3,它存储在内存中也是一个无限小数,既可能是1.299999,也可能是1.300001,所以要尽量避免直接比较两个浮点数是否精确相等。具体方法,将在2.5.4小节中讲述。2.3.6 字符型数据</p><p>Java中的字符,默认情况下是以Unicode码存储的。具有ASCII码的字符,在高字节添上0,就是对应的Unicode码。Java中的字符型数据也有常量和变量两类。</p><p>1. 字符常量</p><p>字符常量是用单引号(即撇号)括起来的一个字符。如'a'、'D'、'$'都是字符常量。单个的汉字和标点符号,比如,'程'、'序'、'!'、'¥'等都是字符。</p><p>某些特殊的字符,比如回车符、换行符、退格符等,无法直接用单引号括起来。为了表示这些字符,Java提供了一种特殊形式的字符常量,就是以一个“\”开头的字符序列,“\”后面的字符不再是原来的含义,所以又被称为转义序列或换码序列。常用的转义序列及其含义如表2.3所示。表2.3 转义序列及其含义【例2.5】 字符常量示例。</p><p>//--------------文件名constCharExample.java,程序编号2.5------------</p><p>最后一行的'\u0041'是字符A的Unicode码,41是它的ASCII码的十六进制表示。输出结果如下:输出汉字字符:好输出换行符:输出反斜杠:\输出单引号:'输出双引号:"输出字符A:A</p><p>中间多出的那个空行是输出'\n'造成的效果。掌握这些转义字符,是处理字符型数据的基础技能。</p><p>2. 字符变量</p><p>一个字符变量占据两个字节,只能存放一个字符。字符变量用关键字char来定义。将一个字符存放到字符变量中,实际上并不是把该字符本身存放到内存单元中,而是将该字符的Unicode码存放到内存单元中。例如,字符A的Unicode码是0x0041,它会以二进制的形式存放在内存中,如下:</p><p>它的存储类型与整型数据的short类型很相似。其实Java是将字符变量作为无符号的短整型数据来处理的,这就决定了Java中的字符数据和整型数据之间可以通用。可以对字符型数据进行算数运算,此时相当于将它的Unicode码看成一个整数进行运算。【例2.6】 字符变量作为整数运算。</p><p>//--------------文件名charExample.java,程序编号2.6------------</p><p>程序中的ch=(char)shTemp表示将shTemp的值赋值给ch。由于shTemp是short类型的,与ch的char类型不同,所以要用(char)进行强制类型转换(关于强制类型转换,会在2.3.10小节中详细介绍)。0x41显示作为整型数赋值给shTemp,随后又被作为Unicode码赋值给ch。本程序的输出结果如下:字符变量ch=A</p><p>读者可能会觉得在这里直接给ch赋值A要简单得多。确实,本例没有多少实际意义,只是用来说明字符变量可以当作整型变量使用而已。Java语言对字符数据做这样的处理,使得程序设计时的自由度大增,程序员对字符做各种转换相当的方便。例如,程序中经常要对字母进行大小写的转换,利用Java的这一特性,就可以很容易地编写如下程序。【例2.7】 大写字母转换成小写字母。</p><p>//--------------文件名upperToLowCase.java,程序编号2.7------------</p><p>程序结果如下:字符变量ch=a</p><p>例2.6和例2.7的这些转换,涉及计算机数据的存储问题,初学者可以不必深究,以免过早拘泥于细节。读者学习应该把主要精力放在基本概念和编程方法上。</p><p>3. 字符串常量</p><p>在前面的例子中,经常出现这样的语句:System.out.println("字符变量ch="+ch);</p><p>其中"字符变量ch=",就是一个字符串常量。直观来看,它是一个由若干个字符组成的序列,以""作为界定符。Java中的字符串常量其实是一个String类型的对象。关于它的详细说明,将在5.2节介绍。2.3.7 布尔型数据</p><p>布尔型数据只有两个值:true和false。通常情况下,也把true称为真,把false称为假。与C/C++不同,它们不对应于任何整数值。布尔型变量要用关键字boolean来定义,在流程控制中经常要用到它。请看下例。</p><p>程序运行的结果如下:bool=true2.3.8 变量赋初值</p><p>程序中通常需要对变量预先设定一些值,然后参与后面的计算,这被称为赋初值。一种常见的赋初值的方法是:int i;i=100;</p><p>这样写需要两条语句,比较繁琐。Java允许在定义变量的同时对变量进行初始化。上面这两条语句与下面这一条语句等价:int i=100;</p><p>Java规定,一个局部变量在使用之前,必须要显示地初始化,否则将无法通过编译。比如下面就是一个错误的例子。【例2.8】 变量未赋初值就使用的错误示例。</p><p>//--------------文件名errorInit.java,程序编号2.8------------</p><p>编译时会报告:</p><p>这样强制要求程序员为变量赋初值,可以避免因为使用了垃圾值而发生的一些很难觉察的错误。2.3.9 变量的作用域</p><p>本章中所有的变量都定义在方法main()中间,这种定义在方法里面的变量称为局部变量。它只在定义它的方法中有效,在此方法以外,是无法使用这些变量的。</p><p>Java并未规定变量定义的具体位置,也就是说,程序员可以在需要使用变量的地方开始定义它,随后就可以使用它,直到包含该定义的方法结束为止,变量都是有效的。这个有效区域,称为变量的作用域。下面这个简单的例子说明了变量的作用域的范围。【例2.9】 变量作用域示例。</p><p>//--------------文件名variableScopeExample.java,程序编号2.9------------</p><p>上例清晰地说明了各个变量定义的位置不同,则作用域也不相同。Java规定,在变量的作用域中,不允许出现同名的变量,但在不同的作用域中,可以出现同名变量,互不 干扰。</p><p>Java还规定,变量可以定义在块语句中,那么它的作用域仅限于块语句。关于块语句的介绍,请参阅2.5.3小节。2.3.10 数据类型转换</p><p>本节中的基本数据类型,除了布尔类型外,其余类型的数据是可以混合在一起运算的。例如:“10+'a'+1.5-5.123*'b'”是合法的。不过在运算时,如果某个运算符两侧的数据类型不一致,就必须要转换成同一类型,然后才能运算。转换的基本原则是:范围小的转换成范围大的,精度小的转换成精度大的。</p><p>按照数据转换时是否会损失精度,Java中的转换可以分成两类:扩展转换和缩减转换。</p><p>1. 扩展转换</p><p>Java规定,凡是符合表2.4的转换,都称为扩展转换。该转换可以由系统自动进行,无需程序员干涉。表2.4 扩展转换</p><p>按照表2.4的规定,如果运算符的两侧,有一个数据是在第一列中,另一个数据在其对应的第二列中,那么第一列中的数据会自动转换成第二列中对应的数据类型。</p><p>例如有:byte+int</p><p>会自动转换成:int+int</p><p>若有:int+double</p><p>则会转换成:double+double</p><p>Java还规定,若是两个数据类型没有出现在同一行的两列中,则两个数据都必须转换成同一类型,若这个目的类型有多个可以选择,则选精度和范围最小的那个。</p><p>例如有:byte+char</p><p>会转换成为:int+int</p><p>由于扩展转换是由系统自动进行的,所以又称自动类型转换。这种转换不会损失精度。</p><p>2. 缩减转换</p><p>Java规定,凡是符合表2.5的转换,就被称为缩减转换。这种转换会损失精度,系统不会自动进行,必须由程序员显示地指定。表2.5 缩减转换</p><p>要将第一列中的数据转换成第二列中的数据类型,则必须使用强制类型转换。它的基本格式是:(数据类型)数据</p><p>例如有:int a;</p><p>要将a转换成为byte类型,需要这样写:(byte)a;</p><p>如果需要将一个表达式的结果进行数据类型转换,则要将整个表达式用括号括起来。</p><p>例如有:int a,b;</p><p>要将a+b的结果转换成byte类型,需要这样写:(byte)(a+b)</p><p>Java在根据程序员的指令进行缩减转换的时候,有一套比较复杂的规则。原则上是在保证符号的情况下,丢弃掉高字节的内容。但这么做并不能保证转换的结果符合程序员的预期效果。下面来看几个简单的例子。【例2.10】 缩减转换错误示例1。</p><p>//--------------文件名narrowingConversion_1.java,程序编号2.10------------</p><p>多数读者可能会认为这个结果应该是0,但程序运行的结果却是– 46。这是因为float类型无法存储10个有效数字,在做缩减类型转换时它的信息会丢失。读者不妨将强制转换符(int)去掉,看看结果是否正确。【例2.11】 缩减转换错误示例2。</p><p>//--------------文件名narrowingConversion_2.java,程序编号2.11------------</p><p>程序运行结果如下:long:-9223372036854775808..9223372036854775807int:-2147483648..2147483647short:0..-1char:0..65535byte:0..-1</p><p>上述结果中,long、int和char的结果是正确的,而byte和short的结果就完全出乎意料。</p><p>以上两个例子都说明:除非程序员有十足的把握,否则不要轻易进行数据类型的缩减转换。</p><p>强制类型转换,除了用在缩减转换中,也可以用在扩展转换中。比如有:int+long+byte</p><p>可以写成:(long)int+long+(long)byte</p><p>这么写的目的一是为了让程序阅读时更为清晰,二是可以让编译器产生更为优化的代码,加快运算的速度。☆注意:无论是扩展转换还是缩减转换,都是产生了原数据的一个副本,转换的结果不会对原数据有任何的影响。2.4 运算符与表达式</p><p>Java中的运算符共有36种,按照运算类型可以分成6大类,如表2.6所示。表2.6 Java中的运算符</p><p>任何一个运算符都要对一个或多个数据进行运算操作,所以运算符又称操作符,而参与运算的数据被称为操作数。一个完整的运算符和操作数组成一个表达式。任何一个表达式都会计算出一个具有确定类型的值。表达式本身也可以作为操作数参与运算,所以操作数可以是变量、常量或者表达式。</p><p>运算符的优先级指表达式求值时,按运算符的优先级由高到低的次序计算。如大家习惯的“先乘除后加减”。Java语言中运算符优先级规则与代数学中的规则是相同的,它是Java能够以正确的次序计算表达式的准则。</p><p>当求值是由多个运算符组成的表达式时,人们熟知的计算顺序是,除非遇到括号,否则,同一优先级的运算总是按从左到右的顺序进行,这就是所谓的运算符的结合性。运算符的结合性是指运算分量对运算符的结合方向。结合性确定了在相同优先级,运算符连续出现的情况下的计算顺序。Java语言的运算符不仅具有不同的优先级,还要受运算符结合性的制约。</p><p>Java语言中,运算符的结合性分为两种,即左结合性(自左至右)和右结合性(自右至左)。比如,算术运算符的结合性是自左至右,即先左后右。例如,表达式a–b+c,则b应先与减号“–”结合,先执行a–b运算,然后再执行+c的运算。这种自左至右的结合方向,就称为“左结合性”。而自右至左的结合方向,称为“右结合性”。最典型的右结合性运算符是赋值运算符。例如,表达式a=b=1,由于赋值运算符“=”具有右结合性,所以应先计算子表达式b=1,即表达式a=b=1等价与a=(b=1)。读者在应用中应注意区别Java运算符的结合性,以避免理解错误。</p><p>Java中也可以根据操作数的个数将这些运算符分成单目运算符、双目运算符和三目运算符。本节将逐一介绍这些运算符,以及由它们所组成的表达式。2.4.1 算术运算符和算术表达式</p><p>1. 加法和减法运算符</p><p>加法运算符是“+”,减法运算符是“–”。它们的用法和普通数学中的用法一样,比如3+5、5–2。它们需要左右两侧均有数据,所以被称为双目运算符。基本类型中,只有boolean类型不能参与加法和减法运算。+的两侧也可以是字符串,关于这一点,将在5.2.1小节详述。它们的一般形式是:<exp1>+<exp2><exp1>-<exp2></p><p>2. 正值和负值运算符</p><p>正值运算符是“+”,负值运算符是“–”。它们的用法和普通数学中的用法也是一样的,比如:–5、–a、+2、+a,它们只有右侧有数据,所以被称为单目运算符。还要注意一点,这两个运算符都不能改变操作数本身的值。比如,a=1,做了–a操作后,a仍然等于1,不会变成–1。boolean类型不能参与此类运算。它们的一般形式是:<+><exp><-><exp></p><p>3. 乘法和除法运算符</p><p>乘法运算符是“*”,除法运算符是“/”。它们都是双目运算符,和数学中的用法相同,比如5.2*3.1、5/3、5.0/3.0。boolean类型不能参与此类运算。它们的一般形式是:<exp1>*<exp2><exp1>/<exp2></p><p>如果“/”两侧的操作数都是整型数据,系统会自动对结果进行取整。比如,5.0/3的结果是1.6666667,而5/3的结果是1。在多个数据进行混合运算时,初学者特别容易在这里犯错误,请看示例2.12。【例2.12】 除法运算示例。</p><p>//--------------文件名division.java,程序编号2.12------------</p><p>读者可以先思考一下它的结果,再对比一下真正的运行结果:5/3*3=35/3*3.0=3.05.0/3*3=5.0</p><p>可以看到,无论后面的数据是什么类型,5/3得到的结果总是1。</p><p>4. 取余运算</p><p>取余运算符是“%”,它是一个双目运算符,和数学中的取余运算规则类似。它的一般形式是:<exp1>%<exp2></p><p>它的操作数通常是正整数,也可以是负数,甚至是浮点数。如果有负数参与此运算,则需要特别注意。对于整数,Java的取余运算规则如下:a%b=a-(a/b)*b</p><p>先来看一个例子。【例2.13】 整型数取余运算示例。</p><p>//--------------文件名intRemainder.java,程序编号2.13------------</p><p>输出结果如下,读者可以自行验算:5%3=25%-3=2-5%3=-2-5%-3=-2</p><p>如果操作数中有浮点数,那么规则就要复杂一些。Java并没有按照IEEE 754的规定计算,而是采用了类似于C库中fmod()函数的计算方法。它的基本规则如下:a%b=a-(b*q),这里的q=(int)(a/b)。</p><p>看下面的例子。【例2.14】 浮点数取余运算示例。</p><p>//--------------文件名floatRemainder.java,程序编号2.14------------</p><p>输出结果如下:5.2%3.1=2.15.2%-3.1=2.1-5.2%3.1=-2.1-5.2%-3.1=-2.1</p><p>由于(int)(5.2/3.1)=1,容易得出:5.2%3.1=5.2–(3.1*1)=2.1。其余的例子读者可以自行验证。</p><p>5. 自加和自减运算</p><p>自加运算符是“++”,自减运算符是“––”,它们都是单目运算符。其中,“++”的作用是将变量值加1,“––”的作用是将变量值减1。它们的操作数只允许是变量,而不能是常量。</p><p>++和––既可以放在变量前面使用,比如:++i和––i。也可以放在变量后面使用,比如i++和i––。放在前面时,称为前缀加(减);放在后面时,称为后缀加(减)。它们的一般形式是:<变量>++ 后缀加<变量>-- 后缀减++<变量> 前缀加--<变量> 前缀减</p><p>对于变量i而言,++i和i++的作用是一样的,都是将i的值加1。但是前缀加(减)和后缀加(减)在参与运算时的方式是不同的。</p><p>Java规定前缀加(减)操作是:将变量的值加(减)1,然后以该值作为表达式的值参与后续运算;而后缀加(减)的操作是:虽然也将变量的值加(减)1,但在新值存入到变量去之前,就将变量当前值作为表达式的值参与后续运算。所以当它们参与其他运算时,得到的结果是不同的。</p><p>下面以示例2.15来说明它们的区别。【例2.15】 前缀加(减)和后缀加(减)的区别示例。</p><p>//--------------文件名compare.java,程序编号2.15------------</p><p>程序的运行结果如下:前缀加运算后:prefix=2 rs=2前缀减运算后:prefix=0 rs=0后缀加运算后:postfix=2 rs=1后缀减运算后:postfix=0 rs=1</p><p>运算结果表明,无论是前缀加(减),还是后缀加(减),对变量自身的影响都是一样的,只是提取的表达式的值不同,所以rs的结果不相同。</p><p>很容易把上面这个例子扩展到更复杂一点的情况,如例2.16所示。【例2.16】 前缀加(减)参与混合运算示例。</p><p>//--------------文件名compare.java,程序编号2.16------------</p><p>它的输出结果如下:前缀加运算后:prefix=2 rs=20前缀减运算后:prefix=0 rs=0后缀加运算后:postfix=2 rs=10后缀减运算后:postfix=0 rs=10</p><p>从例2.16中可以清晰地看出,前缀加(减)是在做乘法的之前进行的;也可以看出,后缀加(减)是用的原值做乘法,但不能确定后缀加(减)是在乘法运算之前还是之后进行的。</p><p>下面来写一个例子程序,以确定前缀加和后缀加操作发生的确切时间。为了达到这一目的,需要让自加变量两次参与运算。【例2.17】 测试自加运算顺序示例。</p><p>//--------------文件名precedence.java,程序编号2.17------------</p><p>先看rs=(++prefix)+(++prefix)这个表达式,Java规定双目运算符+是从左侧向右侧运算。变量prefix的值首先是0,左侧的++prefix运算后,++prefix这个表达式(也就是+号左侧值)的值成为1,而prefix这个变量的值也是1。然后,计算+号右侧的++prefix。由于prefix现在已经是1了,于是++prefix这个表达式(也就是十号右侧值)的值成为2,prefix的值也是2。于是整个表达式成为了:1+2,结果为3,再将此值赋给变量rs。</p><p>再来分析rs=(postfix++)+(postfix++)这个表达式,同样是从左侧向右侧运算。变量prefix的值首先是0,先计算左侧的prefix++,根据规则,prefix++这个表达式(也就是+号左侧值)的值还是0,而prefix的值等于1。然后,计算+号右侧的prefix++。由于prefix现在已经是1了,于是prefix++这个表达式(也就是+号右侧值)的值也是1,而prefix的值成为了2。于是整个表达式成为了:0+1,结果为1,再将此值赋给变量rs。</p><p>这是程序实际运行的结果:prefix=2 rs=3postfix=2 rs=1</p><p>虽然本例仅仅只是对自加进行的分析,但对于自减也同样成立。</p><p>根据上面的分析,读者也可以推算出像(i++)+(i++)+(i++)或者(++i)+(++i)+(++i)这样更为复杂的表达式的值。☆注意:作者强烈建议读者不要在一个表达式中,让一个有自加或自减操作的变量出现两次或两次以上。这样的表达式无论对于程序的编写者还是阅读者都是非常难以理解的,很容易发生错误,而且也没有实际的必要。所以像例2.16这样的情况,仅作为研究之用。</p><p>++和––的结合规则是从右到左,其优先级与取负(–)同级。所以,如下表达式:-i++;</p><p>相当于:}-(i++);</p><p>而不是:(-i)++;</p><p>最后,还要强调一点,自加和自减这两个运算符是Java中仅有的能够改变变量本身值的运算,所以它要求操作数一定要是变量,而不允许是任何形式的常量或表达式。比如3++、(a+b)++这样的式子都是错误的。2.4.2 关系运算符和关系表达式</p><p>关系运算符决定操作数之间的逻辑关系。比如,决定两个运算对象是否相等,数据a比数据b是否大一些等。用关系运算符连接起来的表达式称为关系表达式。任何一个关系表达式的值都是布尔类型的,也就是只有true或者false两个值。</p><p>1. 相等运算符</p><p>Java中的相等运算符是两个连续的等号“==”,它是一个双目运算符。它的一般形式是:<exp1>==<exp2></p><p>它两侧的操作数可以是任意相同或相容类型的数据或表达式。比如:5==3(a*3)==b+2true==true</p><p>如果==两侧的值相等,则返回true,比如2+4==6,可以直观地理解为等式成立;如果不相等,则返回false,比如5==3,表示等式不成立。</p><p>尽管==两侧的操作数可以是浮点数,由于浮点数往往不能精确表示,则一般不会用==来判断浮点数是否相等。</p><p>2. 不相等运算符</p><p>Java中的不相等运算符是“!=”,它是一个双目运算符。它的一般形式是:<exp1>!=<exp2></p><p>其中两侧的操作数可以是任意相同或相容类型的数据或表达式。比如:5!=3(a*3)!=b+2true!=false</p><p>如果!=两侧的值相等,则返回false,比如,2+4!=6,表示判断不成立;如果不相等,则返回true,比如,5!=3,表示判断成立。</p><p>3. 大小关系运算符</p><p>Java中的大小关系运算符共有4个:“>”大于、“<”小于、“>=”大于等于、“<=”小于等于。它们都是双目运算符,一般形式是:<exp1> <关系运算符> <exp2></p><p>这些关系运算符的运算规则和代数中相应符号的规则完全相同。操作数的类型只能是整型或浮点类型。若关系成立,返回true,否则返回false。</p><p>设a==3,b==4,下面这些表达式:a>b结果为falsea<b结果为truea>=b结果为falsea<=b结果为true</p><p>如果参与运算的操作数类型不相同,则会进行自动类型转换。2.4.3 逻辑运算符和逻辑表达式</p><p>关系运算所能反映的是两个运算对象之间是否满足某种“关系”,比如是“大于”还是“小于”。逻辑运算则用来判断一个命题是“成立”还是“不成立”。所以,逻辑运算的判断结果也是boolean值,只有true和false。其中,true表示该逻辑运算的结果是“成立”的(即命题为“真”),false表示该逻辑运算的结果是“不成立”的(即命题为“假”)。</p><p>通常,将参与逻辑运算的数据对象称为逻辑量。用逻辑运算符将关系表达式或逻辑量连接起来的式子称为逻辑表达式。逻辑表达式的值又称为逻辑值。参与逻辑运算的操作数必须是boolean类型的数据或是表达式,不允许是其他类型。</p><p>逻辑运算符有3种:与运算、或运算和非运算(也称取反运算)。它们之间还可以任意组合,成为更复杂的逻辑表达式。逻辑运算极大地提高了计算机的逻辑判断能力。</p><p>逻辑运算表达式通常使用在if~else语句、for语句和while语句的判断部分。</p><p>1. 逻辑与运算</p><p>逻辑与运算符是“&&”,它是双目运算符。它所组成的逻辑表达式的一般形式为:<exp1> && <exp2></p><p>其中,exp1和exp2可以是Java语言中boolean类型的表达式或者数据。比较常见的是前面介绍的关系表达式,或者也是一个逻辑表达式。</p><p>逻辑与表达式exp1 && exp2所表达的语义是:只有当表达式exp1“与”表达式exp2的值同时为true,整个表达式的值才为true;否则,整个表达式的值为false。表2.7归纳了运算符“&&”的含义,这种表通常称为“真值表”。表2.7 逻辑与运算符的真值表</p><p>例如,程序需要判断变量a的值是否处于[0,100]的区间内,通常数学的写法是:0<=a<=100。但在Java中不能这样写,需要将两个不等式分开来写,然后用逻辑与来连接:(0<=a)&&(a<=100)</p><p>当该表达式的值为true时,a的值一定是在[0,100]之间。</p><p>再比如,需要判断一个字母是否为大写的英文字母。可以用下面的逻辑表达式来表达:('A'<=ch)&&(ch<='Z')</p><p>其中,ch是要判断的字符。当且仅当两个关系式(即两个条件)为真时,由“&&”构成的组合条件才为真,也就是说,当该逻辑表达式的值为true时,变量ch一定是大写英文字母。</p><p>2. 逻辑或运算</p><p>逻辑或运算符是“||”,它是双目运算符。它所组成的逻辑表达式的一般形式为:<exp1> || <exp2></p><p>其中,exp1和exp2可以是Java语言中boolean类型的表达式或者数据。比较常见的是前面介绍的关系表达式,或者也是一个逻辑表达式。</p><p>逻辑或表达式exp1 || exp2所表达的语义是:只要表达式exp1“或者”表达式exp2的值中,有一个为true,整个表达式的值就为true;否则,整个表达式的值为false。表2.8是运算符“||”的真值表。表2.8 逻辑或运算符的真值表</p><p>例如,程序需要判断字符ch是否为英文字母,但大小写不限,则可以用如下表达式描述:('A'<=ch)&&(ch<='Z')||('a'<=ch)&&(ch<='z')</p><p>由于运算符“||”的优先级比“&&”低,它们都具有左结合性,因此,只要逻辑或运算符“||”两边表达式的值有一个为true,整个表达式的值也就为true。此时,变量ch可能是小写字母,也可能是大写字母,但绝不会是其他字符。</p><p>因为逻辑运算符“&&”和“||”的优先级比关系运算符都低,而且“||”的优先级比“&&”的优先级低,所以上面的表达式又可以写为下面的形式:'A'<=ch && ch<='Z' || 'a'<=ch && ch<='z'</p><p>但从结构上看,这个表达式显然没有前面的那个清晰。</p><p>需要特别指出的是,逻辑与和逻辑或运算符分别具有以下性质。对于表达式:exp1 && exp2exp1 || exp2</p><p>如果下列条件有一个满足:□ 在逻辑与表达式中,exp1的计算结果为false;□ 在逻辑或表达式中,exp1的计算结果为true。</p><p>则整个表达式计算完毕。因为这时已经能够确定整个表达式的逻辑值,所以exp2不会被计算。这个特性,被称为短路运算。</p><p>由于上述性质,在Java语言中计算连续的逻辑与和逻辑或运算时,实际上对表达式中的各操作数的计算顺序施加了控制。在做“&&”运算时,若左操作数的值为false,则不再计算右操作数,并立即以false为“&&”运算的结果;在计算逻辑“||”运算时,若左操作数的值为true,也不再计算右操作数,并立即以true作为“||”运算表达式的结果。在顺序计算逻辑表达式的过程中,一旦确定了表达式的最终结果,就不再继续计算。例如,设有定义:int a=1,b=2,c=3;</p><p>在计算下列表达式时,a < b ||(b<c)&&(a>c)</p><p>读者也许认为“&&”运算优先级高于“||”运算,所以先分别求关系表达式(b<c)和(a>c)的值,再做“&&”运算,结果为false,最后作“||”运算。因为a<b的关系成立(结果为true),所以整个表达式的结果为true。但实际上编译器在处理这个表达式时,把上述表达式组合成如下形式:a < b ||((b<c)&&(a>c))</p><p>显然,由于左操作数a<b的值为true,所以不必再计算“||”运算的右操作数,即子表达式(b<c)&&(a>c),并立即得到整个表达式的值为true。后面的表达式根本就没有计算过。</p><p>于是可以得到一个结论:在由运算符“&&”或“||”构成的复杂表达式中,这两个运算符能够控制子表达式的求值顺序。即它们的左操作数总是首先被求值,并根据左操作数的值来决定右操作数是否还需要求值。</p><p>因此,读者要特别注意上述两种逻辑运算的这一短路特点。同时,在组织含有“&&”运算的表达式时,将最可能为“假”的条件安排在最左边;在组织含有“||”运算的表达式时,将最可能为“真”的条件安排在最左边。这样可以提高程序的效率。</p><p>3. 逻辑非运算</p><p>逻辑非运算符是“!”,它是单目运算符。它所组成的逻辑表达式的一般形式为:!<exp></p><p>exp可以是Java语言中boolean类型的表达式或者数据。比较常见的是前面介绍的关系表达式,或者也是一个逻辑表达式。</p><p>逻辑非表达式!exp所表达的语义是:只要表达式exp为true,整个表达式的值就为false;否则,整个表达式的值为true。所以它又被称为逻辑反。表2.9是运算符“!”的真值表。表2.9 逻辑非运算符的真值表</p><p>由于逻辑“非”运算符“!”是一个单目运算符,它与其他单目运算符具有同样的优先级,比所有的双目运算符的优先级都高,且具有右结合性。所以,要检测变量x的值是否不小于变量y的值,可用如下表达式描述:!(x < y)</p><p>其圆括号是必需的,以确保表达式的正确计算。当x和y都等于3时,关系式x<y的结果等于false,所以,表达式!(x<y)的值为true。</p><p>巧妙地利用关系运算和逻辑运算,常常能表达复杂的条件判断。例如,编制日历程序需要判定某年是否是闰年。由日历法可知,4年设一闰年,但每100年少一个闰年,即能被4整除但不能被100整除的年份为闰年,每400年又增加一个闰年,即能被400整除的年份也为闰年。记年份为year,则year年是闰年的条件可以用逻辑表达式描述如下:(year % 4==0 && year %100 !=0 )|| year % 400==0</p><p>可以看到这个式子非常的简洁而易懂,这正是使用逻辑表达式的优势。2.4.4 条件运算符和条件表达式</p><p>条件运算符是一个三目运算符,即它需要3个操作数。它使用两个符号(“?”和“:”)来表示这个运算符。Java语言中使用条件运算的原因是它使得编译器能产生比if~else更优化的代码,可以认为它是if~else语句的一种更简便的替代表示法。三目条件运算表达式的一般形式是:<表达式1>?<表达式2>:<表达式3></p><p>表达式1通常是一个关系表达式。三目条件运算表达式的运算规则是:首先计算表达式1,当其结果为true(真)时,三目条件运算表达式取表达式2的值为整个表达式的值,否则取表达式3的值为其值。表达式2和表达式3可以是不同的数据类型,但必须是相容的。</p><p>可见,条件运算符像逻辑运算符一样,也能控制子表达式的求值顺序。</p><p>三目条件运算符最适用于这样的情况:根据某些条件将两个值中的一个,赋值给指定的变量。例如,要将x与y两者中的较大者送给a,可以用如下语句实现:max=(x>y)? x :y;</p><p>执行上述语句时,首先检测条件x>y。如果条件成立(为“真”),那么计算“?”后面的表达式x,并将该表达式的值(即x的值)赋给变量max;如果条件不成立(为“假”),则计算“:”后面的表达式y,并将该表达式的值(即y的值)赋给变量max。☆注意:上述表达式中,子表达式(x>y)的圆括号不是必须的,加上圆括号只是为了增加表达式的可读性。因为,三目条件运算符的优先级只高于赋值运算符,低于其他所有的运算符,而且是“从右到左”结合的。</p><p>下面是三目条件运算符的另一个示例:(x % 2==1)? 1 :0□ 当x为奇数时,整个表达式的值为1。□ 当x为偶数时,整个表达式的值为0。</p><p>例2.18是一个更复杂一点的三目条件运算符的应用实例。执行这个程序,会随机产生一个字符。如果该字符是英文小写字母,将其转换为对应的大写字母输出;否则,输出其自身。【例2.18】 用条件运算符转换字母示例。</p><p>//--------------文件名lowToUpper.java,程序编号2.18------------</p><p>Math.random()是Java中的随机数产生器,会得到一个[0,1]之间的双精度数,将它乘以128并取整,就可以得到一个Unicode码在(0~128)之间的字符。</p><p>由于Java的输入数据比较麻烦,需要用到异常处理机制。所以在学习异常之前,本书都采用随机函数来生成数据,请读者务必理解该函数。</p><p>条件运算符中,如果(originChar>='a')&&(originChar<='z')的结果为“真”,说明originChar的值是小写字母,则返回子表达式(char)(originChar-a'+'A')的值。</p><p>表达式(char)(originChar-a'+'A')用来将小写字母转成大写。不妨设originChar=='b',亦即'b'–'a'+'A',它等同于98–97+65,得结果66,即大写B字符的Unicode码。</p><p>如果表达式(originChar>='a')&&(originChar<='z')运算的结果为“假”,说明originChar的值是不是小写字母,则直接返回originChar的值。</p><p>由于这个程序用到了随机函数,所以每次运行的结果可能都不同。某一次运行的结果如下:随机产生的字符是:y转换后的字符是:Y</p><p>三目条件运算表达式还可以嵌套,即在一个表达式中,可以多次使用这个运算符。 例如:e1 ? e2 :e3 ? e4 :e5</p><p>由于条件运算符满足从右至左的结合律,所以,上述表达式等价于:e1 ? e2 :(e3 ? e4 :e5)</p><p>例2.19的程序实现了这样一个功能:任意产生一个整数,这个数如果小于0,就显示–1;如果等于0,就显示0;如果大于0,就显示1。这实际上就是通常称之为符号函数的实现。由此,我们再一次领略到三目条件运算符的处理能力。【例2.19】 用条件运算实现符号函数示例。</p><p>//--------------文件名 sign.java,程序编号2.19------------</p><p>程序的流程比较简单,就是先产生一个(–10,10)之间的整数,然后利用条件运算符进行判断。它某一次运行的结果为:number=-5 flag=-1</p><p>读者可多运行几次,可以看到不同的结果。2.4.5 位运算符和位表达式</p><p>Java语言提供了多种位运算。位运算将操作数解释成有序的“位”集合,这些位中的某一位或若干位可以具有独立的含义。它使得Java语言也能像C语言一样,可以随心所欲地操纵存储单元中的二进制位(bit)。位运算符通常用于整数,且参与运算的操作数均以补码形式出现。Java语言提供了7种位运算符,表2.10归纳了这些运算符的用法。表2.10 位运算符及其用法</p><p>下面通过实例来说明它们的用法。</p><p>1. 按位与运算</p><p>按位与运算符“&”是双目运算符。其功能是参与运算的两数各对应的二进制位作“与”运算。只有对应的两个二进位均为1时,结果位才为1;否则为0。表达式的一般形式是:<操作数> & <操作数></p><p>在实际应用中,按位与运算通常用于将操作数的某(些)位清0(又称屏蔽)而其他位的值保持不变。例如,设a为长度为16位的short变量,其值为0x4567。现要求将a的高8位清0,保留低8位。可通过表达式a&255实现,如图2.2所示。图2.2 表达式a&255的演算过程</p><p>2. 按位或运算</p><p>按位或运算符“|”是双目运算符。其功能是参与运算的两数各对应的二进位作“或”运算。只要对应的两个二进制位有一个为1时,结果位就为1;对应的两个二进制位同时为0时,结果位才为0。<操作数> | <操作数></p><p>该运算可以很方便地将操作数的某(些)位置1,而其他位仍不改变其值。例如,仍设a的值为0x4567,现要将最高位置为1,其他位保持不变。可通过表达式a | 0x8000实现,如图2.3所示。图2.3 表达式a|0x8000的演算过程</p><p>3. 按位异或运算</p><p>按位异或运算符“^”是双目运算符。其功能是参与运算的两数各对应的二进制位作“异或”运算。仅当两个对应的二进位不相同时,结果为1,否则结果为0。表达式的一般形式是:<操作数> ^ <操作数></p><p>异或运算最普遍的用法是能快速地将变量的值清0。由此可见,也能快速判断两个操作数是否相等。例如,表达式“a=a ^ a”,可以将整数a的值清为“0”,如图2.4所示。图2.4 表达式a^a的演算过程</p><p>异或运算还有一个很重要的特性就是,对于任意的整型数据a、b,有:a^b^b=a</p><p>证明过程如下:(1)设a的某位为1,b的对应位为1,则有:1^1=0,而后,0^1=1。若b的对应位为0,则有:1^0=1,而后,1^0=1。(2)设a的某位为0,b的对应位为1,则有:0^1=1,而后,1^1=0。若b的对应位为0,则有:0^0=0,而后,0^0=0。</p><p>综合(1)、(2)即可得:a^b^b=a。</p><p>异或运算的这一功能可以用来作为简单的加密。即以b为密钥,与a进行异或操作,就可对明文a进行加密。解密时仍然以b为密钥,对密文进行异或,可以还原为明文a。</p><p>4. 取反运算</p><p>取反运算符“~”为单目运算符,具有右结合性。表达式的一般形式是:~<操作数></p><p>取反运算符的功能是对参与运算的数的各二进位按位取反。例如,对short类型的数据9的取反运算,即:~(0000000000001001)</p><p>结果为:1111111111110110☆注意:取反运算不会改变数据本身的值,只是计算的结果为原数据的按位反。</p><p>很容易证明,对于任意整型数据a,有~(~a)=a。这个也可以用于简单的数据加密或 转换。</p><p>5. 左移位运算</p><p>左移运算符是“<<”,它是双目运算符。其表达式的一般形式为:<操作数> << <移位位数></p><p>左移运算符的功能是将运算符“<<”左边的操作数的各二进位全部左移指定的位数。左移时,操作数移出左边界的位被丢弃,从右边开始用0填补空位。例如,设byte a=15,则赋值表达式“a=a<<3”的结果是120。图2.5给出了其运算过程。图2.5 左移3位3</p><p>细心的读者可能会发现a<<3=120=a×2,其实很容易证明,对于任意的整数a和n,有下式成立:2na<<n=a×</p><p>由于移位操作的速度远远超过乘法运算,所以在对运算速度要求比较高的场合,可以用移位运算来代替乘以2的幂运算。</p><p>6. 带符号右移位运算</p><p>带符号右移位运算符是“>>”,它是双目运算符,其表达式的一般形式为:<操作数> >> <移位位数></p><p>其功能是将运算符“>>”左边的操作数的各二进位,全部右移指定的位数。右移时,操作数移出右边界的位被丢弃,从左边开始用符号位填补空位。如果原先最高位是1,则填补1;如果是0,则填补0。例如,设byte a=25,则赋值表达式a=a>>3的结果是3。图2.6给出了其运算过程。</p><p>若a=–25,则用补码表示为:11100111。此时表达式a=a>>3的值为:11111100,这个值用十进制表示是–2。运算过程如图2.7所示。图2.6 右移3位,高位填0图2.7 右移3位,高位填1</p><p>对于正整数而言,每右移1位,相当于被2整除一次。带符号移位,保证了数据的正负不会发生变化。</p><p>7. 不带符号右移位运算</p><p>不带符号右移位运算符是“>>>”,它是双目运算符,其表达式的一般形式为<操作数> >>> <移位位数></p><p>其功能是将运算符“>>>”左边的操作数的各二进位全部右移指定的位数。右移时,操作数移出右边界的位被丢弃,从左边开始用0填补空位。而与原符号位无关。除此之外,它的操作和“>>”并没有什么不同。</p><p>最后,要提醒读者注意的是,不要将位运算与逻辑运算混为一谈。例如,逻辑运算!a,它的操作数只能是boolean值。但按位取反运算~a则不同,它的操作数必须是整型值。</p><p>再来看逻辑与“&&”和逻辑或“||”运算,它们与按位与“&”和按位或“|”的区别主要表现在以下两个方面。□ 在逻辑表达式中,如果左边的操作数已经能够决定整个表达式的值,就不再对其右边的操作数求值。但“&”和“|”运算符两边的操作数都必须求值。□ 逻辑运算主要用于测试逻辑表达式的结果是true还是false,按位运算则用于比较它们的操作数中对应的“位”。2.4.6 赋值运算符和赋值表达式</p><p>Java中的赋值运算符有两种:一种是简单的赋值运算符“=”,另一种是将“=”与其他的运算符复合在一起形成的复合赋值运算符。</p><p>1. 简单赋值运算</p><p>符号“=”被预定义为赋值运算符。它不是代数中的等于运算符,等于运算符被预定义为“==”。由“=”连接的式子。称为赋值表达式。因此,凡是可以出现表达式的地方,都可以赋值。例如,下面是赋值表达式的一个例子。x=y+10</p><p>在这个表达式中,赋值运算“=”由左操作数x和右操作数y+10组成。赋值表达式的求解过程是:先计算赋值运算右操作数的值,然后将右操作数的值存放到左操作数指定的存储位置。但赋值运算本身也是一个表达式,它也应该有一个值,其值就是左操作数的新值。这个新值还可以被引用。</p><p>在Java中,出现在赋值号左边的操作数称为“左值”,出现在赋值号右边的操作数称为“右值”。常量和表达式不能作为左值。</p><p>所谓赋值,其物理意义就是将赋值运算右操作数的值存放到其左操作数所标识的存储单元中。请看下面这个表达式:a=a+1</p><p>从代数的角度看,它不是一个合法的表达式,但它在Java中是合法的。设变量a的值为1,用程序语言来描述这个表达式,就是将a号存储单元的内容加1,其结果再存放到a号单元中。即该表达式被计算后,a号单元的内容被更新为2。</p><p>下面再作几点说明。(1)赋值运算的左操作数必须为其右操作数指明一个确定的可存储位置,而右操作数可以是常量、变量、方法调用或任何合法的Java表达式,包括赋值表达式本身。因此,表达式:a+1=b+1</p><p>是错误的。错误的原因不在于赋值运算的左操作数是一个表达式,而在于这个左操作数表达式不能确定一个可存储位置,从而导致b+1的结果无处存储。(2)赋值运算符具有右结合性。因此,表达式:a=b=c=1</p><p>可理解为:a=(b=(c=1))(3)赋值表达式是带有副作用的。也就是说,整个赋值表达式除了产生一个返回值外,还会更新左操作数所标识的存储单元的值。例如上面所给出的表达式:a=b=c=1</p><p>它的具体计算过程是:根据结合规则,首先计算子表达式c=1,即将数值1赋给变量c,再将计算子表达式c=1得到的返回值(其值也是1)赋给变量b,最后将子表达式b=c=1的返回值(其值还是1)赋给变量a,整个赋值表达式还有一个返回值(仍然是1)。再看一个例子:a=(b=10) + (c=20)</p><p>对于这个表达式,要弄清楚以下两个概念。□ 究竟是先计算子表达式b=10还是先计算子表达式c=20。Java规定是从左向右计算,即先计算b=10,而后计算c=20。□ 要弄清楚“+”运算的运算对象是谁。读者可能认为是把b和c的值相加,但这是错误的理解。正确的理解应该是将两个子表达式的返回值相加。在这里,尽管b+c,与(b=10) + (c=20)在效果上是一样的,即结果都是30,但两者的概念完全不同。后者说明赋值表达式本身还有一个返回值,这个返回值还可以继续参加运算。(4)如果赋值运算符两边的数据类型不相同,需要进行类型转换,即把赋值运算符右边的类型转换成左边的类型(即“向左看齐”),然后再赋值。如果这种转换是扩展转换,系统将自动进行类型转换。如果是缩减转换,则需要程序员用强制类型转换来实现。</p><p>2. 复合赋值运算</p><p>在程序设计中,类似下面的表达式是司空见惯的:x=x + y</p><p>这类运算的特点是参与运算的量既是运算分量,又是存储对象。为避免对同一存储对象的地址重复计算,提高编译效率,Java引入复合赋值运算符。凡是双目运算符都可以与赋值运算符组合成复合赋值运算符。Java提供了11种复合赋值运算符。它们分别是:+=、-=、*=、/=、%=、<<=、>>=、>>>=、&=、^=、|=</p><p>下面举例说明复合赋值运算的含义:</p><p>一般地,记?为某个双目运算符,复合赋值运算:x θ=e</p><p>其等价的表达式为:x=x θ(e)</p><p>注意,当 e 是一个复杂表达式时,其等价表达式的括号是必需的。例如:y *=a+b</p><p>的等价形式是:y=y *(a+b)</p><p>赋值运算符和所有复合赋值运算符的优先级全部相同,并且都具有右结合性,它们的优先级低于Java中其他所有运算符的优先级。2.4.7 表达式的求值顺序</p><p>前面介绍了10多种运算符。当它们混合在一起进行运算的时候,往往会让初学者为一个表达式的求值顺序而伤透脑筋。</p><p>当一个表达式包含多个运算符时,表达式的求值顺序是由3个因素决定的:□ 运算符的优先级;□ 运算符的结合性;□ 运算符是否控制求值顺序。</p><p>这里的第3个因素是指Java语言中的3个运算符:逻辑与“&&”、逻辑或“||”和条件运算符“?:”。它们可以对整个表达式的求值顺序施加控制。它们或者保证某个子表达式能够在另一个子表达式的所有求值过程完成之前进行求值,或者使某个子表达式被完全跳过不再求值。</p><p>如果不考虑第3个因素的影响,Java中求值顺序的基本规则是:两个相邻运算符的计算顺序由它们的优先级决定。如果它们的优先级相同,那么它们的结合性就决定了它们的计算顺序。如果使用了小括号“()”,那么它具有最高优先权。小括号可以改变运算符的优先级和结合性。</p><p>可见,人们熟知的运算符优先级,并不能完全决定表达式的求值顺序。它在表达式求值过程中的主要作用是如何划分表达式的各个部分。考察下面这个表达式:a*b+c*d</p><p>显然,乘法运算应该优先于加法运算,而且3个运算符的结合方向都是从左到右,所以它等价于:(a*b)+(c*d)</p><p>如果程序员想改变它的运算顺序,可以这样使用括号:a*(b+c)*d</p><p>另外一个容易混淆的例子是:a+++b</p><p>似乎理解成为:(a++)+b 或者 a+(++b)</p><p>都可以。但对于计算机而言,这样的歧义是要绝对禁止的,所以Java专门规定了它的处理方法。Java在从左到右扫描运算符时,会尽可能多地扫描字符,以匹配成一个合法的操作符。因此,“a+++b”会被处理成为“(a++)+b”。☆提示:为了避免出现这种使人理解上的歧义,推荐读者自己加上括号。</p><p>求值顺序中还有一个问题,就是对于任何一个双目运算符,都有左右两个操作数,这两个操作数的求值也有一个顺序。与C/C++不同,Java明确规定:左操作数先求值,右操作数后求值。下面通过两个例子来说明这一规则。【例2.20】 求值顺序示例1。</p><p>//--------------文件名showOrder_1.java,程序编号2.20------------</p><p>在表达式“(i=3)*i”中,会先将3的值赋给i,并以3作为操作数的值,然后对右操作数求值。显然,现在i的值已经是3,所以这个式子等效于“3*3”。输出结果如下:9</p><p>如果操作符混合赋值运算符,那么右操作数中会隐含使用左操作数。这时会先将左操作数的值记录下来,再对右操作数进行运算,最后对两个值进行运算并赋值。【例2.21】 求值顺序示例2。</p><p>//--------------文件名showOrder_2.java,程序编号2.21------------</p><p>在第一个例子中,先计算“+=”的左操作数a,其值为9,并记录下来,然后计算右操作数(a=3),其值为3。所以表达式变成了:a=9+3,值为12。</p><p>在第二个例子中,先计算“+”的左操作数b,其值为9,并记录下来,然后计算右操作数(b=3)。其值为3,所以表达式变成了:b=9+3,值为12。</p><p>程序运行结果如下:a=12b=12</p><p>如果在运算双目运算符的左操作数过程中,由于某种原因(比如短路运算或者出现了异常)而中止,那么右操作数将不会被计算。</p><p>最后来看一下Java对于运算优先级的规定,如表2.11所示。表2.11 Java运算符的优先级☆说明:细心的读者可能会发现,表中并没有常用“()”,也没有数组中要用到的“[]”。这是Java和C/C++的一个重要区别。“()”、“ {}”、“[]”、“;”、“,”和“.”都是Java中的分隔符。所以“()”在Java中不是运算符,但可以用它来改变表达式的求值顺序和结合顺序。建议读者使用括号来明确规定表达式中各个子式的运算顺序。2.5 流程控制语句</p><p>语句是程序的控制成分。它具有特定的语法规则和严格的表达方法,用来控制程序的运行。Java语言尽管是面向对象的语言,但是在具体完成某一任务时,仍然需要用一个一个的方法来完成。在这些方法中,面向对象是无法派上用场的,需要的仍然是传统的结构化编程方法。因此,它的语句还是一些结构化的控制结构。这些控制结构可以归为3类:顺序结构、选择结构和循环结构。这3种结构都有一个共同的特征:每种结构严格规定只有一个入口和一个出口。即当程序运行时,只能从唯一的入口进入该控制结构所描述的代码段,也只能从唯一的出口退出。Java语言提供了多种语句来实现这些控制结构,它们可分为5类:表达式语句、复合语句、分支语句、循环语句和跳转语句。本节将逐一介绍这些语句。2.5.1 3种基本控制结构</p><p>用Java编程时,一般会用一个方法来完成一个简单的任务。编写一个方法,所用的仍然是传统的结构化编程方法。1966年,计算机科学家Bohm和Jacopini发表论文,证明了只需用顺序、选择和循环3种基本控制结构,就可以实现任何单入口单出口的程序。这实际上说明,编写任何一个Java中的方法,只需要实现这3种结构的语句就足够了。</p><p>1. 顺序结构</p><p>所谓顺序结构,就是指按照语句出现的先后关系顺序执行程序。图2.8所示的是3条语句的顺序结构,3条语句的执行顺序就是图中所示的顺序。</p><p>图2.8所示的语句通常都是简单语句。Java中的简单语句有两种:表达式语句和空语句。在本节之前介绍的所有程序都只有顺序控制结构。</p><p>2. 选择结构</p><p>选择结构又称为判定结构或分支结构。计算机的基本特性之一是具有判定能力,或者说具有“有条件地选择执行某条语句或语句块”的能力。Java语言可以通过选择控制结构来实现这种功能。它提供了两类选择结构语句:if~else语句和switch语句。图2.9表示了基本的选择结构。图2.8 顺序控制结构</p><p>图2.9所示的选择结构也被称为两路分支,由它可以派生出另一种基本结构:多路分支选择结构,如图2.10所示。图2.9 选择结构图2.10 多路分支选择结构</p><p>3. 循环结构</p><p>在程序中,需要根据某条件重复地执行某项任务,直到满足或不满足某条件为止,这就是循环结构,又被称为重复结构。循环结构不仅可以使程序员用很少的语句,就能让计算机重复完成大量雷同的计算,而且还使得程序的结构在逻辑上更紧凑、清晰和易读。</p><p>循环结构也有两种。一是当型循环结构,如图2.11所示。当条件P成立时,反复执行A操作;直到P为假时,才停止循环;另一种是直到型循环,如图2.12所示。它是先执行A操作,再判断P是否为真。若P为真,则再执行A,如此反复,直到P为假为止。图2.11 当型循环图2.12 直到型循环</p><p>Java语言也提供了3种循环控制语句来对应这两种循环结构,即while语句和for语句对应当型循环,do~while语句对应直到型循环。2.5.2 表达式语句和空语句</p><p>表达式语句,就是在表达式后面加一个分号“;”。分号是语句终结符,除了复合语句以外(2.5.3小节将介绍),其他任何一个Java语句都必须以分号结束,即分号是Java语句必不可少的成分。</p><p>表达式和表达式语句的区别在于表达式代表的是一个数值,而表达式语句则代表一种动作。最常见的表达式语句是赋值语句。方法调用语句也是一种典型的表达式语句。Java语言允许方法调用作为一个独立的语句使用。关于方法调用,将在3.5节详细介绍。</p><p>下面这些都是典型的表达式语句:a +=3;++a;b=a>>2;a=(int)Math.random();</p><p>比表达式语句更简单的语句是空语句。空语句的形式就是一个分号:;</p><p>空语句不执行任何动作,通常被安排在“语法上要求一条语句,而逻辑上并不需要”的地方。</p><p>在程序中,凡可以出现语句的地方都可以出现空语句。它既不会影响程序的结果,也不会产生编译错误。例如,下面的语句:nSum=0; ; //多余的空语句</p><p>它事实上由两条语句组成:表达式语句和空语句。</p><p>表达式语句和空语句都属于简单语句。Java中规定,语句中不再含有其他语句的,称为简单语句。由简单语句构成的语句序列,在默认情况下,它们出现的先后顺序就是其执行顺序。这种结构就是顺序控制结构,简称顺序结构。2.5.3 块语句</p><p>块语句又称复合语句,它是一个以下形式的语句:</p><p>即块语句是以一个左花括号“ {”开始,以一个右花括号“}”结束,其间的“语句序列”可以是一个或多个任何合法的Java语句。注意“}”后面没有分号。</p><p>从形式上看,块语句是多个语句的组合,但在语法意义上它只相当于一条简单语句。在任何简单语句存在的地方都可以是复合语句。例如,下面是一种简单的块语句形式:</p><p>尽管它没必要构成块语句,但它在语法上是完全合法的。块语句是非常有用的,因为在分支语句和循环语句的控制体中,只允许一条简单语句,但必须要多条语句才能实现功能。这时就可以将这多条语句用“ {}”括起来形成一个块语句,放在控制体中。</p><p>块语句可以嵌套,也就是在块语句中间还可以定义块语句,如以下代码所示:</p><p>无论是外层的块语句还是内层的块语句,语法功能都只是一条简单语句。</p><p>和块语句相关的变量是局部变量。前面2.3.9小节已经介绍过局部变量,一般的局部变量是定义在方法体内,作用域也是整个方法体。Java规定,局部变量也可以定义在块语句中,作用域只限于定义它的这一个块语句。【例2.22】 在块语句中定义局部变量。</p><p>//--------------文件名errorCompoundVariable.java,程序编号2.22------------</p><p>由于compoundVariable定义在块语句之中,所以在块语句之外,它就不能使用了。而methodVariable定义在方法体内,只要是在main()方法中,都可以使用。该程序只有在删除掉错误的那一行之后,才能正常编译运行。2.5.4 if~else分支语句</p><p>为了实现选择结构,Java提供了相应的分支语句。从逻辑上说,这些分支语句可以分成两类:一类是两路分支,以if和if~else语句为代表;另一类是多路分支,以switch~case语句和if~else if~else为代表。如果以关键字来分类,则是if~else为一类,switch~case语句为另一类。本小节先介绍if语句。</p><p>if语句是用来判定所给定的条件是否满足,根据判定的结果(真或假)来决定执行给出的两种操作之一。Java提供了3种形式的if语句。</p><p>1. 单一的if语句</p><p>它的语法形式为:if(exp) statement</p><p>其中,exp可以是Java中任意合法的关系表达式、逻辑表达式或者boolean量,但必须放在括号内。</p><p>statement只能是一条语句,这条语句可以是简单语句或块语句。语句statement可以和if(exp)写在同一行。</p><p>该语句的功能是,如果表达式exp的值为真,则执行statement所表示的操作;否则该语句不被执行,程序执行该语句的后续语句。程序流程如图2.13所示。</p><p>下面用一个例子来进一步说明if选择结构的用法。图2.13 if结构流程图【例2.23】 随机产生两个整数,输出其中较大的那个数。</p><p>不妨设这两个整数是x和y,先假设较大的数是x。如果假设正确,就什么事情都不用做;如果y比x更大,就把y的值赋给x。这样,最后只需要输出变量x就可以了。</p><p>//--------------文件名outputMax.java,程序编号2.23------------</p><p>这个程序的思路很简单,但是如果x的值是较小的那个,那么执行x=y之后,它原来的值就会丢失。在某些情况下,需要用y来保留x原来的值,也就是将x和y的值交换一下。这需要用到程序设计中最常用的技巧——交换。</p><p>所谓交换,是将x的值赋给y,然后将y的值赋给x。解决这个问题的思路可以参考日常生活。例如,某人左手拿了一个苹果,右手拿一只香蕉。要将这两个水果换一只手,一种简单的方法是将左手中的苹果暂时放到桌子上,然后把右手的香蕉递给左手,最后用右手把桌子上的苹果拿起来。</p><p>计算机中虽然没有桌子,但是可以设置一个变量来充当桌子,这种变量叫做临时变量。下面的程序演示了如何实现这一思路。</p><p>//--------------文件名outputMax.java,程序编号2.24------------</p><p>程序2.24比程序2.23看上去更复杂一些,但它们的基本功能是一样的。程序2.24的好处是便于以后的扩展,这一点将在5.1.3小节中展示。</p><p>初学者在实际应用if语句时,一个比较常见的错误是,当条件为“真”必须执行多条语句时,往往忘记把这些语句构成复合语句,从而导致结果出错。这类错误称为运行时错误或逻辑错误。编译器并不检查这一类错误,需要程序员通过调试程序来发现。</p><p>例如,程序2.24中的这一段:</p><p>它在逻辑上的含义是:如果x < y,则执行花括号里的3条语句,否则一条都不执行。但如果读者粗心地把它们写成如下程序段:</p><p>虽然编排格式并没有变化,但实际上逻辑含义完全发生了变化。它所表达的逻辑含义变成了:如果x < y,则执行第(1)步。然后无论x是否小于y,(2)和(3)两步都被执行。</p><p>读者要理解的是,这个错误使得编译器不再将(2)和(3)对应的两个语句看作是if语句结构的一部分。因为在语法上,if(expression)后面要求只使用一条语句。(2)和(3)是不属于if语句的独立语句。</p><p>2. if~else语句</p><p>如果说if语句是单路选择结构,那么if~else语句就是双路选择结构。它的语法形式为:</p><p>其功能可以描述为:如果表达式exp的值为真,则执行stat1;否则执行stat2。与if语句相比,它增加了else部分,使得它在表达式exp为真或假时执行不同的动作,二者必居其一。语句stat1和stat2都是一条语句,如果需要多条语句,则需要用“ {}”将其合并为块语句。图2.14 if~else结构流程图程序流程如图2.14所示。</p><p>这里仍然使用例2.23这个问题来说明if~else的使用。下面可以换一种思路,不用交换两个数据的值,而是采用更为直观的方法:如果x大一些,就输出x,否则就输出y。</p><p>//--------------文件名outputMax.java,程序编号2.25------------</p><p>程序中的两个输出语句后面都有分号,这是由于分号是Java语句中不可缺少的部分,这个分号是if语句中的内嵌语句所要求的。如果无此分号,则出现语法错误。□ 不要误认为上面是两个语句(if语句和else语句)。它们都属于同一个if语句。□ else子句不能作为语句单独使用,它必须是if语句的一部分,与if配对使用。</p><p>程序2.25看上去比前面两种方法都更易懂,足见if~else的作用。</p><p>3. if~else if~else语句</p><p>在某些程序设计中,可能根据条件分为多个不同的操作,这称为多路分支。从理论上来说,任意的多路分支都可以用二路分支来实现。但为了方便程序员编程,Java对if~else语句进行了扩展,可以支持多路分支判断。它的一般形式如下:</p><p>关于表达式exp和语句stat的语法规定,和前面if~else的规定完全相同。它的程序流程是从前往后依次进行判断。只要有一个判断为真,就执行对应的语句,后面的判断都不会被执行。如果没有一个判断为真,就执行else后面的语句。当然,这里可以省略else语句,那样有可能一条语句也没有被执行。程序流程如图2.15所示。图2.15 if~else if~else结构流程图</p><p>下面以一个例子来说明该语句的使用方法。【例2.24】 给定一个0~100内的分数(假定是整数),按照下列标准评定其等级并输出:0~59为不及格,60~69为及格,70~79为中等,80~89为良好,90~100为优秀。</p><p>这是一个典型的多路分支。假定成绩用变量score存储,只要判断score属于哪一个分数段就可以输出对应的等级。这里唯一的难点是如何判断score所属的分数段。比如score是95分,属于90~100这个分数段,初学者可能会认为这么来写:90<=score<=100</p><p>但这是代数的写法,在Java中是完全错误的。应该这么来写:90<=score && score<=100</p><p>整个程序如下:</p><p>//--------------文件名 ranking.java,程序编号2.26------------</p><p>这个程序能够很好地工作,但它并不是一个简洁、高效的程序,因为其中某些判断完全是多余的。根据if~else if~else的规则,是一个个else if依次判断过来。比如执行到“else if(70<=score && score<=79)”这一个判断,意味着前面的判断都不成立,也就是说score一定不可能小于70,所以“70<=score”这个判断是多余的,只要判断“score<=79”是否成立就可以了。整个程序可以修改如下:</p><p>//--------------文件名ranking.java,程序编号2.27------------</p><p>程序2.27与程序2.26的功能完全相同,但程序2.27更为简洁高效。</p><p>4. if语句的嵌套</p><p>前面介绍的是if语句的基本用法。在很多情况下,一个简单的if~else语句并不足以满足判断的需要,往往需要在if语句中又包含一个或多个if语句,这种情况称为if语句的嵌套。它的一般形式如下:</p><p>应当注意if与else的配对关系。Java规定,else总是与它上面的最近的、未曾配对的if配对。假如写成:</p><p>程序的编写者把第一个else写在与第一个if(外层if)同一列上,希望else与第一个if对应,但实际上else是与内嵌if(1)配对,因为它们相距最近。</p><p>一种避免上述错误的解决方法是使内嵌if语句也包含else部分(如前面列出的标准形式)。这样if的数目和else的数目相同,从内层到外层一一对应,不致出错。</p><p>如果if与else的数目不一样,为实现程序设计者的意图,可以加花括号来确定配对关系。例如:</p><p>这时“ {}”限定了内嵌if语句的范围,因此第一个else会与第一个if配对。下面通过一个例子来说明嵌套if的使用方法。【例2.25】 用if~else语句嵌套实现2.4.4小节中例2.19所示的符号函数。</p><p>//--------------文件名signByIF.java,程序编号2.28------------</p><p>它的程序流程如图2.16所示。图2.16 程序2.28的流程图</p><p>将程序2.28的if~else部分改动一下,如下所示。</p><p>//--------------文件名signByIF.java,程序编号2.29------------</p><p>再将上面的if~else部分改动一下,如下所示。</p><p>//--------------文件名signByIF.java,程序编号2.30------------</p><p>再将上面的if~else部分改动一下,如下所示。</p><p>//--------------文件名signByIF.java,程序编号2.31------------</p><p>程序2.29~程序2.31的流程图请读者自己画一下,然后分析哪些程序是错误的,并且将这几个程序多运行几次,看运行结果是否符合自己的分析。2.5.5 多路分支switch~case语句</p><p>Java提供了switch~case语句,专门用来处理“多中择其一”的情况语句,故又称之为多路选择语句或开关语句。在这种情况下,使用switch语句写出的程序往往比使用if~else语句写的程序更简洁、清晰,且不易出错。</p><p>switch语句结构包含若干个case标号和一个可以选择的default子句。其一般格式为:</p><p>该语句由以下几部分构成:(1)一个控制开关。关键字switch后面用括号括起来的表达式exp,称为“控制开关”。exp是可求值的表达式或变量,其值必须是整型或布尔型。它的作用是用来控制选择执行后面的哪一个“语句序列”。(2)一组case标号。它由关键字case后加一个常量,或完全由常量组成的表达式及冒号构成。表达式exp1,exp2,…,expn称为标号,因为它们将与控制开关表达式exp的值作比较,所以,其值也必须是整型或布尔型的,且exp1,exp2,…,expn的值应互不相同。各个case语句出现的次序可以由程序员任意安排。(3)与一个或一组case标号相关联的“语句序列”,称为case子句。各case子句允许有0条或多条语句,或是空语句,当是多条语句时不需要构成块语句。执行完一个case后面的语句后,流程控制转移到下一个case继续执行。case的语句标号只起到一个标识语句的作用,并不是在该处进行条件判断。在执行switch语句时,根据switch后面表达式的值找到匹配的入口标号,就从此标号开始执行下去,不再进行判断。这说明多个case可以共用同一组执行语句。(4)break语句(详见2.5.10小节)。它不是必须的,由程序员根据需要取舍。一旦遇到break语句,将跳出整个switch语句。正常情况下,每个case子句的最后是一条break语句。如果程序员有意省略这个break语句,反而被认为是不正常的。此时,程序员应该附加注释,说明此处不是遗漏而是确实不需要break语句。(5)可选的default项。如果控制开关表达式exp的值与任意一个case标号都不匹配,则紧跟在关键字default后面的“语句序列n+1”被执行。如果不使用default关键字,则也不应有“语句序列n+1”。default子句中可以没有break,但为了保证和前面形式上的统一,一般会加上break语句。(6)default并不一定要出现在最后,其实也可以出现在中间,但通常不会这么写。如果没有default,也没有出现任何与case匹配的情况,那么这个switch实际上成了一个空语句,不会进行任何操作。</p><p>下面用一个例子来说明switch~case语句的使用方法。这里使用的题目仍然是例2.24的题目,不过改成用switch~case语句来完成。【例2.26】 给定一个0~100内的分数(假定是整数),按照下列标准评定其等级并输出:0~59为不及格,60~69为及格,70~79为中等,80~89为良好,90~100为优秀。</p><p>这里的一个难点是:case后面是一个标号,不能像if语句那样写成:case 0<=score && score<=59 :</p><p>这样的形式。不过仔细观察题目要求,可以发现,除了不及格这一等级外,在同一等级中,虽然个位数字不同,但十位数字是相同的。所以只要将分数除以10,就可以得到它的十位数字,这个数字相同的,自然就在同一等级。程序如下:</p><p>//--------------文件名rankingBySwitch.java,程序编号2.32------------</p><p>由于100分和90~99分都属于优秀等级,但它们的十位数字并不相同,所以程序中用了两个标号“case 10”和“case 9”共用同一组操作。而对于不及格的,十位数字有6种,可以统一归到default中。这个程序虽然简单,但却用到了switch~case语句实际使用时的大多数常用技巧,请读者仔细体会。2.5.6 当型循环while语句</p><p>在许多问题中需要用到循环结构。例如,要输入全校学生成绩,求若干个数之和,迭代求根等。几乎所有实用的程序都包含循环。循环结构是程序设计的基本结构之一,它和顺序结构、选择结构共同作为各种复杂程序的基本构造单元。因此,熟练掌握选择结构和循环结构的概念及使用,是程序设计的最基本的要求。</p><p>while语句又称当型循环控制语句。它的一般形式为:while(exp)  stat</p><p>其中,exp是任意合法的关系表达式或逻辑表达式,也可以是布尔变量或常量。被称为循环条件测试表达式;stat是一条语句,称为循环体。语法上规定,循环体只能是一条语句。因此,当需要循环重复执行多条语句时,应使用块语句。</p><p>while语句的执行流程是:首先对表达式exp求值,若其值为真,则执行循环体。循环体执行完毕后,再次去判断表达式exp,若其值为真,则又去执行循环体。如此反复,直到表达式exp的值为假,则退出循环。显然,while语句的特点是先判断,后执行。因此,其循环体有可能一次也不执行。while语句的流程如图2.17所示。</p><p>下面通过两个简单的例子来说明while语句的使用。【例2.27】 依次输出1,2,3,…,100。</p><p>题目要求反复做“输出”这个动作,只是每次输出的数据从1,2,…一直变化到100,每次都是增加1,这是最简单也是最典型的循环。</p><p>//--------------文件名showCount.java,程序编号2.33------------图2.17 while语句的流程图</p><p>对于一个循环而言,下面几点是需要重点注意的。</p><p>循环变量必须赋初始值,这里是1。</p><p>循环的终止条件,这里是循环到100为止。由于这个题目很简单,一般不会出错,但对于复杂的问题,循环的终止条件就要特别小心,因为很容易出现“差1错误”。比如本题的循环判断条件如果写成“cnt<100”,就会少输出一个数“100”。</p><p>循环体中,需要做两件事情:一是输出变量cnt的值,每次循环这个值都会不同;二是将循环变量cnt的值加1。因为cnt同时还是循环判断表达式的一个组成部分,如果它的值不变化,就会使得“cnt<=100”永远成立,变成一个死循环。【例2.28】 求1+2+……+100的和。</p><p>累加求和是计算机中最常见的问题之一。对于本题,可以用等差数列求和公式来计算。但对于另外一些无规律的累加问题,就只能将每一个数据相加。为了使读者掌握解决普遍问题的方法,这里使用循环来完成累加求和。</p><p>容易看出,这里一共要进行99次加法,只是每次相加的两个数据不同。右侧的数据每次会要加1,而左侧数据则需要记录前面所有的数据之和。通用的技巧是用一个变量存储每次两个数据相加的和,而后这个变量参与下一次相加,并且再次记录相加的和,如此反复。这个变量被称为累加器。</p><p>//--------------文件名accumulationByWhile.java,程序编号2.34------------</p><p>它的输出结果是:sum=5050</p><p>程序2.33和程序2.34在结构上很相似,只不过后者的循环体中不是输出数据,而是将数据累加。这两个程序,也是大多数循环求解问题的基础。2.5.7 直到型循环do~while语句</p><p>do~while语句是Java中另一种结构形式的循环语句。其语法形式是:do statwhile (exp);</p><p>其中,stat是循环体,它可以是一条简单语句或复合语句。exp是循环结束的条件,它可以是Java中任意合法的关系表达式或逻辑表达式,也可以是逻辑变量或常量。在多数情况下,即使循环体中只有一条语句,也会使用“ {}”,将语句写成下面的形式:do { stat}while (exp);</p><p>这样程序看上去更为清晰,不易和while语句混淆。</p><p>do~while语句的功能是,重复执行由stat构成的循环体,直到紧跟在while后的表达式exp的值为假时才结束循环。do~while语句的流程如图2.18所示。</p><p>与while语句相比,do~while语句有一些不同。</p><p>do~while语句总是先执行循环体,然后再判断循环结束条件。因此,循环体至少被执行一次。do~while循环本身被看成是一条语句,所以while(exp)后面需要一个终止的分号。循环体如果多于一个语句,则应构成复合语句。</p><p>下面仍然以例2.28的题目为例,来说明do~while语句的使用。图2.18 do~while语句的流程</p><p>//--------------文件名图accumulationByDoWhile.java,程序编号2.35------------</p><p>用do~while来解决累加求和问题和while程序的编写难度并没有多大的区别。通常情况下,都会使用while语句来编写。不过在某些情况下,用do~while语句比while语句编写的程序更为简洁。【例2.29】 随机产生一系列的正数并输出,直到产生的数大于100为止,要求最后这个大于100的数也要输出。</p><p>这个题目和前面的题目不相同之处在于:它的循环次数是不能预先确定的,终止条件是输出的数大于100。下面先用do~while语句来写。</p><p>//--------------文件名outputByDoWhile.java,程序编号2.36------------</p><p>某一次运行的结果如下:93 19 51 86 92 29 107</p><p>另一次运行的结果是:128</p><p>下面用while语句来编写这个程序。</p><p>//--------------文件名outputByWhile.java,程序编号2.37------------</p><p>这个程序明显没有程序2.36那个简洁,产生数据和输出数据都出现了两次,一次在循环体前,一次在循环体内。这是因为while的终止判断条件表达式中,用到了变量number,这个数必须有一个初始值。本程序无论怎样都至少要输出一个数据,这正好符合do~while语句的流程。所以,在某些情况下,用do~while语句可以简化程序的编写。2.5.8 当型循环for语句</p><p>Java中的for语句使用最为灵活,不仅可以用于循环次数已经确定的情况,也可以用于循环次数不确定而只给出循环条件结束的情况,它完全可以替代while语句。for语句的一般形式为:for(exp1;exp2;exp3)  stat</p><p>exp1是循环初值表达式。它在第一次循环开始前计算,且仅计算一次,其作用是给循环控制变量赋初值。</p><p>exp2是循环条件测试表达式。它在每次循环即将开始前计算,以决定是否继续循环。因此,正常情况下,该表达式决定了循环的次数。</p><p>exp3是循环控制变量调整表达式。它在每次循环结束时计算,以更新循环控制变量的值。</p><p>stat是循环体。其可以是简单语句或块语句。当有多条语句时,必须使用块语句。最简单的循环体只有一个空语句。</p><p>for循环的执行过程如图2.19所示。</p><p>首先计算exp1,然后再计算exp2。若exp2的值为真,则执行循环体;否则,退出for循环,执行for循环后的语句。如果执行了循环体,则循环体每执行完一次,都要重新计算exp3,然后再计算exp2,依此循环,直至exp2的值为假。for循环的流程图如图2.19所示。图2.19 for循环的流程图</p><p>for语句最简单的应用形式也是最容易理解的形式。其语法结构如下:for(循环变量赋初值;循环条件;循环变量改变值)  语句</p><p>下面仍然以例2.28的题目为例,用for语句来完成累加求和。</p><p>//--------------文件名accumulationByFor.java,程序编号2.38------------</p><p>它的执行过程与程序2.34完全一样。其中的语句:int cnt;for(cnt=1;cnt<=100;cnt++) sum +=cnt;</p><p>与下面的语句相同。int cnt=1;while(cnt<=100) { sum=sum+cnt;//累加 cnt++;}</p><p>显然,用for语句更为简单方便。对于for语句的一般形式也可以改写如下形式:exp1;while(exp2) { stat exp3;}</p><p>使用for语句时,还应注意下面几点。(1)for语句一般形式中的exp1可以省略,此时应在for语句之前给循环变量赋初值。注意省略exp1时,其后的分号不能省略。例如:for(;cnt<=100;cnt++) sum+=cnt;</p><p>执行时,跳过“求解exp1”这一步,其他不变。(2)如果exp2省略,即不判断循环条件,循环将无终止地进行下去。也就是认为exp2始终为真。例如:for(cnt=1; ;cnt++) sum+=cnt;</p><p>exp1是一个赋值表达式,exp2空缺。它相当于:cnt=1;while(true) { sum+=cnt; cnt++;}(3)exp3也可以省略,但此时程序设计者应另外设法保证循环能正常结束。例如:for(cnt=1;cnt<=100;) { sum+=cnt; cnt++;}</p><p>在上面的for语句中只有exp1和exp2,而没有exp3。cnt++的操作不放在for语句exp3的位置处,而作为循环体的一部分,效果是一样的,都能使循环正常结束。(4)可以省略exp1和exp3,只有exp2,即只给循环条件。例如:for(;cnt<=100;) { sum+=cnt; cnt++;}</p><p>相当于:while(cnt<=100) { sum+=cnt; cnt++;}</p><p>在这种情况下,for语句完全等同于while语句。可见for语句比while语句功能强,其除了可以给出循环条件外,还可以赋初值,使循环变量自动增值等。(5)3个表达式都可省略,例如:for(;;)语句</p><p>相当于:while(true)语句</p><p>即不设初值,不判断条件(认为表达式2为真值),循环变量不增值,将无终止地执行循环体。(6)exp1可以是设置循环变量初值的赋值表达式,也可以是与循环变量无关的其他表达式。例如:for(sum=0;cnt<=100;cnt++) sum+=cnt;</p><p>exp3也可以是与循环控制无关的任意表达式。(7)exp1和exp3可以是一个简单的表达式,也可以是逗号表达式,即包含一个以上的简单表达式,中间用逗号间隔。例如:for(sum=0,cnt=1;cnt<=100;cnt++) sum=sum+cnt;</p><p>或for(i=0,j=100;i<=j;i++,j--) k=i+j;</p><p>exp1和exp3都是逗号表达式,各包含两个赋值表达式,即同时设两个初值,使两个变量增值。逗号表达式是从左到右依次运算。在上面的exp1中,先算i=0,再算j=100。(8)在exp1中,可以定义变量,例如:for(int i=0;i<10;i++)</p><p>这个变量i也是一个局部变量,它的作用域仅限于for语句的循环体内。(9)Java中的for语句功能很强。其可以把循环体和一些与循环控制无关的操作,也作为exp1或exp3出现,这样可以使程序短小简洁。但过分地利用这一特点会使for语句显得杂乱,可读性降低。因此最好不要把与循环控制无关的内容放到for语句的括号中。</p><p>最后再以一个求阶乘的问题作为for语句的示例。【例2.30】 求n!=1×2×3×……×n,设n=10。</p><p>求阶乘就是一个累乘的过程,它与累加求和基本相同,不过是将加法改成了乘法。另外,用于累乘的变量初始值应该为1。</p><p>//--------------文件名factorial.java,程序编号2.39------------</p><p>程序输出结果如下:10!=3628800</p><p>理论上来说,while语句、do~while语句和for语句是可以相互替代的,不过在具体编程时,需要视情况选择最合适的循环语句。一般情况下,如果循环次数能够预先确定,或者循环变量是递增或递减,都会选择for循环;如果至少要循环一次,就会选择do~while循环;如果是无限循环,通常会选择while循环。2.5.9 增强的for循环</p><p>Java SE5以后的版本中,引入了一种新的更加简洁的for语法用于数组和容器,即增强的for循环,通常也称为Java中的foreach语法,表示不必创建int变量去对由访问项构成的序列进行计数,foreach将自动产生每一项,其标准语法格式是:for(ElementType element:arrayName) { stat;};</p><p>其中arrayName是一个数组对象,或者是带有泛性的集合,此集合中的类型与ElementType一致。</p><p>element定义了一个局部变量,这个局部变量的类型与arrayName中的对象元素的类型是一致的。</p><p>stat定义的是循环体。【例2.31】 假设有一个float数组,编程遍历数组中的每一个元素。</p><p>如果用普通的for循环实现,就是定义一个自增的数组下标,然后依次取出数组中每个下标的值,实现遍历。用增强的for循环实现代码如下:</p><p>//--------------文件名foreachExample.java,程序编号2.40------------</p><p>程序输出结果如下:0.72998240.79079540.105524780.418385570.910689060.308083530.15551740.077473520.66986210.0027896166</p><p>在上述代码中,同时用了普通的for循环和增强的for循环,float数组需要通过普通的for循环进行组装,因为在组装时必须按索引来访问,而在遍历是则使用了增强的for循环,所用到的就是foreach语法:for(float x :f)</p><p>这条语句定义了一个float类型的变量x,继而将每一个f的元素赋值给x。在Java中,任何返回一个数组的方法都可以使用这类增强的for循环,这种循环方法不仅在录入代码时可以节省时间,更重要的是,它阅读起来也要容易的多,其目的侧重于业务的实现,而不关注实现的细节,在很多应用中可以提高编程效率。【例2.32】 遍历字符串“today is a good day”中全部的字符。</p><p>在Java应用中,有时需要对某一字符串中的字符进行遍历,或统计字符串中某类字符出现的次数等。String类中有一个方法toCharArray(),它返回一个char数组,因此可以用增强的for循环迭代字符串里所有的字符。</p><p>//--------------文件名foreachString.java,程序编号2.41------------</p><p>程序输出结果如下:t o d a y i s a g o o d d a y</p><p>增强的for循环也有自己的局限性,比如,不能在循环体中访问位置信息、不能对元素进行定点删除等。在实际的Java编程中,需要根据具体的应用场景来灵活地选择对应的循环方法。2.5.10 循环的嵌套</p><p>前面所讨论的3种循环结构的循环体,在语法上要求是一条语句。如果这条语句又是一个循环语句,或者是包含循环语句的块语句,则称这个循环结构是二层循环结构。依此类推,可能出现三层、四层乃至更多层循环结构。这种循环体中又套有另一个循环的结构叫做循环的嵌套。3种循环语句for、while和do~while可以互相嵌套、自由组合。但要注意的是,各循环必须完整,相互之间绝不允许交叉。例如下面几种都是合法的形式。</p><p>当然合法的形式远不止这几种,嵌套的层次也可以更深。Java并没有规定最多的嵌套层数,但如果嵌套层次过多,将影响程序的可读性。所以建议嵌套层次不要超过3层,如果有必要嵌套多层,可将内层的循环另外写成方法,供外层循环调用。【例2.33】 依次输出1,2,3,……,100,每行10个数字,共有10行。</p><p>这个题目可以用一层循环来实现,但用双层循环嵌套更为直观。外层循环用来控制输出的行数,这里共有10行。内层循环用来控制每行输出的数字,数字是递增的,每行10个。其中,内外两层循环都是定长循环,可用for来实现。另外用一个变量从1开始逐步增加,作为输出用的数据。</p><p>//--------------文件名showDoubleLoop.java,程序编号2.42------------</p><p>程序的输出如下:1 2 3 4 5 6 7 8 9 1011 12 13 14 15 16 17 18 19 2021 22 23 24 25 26 27 28 29 3031 32 33 34 35 36 37 38 39 4041 42 43 44 45 46 47 48 49 5051 52 53 54 55 56 57 58 59 6061 62 63 64 65 66 67 68 69 7071 72 73 74 75 76 77 78 79 8081 82 83 84 85 86 87 88 89 9091 92 93 94 95 96 97 98 99 100</p><p>这个程序也可以改动一下,不用变量cnt作为输出变量,可以直接使用j来输出,不过这样需要考虑j的变化规律。可以发现,每一行的第一个数字与该行的i有着一个简单的对应关系:10×(i–1)+1,如果i是从0开始编号,则该关系变为:10×i+1。程序如下:</p><p>//--------------文件名showDoubleLoopSe.java,程序编号2.43------------</p><p>程序2.43比程序2.42虽然要简短一些,但似乎更难想到一些。不过这种查找内层变量和外层变量规律的技巧,在很多程序中都会用到。本章最后一节将会有更为详细的演示。</p><p>细心的读者可能还发现,程序2.43的外层循环已经变成了:for(int i=0;i<10;i++)</p><p>i的初始值是0,终止条件是i<10,它的循环次数仍然是10次。但当循环终止时,i的值为10。而当程序2.42的外层循环终止时,i的值为11。读者要仔细体会两者的区别。2.5.11 跳转语句break</p><p>break语句用来实现控制转移。在前面学习switch语句时,已经接触到break语句。在case子句执行完后,通过break语句使控制立即跳出switch结构。在循环结构中,有时需要在循环体中提前跳出循环。break语句就是用来提前退出某个循环。</p><p>1. 一般的break语句</p><p>它的形式是:break;</p><p>它的功能是将程序流程转向所在结构之后。在switch结构中,break语句使控制跳转到该switch结构之后。在循环结构中,break语句使控制跳出包含它当前的循环层,并从循环之后的第一条语句继续执行。break语句不能用于循环体和switch语句之外的任何地方。【例2.34】 判断一个数是否为素数。</p><p>所谓素数,是指除了1和它本身之外,不能被任何其他数整除的正整数。根据这一定义,设某位数number,它的平方根等于s,依次测试它被2,3,…,s之间的数整除的结果。如果它能被2~s之中的任何一个整数整除,则提前结束循环,此时循环变量必然小于或等于s;如果number不能被2~s之间的任何一个整数整除,则在完成最后一次循环后,循环变量还要加1,因此它会等于s+1,然后才终止循环。在循环之后判别循环变量的值是否大于s,若是,则表明未曾被2~s之间的任何一个整数整除过,因此输出“是素数”。</p><p>//--------------文件名isPrime.java,程序编号2.44------------</p><p>程序某一次的运行结果如下:139是素数</p><p>2. 带标号的break语句</p><p>Java语言没有提供受到广泛争议的goto语句,而是为break语句提供了标号功能。所谓标号,就是用来标记某一条语句的位置。其基本的语法格式为:<标号> :<语句></p><p>使用标号的break语句形式为:break 标号;</p><p>当执行到本条语句时,立即跳转到标号所标记的语句处。对于break而言,所标记的语句只能是包含本break语句的内层或外层循环语句。比如以下代码是非法的:for (i=0;i<10;i++) inner:System.out.println(i);break inner;</p><p>对于break语句而言,如果标号标记的是包含它的循环语句,则跳转到该语句处相当于跳转出这一层循环。所以程序2.44可以改写成程序2.45,如下所示。</p><p>//--------------文件名isPrime.java,程序编号2.45------------☆说明:虽然加上了标记outer,但实际上,程序2.45和程序2.44是完全等价的程序。</p><p>如果有多层循环嵌套,利用标号可以从最内层的循环一步就跳转出所有的循环嵌套。下面这个简单的例子演示了它的使用方法。【例2.35】 利用标号语句跳转出所有的循环嵌套。</p><p>//--------------文件名showBreak.java,程序编号2.46------------</p><p>程序2.46中,外层循环while是一个无限循环。但由于被outer所标记,所以当执行到内层循环中的break outer;时,仍然可以跳出该循环。程序的输出如下:1 2 3 4 52.5.12 跳转语句continue</p><p>continue语句的功能是,使当前执行的循环体中止,即跳过continue语句后面尚未执行的该循环体中所有语句,但不结束整个循环,而是继续进行下一轮循环。和break语句一样,continue也有两种用法。</p><p>1. 一般的continue语句</p><p>它的形式是:continue;</p><p>continue语句只能出现在循环体中。执行本语句,会立即跳转到包含本语句的当前循环语句的循环条件表达式处,进行条件测试,以确定是否进行下一次循环。图2.20演示了包含continue语句的for循环的执行流程。图2.20 包含continue语句的for循环流程【例2.36】 输出[100,200]之间不能被3整除的数。</p><p>//--------------文件名notMultipleOfThree.java,程序编号2.47------------</p><p>当num能被3整除时,执行continue语句,结束本次循环(即跳过输出语句)。只有当num不能被3整除时才执行输出语句。</p><p>程序2.47不用continue语句也完全可以写出来,这里只是说明它的用法。</p><p>2. 带标号的continue语句</p><p>continue后面也可以跟标号,它的语法形式是:continue 标号;</p><p>标号的规则与break语句使用标号的规则完全相同。如果标号所标记的是包含continue的外层循环,则内层循环被终止,计算外层循环的条件测试,以确定是否进行下一趟循环。【例2.37】 输出[100,200]之间的所有素数。</p><p>例2.32已经讲解了如何判断一个数是否为素数。本例中要做的不过是反复判断多个数是否为素数。做重复的事情,正是计算机最拿手的本领。只要在程序2.44外面套一个外层循环就可以完成该任务。</p><p>//--------------文件名primeNumber.java,程序编号2.48------------</p><p>程序的输出结果如下:** prime numbers between 100 and 200 **101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199</p><p>break语句和continue语句都会中断循环语句的正常运行,造成循环语句有多个出口。因此有人认为这破坏了结构化的原则,降低了程序的可读性。特别是带标号的break和continue,可能会导致人们阅读程序时理解流程的混乱。所以除非有特别的需要(例如可以极大地提高程序的效率),否则应尽量避免使用这两种语句。</p><p>事实上,任何含有break和continue语句的程序,都可以改成不含这两种语句的形式。例如对于例2.35,可以改写成如下形式:</p><p>//--------------文件名primeNumber.java,程序编号2.49------------</p><p>程序2.49中去掉了continue语句,取而代之的是一个boolean变量isPrime,该变量初始值为true,即默认当前数为素数。一旦这个数据能被某个数整除,就将该变量改成false,表示它不是素数。同时,为了终止掉内层循环,又将这个变量作为循环判断表达式的一部分。这样程序的效率和程序2.35是一样的,但是可读性要好得多。2.6 Java基础语法实战演习</p><p>前面已经介绍了Java的基础知识,本节将综合运用这些知识编制一些简单的程序,以帮助读者巩固所学知识,并逐步熟悉编程的一些常见技巧。2.6.1 判断闰年【例2.38】 随机产生一个年份,判断是不是闰年。</p><p>闰年是满足下面两个条件之一的年份:(1)能被4整除但不能被100整除;(2)能被400整除。</p><p>这个问题可以用很多方法来解,这里先用if~else语句来求解。用变量year代表年份,判断的流程如图2.21所示。</p><p>根据流程图2.21,可以编写程序如下:</p><p>//--------------文件名leapYearByIf.java,程序编号2.50------------图2.21 闰年的判断流程</p><p>程序2.50用了多个if~else嵌套,如果直接阅读程序,读者可能会觉得有点难懂,需要对照图2.21来看。其实对于本题,还有更简单的方法,就是利用逻辑表达式来做。根据闰年的两条规则,用表达式(1)对应规则(1):(1)year%4==0 && year%100!=0</p><p>用表达式(2)对应规则(2):(2)year%400==0</p><p>且表达式(1)和(2)之间是或关系,于是可以得到下面的逻辑表达式:(year%4==0 && year%100!=0)||(year%400==0)</p><p>凡是满足上式的就是闰年,否则不是闰年。程序如下:</p><p>//--------------文件名leapYearByLogical.java,程序编号2.51------------</p><p>这个程序明显比程序2.50简洁、易懂。所以灵活运用逻辑表达式,也是程序员的基本技巧之一。2.6.2 求最大公约数和最小公倍数</p><p>对于两个正整数m和n,求它们的最大公约数。对于这个问题,有一种简单的解决方法,就是给定一个数r,它的初始值为min(n,m),测试它是否能同时被m和n整除,如果不能,则将其值减1,再测试。依次类推,一旦它能被两个整数整除,那么这个r就是最大公约数。但这种方法的效率比较低,下面介绍一种效率更高的算法:欧几里德辗转相除法。(1)求余数,r=m%n。(2)令m←n,n←r。(3)若r==0,则m为最大公约数,退出循环;否则转到第(1)步。</p><p>本书不对算法的正确性做出证明,读者可以自行思考。这个算法很明显就是一个直到型循环,可以用do~while语句来实现。求出最大公约数q之后,最小公倍数只要用m×n/q就可以了。【例2.39】 用辗转相除法求最大公约数和最小公倍数。</p><p>//--------------文件名GcdAndGcm.java,程序编号2.52------------</p><p>某一次运行的结果如下:760和612的最大公约数是:4760和612的最小公倍数是:116280</p><p>注意,程序2.52中的do~while循环,几乎就是将前面的算法一句句翻译成Java语言。这其实就是编程的实质:将自然语言所描述的算法翻译成为程序设计语言。对于初学者,掌握程序设计语言是基本任务,但如果要更进一步提高编程能力,学习常用的算法是必须的。2.6.3 Fibonacci数列</p><p>菲波那契(Fibonacci)数列是数学中非常有名的一个数列,因为是数学家Fibonacci发现的而得名。它的递推公式为:</p><p>K=1      当n=1或2时N</p><p>K=K+K  当n≥3时nn-1n-2</p><p>即从第三项起,每一项都是它前面两项之和。【例2.40】 求Fibonacci数列的前24项。</p><p>设当前所求项为K3,K3的前一项为K2,K2的前一项为K1,则K3=K2+K1。任意项的求解过程如图2.22所示。图2.22 Fibonacci数列求值过程示意图</p><p>这里只有3个变量:K3、K2和K1,却要输出24项,所以要用到一个迭代技巧。每当输出一个项K后,K项不再有用,于是可以将nn-2K赋给K,将K赋给K,把K空出来,继续求K。n-1n-2nn-1nn+1</p><p>//--------------文件名Fibonacci.java,程序编号2.53------------</p><p>程序运行结果如下:1 1 2 3 5 8 13 2134 55 89 144 233 377 610 9871597 2584 4181 6765 10946 17711 28657 46368</p><p>从以上运行结果中可以看出,Fibonacci数列的增长速度很快。当到第47项时,就会超出int类型的存储范围。所以,如果要求的项数比较大,则需要用long甚至是BigInteger对象。2.6.4 逆向输出数字</p><p>对数字进行处理,是计算机中的常见问题,也属于比较基础的知识。【例2.41】 将任意一个正整数逆向输出。例如,有一个整数32496,逆向输出为69423。</p><p>解决此问题的基本思路是要将数据分离成一个一个的数字。对于32496,先将6分离出来,再将9分离出来……最后将3分离出来。在分离的过程中,将数据依次输出就可以得到逆序的序列。</p><p>要分离出最低位是很简单的事情,只要对10取余即可。问题是如何将中间的数字也分离出来。其实,在最低位分离出来之后,最低位不再有用,完全可以抛弃,然后将第2位变成最低位。这只需要将数字用10整除,比如32496/10=3249,现在就可以对9进行操作了。</p><p>上面这两步反复做下去,形成一个循环,就可以依次处理百位、千位、……但是还有一个问题,就是什么时候终止这个循环。容易想到,当最高位被分离出来之后,再整除10,结果会为0,这时就不用再循环下去了。</p><p>程序实现如下:</p><p>//--------------文件名converseNumber.java,程序编号2.54------------</p><p>某一次运行的结果如下:要处理的数字是:973981逆向输出的数字是:1893792.6.5 求水仙花数【例2.42】 打印出所有的水仙花数。所谓水仙花数是指一个三位数,其各位数字的立方和等于该数字本身。例如,153是一个水仙花数,因为153=13+53+33。</p><p>这个问题的核心也是分离数字,单个的数字分离出来之后,再累加求立方和就可以得到结果了。这两个技巧在前面都已经讲解过,不再重复。程序实现如下:</p><p>//--------------文件名daffodilNumber.java,程序编号2.55------------</p><p>程序的输出结果如下:所有的水仙花数如下:153 370 371 407</p><p>这个程序使用二重循环来查找所有符合要求的数字。内层循环和核心,它把前面的分离数字以及累加求和的技巧综合在一起。在多数情况下,即便是看上去很复杂的程序,其实是把各种小的技巧、方法综合在一起。2.6.6 输出图形【例2.43】 编程输出一个如下所示的图形(行数可以更多,比如10行)。***************</p><p>这个图形输出很有规律,它是一个直角三角形,每行输出的“*”都在依次增加,而且第一个“*”的输出位置总是靠在最左边。</p><p>容易想到用这样的双层循环来实现:外层循环控制输出的行数,这是个定值;内层循环控制本行输出“*”的数目,这个数目恰好是本行的行号;每一行输出完毕后,需要输出一个换行符。</p><p>程序如下:</p><p>//--------------文件名triangleStar.java,程序编号2.56------------【例2.44】 编程输出如下图形:</p><p>这个菱形要比上面的直角三角形更为复杂,可以把它分成两个三角形:上面部分是一个5行的正三角形,下面部分是一个4行的倒三角形。要输出这样两个三角形,需要解决两个问题。(1)每行“*”的数目。对于上面的正三角形,若以i表示它的行号,那么每行“*”的数目等于2i–1。对于下面的倒三角形,它的数目也与i有关,不过更为复杂一点,需要对i重新编号,即倒置三角形的最上面一行编号从1开始,这样“*”的数目就等于2(LINES–i)–1。其中LINES是一个常量,就等于正三角形的行数。(2)每行第一个“*”位置的确定。第一个“*”的位置可以由空格来决定,即在每行的前面先输出若干个空格,然后再开始输出“*”,就可以形成上面的效果。问题是输出多少个空格。这个数目显然也与行号i有关。不过对于正三角形,输出的空格越来越少;而下面的倒三角形,输出的空格越来越多。这里先不给出这个函数关系,读者可以先想一想,再看下面的程序。</p><p>//--------------文件名lozengeStar.java,程序编号2.57------------</p><p>从程序2.57中可以看出,其实上下两个三角形的输出方法几乎完全相同,只是循环控制表达式的编写不同。将一个大问题分解成为几个较小的问题来解决,是求解问题的基本思路。2.6.7 输出九九口诀表【例2.45】 输出一个如下所示的九九乘法口诀表。1×1=11×2=2 2×2=41×3=3 2×3=6 3×3=91×4=4 2×4=8 3×4=12 4×4=161×5=5 2×5=10 3×5=15 4×5=20 5×5=251×6=6 2×6=12 3×6=18 4×6=24 5×6=30 6×6=361×7=7 2×7=14 3×7=21 4×7=28 5×7=35 6×7=42 7×7=491×8=8 2×8=16 3×8=24 4×8=32 5×8=40 6×8=48 7×8=56 8×8=641×9=9 2×9=18 3×9=27 4×9=36 5×9=45 6×9=54 7×9=63 8×9=72 9×9=81</p><p>这个乘法口诀表看上去比较复杂,其实仔细分析一下,它具有很强的规律性。(1)若把每一个等式看成一个整体的输出项——比如把它想象成“*”,那么它和例2.41的输出完全一样。(2)再来分析它的每一个等式。例如,“2×6=12”,它由两个部分构成:一部分是“×”和“=”,这个是不变的。另一部分是“2、6、12”,这个都是变化的。其中,“6”与它所在的行号相同,且同一行中所有的等式这个值都相同;“12”是“2×6”计算的结果,如果能够确定“2”,也就能确定“12”;而“2”恰好是这个等式在本行中的序号。因此,3个变量与行号、序号的关系就确定下来了。根据这个关系,不难写出下面的程序:</p><p>//--------------文件名multiplyTable.java,程序编号2.58------------</p><p>j是内层循环控制变量,它在每一个输出项目中都要变化,所以用作被乘数。i是外层循环变量,它在同一行的输出中是不会变化的,所以用作乘数。等式右边的结果就是这两个变量的乘积。2.7 本章小结</p><p>本章介绍了Java中最基础的知识:数据类型和流程控制语句。读者不必死记这些语法规则,而应该结合程序来学习。通过大量的编程练习,自然会掌握这些基础知识。本章中提供的例程都是一些小程序,并没有过于复杂的算法,读者只要用心分析,就一定能从问题中找到隐藏的规律。善于分析问题,找到规律,这是成为一名合格程序员的基本要求。2.8 实战习题</p><p>1. Java语言标识符的命名规则是什么?</p><p>2. Java有哪些基本的数据类型,它们的常量又是如何书写的?</p><p>3. 指出下列内容哪些是Java语言的整型常量,哪些是浮点数类型常量,哪些两者都不是?</p><p>1)E-4</p><p>2)A423</p><p>3)-1E-31</p><p>4)0xABCL</p><p>5).32E31</p><p>6)087</p><p>7)0x L</p><p>8)003</p><p>9)0x12.5</p><p>10)077</p><p>11)11E</p><p>12)056L</p><p>13)0.</p><p>4. Java字符能参加算术运算吗?</p><p>5. 用Java编写一个条件表达式,表示x=1与y=2有且只有一个成立。</p><p>6. 编写一个程序,示意前缀++和后缀++的区别,以及前缀––和后缀––的区别。</p><p>7. 若一个数恰好等于它的因子之和,则这个数称为“完全数”。编写程序求1000之内的所有完全数。(提示:什么是数的因子?因子就是所有可以整除这个数的数,但是不包括这个数自身。首先对1000以内的数进遍历,然后对每个数进行判断,求出所有因子并累加求和,最后根据判断结果输出满足条件的完全数。)</p><p>8. 编程序解百鸡问题:鸡翁一,值钱五,鸡母一,值钱三,鸡雏三,值钱一,百钱买百鸡,求鸡翁、鸡母、鸡雏各几何?(提示:此题是Java求职笔试中常出现的问题,主要用到Java循环和求模运算等方面的知识。)</p><p>9. 回文整数是正读反读相同的整数,编写一个程序,输入一个整数,判断是否为回文整数。(提示:在Java中,通过命令行向控制台中输入参数的方法为:</p><p>Scanner consoleScanner=new Scanner (System.in);</p><p>将输入的参数保存到一个变量中,并将输入数的各个位处理后保存到数组中,再根据回文数的特征对数组中的各个位进行判断,返回判断结果。)第2篇 Java面向对象编程◆ 第3章 对象和类◆ 第4章 继承与多态第3章 对象和类</p><p>在当今的计算机大型应用软件开发领域,面向对象技术正在逐步取代面向过程的程序设计技术。本章将介绍面向对象的基本知识和Java实现面向对象程序设计的主要工具——类。如果读者缺乏关于面向对象程序设计的背景,一定要仔细阅读本章。如果读者有C++编程经验,也要注意二者之间的区别,毕竟Java在类的具体实现上与C++有较大的差别。</p><p>学习本章面向对象的相关知识,主要内容有以下几点:□ 面向对象的基本概念;□ 对象与类的理解;□ 成员变量的定义与使用;□ 方法的定义及实现;□ 方法调用;□ 构造方法与静态方法;□ 终结处理与垃圾回收。3.1 什么是面向对象</p><p>面向对象(Object Oriented,OO)是当前计算机界关心的重点,它是20世纪90年代软件开发方法的主流。面向对象的概念和应用已超越了程序设计和软件开发,扩展到很广的范围。例如,数据库系统、交互式界面、应用结构、应用平台、分布式系统、网络管理结构、CAD技术和人工智能等领域。</p><p>面向对象是一种对现实世界理解和抽象的方法,是计算机编程技术发展到一定阶段后的产物,它是相对于面向过程而言的。通过面向对象的方式,将现实世界的物抽象成对象,现实世界中的关系抽象成类、继承等,以更直观、清晰地完成对现实世界的抽象与数字建模。讨论面向对象方面的文章非常多。但是,明确地给出“面向对象”的定义却非常少。最初,“面向对象”是专指在程序设计中采用封装、继承和抽象等设计方法。可是,这个定义显然不能再适合现在的情况。面向对象的思想已经涉及到软件开发的各个方面。例如,面向对象的分析(Object Oriented Analysis,OOA)、面向对象的设计(Object Oriented Design,OOD)以及经常说的面向对象的编程(Object Oriented Programming,OOP)。许多有关面向对象的文章,都只是讲述在面向对象的开发中所需要注意的问题,或所采用的比较好的设计方法。看这些文章只有真正懂得什么是对象,什么是面向对象,才能最大程度地收获知识。☆说明:在本章中,着重讨论OOP,有关OOA和OOD请读者查阅有关软件工程的书籍。OOP从所处理的数据入手,以数据为中心而不是以服务(功能)为中心来描述系统。它把编程问题视为一个数据集合,因为数据相对于功能而言,具有更强的稳定性。OOP同结构化程序设计相比最大的区别就在于:前者首先关心的是所要处理的数据,而后者首先关心的是功能。在计算机编程中使用OOP方法,更利于从人理解的方式对于复杂系统的进行分析、设计与编程。同时能有效提高编程的效率,通过封装技术,消息机制可以像搭积木的一样快速开发出一个全新的系统。3.1.1 对象的理解</p><p>OOP是一种围绕真实世界的概念来组织模型的程序设计方法,它采用对象来描述问题空间的实体。可以说,“对象”这个概念是OOP最本质的概念之一,在面向对象的编程过程中,首先根据客户需求抽象出业务对象;然后对需求进行合理分层,构建相对独立的业务模块;之后设计业务逻辑,利用多态、继承、封装和抽象的编程思想,实现业务需求;最后通过整合各模块,达到高内聚、低耦合的效果,从而满足客户要求。但是,如何给“对象”下一个严谨的定义,却是一个棘手的问题,目前还没有统一的认识。</p><p>在现实生活中,一般认为对象是行动或思考时作为目标的各种事物。对象所代表的本体可能是一个物理存在,也可能是一个概念存在。例如一枝花、一个人、一项计划等。在使用计算机解决问题时,对象是作为计算机模拟真实世界的一个抽象,一个对象就是一个物理实体或逻辑实体,它反映了系统为之保存信息和(或)与它交互的能力。</p><p>在计算机程序中,对象相当于一个“基本程序模块”,它包含了属性(数据)和加在这些数据上的操作(行为)。对象的属性是描述对象的数据,属性值的集合称为对象的状态。对象的行为则会修改这些数据值并改变对象的状态。因此,在程序设计领域,可以用“对象=数据+作用于这些数据上的操作”这一公式来表达。</p><p>下面以一个生活中常见的例子来说明对象这个概念。例如“椅子”这个对象,它是“家具”这个更大的一类对象的一个成员。椅子应该具有家具所具有的一些共性,如:价格、重量和所有者等属性。它们的值也说明了椅子这个对象的状态。例如,价格为100元,重量为5公斤,所有者是小王等。类似地,家具中的桌子、沙发等对象也具有这些属性。这些对象所包含的成分可以用图3.1来说明。</p><p>对象的操作是对对象属性的修改。在面向对象的程序设计中,对象属性的修改只能通过对象的操作来进行,这种操作又称为方法。比如上面的对象都有“所有者”这一个属性,修改该属性的方法可能是“卖出”,一旦执行了“卖出”操作,“所有者”这个属性就会发生变化,对象的状态也就发生了改变。现在的问题是,所有的对象都有可能执行“卖出”操作,那么如何具体区分卖出了哪个对象,这是需要考虑的。面向对象的设计思路把“卖出”这个操作包含在对象里面,执行“卖出”操作,只对包含了该操作的对象有效。因此,整个对象就会变成图3.2这个样子。</p><p>由于对象椅子已经包含了“卖出”操作,因此,当执行“卖出”操作时,对象外部的使用者并不需要关心它的实现细节,只需要知道如何来调用该操作,以及会获得怎样的结果就可以了,甚至不需要知道它到底修改了哪个属性值。这样做不仅实现了模块化和信息隐藏,有利于程序的可移植性和安全性,也有利于对复杂对象的管理。图3.1 对象的属性集合图3.2 封装了属性和操作的对象3.1.2 什么是类“物以类聚”是人们区分、归纳客观事物的方法。在面向对象系统中,人们不需要逐个去描述各个具体的对象,而是关注具有同类特性的一类对象,抽象出这样一类对象共有的结构和行为,进行一般性描述,这就引出了类的概念。</p><p>椅子、桌子、沙发等对象都具有一些相同的特征,由于这些相同的特征,它们可以归为一类,称为家具。因此,家具就是一个类,它的每个对象都有价格、重量及所有者这些属性。也可以将家具看成是产生椅子、桌子、沙发等对象的一个模板。椅子、桌子、沙发等对象的属性和行为都是由家具类所决定的。</p><p>家具和椅子之间的关系就是类与类的成员对象之间的关系。类是具有共同属性、共同操作的对象的集合。而单个的对象则是所属类的一个成员,或称为实例(instance)。在描述一个类时,定义了一组属性和操作,而这些属性和操作可被该类所有的成员所继承,如图3.3所示。图3.3 由类到对象的继承</p><p>图3.3表明,对象会自动拥有它所属类的全部属性和操作。正因为这一点,人们才会知道一种物品是家具时,主动去询问它的价格、尺寸和材质等属性。</p><p>对于初学者而言,类和对象的概念最容易混淆。类属于类型的范畴,用于描述对象的特性。对象属于值的范畴,是类的实例。从集合的角度看,类是对象的集合,它们是从属关系。也可以将类看成是一个抽象的概念,而对象是一个具体的概念。例如苹果是一个类,而“桌子上的那个苹果”则是一个对象。</p><p>从编程的角度看,类和对象的关系可以看成是数据类型和变量的关系。还可以认为类是一个静态的概念,而对象是一个动态的概念,它具有生命力。类和对象的关系可以用如图3.4所示这个实例来演示,如图3.4所示。图3.4 类与对象的关系3.1.3 消息的定义</p><p>由上述内容可知,对象的行为是通过其定义的一组方法来描述,对象的结构特征是由它的属性来表现。但是,对象不会无缘无故地执行某个操作,只有在接受了其他对象的请求之后,才会进行某一操作,这种请求对象执行某一操作,或是回答某些信息的要求称为消息。对象之间通过消息的传递来实现相互作用。</p><p>消息一般有3个组成部分:消息接收者(接收对象名)、接收对象应采用的方法以及方法所需要的参数。同时,接收消息的对象在执行完相应的方法后,可能会给发送者返回一些信息。</p><p>例如,教师向学生布置作业“07级计算机1班做5道习题”。其中,教师和学生都是对象,“07级计算机1班”是消息的接收者,“做习题”是要求目标对象——学生执行的方法,“5道”是要求对象执行方法时所需要的参数。学生也可以向教师返回作业信息。这样,对象之间通过消息机制,建立起了相互关系。由于任何一个对象的所有行为都可以用方法来描述,所以通过消息机制可以完全实现对象之间的交互。在Java程序设计中,所需完成的功能任务就在对象之间的消息传递与相互作用之间完成。3.1.4 面向对象的基本特征</p><p>在上述面向对象的基本概念基础之上,不可避免地要涉及到面向对象程序设计所具有的4个共同特征:抽象性、封装性、继承性和多态性。</p><p>1. 抽象</p><p>抽象是人们认识事物的常用方法,比如地图的绘制。抽象的过程就是如何简化、概括所观察到的现实世界,并为人们所用的过程。</p><p>抽象是软件开发的基础。软件开发离不开现实环境,但需要对信息细节进行提炼、抽象,找到事物的本质和重要属性。</p><p>抽象包括两个方面:过程抽象和数据抽象。过程抽象把一个系统按功能划分成若干个子系统,进行“自顶向下逐步求精”的程序设计。数据抽象以数据为中心,把数据类型和施加在该类型对象上的操作作为一个整体(对象)来进行描述,形成抽象数据类型ADT。</p><p>所有编程语言的最终目的都是提供一种“抽象”方法。一种较有争议的说法是:解决问题的复杂程度直接取决于抽象的种类及质量。其中,“种类”是指准备对什么进行“抽象”。汇编语言是对基础机器的少量抽象。后来的许多“命令式”语言(如FORTRAN、BASIC和C)是对汇编语言的一种抽象。与汇编语言相比,这些语言已有了较大的进步,但它们的抽象原理依然要求程序设计者着重考虑计算机的结构,而非考虑问题本身的结构。在机器模型(位于“方案空间”)与实际解决的问题模型(位于“问题空间”)之间,程序员必须建立起一种联系。这个过程要求人们付出较大的精力,而且由于它脱离了编程语言本身的范围,造成程序代码很难编写,而且要花较大的代价进行维护。由此造成的副作用便是一门完善的“编程方法”学科。</p><p>为机器建模的另一个方法是为要解决的问题制作模型。对一些早期语言来说,如LISP和APL,它们的做法是“从不同的角度观察世界”、“所有问题都归纳为列表”或“所有问题都归纳为算法”。PROLOG则将所有问题都归纳为决策链。对于这些语言,可以认为它们一部分是面向基于“强制”的编程,另一部分则是专为处理图形符号设计的。每种方法都有自己特殊的用途,适合解决某一类的问题。但只要超出了它们力所能及的范围,就会显得非常笨拙。</p><p>面向对象的程序设计在此基础上则跨出了一大步,程序员可利用一些工具来表达问题空间内的元素。由于这种表达非常普遍,所以不必受限于特定类型的问题。人们将问题空间中的元素以及它们在方案空间的表示物称作“对象”。当然,还有一些在问题空间没有对应体的其他对象。通过添加新的对象类型,程序可进行灵活的调整,以便与特定的问题配合。所以在阅读方案的描述代码时,会读到对问题进行表达的话语。与以前的方法相比,这无疑是一种更加灵活、更加强大的语言抽象方法。</p><p>总之,OOP允许人们根据问题,而不是根据方案来描述问题。然而,仍有一个联系途径回到计算机。每个对象都类似一台小计算机;它们有自己的状态,而且可要求它们进行特定的操作。与现实世界的“对象”或者“物体”相比,编程“对象”与它们也存在共通的地方:它们都有自己的特征和行为。</p><p>2. 封装</p><p>封装是面向对象编程的特征之一,也是类和对象的主要特征。封装将数据以及加在这些数据上的操作组织在一起,成为有独立意义的构件。外部无法直接访问这些封装了的数据,从而保证了这些数据的正确性。如果这些数据发生了差错,也很容易定位错误是由哪个操作引起的。</p><p>如果外部需要访问类里面的数据,就必须通过接口(Interface)进行访问。接口规定了可对一个特定的对象发出哪些请求。当然,必须在某个地方存在着一些代码,以便满足这些请求。这些代码与那些隐藏起来的数据叫做“隐藏的实现”。站在过程化程序编写(Procedural Programming)的角度,整个问题并不显得复杂。一种类型含有与每种可能的请求关联起来的函数。一旦向对象发出一个特定的请求,就会调用那个函数。通常将这个过程总结为向对象“发送一条消息”(提出一个请求)。对象的职责就是决定如何对这条消息作出反应(执行相应的代码)。</p><p>若任何人都能使用一个类的所有成员,那么可对这个类做任何事情,则没有办法强制他们遵守任何约束——所有东西都会暴露无遗。</p><p>有两方面的原因促使了类的编制者控制对成员的访问。第一个原因是防止程序员接触他们不该接触的东西——通常是内部数据类型的设计思想。若只是为了解决特定的问题,用户只需操作接口即可,无需明白这些信息。类向用户提供的实际是一种服务,因为他们很容易就可看出哪些对自己非常重要,以及哪些可忽略不计。进行访问控制的第二个原因是允许库设计人员修改内部结构,不用担心它会对客户程序员造成什么影响。例如,编制者最开始可能设计了一个形式简单的类,以便简化开发。后来又决定进行改写,使其更快地运行。若接口与实现方法早已隔离开,并分别受到保护,就可放心做到这一点,只要求用户重新链接一下即可。</p><p>封装考虑的是内部实现,抽象考虑的是外部行为。符合模块化的原则,使得软件的可维护性、扩充性大为改观。</p><p>3. 继承</p><p>继承是一种联结类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类的继承。新类继承了原始类的特性,新类称为原始类的派生类(子类),而原始类称为新类的基类(父类)。派生类可以从它的基类那里继承方法和实例变量,并且派生类可以修改或增加新的方法使之更适合特殊的需求。这也体现了大自然中一般与特殊的关系。继承性很好地解决了软件的可重用性问题。比如说,所有的Windows应用程序都有一个窗口,它们可以看作都是从一个窗口类派生出来的。但是有的应用程序用于文字处理,有的应用程序用于绘图,这是由于派生出了不同的子类,各个子类添加了不同的特性。</p><p>关于继承的详细讨论,将在本书4.1~4.2节进行。</p><p>4. 多态</p><p>多态也叫多态性,是指允许不同类的对象对同一消息作出响应。比如同样的加法,把两个时间加在一起和把两个整数加在一起肯定完全不同。又比如,同样的选择“编辑”和“粘贴”操作,在字处理程序和绘图程序中有不同的效果。多态性包括参数化多态性和运行时多态性。多态性语言具有灵活、抽象、行为共享和代码共享的优势,很好地解决了应用程序函数同名问题。</p><p>关于多态性的讨论,将在4.4节进行。</p><p>最后,以Alan Kay的话作为本节的结束语。他总结了Smalltalk(这是第一种成功的面向对象程序设计语言,也是Java的基础语言)的5大基本特征。通过这些特征,读者可以理解“纯粹”的面向对象程序设计方法。(1)所有东西都是对象。可将对象想象成一种新型变量,它保存着数据,但可要求它对自身进行操作。理论上讲,可从要解决的问题上,提出所有概念性的组件,然后在程序中将其表达为一个对象。(2)程序是一大堆对象的组合。通过消息传递,各对象知道自己该做些什么。为了向对象发出请求,需向那个对象“发送一条消息”。更具体地讲,可将消息想象为一个调用请求,它调用的是从属于目标对象的一个子例程或函数。(3)每个对象都有自己的存储空间,可容纳其他对象。或者说,通过封装现有对象,可制作出新型对象。所以,尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。(4)每个对象都有一种类型。根据语法,每个对象都是某个“类(Class)”的一个“实例”。其中,“类”是“类型(Type)”的同义词。一个类最重要的特征就是“能将什么消息发给它?”。(5)同一类所有对象都能接收相同的消息。这实际是别有含义的一种说法,读者不久便能理解。例如,由于类型为“圆(Circle)”的一个对象也属于类型为“形状(Shape)”的一个对象,所以一个“圆”完全能接收“形状”的消息。这意味着可让程序代码统一指挥“形状”,令其自动控制所有符合“形状”描述的对象,其中自然包括“圆”。这一特性称为对象的“可替换性”,是OOP最重要的概念之一。3.2 类与对象</p><p>3.1节从理论角度阐述了对象和类的有关概念。类是组成Java程序的基本要素,在Java中有两种类:一种是在JDK中已经设计好的类,可以在程序中直接使用;另一种需要程序员根据现有的任务,自行设计和编写,这被称为用户自定义类。本节将着重介绍在Java中如何来自定义类以及如何使用这些类。3.2.1 类的基本结构</p><p>在前两章的例子中已经定义了一些简单的类,如HelloWorldApp类:</p><p>这个类虽然非常简单,但仍然可以看出,一个类的实现至少包含两部分的内容:</p><p>下面分别对每一部分进行详细讲述。3.2.2 类的声明</p><p>一个最简单的类声明如下:class className</p><p>其中,class是关键字,用于定义类。classname是类的名字,它必须遵循用户标识符的定义规则。例如:class Point</p><p>同时,在类声明中还可以包含类的父类(超类),类所实现的接口以及访问权修饰符、abstract或final,所以更一般的声明如下:[类修饰符]class 类名[extends 父类名][implements 接口名列表]</p><p>class、extends和implements都是关键字。类名、父类名和接口名列表都是用户标识符。</p><p>父类。新类必须在已有类的基础上构造,原有类即为父类,新类即为子类。Java中的每个类都有父类,如果不含父类名,则默认其父类为Object类。</p><p>接口。接口也是一种复合数据类型,它是在没有指定方法实现的情况下声明一组方法和常量的手段,也是多态性的体现。</p><p>修饰符。规定了本类的一些特殊属性,它可以是下面这些关键字之一:(1)final——最终类,它不能拥有子类。如果没有此修饰符,则可以被子类所继承。(2)abstract——抽象类,类中的某些方法没有实现,必须由其子类来实现。所以这种类不能被实例化。如果没有此修饰符,则类中间所有的方法都必须实现。(3)public——公共类,public表明本类可以被所属包以外的类访问。如果没有此修饰符,则禁止这种外部访问,只能被同一包中的其他类所访问。</p><p>final和abstract是互斥的,其他情况下可以组合使用。下面声明了一个公共最终类,它同时还是Human的子类,并实现了professor接口:public final class Teacher extends Human implements professor3.2.3 创建类体</p><p>类体中定义了该类所有的变量和该类所支持的方法。通常变量在方法前面部分定义(并不一定要求)。方法分为构造方法和成员方法,如下所示:</p><p>从以上代码中可以看出,类体中有3个部分:成员变量、成员方法和构造方法。其中,成员变量又被称作属性,成员方法又被称作行为,二者也可统称为类的成员。</p><p>下例定义了一个Point类,并且声明了它的两个变量x、y坐标。定义了一个成员方法move(),可以改变x、y的值。定义了一个构造方法Point(),可以为x、y赋初值。【例3.1】 一个简单的Point类。</p><p>//-----------文件名Point.java,程序编号3.1------------------</p><p>类中所定义的变量和方法都是类的成员。对类的成员可以规定访问权限,来限定其他对象对它的访问。访问权限有以下几种:private、protected、pubic和friendly,这些将在3.3节中详细讨论。同时,对类的成员来说,又可以分为实例成员和类(静态)成员两种,这些将在3.8节中详细讨论。</p><p>至此,类完整的声明和定义形式已经全部给出。注意类体定义中,并非每个部分都需要。一个最简单的类可能是下面这个样子,它什么事情也不能做。class empty {}3.2.4 对象的生命周期</p><p>定义类的最终目的是要使用它。一般情况下,要使用类需要通过类的实例——对象来实现。在定义类时,只是通知编译器需要准备多大的内存空间,并没有为它分配内存空间。只有用类创建了对象后,才会真正占用内存空间。</p><p>Java的对象就像有生命的事物一样,也要经历3个阶段:对象的创建、对象的使用和对象的清除。这被称为对象的生命周期。对象的生命周期长度可用如下的表达式表示:T=T1 + T2 +T3</p><p>其中,T1表示对象的创建时间,T2表示对象的使用时间,而T3则表示其清除时间。由此可以看出,只有T2才是真正有效的时间,而T1和T3则是对象本身的开销。3.2.5 对象的创建</p><p>创建一个对象也被称为实例化一个类。它需要用到下面的语句:类名 对象名=new 构造方法名([参数列表]);</p><p>例如,使用例3.1的Point类来创建一个对象p:Point pt=new Point (1,2);</p><p>该语句创建了一个新对象,它的名字叫做pt。new是Java的关键字,用于创建对象。表达式new Point(1,2)创建了一个Point类的实例对象,它同时指明坐标值为(1,2)。对象的引用被保存在变量pt中。</p><p>它虽然是一条语句,但实际上包括3个步骤:声明、实例化和初始化。一般情况下,创建和使用对象都要经过这3个步骤。</p><p>1. 声明对象</p><p>对象的声明和基本类型的数据声明在形式上是一样的:类名 对象名;</p><p>对象名也是用户标识符,和基本类型的变量遵循同样的命名规则和使用规则。例如,声明一个Point类型的变量pt:Point pt;</p><p>和C++不同,Java中像上面这样声明一个变量,并不会分配一个完整的对象所需要的内存空间,这一点也和简单数据类型的变量不同。它会将pt看成是一个引用变量,并为它分配所需内存空间,它所占用的空间远远小于一个Point对象所需要的空间。</p><p>如此处理,使得Java中声明一个对象的消耗很小,但也有一个副作用,就是对象不能马上使用,还需要对它进行实例化。</p><p>2. 实例化对象</p><p>Java中使用new关键字创建一个新对象,即进行实例化。格式如下:new 构造方法([参数列表])</p><p>实例化的过程就是为对象分配内存空间的过程,此时,对象才成为类的实例。new所执行的具体操作是调用相应类中的构造方法(包括祖先类的构造方法),来完成内存分配以及变量的初始化工作,然后将分配的内存地址返回给所定义的变量。</p><p>使用new创建Point对象的示例如下:pt=new Point (1,2);</p><p>用new来创建对象需要比较大的时间开销,远远比声明一个对象的消耗要大得多。一些常见操作的时间消耗如表3.1所示。表3.1 一些操作所耗费时间的对照表</p><p>从表3.1中可以看出,新建一个对象需要980个单位的时间,是本地赋值时间的980倍,是方法调用时间的166倍,而若新建一个数组所花费的时间就更多了。</p><p>3. 初始化对象</p><p>当一个对象生成时,通常要为这个对象确定初始状态,这个过程就是对象的初始化。由于创建对象是通过类及其祖先类的构造方法来进行的,所以初始化工作也会在这里完成。</p><p>注意到前面的这个说明:类名 对象名=new 构造方法名([参数列表]);</p><p>其中的参数列表就是传递给构造方法的一些值。构造方法获得这些值后,就可以为成员变量赋初始值。比如:Point pt=new Point (1,2);</p><p>它会将成员变量x和y分别赋值为1和2。Java还规定,如果成员变量没有被显示地赋初值,系统将自动为它们赋初值。具体规定为:所有的简单变量除boolean类型外均赋初值为0,boolean类型赋初值为false,其他类型的对象均赋初值为null。3.2.6 对象的使用</p><p>创建对象之后,就可以开始使用对象了。所谓使用对象,就是通过消息来调用成员方法,或者直接读取或修改成员变量,二者的结果都是获取或改变对象的状态。下面简单介绍这两种方法。</p><p>1. 对象变量的使用</p><p>要使用对象的变量,需要先创建对象。如果成员变量的访问权限允许,可以直接利用下面的方法:对象名.成员变量名</p><p>仍以Point的对象pt为例,可以这么使用它的成员变量:pt.x=6;pt.y=6;</p><p>这样就将它的两个成员变量x和y都置为6。☆说明:这样直接通过对象来使用成员变量,违反封装原则,除非确有必要,否则最好不要使用。其实多数类的成员变量在定义时会用private来修饰,根本就无法使用这种方法来访问。</p><p>2. 对象方法的调用</p><p>大多数的成员方法都是被设计供外部调用的。它的一般语法格式为:对象名.成员方法名([参数列表])</p><p>例如,要将点pt从原来的坐标(1,1)移动到(6,6),这需要调用它的成员方法move():pt.move(5,5)</p><p>它的效果与前面的pt.x=6;pt.y=6完全一样,不过这么做要安全得多,符合封装原则。3.3 成员变量的定义与使用</p><p>成员变量又称为成员属性,它是描述对象状态的数据,是类中很重要的组成成分。本节详细讨论如何来定义成员变量、成员变量的访问权限,以及静态成员变量与实例成员变量之间的区别。3.3.1 成员变量的定义</p><p>在第2章中,已经介绍和使用过变量。不过那些变量都是定义在某个方法中,被称为局部变量。成员变量是定义在类里面,并和方法处于同一层次。定义成员变量的语法如下:[变量修饰符]类型说明符 变量名</p><p>类的成员变量和在方法中所声明的局部变量都是用户标识符,它们的命名规则相同。变量修饰符是可选项。一个没有变量修饰符的变量定义如下:</p><p>成员变量的类型可以是Java中的任意数据类型,包括基本类型、数组、类和接口。在一个类中,成员变量应该是唯一的,但是成员变量的名字可以和类中某个方法的名字相同,例如:</p><p>其中,方法x()和变量x具有相同的名字,但笔者不赞成这样写,因为这会引起不必要的混淆。</p><p>可以用成员变量修饰符来规定变量的相关属性,这些属性包括:□ 成员变量的访问权限。一共有4种访问权限可供选择,在3.3.2小节将详细介绍。□ 成员变量是否为静态。默认情况下,成员变量是实例成员,在外部需要通过对象才能操作。如果用static修饰,就成为了静态成员,也称为类变量,无需通过对象就可以操作。□ 是否为常量。默认的是变量,如果前面加上final关键字,它就是一个常量。</p><p>这些修饰符可以任意组合使用。加上修饰符的成员变量如下所示:</p><p>虽然Java并没有规定,成员变量必须定义在类的开始部分,不过在实际编程中,多数程序员将成员变量定义在成员方法的前面。3.3.2 成员变量的访问权限</p><p>访问权限修饰符声明了成员变量的访问权限。Java提供的显示的访问权限修饰符有3种,分别是:私有(private)、保护(protected)和公共(public)。除此之外,还有一种默认的访问权限:friendly,它并不是Java的关键字,只有当变量前面没有写明任何访问权限修饰符时,就默认以friendly作为访问权限。为了表达上的方便,省略了其中“成员”两字,将被这些修饰符所修饰的变量分别称为私有变量、保护变量和公共变量。下面分别讨论各个修饰符的用法。</p><p>1. 公共变量</p><p>凡是被public修饰的成员变量,都称为公共变量,它可以被任何类所访问。即允许该变量所属的类中所有的方法访问,也允许其他类在外部访问。如例3.2所示。【例3.2】 公共变量使用示例。</p><p>//-----------文件名declarePublic.java,程序编号3.2------------------</p><p>在类declarePublic中声明了一个公共变量publicVar,它可以被任何类所访问。下面这段程序中,类otherClass可以合法地修改变量publicVar的值,而无论otherClass位于什么地方。</p><p>//-----------文件名otherClass.java,程序编号3.3------------------</p><p>用public修饰的变量,允许任何类在外部直接访问,这破坏了封装的原则,造成数据安全性能下降,所以除非有特别的需要,否则不要使用这种方案。</p><p>2. 私有变量</p><p>凡是被private修饰的成员变量,都称为私有变量。它只允许在本类的内部访问,任何外部类都不能访问它。【例3.3】 私有变量使用示例。</p><p>//-----------文件名declarePrivate.java,程序编号3.4------------------</p><p>如果企图在类的外部访问私有变量,编译器将会报错。</p><p>//-----------文件名otherClass.java,程序编号3.5------------------</p><p>为了让外部用户能够访问某些私有变量,通常类的设计者会提供一些方法给外部调用,这些方法被称为访问接口。下面是一个改写过的declarePrivate类。</p><p>//-----------文件名declarePrivate.java,程序编号3.6------------------</p><p>私有变量很好地贯彻了封装原则,所有的私有变量都只能通过程序员设计的接口来访问,任何外部使用者都无法直接访问它,所以具有很高的安全性。但是,在下面这两种情况下,需要使用Java另外提供的两种访问类型。□ 通过接口访问私有变量,将降低程序的性能,在程序性能比较重要的情况下,需要在安全性和效率间取得一个平衡。□ 私有变量无法被子类继承,当子类必须继承成员变量时,需要使用其他的访问类型。</p><p>3. 保护变量</p><p>凡是被protected修饰的变量,都被称为保护变量。除了允许在本类的内部访问之外,还允许它的子类以及同一个包中的其他类访问。子类是指从该类派生出来的类。包是Java中用于管理类的一种松散的集合。二者的详细情况将在第4章介绍。下面是一个简单的例子。【例3.4】 保护变量使用示例。</p><p>下面这个程序先定义一个名为onlyDemo的包,declarProtected类就属于这个包。</p><p>//-----------文件名declareProtected.java,程序编号3.7------------------☆说明:读者编译这个文件时,需要用这个命令(下同):javac -d . 文件名</p><p>下面这个otherClass类也定义在onlyDemo包中,与declareProtected类同属于一个包。</p><p>//-----------文件名otherClass.java,程序编号3.8------------------</p><p>下面这个deriveClass类是declareProtected的子类,它并不在onlyDemo包中。它也可以访问保护变量protectedVar,但是只能通过继承的方式访问。</p><p>//-----------文件名declareProtected.java,程序编号3.9------------------☆说明:import是Java中的关键字,用于引入某个包。这将在4.7节中详细介绍。</p><p>子类如果不在父类的同一包中,将无法通过“对象名.变量名”的方式来访问protected类型的成员变量,比如下面这种访问是非法的。</p><p>//-----------文件名deriveClass.java,程序编号3.10-----------------</p><p>4. 默认访问变量</p><p>如果在变量前不加任何访问权修饰符,它就具有默认的访问控制特性,也称为friendly变量。它和保护变量非常像,它只允许在同一个包中的其他类访问,即便是子类,如果和父类不在同一包中,也不能继承默认变量(这是默认访问变量和保护变量的唯一区别)。因为它限定了访问权限只能在包中,所以也有人称默认访问权限为包访问权限。【例3.5】 默认访问变量使用示例。</p><p>//-----------文件名declareDefault.java,程序编号3.11------------------</p><p>onlyDemo包中的其他类,可以访问defaultVar变量。</p><p>//-----------文件名otherClass.java,程序编号3.12------------------</p><p>下面是它的子类,也在onlyDemo包中。它除了可以像包中其他类那样通过“对象名.变量名”来访问默认变量,还可以通过继承的方式来访问。</p><p>//-----------文件名deriveClass.java,程序编号3.13------------------</p><p>如果子类不在onlyDemo包中,就不会继承默认变量,也就无法像上面那样来访问。</p><p>//-----------文件名deriveClass.java,程序编号3.14------------------3.3.3 实例成员变量和静态成员变量</p><p>1. 实例成员变量</p><p>在3.3.2小节中,所有的对象都是实例成员变量。它们的最大特色是:如果所属的对象没有被创建,它们也就不存在。如果在类的外部使用它,需要先创建一个对象,然后通过“对象名.变量名”来访问。前面所有的例子都遵循了这一规则。在类的内部,实例成员方法也可以直接访问实例成员变量,比如例3.5,具体原因,将在3.5节中讲述。</p><p>不同的对象,拥有不同的实例成员变量,它们互不干扰。【例3.6】 不同对象的实例成员变量使用示例。</p><p>//-----------文件名instanceVar.java,程序编号3.15------------------</p><p>下面这个showInstVar类用两个对象来访问它的实例成员变量。</p><p>//-----------文件名showInstVar.java,程序编号3.16------------------</p><p>程序3.16输出的结果如下:one.instVar=100two.instVar=200</p><p>从本例中明显地看出,不同对象的成员变量是不相同的,它们互不干涉。</p><p>2. 静态成员变量</p><p>在某些情况下,程序员希望定义一个成员变量,可以独立于类的任何对象,即所有的对象都共用同一个成员变量。由于Java中不能像C一样定义全局变量,因此,Java中引入了静态成员变量。</p><p>在成员变量前加上static标识符就可以定义一个静态成员变量。相对于实例成员变量,静态成员变量具有以下特点:□ 它被类的所有对象共享,因此又被称为类变量。□ 它不是属于某个具体对象,也不是保存在某个对象的内存区域中,而是保存在类的公共存储单元。因此,可以在类的对象被创建之前就能使用。□ 它既可以通过“对象名.变量名”的方式访问,也可以通过“类名.变量名”的方式访问。它们是完全等价的。【例3.7】 静态成员变量使用示例。</p><p>//-----------文件名staticVar.java,程序编号3.17------------------</p><p>下面这个程序使用不同的方法来访问这个静态变量。</p><p>//-----------文件名showStaticVar.java,程序编号3.18------------------</p><p>程序3.18输出结果如下:staticVar.stat=100one.stat=300two.stat=300staticVar.stat=300</p><p>从上述结果中可以看到,静态变量stat是一个公共变量,无论哪个对象改变了它的值,对其他所有该类对象都有效。静态变量的一个重要作用是当作同类各个对象之间传递信息使用,类似于C语言中的全局变量。但这样破坏了数据的封装原则,往往会留下隐患,所以使用这类变量时需要万分谨慎。</p><p>静态变量的另一个用途是定义静态常量,比如:public static double PI=3.1415926;</p><p>这样的静态常量可以无需创建对象就直接使用,省略了创建对象的步骤,类似于C语言中用define定义的常量。这样定义常量,不仅使用方便,而且节省内存空间。在JDK中,存在着大量的这种静态常量。☆说明:本节中所有的成员变量的类型都是基本类型,其实它们也都可以是复合类型,比如数组、类和接口等类型。3.4 方法的定义和实现</p><p>在类中,除了变量以外,另一个重要的组成成分就是方法。方法是用来实现类的行为,其实相当于C语言中的函数,但它一般只对类中的数据进行操作。一个方法,通常只完成某一项具体的功能,这样做使得程序结构清晰,利于程序模块的重复使用。</p><p>可以把方法看成一个“黑盒子”,方法的使用者只要将数据送进去就能得到结果,而方法内部究竟是如何工作的,外部程序是不知道的。外部程序所知道的仅限于输入给方法什么,以及方法输出什么。Java中没有限制一个类中所能拥有的方法的个数。如果说有什么限制,就是只有main方法可以作为应用程序的入口。</p><p>本书前面的例子已经使用过很多方法,比如System.out.println、Math.random以及main方法。其中,某些方法(比如前两个)是系统已经定义在类中的标准方法,可以直接拿来使用;而另外一些方法(比如main方法)则需要由程序员自己来编写方法体,被称为自定义方法。本节将讲述如何来声明和定义一个自定义方法。3.4.1 方法的声明</p><p>方法声明,就是声明一种新的功能,或者说创造一种新的功能。在Java中,除了抽象方法之外,它通常和方法的定义在一起,位于方法体的前面。一个方法的声明,通常格式如下:[方法修饰符][方法返回值类型] 方法名([形式参数表])</p><p>方法修饰符和成员变量的修饰符一样,有访问权限修饰符、final和static 3种。□ 访问权限修饰符又包括private、protected、public和默认。它们的含义也和成员变量中的完全相同。□ final表示最终方法,将在4.6节中讲述。□ static表示静态方法,将在3.7节中讲述。□ 方法的返回值类型也和成员变量的数据类型一样,可以是基本类型:int、char和double等,也可以是类类型。□ 返回值类型可以是void,表示没有返回值。□ 形式参数列表是方法可以接收的参数,它由外部调用者提供具体的值。参数可以有多个,中间用逗号分隔。也可以没有参数,但圆括号不能省略。【例3.8】 几个方法声明的例子。public int max(int a,int b)</p><p>本方法是一个公共方法,将返回一个整型值。调用者需提供两个整型参数。private void setX(double x)</p><p>本方法是一个私有方法,它没有返回值,调用者需提供一个双精度参数。protected double getX()</p><p>本方法是一个保护方法,它有一个双精度返回值,调用者无需提供参数。3.4.2 创建方法体与return语句</p><p>方法体中包含了各种Java语句,它是完成方法功能的主体。它紧跟在方法的声明之后,用一对“ {}”括起来。方法体和方法的声明合起来,称为方法的定义。</p><p>方法体中除了可执行语句外,也可以有属于本方法的局部变量。本小节以前的例子中,多数都是这种情况。下面是一个简单的例子。【例3.9】 方法体定义的例子。</p><p>//-----------文件名showMethod.java,程序编号3.19------------------</p><p>程序3.19不能直接由系统运行,因为它没有main方法,但它仍然是一个合法的类,可以用来创建对象。</p><p>在方法getX()中,有一条语句“return x”。return是Java中的关键字,它构成的语法格式为:return[表达式];</p><p>Java规定,任何一个返回值不为void的方法,都必须至少有一条return语句。return后面的表达式类型必须与返回类型相容。这里的“相容”是指:或者类型完全相同,或者表达式的类型可以经过扩展类型转换成与返回类型相同的类型。否则编译器就会报错。</p><p>如果是void类型的方法,则可以不需要return语句。如果有return语句,则return语句后面的表达式应为空。【例3.10】 return语句使用示例1。</p><p>//-----------文件名showReturn_1.java,程序编号3.20------------------</p><p>在一个方法中,如果没有return语句,则该方法一直执行到最后一条语句完毕后,返回调用者调用的位置。如果有return语句,则只要执行到return语句,立即返回调用者,无论该return语句后面还有多少语句,都不会被执行。一个方法中可以有多条return语句,但一次调用时,只能执行某一条return语句,其余的无效。【例3.11】 return语句使用示例2。</p><p>//-----------文件名showReturn_2.java,程序编号3.21-----------------3.4.3 局部变量和成员变量的区别</p><p>在方法内部可以定义变量,被称为局部变量。局部变量的一般形式如下:[变量修饰符]变量类型 变量名;□ 变量修饰符可以是final,表示这是常量。□ 变量类型可以是Java中任意合法的基本类型或复合类型。□ 变量名是用户自定义标识符,遵循标识符的一般规则。□ 可以在一行中定义多个局部变量,以逗号分隔。□ 定义变量时可以同时赋初值。□ 局部变量必须要先定义后使用。</p><p>例如,下面就是一些局部变量的定义:final double PI=3.1416;int ix,iy;final int MAIL=0;</p><p>从形式上看,局部变量和类的成员变量十分相似,但在使用上它们的区别很大。□ 局部变量没有访问权限修饰符,不能用public、private和protected来修饰。这是因为它只能在定义它的方法内部使用。□ 局部变量不能用static修饰,没有“静态局部变量”,这是Java和C/C++的一个细微差别。□ 系统不会自动为局部变量赋初值,但对于成员变量,系统会自动赋初值。基本类型的初值为0,复合类型的初值为null。□ 局部变量的作用域仅限于定义它的方法,在该方法的外部无法访问它。成员变量的作用域在整个类内部都是可见的,所有成员方法都可以使用它。如果访问权限允许,还可以在类的外部使用成员变量。□ 局部变量的生存周期与方法的执行期相同。当方法执行到定义局部变量的语句时,局部变量被创建;执行到它所在的作用域的最后一条语句时,局部变量被销毁。类的成员变量,如果是实例成员变量,则它和对象的生存期相同。而静态成员变量的生存期是整个程序运行期。□ 在同一个方法中,不允许有同名的局部变量。在不同的方法中,可以有同名的局部变量,它们互不干涉。□ 局部变量可以和成员变量同名,且在使用时,局部变量具有更高的优先级。【例3.12】 局部变量使用示例。</p><p>//-----------文件名localVariable.java,程序编号3.22-----------------【例3.13】 局部变量与成员变量同名问题示例。</p><p>//-----------文件名localVSmember.java,程序编号3.23-----------------</p><p>在程序3.23中,同名的局部变量会屏蔽掉成员变量。为了访问被屏蔽的成员变量,需要使用一个前缀this,它表示的是“本对象”。关于this的详细用法,将在3.5.3小节中介绍。3.4.4 方法的访问权限</p><p>方法与成员变量一样,都是类的成员,因此它和成员变量一样,也有访问权限的问题。方法的访问权限也有4种:public、private、protected和默认,而且它们和成员变量的规则完全一样,这里不再重复举例。</p><p>关于成员的各种权限的访问规则,可以总结成表3.2。表3.2 各种访问权限的规则</p><p>表3.2中的“√”表示可以访问,“×”表示不能访问。不在同一个包中的子类,可以继承父类中的protected成员方法和成员变量,并且通过这种方式来访问。但不允许通过“对象名.方法名()”的方式来访问。</p><p>从表3.2中可以看出,在类的内部使用成员时,根本无需考虑访问权限的问题。在外部访问成员时,public的限制最宽松,private的限制最严格,protected和默认的限制介于两者之间。初学者不必死记这些访问限制,通过大量的编程实践,将逐步掌握其中的规律。3.5 方法的调用</p><p>多数情况下,使用方法需要进行显式的方法调用。方法被调用之后,就会执行方法体内部的语句,完成预定义的功能。3.5.1 方法调用的形式</p><p>根据方法的调用者与被调用的方法所处的位置,方法调用的形式可以分为两种。□ 调用者和被调用方法位于同一类中,形式如下:[this.]方法名([实际参数列表])</p><p>在大多数情况下,关键字this可以省略。□ 调用者位于被调用方法所在类的外部,形式如下:对象名.方法名([实际参数列表]) 或者 类名.方法名([实际参数列表])</p><p>实际参数列表是对应方法的形式参数列表,可以是0个或多个变量或表达式,如果超过1个,需用逗号分隔。</p><p>下面是方法调用的两个例子。【例3.14】 同一类中调用方法示例。</p><p>//-----------文件名invokeMethod.java,程序编号3.24-----------------</p><p>程序的输出如下:This is showMsg method</p><p>在程序3.24中,方法callOther()和方法showMsg()处在同一个类中,所以调用后者时,直接使用方法名就可以。</p><p>在main()方法中,调用callOther()方法时,需先创建一个对象ob,再用“对象名.方法名()”的格式来调用该方法,这么做是因为main()方法是一个静态方法,它由系统来调用。系统在调用它的时候,并没有创建一个invokeMethod的对象,而callOther()和showMsg()方法都是实例方法,它们被调用时,都必须有对象的存在。所以必须在main中先创建一个对象ob,才能调用这两个方法。从这一点来看,main方法虽然处在invokeMethod类的内部,但它的表现却如同在类的“外部”一样。</p><p>这么解释,读者可能还会有疑惑:为什么callOther()又能够直接调用showMsg()方法,难道它能保证在调用后者时,对象已经存在?答案确实如此,因为callOther()本身是实例方法,它在被执行时,一定是有对象存在的。基于这个前提,它才能够直接调用showMsg()方法。【例3.15】 外部类调用方法示例。</p><p>这里仍然利用程序3.24,另外再写一个类来使用invokeMethod类中的两个方法。</p><p>//-----------文件名invokeOther.java,程序编号3.25-----------------☆注意:需要将invokeMethod.java和invokeOther.java两个方法放在同一个目录下面,然后分别编译。后面如无特殊说明,需要用到两个或两个以上文件的,都必须放在同一目录下编译。</p><p>程序3.25和程序3.24的输出结果完全一样。细心的读者还会发现,在invokeOther类中的main()方法和invokeMethod类中的main()方法代码完全一样。在3.7和3.8节中,还将进一步解释这一现象。</p><p>在invokeOther类中,还可以调用showMsg()方法,形式还是ob.showMsg()。读者可以自己改动程序3.25查看效果。3.5.2 方法调用的参数</p><p>在定义一个方法时,程序员可能会根据需要列出一个参数表,这些参数被称为形式参数,简称为形参。在调用方法时,需要调用者提供与之相匹配的参数表,被称为实际参数,简称为实参。</p><p>这里的匹配有两个条件:□ 实参和形参的个数要相等。□ 实参和形参对应位置上的数据类型要相容。即数据类型相同,或者实参可以做自动类型转换转换成形参类型。</p><p>在方法调用发生时,系统会将实参的值按照位置关系一个一个传递给形参,即第一个实参传给第一个形参,第二个实参传给第二个形参,……这个过程中,不会考虑形参和实参的名字。如图图3.5 方法调用的传值过程3.5所示。</p><p>由于在Java中存在两种类型的数据:基本类型和复合类型。这两种类型的数据作为参数传递时,是有区别的。本小节将分别介绍这两种情况。</p><p>1. 基本类型作为参数</p><p>当方法的参数是基本类型(包括整型、浮点和布尔型)时,它是通过传值方式进行调用的。这种传递方式的特点是:□ 它所传递的实参的值是一个副本。□ 单值传递。实参本质上是一个可求值的表达式,它所求出来的值是一个基本类型。□ 单向传递。方法内部可以修改形参的值,但这种修改不会影响到对应的实参。</p><p>直观来看,传值过程相当于一个赋值过程,实参是右值,形参是左值。它们发生联系只在调用的那一瞬间,以后二者之间再无关系。【例3.16】 单向传值示例。</p><p>//-----------文件名invokeByValue.java,程序编号3.26-----------------</p><p>程序的输出如下:调用tryChange方法之前,ix=10调用tryChange方法之后,ix=10</p><p>从本例中可以看出,尽管在tryChange()方法中,改变了形参ix的值,但对于实参ix并没有影响。从这个例子还可以看出,形参实际上是一个局部变量,它的作用域仅限于定义它的方法体内部,实参的名字是否和它相同都没有影响。</p><p>单向传值可以防止程序员在无意的情况下改变实参的值,起到了降低程序间数据耦合度的作用。但在某些情况下,单向传值却会阻碍某些功能的实现。比如,要写一个方法实现两个参数交换值的功能。初学者可能会写成下面这个样子:</p><p>然后这样来调用它:swap (a,b);</p><p>很不幸,调用过后,会发现,a和b的值没有任何改变。因为在方法swap中交换的只是形参a和b的值,这对于实参a和b来说,没有任何影响。</p><p>实际上,在Java中,没有任何简单的方法能够实现上述交换两个基本变量的值,而只能把上面这段代码写在需要交换的地方。</p><p>2. 复合类型作为参数</p><p>如果形式参数不是基本类型,而是复合类型,比如类类型,那么实参和形参的表现行为和基本类型的参数会有一些区别。</p><p>如果实参是一个类的对象,那么在调用相应的方法时,系统会将该对象的地址值传递给形参。例如,有一个类onlyTest,actual是它的一个对象作为实参,form是它定义的形参对象,则调用时的传值情形如图3.6所示(假定类实例在图3.6 对象传值调用示例内存中的存储地址为0x00ff)。</p><p>在Java中虽然没有“指针”这一概念,程序员也不需要掌握它,但在系统内部,仍然是存在指针的。图3.6就是指针运用的示例。actual和form指向了同一个对象实例,其中任何一个变量改变类实例中的值,都会对另外一个变量有所影响。</p><p>对象的传值过程,其实是借用了C/C++中指针传值的方法,造成的效果也完全相同。下面这个例子展示了对象传值的效果。【例3.17】 对象传值示例。</p><p>//-----------文件名onlyTest.java,程序编号3.27-----------------</p><p>下面这个程序使用上面这个类,分别声明了一个实参和一个形参。</p><p>//-----------文件名invokeByObject.java,程序编号3.28-----------------</p><p>在程序3.28中,showDiffer()方法先定义一个actual对象,并将成员x的值置为100。而后调用方法tryChange(),它的形参form接收actual的值,根据图3.6所示内容,它们将共用同一个对象。在tryChange()中改变了form的x值,这一改变,对actual也是有效的。程序的输出印证了这一点:调用tryChange方法之前,x=100调用tryChange方法之后,x=200</p><p>由于C++中提供了传值调用和引用调用两种方式,于是有些程序员也认为Java的对象参数是采用的引用调用。这其实是一种误解,Java采用的是传地址值的调用方式,在某些情况下,虽然和引用调用效果相同(比如上例),但在另外一些情形下,还是可以看出两者的区别。下面这个例子说明了这一区别。【例3.18】 对象传地址值而非引用示例。</p><p>这里仍然使用例3.17中的类onlyTest,再另外编写一个程序trySwap。</p><p>//-----------文件名trySwap.java,程序编号3.29-----------------</p><p>在方法swap()中,形参是两个onlyTest的对象。如果是引用调用,那么交换这两个对象的值,将对实参ox和oy产生影响。程序实际运行后输出结果如下:</p><p>从以上输出结果中可以看出,ox和oy的值没有受到丝毫影响,因此它不是引用调用。调用过程可以用图3.7和图3.8来说明。图3.7 调用swap()时的传值过程图3.8 执行swap()之后的情形</p><p>补充说明一下:若有对象A和B,执行语句:A=B,则A和B都指向了同一个对象,它们的行为与上述参数传递的行为完全相同。☆注意:通过上述分析可以看出,在Java中,虽然没有出现显示的指针,也没有指针这个概念,但用普通类声明的变量,本质上和C/C++中的对象指针是一样的。而且,Java中也没有和C++中的引用类型完全等效的概念。</p><p>最后总结一下方法参数的使用情况:□ 方法不能修改一个基本数据类型的参数;□ 方法可以改变一个对象参数的状态;□ 方法不允许让一个对象参数引用一个新的对象。3.5.3 隐含参数this</p><p>回顾3.4.3小节中的例3.13,当方法中的局部变量和成员变量同名时,局部变量会屏蔽掉同名的成员变量。为了访问该成员变量,需要使用“this.成员变量”的形式。</p><p>这个this是Java定义中的一个关键字。为了让程序员能够在方法中使用this,Java会将this作为一个隐含的参数传递给每一个实例方法。它其实是指向当前对象的一根指针,直观地理解,它就是表示“本对象”的意思。</p><p>this作为隐含参数传递,最重要的作用是区分各个对象所拥有的成员。先来回顾3.5.2小节程序3.27中的类onlyTest,它拥有一个成员变量x,两个方法——setX()和getX()。程序员可以使用这个类来创建若干个对象,这些对象分别拥有自己的成员变量,相互之间不会干扰。如例3.19所示。【例3.19】 使用类onlyTest创建多个对象示例。</p><p>//-----------文件名useOnlyTest.java,程序编号3.30-----------------</p><p>程序中分别为两个对象oa和ob的成员变量赋了不同的值,然后再分别显示它们的值。程序的输出结果如下:oa的成员变量 x=100ob的成员变量 x=200</p><p>这个结果完全在预料之中。但如果深入研究一下,还是会存在一些疑问:到底系统是如何来管理这些对象的?显然,不同对象的成员变量一定是单独存放的,那么当它们都调用setX()方法的时候,这个方法如何知道要为哪一个对象的成员变量x赋值?一种简单的解决办法,是让每个对象的成员方法也单独存放,并且和成员变量存放在一起,它只处理本对象的成员变量。但这种方法实在是太笨,因为为每个对象存储一套成员方法(而且这些方法的执行语句是完全一样的)需要大量的空间,完全不符合代码重用的原则。所以,所有对象共用一套成员方法显然要经济高效得多。但这样又会带来一个问题,就是这些方法怎样才能区分目前要处理的是哪一个对象的成员变量。</p><p>解决的答案就是this关键字。系统会将this指向当前对象,然后作为参数传递给成员方法。在方法访问成员变量时,系统会自动为成员变量加上一个this作为前缀,这样就可以区分是哪个对象的成员变量。当然,程序员也可以显式地加上this作前缀。比如,onlyTest类与下面这种形式等价:</p><p>对于方法:public void setX(int ix)</p><p>系统会自动加上形参this,如下:public void setX(onlyTest this,int ix)</p><p>当通过oa.setX(100)来调用方法时,系统生成的是oa.setX(oa,100),这样就很好地解决了区分各个对象成员的问题。3.6 构造方法</p><p>构造方法是类中一种特殊的方法,它一般由系统在创建对象(即类实例化)时自动调用。构造方法是对象中第一个被执行的方法,主要用于申请内存以及对类的成员变量进行初始化等操作。构造方法虽然也位于类里面,但在很多情况下与普通成员方法表现不同,所以也有人认为它不是成员方法,而且将其称为“构造器”。本书仍然沿用通常的称呼,将其称为构造方法。构造方法的一般形式为:</p><p>其中,this是调用其他的构造方法,super是调用父类的构造方法。它们都必须放在其他语句的前面。</p><p>编写式构造方法要注意以下几点。□ 构造方法的名字必须和类的名字完全相同。□ 除了访问权修饰符之外,不能有其他任何修饰符,也就不能有返回值。□ 尽管没有返回值,但并不能用void修饰。□ 构造方法不能用static和final来修饰。一般也不用private修饰,这会导致无法在外部创建对象。□ 构造方法不能由对象显式地调用。一般通过new关键字来调用,或者用this和super来调用。□ 构造方法的参数列表可以为空,也可以有参数。根据参数的有无,可以将构造方法分为无参数的构造方法和带参数的构造方法。□ 用户定义的类可以拥有多个构造方法,但要求参数列表不同。□ 当用户定义的类未提供任何构造方法时,系统会自动为其提供一个无参数的构造方法。3.6.1 无参数构造方法的定义和使用</p><p>定义一个无参数的构造方法,从语法上来讲很简单,请看下面的示例。【例3.20】 无参数的构造方法示例。</p><p>//-----------文件名constructNoPara.java,程序编号3.31-----------------</p><p>调用构造方法使用的是new constructNoPara(),这是一种隐式的调用方法,不能写成oa.constructNoPara()的形式。</p><p>注意到成员变量x,它在定义的时候已经赋了初值。在构造方法中,先是输出一条信息,然后再次为x赋值。由于构造方法的执行在定义成员变量之后,它会覆盖掉原来x的初值,所以x的值为100。程序的输出结果如下:这是无参数的构造方法x=100</p><p>对于初学者而言,最容易犯的错误是在构造方法之前加上void,变成下面这个样子:</p><p>这个程序仍然可以通过编译,但运行结果可能会出人意料。它的输出结果如下:x=0</p><p>这表明,程序员自己定义的无参数的构造方法根本就没有执行。这是因为加上void修饰符之后,constructNoPara()不再是一个构造方法,而成了一个普通方法。</p><p>语句constructNoPara oa=new constructNoPara();并不是调用程序员自己定义的“构造方法”,而是调用了系统提供的默认的无参数的构造方法,这个方法其实什么事情也没做,自然也就不会更改x的值。☆说明:C++程序员不会犯此类错误。因为在C++中,如果在构造方法前面加上void,编译器将报错。☆注意:构造方法前的访问权限修饰符同样有4种,但通常不会是private类型。因为用它来修饰的话,将无法在外部使用该构造方法。3.6.2 带参数构造方法的定义和使用</p><p>在很多时候,需要根据不同的情况为成员变量赋不同的初值,这就需要传递参数给构造方法。因此,Java中允许定义带参数的构造方法,而且这种带参数的构造方法还可以定义多个(前提是参数列表有区别),这种现象被称为构造方法的重载。</p><p>Java规定,如果程序员一个构造方法都不定义,那么系统会自动为其加上一个不带参数的构造方法。如果程序员至少定义了一个构造方法,那么系统不会再提供不带参数的构造方法。</p><p>当用new来创建对象时,需要提供类型相容的参数,否则编译器将报错。【例3.21】 带参数的构造方法示例。</p><p>//-----------文件名constructWithPara.java,程序编号3.32-----------------</p><p>这个程序的流程和程序3.31完全一样,只是其中的构造方法多了一个参数而已。程序运行的结果如下:这是带参数的构造方法x=100</p><p>这个程序从表面上看没有什么问题,但实际上它存在着一个很大的隐患。如果将类constructWithPara提供给其他的程序员使用,使用者很有可能会按照一般的习惯这么来创建一个对象:

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载