内容


Java 编程介绍

Comments

关于本教程

本教程的内容

本教程向您介绍利用 Java 语言进行面向对象编程(object-oriented programming,OOP)。Java 平台是一个广泛的主题,所以在本教程中无法全面涵盖,但是起码能够让您可以开始编程。下一期的教程将会提供 Java 编程过程中所需的更多信息和指导。

Java 语言无疑有它的拥趸者和反对者,但是它对软件开发行业的影响是不可否认的。正面来讲,Java 语言比 C++ 对程序员的束缚要少。它减少了一些比较繁重的编程任务,比如显式内存管理,并允许程序员将精力集中于业务逻辑之上。负面来讲,据 OO 纯化论者所说,Java 语言具有太多的非 OO 残留痕迹,使得它无法成为一个好工具。但是,不管您的观点如何,当 Java 语言对于您的工作来说是一个合适的工具时,知道如何使用它作为工具总是一个明智的职业选择。

我应该阅读本教程吗?

本教程的内容适合于那些可能不熟悉 OOP 概念或者不特别熟悉 Java 平台的初级 Java 程序员。本教程假定读者对下载和安装软件、编程和数据结构(如数组)有一般的了解,但是不假定有些了解 OOP。

本教程将一步一步地指导您在自己的机器上设置 Java 平台,以及安装和使用 Eclipse,以编写 Java 代码。Eclipse 是一个免费的集成开发环境(integrated development environment,IDE)。从这里开始,您将学习 Java 编程的基本知识,包括 OOP 范型以及如何将之应用于 Java 编程;Java 语言的语法和使用;创建对象和添加行为,使用集合,处理错误;以及编写更好的代码的技巧。阅读完本教程,您将成为一个 Java 程序员——初级 Java 程序员,但是不管怎么说,也是一个 Java 程序员。

软件需求

要运行本教程中的例子或者示例代码,需要具有 1.4.2 及以上版本的 Java 2 Platform Standard Edition (J2SE),并且机器上安装了 Eclipse IDE。如果还没有安装这些软件包,也不用担心,我们将会在 开始 一节中告诉您怎么做。本教程中的所有代码例子都用运行在 Windows XP 上的 J2SE 1.4.2 测试过了。但是 Eclipse 平台的其中一个优点就是,它几乎可以运行在您所喜欢的任何 OS 上,包括 Windows 98/ME/2000/XP、Linux、Solaris、AIX、HP-UX,甚至还包括 Mac OS X。

开始

安装指导

在接下来的这几屏中,我将带您遍历下载及安装 1.4.2 版 Java 2 Platform Standard Edition (J2SE) 和 Eclipse IDE 的每一步。前者用于编译和运行 Java 程序。后者提供一个强大且用户友好的方式来用 Java 语言编写代码。如果已经安装了 Java SDK 和 Eclipse,那么您可以跳到 简短的 Eclipse 旅程 一节或者跳到下一节 OOP 概念 ,只要您觉得跳到那里合适即可。

安装 Java SDK

Java 语言最初的意图是让程序员编写一个可以运行在任何平台上的程序,其思想概括为一句精辟的话就是“一次编写,到处运行(Write Once, Run Anywhere,WORA)。在实际中,并没有这么简单,但是正在变得越来越简单。Java 技术的各种组件都支持这一努力方向。Java 平台有三种版本,即标准版(Standard)、企业版(Enterprise)和移动版(Mobile),后两者用于企业移动设备开发。我们将要使用的是 J2SE,它包含所有的核心 Java 库。您所要做的就是下载并安装它。

要下载 J2SE 软件开发工具包(software development kit,SDK),请执行以下步骤:

  1. 打开浏览器,并定位到 Java Technology 主页。在页面的中上部,将会看到各种 Java 技术主题区域的链接。选择 J2SE (Core/Desktop)
  2. 在当前 J2SE 版本的列表中,单击 J2SE 1.4.2
  3. 在结果页面的左导航栏中,单击 Downloads
  4. 在该页面上有好几个下载。找到并单击 Download J2SE SDK 链接。
  5. 接受许可的条件,并单击 Continue
  6. 将会看到一个按平台排列的下载列表。为您所使用的平台选择适当的下载。
  7. 将文件保存到硬盘。
  8. 当下载完成时,运行安装程序以在硬盘上安装 SDK,最好是安装在硬盘根目录下的一个良好命名(well-named)的文件夹中。

就这么简单!您现在的机器上就有了一个 Java 环境。下一步是安装集成开发环境 (IDE)。

安装 Eclipse

集成开发环境 (IDE) 隐藏了使用 Java 语言的大量琐碎的技术细节,所以您可以集中精力编写和运行代码。您刚才安装的 JDK 包含几个命令行工具,使您可以在没有 IDE 的情况下编译和运行 Java 程序,但是除了最简单的程序以外,用这些工具来编译和运行程序很快就会让人痛苦不堪。使用 IDE 隐藏细节,会带给您强大的工具,以助于您更快更好地编程,而且简直就是一种更加愉快的编程方式。

不再需要花钱购买优秀的 IDE 了。Eclipse IDE 是一个开放源代码项目,因而您可以免费下载。Eclipse 在保存在您的文件系统上的可读文件中保存和跟踪 Java 代码。(您也可以使用 Eclipse 来处理 CVS 存储库中的代码。)好消息是,只要您愿意,Eclipse 可以让您处理文件,如果您只想处理各种 Java 构造(比如类),Eclipse 可以隐藏文件细节(这将在后面详细讨论)。

下载和安装 Eclipse 很简单。请执行以下步骤:

  1. 打开浏览器并定位到 Eclipse Web 站点
  2. 单击页面左边的 Downloads 链接。
  3. 单击 Main Eclipse Download Site 链接,以定位到 Eclipse 项目下载页面。
  4. 将会看到构建类型和名称的列表。单击 3.0 链接。
  5. 在页面的中部,将会看到一个按平台排列的 Eclipse SDK 列表;选择合适您系统的一个 SDK。
  6. 将文件保存到硬盘。
  7. 当完成下载时,运行安装程序并在硬盘上安装 Eclipse,最好是安装在硬盘根目录下的一个良好命名的文件夹中。

现在剩下的工作就是设置 IDE 了。

设置 Eclipse

要使用 Eclipse 来编写 Java 代码,就必须告诉 Eclipse,Java 位于您机器上的哪里。请执行以下步骤:

  1. 双击 eclipse.exe 或者您平台上的等价可执行文件,从而启动 Eclipse。
  2. 当出现欢迎屏幕时,单击 Go To The Workbench 链接。这将把您带到所谓的 Resource 透视图(后面将做更多介绍)。
  3. 单击 Window>Preferences>Installed JREs,这将允许您指定 Java 环境安装在机器的什么地方(参见图 1)。

    图 1. Eclipse Preferences
    Preferences

    Preferences

  4. 很好,Eclipse 将会发现一个已安装的 Java 运行时环境(Java Runtime Environment,JRE),但是您应该显式地指向您在 安装 Java SDK 一节中安装的那个 JRE。这一切可以在 Preferences 对话框中完成。如果 Eclipse 列出一个现有的 JRE,那么单击它并按 Edit ,否则单击 Add
  5. 指定到您在 安装 Java SDK 一节中安装的 JDK 的 JRE 文件夹的路径。
  6. 单击 OK

Eclipse 现在已经设置好,可以编译和运行 Java 代码了。在下一屏中,我们将经历一次简短的 Eclipse 环境旅程,让您熟悉熟悉该工具。

简短的 Eclipse 旅程

使用 Eclipse 是一个很大的主题,多半超出了本教程的范围。请参阅 参考资料,其中有很多到 Eclipse 更多信息的链接。这里,我们将要介绍的只是够您熟悉 Eclipse 是如何工作的,以及如何将它应用于 Java 开发。

假设已经可以运行 Eclipse 了,现在就来看看 Resource 透视图。Eclipse 在您编写的代码上提供一组透视图。Resource 透视图显示正在使用的 Eclipse 工作空间内的文件系统的一个视图。工作空间 里保存着与 Eclipse 开发相关的所有文件。现在,工作空间里还没有您真正关心的东西。

一般来说,Eclipse 具有包含视图透视图。在 Resource 透视图中,将会看到 Navigator 视图、Outline 视图,等等。可以将这些视图拖放到任何您希望的地方。Eclipse 几乎是一个可无限配置的环境。但是就现在来说,默认的排列就足够好了。但是我们并不能随心所欲。在 Eclipse 中编写 Java 代码的第一步是创建一个 Java 项目。Java 项目并不是 Java 语言构造,而只是一个有助于组织 Java 代码的 Eclipse 构造。请执行以下步骤,创建一个 Java 项目:

  1. 单击 File>New>Project 以显示 New Project 向导(参见图 2)。这实际上是一个“向导的向导”,换句话说,这个向导让您选择使用哪个向导(New Project 向导、New File 向导,等等)。

    图 2. New Project 向导
    New Project

    New Project

  2. 确保选中了 Java Project 向导,并单击 Next
  3. 输入任意的项目名称(“Intro”就很好),保留所有默认选项,并单击 Finish
  4. 此时,Eclipse 应该会询问是否切换到 Java 透视图。单击 No

您刚才创建了一个叫做 Intro 的 Java 项目,在屏幕左上角的 Navigator 视图中应该可以看到该项目。在创建项目之后,我们没有切换到 Java 透视图,因为对于我们当前的目的,可以使用一个更好的透视图。在窗口右上角的选项卡上单击 Open Perspective 按钮,然后选择 Java Browsing 透视图。该透视图显示了容易地创建 Java 程序所需看到的东西。在创建 Java 代码时,我们将了解更多一些 Eclipse 特性,以便学习如何创建、修改和管理代码。但是在这之前,我们将在下一节介绍一些基本的面向对象编程概念。现在,我们来看一些在线 Java 文档,以结束本节内容。

Java API 在线文档

Java 应用编程接口(application programming interface,API)非常之多,所以学会如何找到合适的很重要。Java 平台足够地大,几乎可以提供程序员所需的任何工具。学习利用可能性需要花费与学习语言的结构一样多的精力。

如果定位到 Sun 的 Java 文档页面(参见 参考资料 中的链接),将会看到每个版本的 SDK 的 API 文档的一个链接。单击对应于版本 1.4.2 的那个链接,看其文档是什么样的。

将会在浏览器中看到三个框架:

  • 左上部是内置包的一个列表。
  • 左下部是所有类的一个列表。
  • 右边是所选择的详细信息。

SDK 中的每个类都在这里。选择类 HashMap。马上就会在右边看到这个类的描述。在上部会看到名称和它所在的包、它的类层次、它的已实现接口 (这超出了本教程的范围),以及它可能具有的任何直接子目录。在这之后,会看到该类的描述。有时,该描述包括例子用法、相关链接、样式推荐,等等。在描述之后,会看到一个构造函数列表,然后是操作该类的所有方法的一个列表,然后是所有继承的方法,然后是所有方法的详细描述。这非常完备,并且在右边框架的顶部和底部有一个穷举索引。

此时,前一段中的许多术语(比如)对您来说还是新接触的。不要担心,我们都将进行详细介绍。现在最需要知道的是,Java 语言文档可以在线获得。

OOP 概念

什么是对象?

Java 就是一种所谓的面向对象 (OO) 语言,利用它可以进行面向对象编程 (OOP)。这与结构化编程非常不同,大多数非 OO 程序员对之会感到陌生。第一步是理解什么是对象,因为它是 OOP 所基于的基础。

对象 是一种自包含的代码单元,它了解自己,并且在其他对象向它询问它理解的问题时,它会把自己的情况告诉对方。对象具有数据成员(变量)和方法,后者是它知道如何回答的问题(即使它们可能没有组织成问题)。对象知道如何响应的方法集是对象的接口。一些方法对大家是公开的,意味着另一个对象可以调用它们。该方法集就是对象的公共接口

当一个对象调用另一个对象的方法时,这就叫做发送消息消息发送。这无疑是 OO 术语,但是在 Java 世界中,人们通常会说“调用该方法”,而不是说“发送该消息”。在下一屏中,我们将会来看一个使得该术语更加清晰的概念上的例子。

概念上的对象例子

假设我们有一个 Person 对象。每个人(Person)都有姓名(name)、年龄(age)、种族(race)和性别(gender)。每个 Person 也都知道如何说话和走路。一个 Person 可以问另一个 Person 有多大了,或者可以要求另一个 Person 开始(或停止)走路。用编程术语来说,可以创建一个 Person 对象,并给它一些变量(比如 name 和 age)。如果创建了另一个 Person 对象,它可以问第一个 Person 对象有多大了,或者要求它开始走路。这通过调用第一个 Person 对象上的方法来完成。当我们开始用 Java 语言编写代码时,您就会看到该语言是如何实现对象的概念的。

一般来说,在 Java 语言和其他 OO 语言之间,对象的概念是相同的,尽管每种语言实现对象的方式不尽相同。概念是通用的。因为有一点是不变的,即 OO 程序员不管使用哪一语言编程,他们与过程化程序员使用的术语都不同。过程化程序员通常使用“函数”和“模块”。OO 程序员则使用“对象”,而且通常用人称名词来表示对象。如果听到一个 OO 程序员对另一个 OO 程序员说:“这个 Supervisor(管理人)对象对 Employee(雇员)对象说‘把你的 ID 给我’,因为他需要 Employee 的 ID 号才能给 Employee 分配任务。” ,这不足为奇。

过程化程序员可能会认为这样的谈话很奇怪,而 OO 程序员则认为再自然不过的了。在 OO 程序员的编程世界里,任何东西都是对象(Java 语言中有一些值得注意的例外),程序是相互交互(或“交谈”)的对象。

基本 OO 原则

对象的概念对于 OOP 很重要,当然,对象与消息进行通信的思想也同样重要。但是还有三个其他的基本原则需要理解。

可以用首字母缩写词 PIE 来记住这三个基本的 OO 原则:

  • 多态(Polymorphism)
  • 继承(Inheritance)
  • 封装(Encapsulation)

这些名词有些奇异,但是概念并不真正那么难理解。在接下来的几屏中,我们将以相反的顺序依次详细介绍这三个原则。

封装

记住,对象是自包含的,其中包含数据元素和它可以在这些数据元素上执行的操作。这是一个叫做信息隐藏 的原则的实现。其思想是,对象了解它自己。如果另一个对象想知道关于第一个对象的情况,它就必须进行询问。用 OOP 术语来说,它需要向第一个对象发送一条消息,询问其年龄。用 Java 术语来说,它需要在将会返回年龄的对象上调用一个方法。

封装确保每个对象是独特的,并确保程序是对象之间的对话。Java 语言允许程序员违反这一原则,但是这样做几乎总是一个坏主意。

继承

您出生之后,生理上就具备说话的能力,您是您的双亲的 DNA 的一个组合体。您不会完全像他们中的任何一个,但是与他们俩都有相似之处。OO 的对象具有与之相同的原则。再次来看 Person 对象。回想一下,每个人都属于某一种族。并不是所有的人都属于同一种族,但他们是不是还是很相似?是的!他们不是马(Horse)、黑猩猩(Chimp),也不是鲸(Whale),而是人(People)。所有的人都具有某些共同的特性,使他们有别于其他动物。但是他们相互之间也是稍有不同的。婴儿(Baby)与成年人(Adult)相同吗?不同。他们的动作和说话都不同。但是婴儿肯定也是人。

用 OO 术语来说,Person 和 Baby 是相同层次 中的东西的,并且(很可能)Baby继承 它的双亲 的特征和行为。我们可以说,一个特定的 Baby 是一类 Person,或者说 Baby继承 Person。反之则不然—— Person 不一定是 Baby。每个 Baby 对象是 Baby 类的一个实例,我们在创建 Baby 对象时,就会实例化 该对象。可以把类看作该类的实例的模板。一般来说,对象所能做的事情依赖于对象的类型,或者换句话说,它是哪个类的实例。比如,Baby 和 Adult 的类型都是 Person,但是一个可以有工作,一个不可以有工作。

用 Java 术语来说,Person 是 Baby 和 Adult 的父类,Baby 和 Adult 则是 Person 的子类。另一个相关概念是抽象 的思想。Person 比 Baby 或 Adult 所处的抽象级别更高。Baby 和 Adult 都是 Person 类型,但是二者又稍有不同。尽管如此,所有 Person 对象都具有一些共同的东西(比如 name 和 age)。您可以实例化一个 Person 吗?真正意义上是不能。您要么有一个 Baby,要么有一个 Adult。用 Java 术语来说,Person 是一个抽象类。您不能直接具有 Person 的一个实例。您将有一个 Baby 或者有一个 Adult,两者都是 Person 类型,只不过是实际的 Person。抽象类超出了本教程的范围,所以我们放在最后介绍。

现在,再次来看 Baby“说话”是怎么回事。我们将在下一屏考虑隐含(implication)。

多态

Baby“说话”与 Adult 一样吗?当然不一样。Baby 发出声音,但是不一定是像 Adult 所使用的可识别的字词。所以,如果我实例化一个 Baby 对象(说“实例化一个 Baby”也是一样的——“对象”一词是假定的)并要它说话,它可能会发出“咕咕”或“咯咯”的声音。人们可能希望 Adult 应该是易懂的。

在人类层次中,Person 位于最上层,Baby 和 Adult 作为子类位居其下。所有的 People 都能够说话,所以 Baby 和 Adult 也能够说话,但是他们说话的方式不同。Baby 咯咯地发出一些简单的声音,而 Adult 则能够说出词来。这就是所谓的多态:对象有其各自的做事方式。

Java 语言如何是(又不是)OO

正如我们将会看到的,使用 Java 语言可以创建对象,但是并不是该语言中的任何东西都是对象。这与有些 OO 语言(比如 Smalltalk)完全不同。Smalltalk 是纯 OO,也就是说,它之中的任何东西都是对象。Java 语言是对象和非对象的混合体。它还会让对象了解另一个对象的内部本质,只要程序员允许这种情况发生。这违背了封装原则。

但是,Java 语言也为每个 OO 程序员提供必要的工具,以遵循所有的 OO 规则并生成非常好的 OO 代码。但是这需要遵守规定。Java 语言并不强迫您做正确的事情。

尽管许多对象纯化论者义正词严地争论 Java 语言是不是 OO,但是这实际上不是一个有生产效益的争论。Java 平台还在这里。自己学习尽可能好地用 Java 代码来进行 OOP,让别人去做纯度争论吧。使用 Java 语言可以编写清晰的、相当精确的且可维护的程序,这在我的书中针对大多数专业情形都已经足够好了。

Java 语言的内幕

Java 平台如何工作

当用 Java 语言编写代码时,与许多其他语言一样,首先是编写源代码,然后再进行编译;编译器根据语言的语法规则检查代码。但是 Java 平台除此之外还增加了另外一个步骤。编译 Java 代码时,最终得到的是字节码。然后,Java 虚拟机(Java virtual machine,JVM)在运行时(也就是说当您要求 Java 去运行程序时)解释这些字节码。

用文件术语来说,编写代码时要创建一个 .java 文件。当您编译该文件时,Java 编译器会创建一个 .class 文件用于包含字节码。JVM 在运行时读取并解释该文件,但是这到底如何进行,依赖于您运行所在的平台。要运行在不同的平台上,需要根据特定于平台的库来编译源代码。正如您所想像的,“一次编写,到处运行”的承诺最终变成了“一次编写,到处测试(Write Once, Test Anywhere)”。平台之间存在微妙(或者不怎么微妙)的差异,可能会使得代码在不同的平台上的行为有些不同。

无用单元收集

当您创建 Java 对象时,JRE 会从 中自动为该对象分配内存空间。堆是机器上可用内存的一个大池。运行时然后会跟踪这个对象。当程序不再使用它时,JRE 就会释放它,根本不用您去担心。

如果使用 C++ 语言(它也是一种有争议的 OO 语言)编写了任何软件,您知道作为程序员,必须使用函数 malloc()free() 显式地为对象分配和解除分配内存。这对于程序员来说非常麻烦。这也很危险,因为它允许程序中出现内存泄漏。内存泄漏其实就是说程序以疯狂的速度吞噬内存,这会给运行它的机器上的处理器带来负担。Java 平台使您根本不用担心内存泄漏,因为它具有所谓的无用单元收集(garbage collection)功能。

Java 无用单元收集器是一个后台进程,它会释放不再使用的对象,而不是强迫您去显式地释放。计算机擅长于跟踪成千上万个对象和分配资源,Java 平台就让计算机来做这些事情。计算机在内存中保存着每个对象指针的计数,这个计数是不断变化的。当计数为 0 时,无用单元收集器就回收该对象使用的内存。您可以手动调用无用单元收集器,但是在我的职业生涯中,我还从来没有不得不这样做过。通常都是它自己处理,本教程中的每个编码例子无疑也是这样。

IDE 与命令行工具

我们前面提到过,Java 平台带有用于编译(javac)和运行(java)Java 程序的命令行工具。那么为什么还要使用 Eclipse 这样的 IDE 呢?原因很简单,因为对于稍微有点复杂的程序,命令行工具使用起来就很费劲。这些工具搁在这里只是作为您需要时的一种选择,大多数情况下,使用 IDE 才是更明智的选择。

为什么说更明智呢?主要原因在于,IDE 为您管理文件和路径,并且具有向导,可以协助您改变运行时环境。当我想要用 javac 命令行工具编译一个 Java 程序时,必须要想到提前设置 CLASSPATH 环境变量,JRE 才知道我的类在哪里,或者必须在编译时设置该变量。在 Eclipse 这样的 IDE 中,我所要做的只是告诉 Eclipse 到哪里去找我的 JRE。如果我的代码使用的不是我编写的类,我所要做的就是告诉 Eclipse,我的项目引用的是哪些库并到哪里去找到它们。这比用命令行工具键入长长的语句去指定类路径要简单得多。

如果想要或者需要使用命令行工具,可以在 Sun 的 Java Technology Web 站点(参见 参考资料)找到有关如何使用它们的更多资料。

使用 Java 技术进行 OOP

介绍

Java 技术涵盖范围很广,但语言本身并不庞大,然而要用文字来介绍它也并非易事。本教程的这一节不会详尽地介绍该语言,而是介绍入门所需的知识以及初级程序员最常遇到的情况。其他教程(参考资料 中有到这些教程的链接)介绍了该语言的各个方面、来自 Sun 和其他来源的附加的有用的库,甚至还介绍了 IDE。

这里,我们将以叙述的方式并辅以代码例子,介绍足够多的知识,使您可以开始编写 Java 程序,并学习如何在 Java 环境中做好 OOP。以后,就只是实践和练习的事情了。

多数介绍性教程读起来都像语言规范参考书。首先看到的是所有的语法规则,然后是一些用法例子,然后再介绍一些更加高级的主题,比如对象。这里,我们不走这种套路。因为用 Java 语言编写的不好的 OO 代码,主要原因就在于初级程序员没有从一开始就去掌握对象方面的知识。对象往往被看作是插件或者辅助主题。相反,我们将把 Java 语法知识的学习贯串在 Java OO 学习过程当中。这样,您学完之后,就会对如何在 OO 环境中使用 Java 语言有一个整体的思路。

Java 对象的结构

记住,对象是一个封装 的东西,它知道关于它自己的情况,并且还可以在适当要求下做一些事情。每种语言都具有关于如何定义对象的规则。在 Java 语言中,对象一般类似于下面的清单,尽管有可能没有包含所有的部分:

package packageName;

import packageNameToImport;

accessSpecifier class ClassName {
	accessSpecifierdataTypevariableName [= initialValue];
	...
	
	accessSpecifier ClassName( arguments ) {
		constructor statement(s)
	}

	accessSpecifierreturnValueDataTypemethodName( arguments ) {
		statement(s)
	}	
}

这里存在一些新的概念,我们将在接下来的几屏中讨论这些概念。

在定义类时,包声明 出现在最前面:

package packageName;

每个 Java 对象都存在于 包(package) 中。如果您不显式地指出对象属于哪个包,Java 语言将把它放入默认包 中。 就是一个对象集合,其中所有的对象(一般来说)都以某种方式相互关联。 指向文件系统中的一个文件路径。包名称使用点号将文件路径转换为 Java 平台所能理解的东西。 名称的每一部分都叫做一个节点

例如,在名叫 java.util.ArrayList 中,java 是一个节点,util 是一个节点,ArrayList 也是一个节点。最后一个节点指向文件 ArrayList.java

Import 语句

在定义类时,接下来是 import 语句:

import packageNameToImport;
...

当您的对象使用其他 中的对象时,Java 编译器需要知道到哪里去找到这些对象。import 语句会告诉编译器到哪里去找到您所使用的类。例如,如果我想要使用 java.util 包中的 ArrayList 类,就得像下面这样导入它:

import java.util.ArrayList;

与 Java 语言中的大多数语句一样,每条 import 语句都以分号结尾。您需要多少条 import 语句告诉 Java 到哪里去找到您所使用的类,就可以使用多少条 import 语句。例如,如果我想要使用 java.util 包中的 ArrayList 类和 java.math 中的 BigInteger 类,我就得像下面这样导入它们:

import java.util.ArrayList;
import java.math.BigInteger;

如果想要从同一个包导入多个类,可以使用一个快捷方式,说您想要导入该包中的所有类。例如,如果我想要使用 java.util 包中的 ArrayListHashMap,就得像下面这样导入它们:

import java.util.*;

对于每一个将从中导入类的 ,都需要使用一个 import 语句。

声明类

在定义类时,接下来是类声明

accessSpecifier class ClassName {
	accessSpecifierdataTypevariableName [= initialValue];
	...
	
	accessSpecifier ClassName( arguments ) {
		constructor statement(s)
	}

	accessSpecifierreturnValueDataTypemethodName( arguments ) {
		statement(s)
	}	
}

将 Java 对象定义为 。可以把 看作是对象的模板,即类似于糕点模具一样的东西。 定义您可以用它创建的对象的类型。您想要“印制”多少该类型的对象,就可以印制多少。当印制对象时,就创建了类的一个实例,或者换个角度说,就实例化 了一个对象。(注意:对象 一词通常既可用于指代类,也可指代类的实例。)

类的访问说明符 可以有好几个值,但是多部分时间是 public,本教程中也只讨论这一个值。您想给类取什么样的名称都可以,但是按照惯例,类名一般以一个大写字母打头,并且名称中后续的每个单词也都以大写字母打头。

类有两种类型的成员:变量(或者数据成员)和方法。类的所有成员都定义在类的主体 中,主体位于类的一对大括号(或者花括号)之间。

变量

类的变量的值可以区别类的每个实例,因此变量通常又叫做实例变量。一个变量具有一个访问说明符、一个数据类型、一个名称 和一个(可选的)初始值。下面是访问说明符及其含义的一个列表:

  • public:任何包中的任何对象都可以看到该变量。
  • protected:类的任何实例、同一包中的子类和同一包中的任何非子类可以看到该变量。其他包中的子类不能看到该变量。
  • private:只有该类的一个特定实例可以看到该变量,就连子类也看不到该变量。
  • 没有说明符(或者受保护的包):只有与包含该变量的类位于相同包中的类可以看到该变量。

如果您试图访问一个您访问不到的变量,编译器会告诉您,该变量对您不可见。应该在什么情况下使用哪个说明符是一个难以回答的问题,我们后面还会遇到这个问题。

方法

类的方法 定义类可以做什么。Java 语言中有两种风格的方法:

  • 构造函数
  • 其他方法

每种风格的方法都具有访问说明符(指明其他哪些对象可以使用它们)和主体(位于一对花括号之间),并且都包含一条或多条语句。除此之外,它们的形式和功能都完全不同。在接下来的两屏中,我们将依次介绍这两种风格的方法。

构造函数

构造函数 用于指定如何实例化一个类。可以像下面这样声明一个构造函数:

accessSpecifier ClassName( arguments ) {
	constructor statement(s)
}

对于您创建的每一个类,都可以自动获得一个默认构造函数(没有参数),您甚至不必定义它。构造函数看起来与其他方法不同,它们不具有返回值数据类型,因为其返回值数据类型就是类本身。可以像下面这样在您的代码中调用构造函数:

ClassName variableHoldingAnInstanceOfClassName = new ClassName( arguments );

当调用构造函数时,您使用 new 关键词。构造函数可以有参数,也可以没有(默认构造函数就没有参数)。严格意义上讲,构造函数既不是方法,也不是类的成员,而是 Java 语言中的一种特殊“动物”。但是实际上,它们大部分时候在外表和行为上都像方法,因而很多人把二者混为一谈。只要记住它们是特殊的就行了。

非构造函数方法

Java 语言中的非构造函数方法是您使用得最多的方法。可以像下面这样声明它们:

accessSpecifierreturnValueDataTypemethodName( arguments ) {
	statement(s)
}

每个方法都具有一个返回类型,但是并不是每个方法都会返回东西。如果方法不返回东西,您就使用关键词 void 作为返回类型。您可以给方法取任何名称,只要是有效的标识符即可(例如,不能以点号 (.) 打头),但是按照惯例,方法名应该:

  • 是由字母组成的。
  • 以一个小写字母打头。
  • 后续的单词以大写字母打头。

可以像下面这样调用方法:

returnType variableForReturnValue = 
    instanceOfSomeClass.methodName(parameter1, parameter2, ...);

这里,我们是在 instanceOfSomeClass 对象上调用 methodName(),并输入一些实参。形参(parameter)实参(argument)之间的区别很少提及,一般就统称为参数,但是二者之间是不同的。方法接受形参。当您调用方法时向方法传递特定的 值,这些值就是针对该调用的实参。

您的第一个 Java 对象

创建一个包

首先转到 Eclipse 中的 Java Browsing 透视图。我们将要为您创建第一个 Java 类做好准备。第一步是为类创建一个存放的地方。

不使用默认包,我们来为 Intro 项目特定创建一个包。单击 File>New>Package。这将出现 Package 向导(参见图 3)。

图 3. Package 向导
Package 向导

Package 向导

键入 intro.core 作为包名并单击 Finish。就会在工作空间的 Packages 视图中看到下面的包:

intro.core

注意,包左边的图标是半透明的(ghosted),也就是说,它看起来像是一个灰色不可用的包图标。这是针对空项的常见 Eclipse 用户界面约定。您的包中还没有任何 Java 类,所以其图标是半透明的。

声明类

您可以利用 File>New 在 Eclipse 中创建 Java 类,但是我们将使用工具条来创建。看 Packages 视图的上面,会看到用于项目、包和类的创建工具。单击 New Java Class 工具(绿色的“C”),显示 New Java Class 向导。输入 Adult 作为类名,并单击 Finish 接受所有的默认值。现在您会看到一些变化:

  • Adult 类出现在 Classes 视图中,该视图位于 Packages 视图的右边(参见图 4)。

    图 4. 工作空间
    工作空间

    工作空间

  • intro.core 包图标不再是半透明的了。
  • Adult.java 的一个编辑器显示在这些视图的下面。

此时,该类看起来像下面这样:

package intro.core;

public class Adult {
}

Eclipse 为您的这个类生成了一个 shell 或者说模板,并在顶部包含 package 语句。现在类主体还是空的,我们只要向其中填充东西就行了。您可以在前面使用过的 Preferences 向导(Window>Preferences)中,为新的类和方法等配置模板。您可以在预置路径(preferences path)Java>Code Style>Code Templates 中配置代码模板。实际上,从现在起,为了简化代码显示,我将删除模板中的所有注释,也就是说,任何以 // 注释 打头、或者包裹在 /* 注释 */ 之中、或者包裹在 /** 注释 */ 之中的行都被删除。从现在起,您不会在代码中看到任何注释,除非我们是特地讨论注释的使用,这是下一屏的内容。

但是在继续之前,让我们先来论证 Eclipse IDE 使得您的生活更加容易的一个方式。在编辑器中,将单词 class 改变为 clas 并等上几秒钟。注意,Eclipse 给它下面划上了一条红色波浪线。如果鼠标停留在这个带下划线的单词上,Eclipse 会弹出一个信息窗口,告诉您犯了一个语法错误。Eclipse 通过不断地编译代码并在碰到问题时悄悄地发出警报来帮助您。如果使用命令行工具 javac 的话,您就必须编译代码并等待错误,这会使开发变得奇慢无比。Eclipse 将您从这一痛苦中解救出来。

注释

像几乎所有语言一样,Java 语言也支持注释,注释其实就是编译器在进行语法检查时将会忽略的语句。Java 具有注释的几种变体:

// Single-line comment. The compiler ignores any text after the forward slashes.
/* Multi-line comment. The compiler ignores any text between the asterisks. */
/**javadoc comment. Compiler ignores text between asterisks, 
and the javadoc tool uses it. */

最后一种最有趣。总之,您安装的 Java SDK 附带的 javadoc 工具可以帮助您为代码生成 HTML 文档。您可以为自己的类生成文档,它看起来非常类似于我们在 Java API 在线文档 一节中看到的 Java API 文档。一旦为代码加上了适当的注释,就可以从命令行运行 javadoc 了。在 Java Technology Web 站点(参见 参考资料),可以找到做这件事的指导和关于 javadoc 的所有可用信息。

保留字

在开始编写编译器将会检查的代码之前,还有一点要介绍。Java 具有一些单词不能用作变量名。下面是这些单词的列表:

abstractbooleanbreakbyte
casecatchcharclass
constcontinuecharclass
defaultdodoubleelse
extendfalsefinalfinally
floatforgotoif
implementsimportintinstanceof
interfacelongintnative
newnullpackageprivate
protectedpublicpackageprivate
staticstrictfpsuperswitch
synchronizedshortsuperthis
throwthrowstruetry
transientreturnvoidvolatile
whileasserttruefalse
null

这个列表并不长,但是当您键入时,Eclipse 会使保留字成为粗体,所以根本不用记住这些单词。除了后三个之外,其他都是 Java 关键字。后三个是保留字。二者之间的区别对于我们的目的来说并不重要;二者您都不能使用。

现在,开始编写真正的代码。

添加变量

前面我已经说过,Adult 实例知道它的姓名(name)、年龄(age)、种族(race)和性别(gender)。通过将它们声明为变量,可以将这些数据添加到我们的 Adult 类。然后,Adult 类的每个实例都会具有这些数据。很有可能每个 Adult 对于这些变量将具有不同的值。这就是为什么每个对象的变量通常叫做实例变量 ——它们区分了类的每个实例。让我们来添加这些变量,使用 protected 作为每个变量的访问说明符:

package intro.core;

public class Adult {
	protected int age;
	protected String name;
	protected String race;
	protected String gender;
}

现在,每个 Adult 实例都将包含这些数据。注意,这里的每行代码都以分号结尾。Java 语言要求这样。还请注意,每个变量确实都具有数据类型。我们有了一个整型变量和三个字符串变量。变量的数据类型有两种风格:

  • 基本数据类型。
  • 对象(用户定义的或者 Java 语言内部的),也叫做 指针变量(reference variable)

基本数据类型

有九种您可能经常会看到的基本数据类型:

类型大小默认值例子
booleanN/Afalsetrue
byte8 位02
char16 位'u/0000''a'
short16 位012
int32 位0123
long64 位09999999
float32 位加一个小数点0.0123.45
double64 位加一个小数点0.0999999999.99999999

我们对 age 使用一个 int,因为我们不需要小数值,并且整数已经足够保存任何实际的人的年龄了。我们对其他三个变量使用 String,因为它们不是数值。Stringjava.lang 包中的一个类,您可以在任何时候在您的 Java 代码中自动地访问这个包(我们将 字符串 一节中讨论关于这一点的更多内容)。您也可以将变量声明为用户定义类型的,比如 Adult

我们在单独的行中定义每个变量,但是并不是必须这样。当有两个或多个相同类型的变量时,您可以在同一行中定义它们,像下面这样用逗号分隔:

accessSpecifier dataType variableName1, variableName2, variableName3,...

如果想要在声明时初始化这些变量,只要在每个变量名后添加初始值即可:

accessSpecifier dataType variableName1 = initialValue, 
    variableName2 = initialValue, ...

现在,我们的类知道它自己了,并且我们可以证明这一点。我们接下来就来证明。

main() 方法

有一个特殊的方法,您可以将它包含在任何类中,以便 JRE 可以执行代码。它就叫做 main()。每个类只可以有一个 main() 方法。当然,并不是每个类都有一个 main() 方法。但是因为 Adult 是我们目前具有的惟一的类,所以我们会给它添加一个 main() 方法,以便我们可以实例化一个 Adult 并检查它的实例变量:

package intro.core;

public class Adult {
	protected int age;
	protected String name;
	protected String race;
	protected String gender;
	
	public static void main(String[] args) {
		Adult myAdult = new Adult();
		
		System.out.println("Name: " + myAdult.name);
		System.out.println("Age: " + myAdult.age);
		System.out.println("Race: " + myAdult.race);
		System.out.println("Gender: " + myAdult.gender);
	}
}

main() 的主体中,我们实例化一个 Adult,然后输出其实例变量的值。来看第一行。这里的情况就让 OO 纯化论者对 Java 语言感到混乱。他们说,new 应该是 Adult 上的一个方法,因而您应该这样调用它:Adult.new()。我当然明白他们的观点,但是 Java 语言不是那样工作的,这正是 OO 纯化论者宣称 Java 语言不是纯 OO 的原因之一。再来看第一行。记住每个 Java 类都有一个默认构造函数,我们这里使用的就是默认构造函数。

在实例化 Adult 之后,我们将之保存在一个叫做 myAdult局部变量 中。然后我们输出它的实例变量的值。在几乎任何语言中,都可以将数据输出到控制台。Java 语言也不例外。在 Java 代码中实现这一输出的方式是,在 System 对象的 out 流上调用 println() 方法。现在不要去理解这一过程的所有细节,只要知道我们是使用一种有帮助的方法调用来进行输出。对于每次调用,我们传递一个字符串常量,并与 myAdult 上的一个实例变量的值连接。后面我们将详细介绍这些方法。

在 Eclipse 中执行代码

要执行该代码,还需要在 Eclipse 中做少量工作。单击 Types 视图中的 Adult 类,并单击工具条上的“跑步人”图标。应该会看到 Run 对话框,它允许您为自己的程序创建一个启动配置。将 Java Application 设置为您想要创建的配置的类型,然后单击 New。Eclipse 将会指定“Adult”作为该配置的默认名称,这就很好。单击 Run 查看结果。Eclipse 将会在您的代码编辑器下面显示一个 Console 视图,类似于图 5。

图 5. 运行的结果
控制台中的结果

控制台中的结果

注意,变量包含了默认值。默认情况下,任何用户定义的或者内置类型的实例变量都包含 null。但是,显式地初始化变量几乎总是一个好主意,尤其是对象,您可以确信它们所包含的值。返回去初始化这些值为:

变量
name"Bob"
age25
race"inuit"
gender"male"

再次单击“跑步人”图标,重新运行代码。您应该在控制台上看到新的值。

现在,我们来使得我们的 Adult 可以将关于它自己的数据告诉其他对象。

添加行为

存取器

通过直接引用变量来探视 Adult 对象的内部很方便,但是一个对象像这样进入另一个对象的内部,通常不是一个好主意。这违背了我们前面讨论过的封装原则,并且允许一个对象弄乱另一个对象的内部状态。一个更明智的选择是,让一个对象可以在被要求时将自己的实例变量的值告诉另一个对象。您使用存取器(或者存取方法)来实现这一点。

存取器是与任何其他方法一样的方法,只是它们一般要遵守一个特定的命名约定。要将一个实例变量的值提供给另一个对象,创建一个名叫 getVariableName() 的方法。同样,要允许其他对象在您的对象上设置实例变量,创建一个名叫 setVariableName() 的方法。

在 Java 社团中,这些存取方法通常叫做 gettersetter,因为它们的名称以 getset 打头。这些是您将看到的最简单的方法,所以非常适合用它们来阐述基本的方法概念。您应该知道,存取器 是对用于获得对象信息的方法的总称。我们后面将会看到,并不是所有的存取器都遵守 getter 和 setter 的命名约定。

下面是 getter 和 setter 的一些公共特征:

  • getter 和 setter 的访问说明符一般是 public
  • getter 一般没有任何参数。
  • setter 一般有参数,并且通常只有一个参数,这个参数就是 setter 所设置的实例变量的一个新的值。
  • getter 的返回类型一般与它报告值的实例变量的返回类型相同。
  • setter 的返回类型一般为 void,表示它们不返回任何东西(它们只是设置实例变量的值)。

声明存取器

我们可以在 Adult 上为 age 实例变量添加存取器,看起来像下面这样:

public int getAge() {
	return age;
}
public void setAge(int anAge) {
	age = anAge;
}

getAge() 方法通过使用 return 关键字,以 age 变量的值作出响应。不返回任何东西的方法都在末尾有一个隐式的 return void; 语句。在该 getter 中,我们通过使用名称来引用 age

我们也可以说成 return this.age;this 变量指向当前对象,当您直接引用实例变量时,它是隐含的。一些来自 Smalltalk 世界的 OO 程序员在引用实例变量时喜欢使用 this,就像他们在 Smalltalk 中编写代码总是使用 self 关键字一样。我本人不喜欢这种方式,且 Java 语言也不作这一要求,并且它还会在屏幕上显示更多的东西,所以本教程中的例子将不使用 this,除非没有它代码就不清晰。

调用方法

现在有了存取器,我们应该用方法调用取代 main() 方法中直接的 age 访问。main() 方法现在看起来应该像下面这样:

public static void main(String[] args) {
	Adult myAdult = new Adult();
	System.out.println("Name: " + myAdult.name);
	System.out.println("Age: " + myAdult.getAge());
	System.out.println("Race: " + myAdult.race);
	System.out.println("Gender: " + myAdult.gender);
}

如果再次运行该代码,结果应该是相同的。注意,在对象上调用方法很容易。使用该形式:

instanceName.methodName()

如果方法没有参数(比如我们的 getter),在调用时仍然需要在方法名后面包含一对括号。如果方法有参数(比如我们的 setter),就将参数包含在括号中,有多个参数的话,就用逗号分隔。

在继续之前,关于 setter 还有一点需要注意。它接受一个名叫 anAgeint 参数。它然后将该参数的值赋给实例变量 age。我们可以给参数取任何名称。名称并不重要,但是在方法中引用该参数时,就必须使用您给它所取的名称。

在继续之前,让我们尝试使用该 setter。将下面这一行添加到 main() 中紧接在实例化 Adult 之后:

myAdult.setAge(35);

现在重新运行该代码。结果会是一个年龄 35。下面就是幕后进行的工作:

  • 我们将一个整型 作为参数传递给方法。
  • JRE 为参数分配内存,并将之命名为 anAge

非存取器方法

存取器是有帮助的,但是我们想要 Adult 对象能做除了共享它们的数据之外的事情,所以我们需要添加其他方法。我们想要 Adult 说话,所以我们就从这里开始吧。speak() 方法应该看起来像下面这样:

public String speak() {
	return "hello";
}

到现在,语法应该是类似的。方法返回一个字符串常量。我们来使用该方法,并清除 main(),将对 println() 的第一次调用改变为:

System.out.println(myAdult.speak());

重新运行该代码。您应该在控制台上看到 hello

字符串

到目前为止,我们已经使用了好几个 String 类型的变量,但是我们还没有讨论它们。在 C 中处理字符串非常费劲,因为它们是由 8 位字符组成的以 null 终止的数组,而且必须由您自己来操纵。在 Java 语言中,字符串是 String 类型的第一类对象,具有能帮助您操纵它们的方法。关于字符串,与 C 世界中最接近的 Java 代码是 char 基本数据类型,它可以保存一个单个的 Unicode 字符,比如 'a'

您已经看到了如何实例化一个 String 对象并设置它的值,但是还有几种其他方式可以做这件事。下面是创建一个值为“hello”的 String 实例的两种方式:

String greeting = "hello";
String greeting = new String("hello");

因为 Java 语言中的字符串是第一类对象,所以您可以使用 new 来实例化它们。设置一个 String 类型的变量具有相同的结果,因为 Java 语言创建一个 String 对象来保存字符串常量,然后将该对象赋给实例变量。

您可以对 String 做很多事情,并且该类具有大量有帮助的方法。甚至没有使用方法,我们就已经通过连接 两个字符串而对 String 做了一些有趣的事情。所谓连接,就是将两个字符串一个接一个组合在一起:

System.out.println("Name: " + myAdult.getName());

不使用 +,我们可以在一个 String 上调用 concat(),以将它与另一个字符串相连接:

System.out.println("Name: ".concat(myAdult.getName()));

该代码看起来可能有点怪,所以让我们从左到右简要地剖析一下:

  • System 是一个内置的对象,它让您与系统环境中的各种东西交互(包括 Java 平台本身的一些功能)。
  • outSystem 上的一个类变量,这意味着不用具有 System 的实例,它就是可访问的。它代表控制台。
  • println()out 上的一个方法,它接受一个 String 参数,将之输出到控制台,并在后面跟上一个换行符以开始一个新行。
  • "Name: " 是一个字符串常量。Java 平台将该常量看作 String 的一个实例,所以我们可以直接在它上面调用方法。
  • concat()String 上的一个实例方法,它接受一个 String 参数,并将之与您在其上调用方法的 String 相连。
  • myAdult 是我们的 Adult 实例。
  • getName()name 实例变量的存取器。

所以,JRE 获得 Adult 的姓名,对它调用 concat(),并将 "Bob" 附加在 "Name: " 后面。

在 Eclipse 中,通过将插入点放置在包含实例的变量后面的点号后面,并按 Ctrl-空格键,就可以看到任何对象的可用方法。这将弹出点号左边的对象上的方法的列表。您可以用键盘上的箭头键在列表中滚动(列表卷起来了),突出显示您想要的那个方法,并按回车键进行选择。例如,要查看 String 对象上可用的所有方法,请将插入点放置在 "Name: " 后面的点号后面并按 Ctrl-空格键。

使用字符串

现在,让我们在 Adult 类中使用连接。此时,我们已经有了一个 name 实例变量。如果有一个 firstnamelastname 的话会比较好,然后当某人询问 Adult 的姓名时,就将这二者连接起来。没有问题!添加下面的方法:

public String getName() {
	return firstname + " " + lastname;
}

Eclipse 应该会在方法中显示红色波浪线,因为这些实例变量还没有存在,这意味着代码不能通过编译。现在用这两个实例变量(具有更加有意义的默认值)取代现有的 name 实例变量:

protected String firstname = "firstname";
protected String lastname = "lastname";

接下来,将第一个 println() 调用更改为下面这样:

System.out.println("Name: " + myAdult.getName());

现在,针对我们这两个与姓名有关的变量,有了一个更优美的 getter。它将它们很好地连接起来,为 Adult 形成一个全名。或者,也可以将 getName() 改变成下面这样:

public String getName() {
	return firstname.concat(" ").concat(lastname);
}

该代码做同样的事情,但是它说明了 String 上的一个方法的显式使用,并且还说明了链式方法调用。当我们用一个字符串常量(一个空格)在 firstname 上调用 concat() 时,它返回一个新的 String,即两个字符串的组合。然后我们马上在 lastname 上调用 concat(),以将 firstname 和空格与 lastname 连接起来。这给我们一个格式良好的全名。

算术运算符和赋值运算符

我们的 Adult 可以说话,但是不能移动。让我们来添加一些走路的行为。

首先,我们添加一个实例变量,用于跟踪每个 Adult 所走的步数:

public int progress = 0;

现在我们来添加一个叫做 walk() 的方法:

public String walk(int steps) {
	progress = progress + steps;
	return "Just took " + steps + " steps";
}

我们的方法接受一个用于表示要走步数的整型参数,更新 progress 以反映该步数,然后报告一些结果。还应该为 progress 添加一个 getter,而不是 setter。为什么呢?哦,允许其他对象指使我们向前走一定的步数可能不太聪明。如果另一个对象想要让我们走路,那么它可以调用 walk()。这无疑是一个难以回答的问题,并且这里所举的是一个过于简单的例子。在实际的项目上,总是需要作出这些类型的设计决策,并且通常不能提前作出,不管 OO 设计(OO design,OOD)领导说得有多严重。

在我们的方法中,我们通过将 steps 添加到 progress 来更新 progress,并把结果再次保存在 progress 中。我们使用最基本的赋值运算符= 来保存结果,使用 +算术运算符 来将两个数相加。还有其他的方式可以达到相同的目的。下面的代码将做同样的事情:

public String walk(int steps) {
	progress += steps;
	return "Just took " + steps + " steps";
}

使用 += 赋值运算符比我们使用的第一种方法要稍微好一点,使我们不要两次引用 progress 变量。但是所做的事情是相同的:它将 steps 添加到 progress 并将结果保存在 progress 中。

下表是最常见的 Java 算术和赋值运算符列表及其简要描述(注意,有些算术运算符是二元的,具有两个操作数,而有一些则是一元的,只有一个操作数)。

运算符用法描述
+a + bab 相加
++a如果 a 是一个 byteshort 或者 char,则将它转换为 int
-a - ba 减去 b
--a算术取反a
*a * bab 相乘
/a / ba 除以 b
%a % b返回 a 除以 b 所得的余数
(换句话说,这是取模运算符)
++a++使 a 增 1;在增量之前计算 a 的值
++++a使 a 增 1;在增量之后计算 a 的值
--a--使 a 减 1;在减量之前计算 a 的值
----a使 a 减 1;在减量之后计算 a 的值
+=a += b等同于 a = a + b
-=a -= b等同于 a = a - b
*=a *= b等同于 a = a * b
%=a %= b等同于 a = a % b

我们还看到了 Java 语言中的其他一些运算符。例如,.(点号),用于限定包名称和调用方法;(params),用于定义方法的一个以逗号分隔的参数列表;new,当后跟一个构造函数名时,实例化一个对象。在下一节中将会看到更多的运算符。

条件执行

介绍

没有更改方向、只是从第一条语句运行到最后一条语句的代码实际上做不了多少事情。为了更有用,程序需要做决策,并在不同的情形下完成不同的动作。与任何有用的语言一样,Java 语言也提供这样做的工具,即各种语句和运算符。本节将大量介绍编写 Java 代码时可用的这样的语句和运算符。

关系运算符和条件运算符

Java 语言提供一些运算符和流程控制语句,让您在代码中做决策。通常,代码中的决策以一个布尔表达式(计算为 true 或 false)开始。这些表达式使用关系运算符条件运算符,其中前者将一个操作数或表达式与另一个进行比较。下面是一个列表:

运算符用法返回 true,如果……
>a > ba 大于 b
>=a >= ba 大于或等于 b
<a < ba 小于 b
<=a <= ba 小于或等于 b
==a == ba 等于 b
!=a != ba 不等于 b
&&a && b ab 同时为 true。有条件地计算 b
(如果 a 为 false,就不必计算 b 了)
||a || ba 或者 b 为 true。有条件地计算 b
(如果 a 为 true,就不必计算 b 了)
!!aa 为 false
&a & bab 同时为 true。总是计算 b
|a | ba 或者 b 为 true。总是计算 b
^a ^ bab 不同(当 a 为 true 且 b 为 false,
或者反之时为 true,但是不能同时为 true 或 false)

利用 if 的条件执行

现在我们需要使用这些运算符。让我们向 walk() 方法添加一些简单的逻辑:

public String walk(int steps) {
	if (steps > 100)
		return "I can't walk that far at once";
	
	progress = progress + steps;
	return "Just took " + steps + " steps.";
}

现在,方法中的逻辑检查 steps 有多大。如果太大了,方法就马上返回,并说太远了。每个方法都只可以返回一次。但是这里不是有两条返回语句吗?是的,但是只会执行一条。Java 的 if 条件使用下面的形式:

if ( boolean expression ) {
	statements to execute if true...
} [else {
	statements to execute if false...
}]

如果 if 和/或 else 后面只有一条语句,就不需要花括号,所以我们的代码没有使用花括号。不一定要有 else 子句,我们的例子中就没有。我们可以将方法中的其余代码放在一个 else 子句中,但是效果是一样的,并且这会增添所谓的不必要的语法糖,降低代码的可读性。

变量作用域

Java 应用程序中的每个变量都具有作用域 或特征,定义了只使用变量名您能够在什么地方访问该变量。如果变量在作用域内,您就可以通过变量名与之交互。如果在作用域外,则不能这样与之交互。

Java 语言中有几个作用域级别,由变量是在哪里声明的来确定(注意:这些级别都不是官方的,据我所知,它们只是程序员使用的典型名称(typical name):

public class SomeClass {
	
	member variable scope
	
	public void someMethod( parameters ) {
		method parameter scope
		
		local variable declaration(s)
		local scope
		
		someStatementWithACodeBlock {
			block scope
		}
	}
}

变量的作用域一直扩展到它声明在其中的那一部分(或块)代码的末尾。例如,在我们的 walk() 方法中,我们只是用它的名称来引用 steps 参数,因为它在作用域内。在该方法外,引用 steps 将会产生编译错误。代码也可以引用在比该代码更宽的作用域内声明的变量。例如,我们可以在 walk() 方法内引用 progress 实例变量。

if 的其他形式

使用 if 语句的另一种形式,我们可以使得条件测试更优美一些:

if ( boolean expression ) {
	statements to execute if true...
} else if ( boolean expression ) {
	statements to execute if false...
} else if ( boolean expression ) {
	statements to execute if false...
} else {
	default statements to execute...
}

也许我们的方法看起来将像下面这样:

if (steps > 100)
	return "I can't walk that far at once";
else if (steps > 50)
	return "That's almost too far";
else {
	progress = progress + steps;
	return "Just took " + steps + " steps.";
}

还有 if 的一个简写版本,它看起来有点奇特,但是实现相同的目标,尽管它在 if 部分或者 else 部分都不允许多条语句。它使用 ?:三元运算符(一个三元运算符处理三个操作数)。我们可以像下面这样重新编写我们的简单 if 条件:

return (steps > 100) ? "I can't walk that far at once" : "Just took " + steps + " steps.";

然而这不能实现我们的目标,因为当 steps 小于 100 时,我们想要返回一条消息并且更新 progress。所以在这种情况下,不能使用简写的 ?: 运算符,因为它不允许执行多条语句。

switch 语句

if 只是可以让您在代码中测试条件的其中一种语句。另一种您很可能遇到的是 switch 语句。它计算一个整型表达式,然后基于该表达式的值,执行一条或多条语句。它的语法一般像下面这样:

switch (integer expression) {
	case 1:
		statement(s)
		[break;]
	case 2:
		statement(s)
		[break;]
	case n:
		statement(s)
		[break;]
	[default:
		statement(s)
		break;]
}

JRE 计算该整型表达式,挑选出应用的 case,然后执行针对该 case 的语句。除了最后一个 case 以外,每个 case 的最后一条语句都是 break;。这条语句“跳出” switch 语句,并且控制继续到其后的下一条语句。技术上讲,没有哪一个 break; 语句是必需的。最后一个尤其没有必要,因为无论如何,控制都将跳转到该语句外面去了。但是包含这些语句是一个好习惯。如果不在每个 case 中包含 break;,程序执行将依次进入到下一个 case,直到遇到一个 break; 或者到达语句的末尾。如果整型值没有触发任何一个 case,就会执行 default case,该 case 是可选的。

其实,switch 语句实际上就是带有整型条件的 if-else if 语句。如果条件基于一个整型值,那么可以任选二者之一。我们能够将 walk() 方法中的 if 条件重新编写为 switch 吗?不能,因为我们这里检查的是一个布尔表达式(steps > 100)。这在 switch 中是不允许的。

一个 switch 例子

下面是一个使用 switch 的小例子(一个相当经典的例子):

int month = 3;
switch (month) {
	case 1: System.out.println("January"); break;
	case 2: System.out.println("February"); break;
	case 3: System.out.println("March"); break;
	case 4: System.out.println("April"); break;
	case 5: System.out.println("May"); break;
	case 6: System.out.println("June"); break;
	case 7: System.out.println("July"); break;
	case 8: System.out.println("August"); break;
	case 9: System.out.println("September"); break;
	case 10: System.out.println("October"); break;
	case 11: System.out.println("November"); break;
	case 12: System.out.println("December"); break;
	default:  System.out.println("That's not a valid month number."); break;
}

month 是一个整型变量,表示一个月份。因为它是一个整数,所以可以使用 switch。对于每个有效的 case,我们输出月份的名称,然后跳出该语句。default case 处理有效月份范围之外的数字。

最后,下面是一个例子,即如何让 case 往下跳转是一个小小的技巧:

int month = 3;
switch (month) {
  case 2: 
  case 3: 
  case 9: 
      System.out.println("My family has someone with a birthday in this month."); 
      break;
  case 1: 
  case 4: 
  case 5: 
  case 6: 
  case 7: 
  case 8: 
  case 10: 
  case 11: 
  case 12: 
      System.out.println("Nobody in my family has a birthday in this month."); 
      break;
  default: 
      System.out.println("That's not a valid month number."); 
      break;
}

这里我们看到,case 2、3 和 9 得到相同的对待,其余 case 得到另一种对待。注意,case 不必有序, 并且可以根据我们的具体情况来让 case 跳转。

回放

在本教程的开始,我们无条件地做每件事情,这暂时没有问题,但是也有其限制。类似地,有时我们想要代码反复做同一件事情,直到工作完成。例如,假设我们想要让 Adult 说不止一次 "hello"。用 Java 代码来做这件事相对比较容易(尽管没有使用 Groovy 这样的脚本语言容易)。Java 有几种方式在代码上迭代,或者说多次执行代码:

  • for 语句
  • do 语句
  • while 语句

这些通常叫做循环(例如,“for 循环”),因为它们在代码块上迭代,直到您要它停止。在接下来的几屏中,我们将简要介绍每一种循环,并使用它们来把 speak() 方法变得更加“健谈”一点。

for 循环

Java 语言中最基本的循环构造是 for 语句,它让您在一个值范围内迭代,以确定要执行多少次循环。其最常用的语法看起来像下面这样:

for (initialization; termination; increment) {
	statement(s)
}

初始化(initialization) 表达式确定循环从哪里开始。终止(termination) 表达式确定循环到哪里停止。增量(increment) 表达式确定在循环过程中初始化变量每次按多少增量。每次通过循环,循环都执行块中的语句,块是花括号之间的语句集合(记住,Java 代码中的任何代码块都位于花括号之间,不只是 for 循环中的代码如此)。

for 的语法和功能在 Java 5.0 中有所不同,所以请查阅 John Zukowski 的专栏,了解最近发布的语言版本中的新的和有趣的特性(参见 参考资料 中的链接)。

使用 for 循环

让我们使用 for 循环,将 speak() 方法改变为说三次 "hello"。当进行改写时,我们将会学到一个内置的 Java 类,它使得装配字符串变得容易:

public String speak() {
	StringBuffer speech = new StringBuffer();
	for (int i = 0; i < 3; i++) {
		speech.append("hello");
	}
	return speech.toString();
}

java.lang 包中的 StringBuffer 类让您轻松地操纵字符串,并且非常适合于将字符串拼接在一起(也就是连接字符串)。我们只是实例化一个该类,然后每次想要向每个 Adult 说的话添加东西时就调用 append()for 循环是真正的业务发生的地方。在循环的括号中,我们声明了一个整型变量 i 来作为循环计数器(字母 i、jk 是非常常见的循环计数器,但是您可以给该变量取任意您喜欢的名称)。下一个表达式表示,我们将一直循环,直到该变量达到一个小于 3 的值。后一个表达式表示,每次循环我们会使计数器增 1(还记得 ++ 运算符吗?)。每次通过循环,我们将在 speech 上调用 append(),并在后面附加另一个 "hello"。

现在,用这个方法取代老的 speak() 方法,从 main() 删除所有的 println 语句,然后添加一条在 Adult 上调用 speak() 的语句。完成之后,该类看起来应该像下面这样:

package intro.core;

public class Adult {
	protected int age = 25;
	protected String firstname = "firstname";
	protected String lastname = "lastname";
	protected String race = "inuit";
	protected String gender = "male";
	protected int progress = 0;

	public static void main(String[] args) {
		Adult myAdult = new Adult();
		System.out.println(myAdult.speak());
	}
	public int getAge() {
		return age;
	}
	public void setAge(int anAge) {
		age = anAge;
	}
	public String getName() {
		return firstname.concat(" ").concat(lastname);
	}
	public String speak() {
		StringBuffer speech = new StringBuffer();
		for (int i = 0; i < 3; i++) {
			speech.append("hello");
		}
		return speech.toString();
	}
	public String walk(int steps) {
		if (steps > 100)
			return "I can't walk that far at once";
		else if (steps > 50)
			return "That's almost too far";
		else {
			progress = progress + steps;
			return "Just took " + steps + " steps.";
		}
	}
}

运行它时,应该在控制台上得到 hellohellohello。但是使用 for 只是完成该工作的方式之一。Java 语言还给您两个另外的替代方法,接下来我们就会介绍。

while 循环

我们首先介绍 while。下面这个版本的 speak() 产生与前一屏所见版本相同的结果:

public String speak() {
	StringBuffer speech = new StringBuffer();
	int i = 0;
	while (i < 3) {
		speech.append("hello");
		i++;
	}
	return speech.toString();
}

while 循环的基本语法看起来像下面这样:

while (boolean expression) {
	statement(s)
}

while 循环执行其块中的代码,直到其表达式返回 false。如何控制循环?必须确保表达式在某一点变成 false;否则,就是无限循环。在我们的例子中,我们在循环外面声明一个叫做 i 的局部变量,将它初始化为 0,然后在循环表达式中测试它的值。每次通过循环,都增量 i。当它不再小于 3 时,循环将会结束,我们将返回保存在缓冲区中的 String

这里,我们明白了 for 是多么方便。在 for 版本中,我们在一行代码中声明并初始化控制变量,测试它的值,并增量它。while 版本需要更多的内务处理。如果忘记对计数器进行增量,就会出现无限循环。如果没有初始化计数器,编译器就会抱怨。但是如果要测试一个复杂的布尔表达式,while 版本将会非常有用(放在 for 循环的一行代码中测试会降低可读性)。

现在我们已经看了 forwhile 循环,下一屏将介绍第三种方式。

do 循环

下面的代码将与我们前面看过的两种循环做完全相同的事情:

public String speak() {
	StringBuffer speech = new StringBuffer();
	int i = 0;
	do {
		speech.append("hello");
		i++;
	} while (i < 3) ;
	return speech.toString();
}

do 循环的基本语法看起来像下面这样:

do {
	statement(s)
} while (boolean expression) ;

do 循环实际上与 while 循环相同,只是它在每次循环块执行之后 测试其布尔表达式。对于 while 循环,如果表达式在第一次测试时就计算为 false 会怎么样?循环一次都不会执行。对于 do 循环,必须保证循环至少执行一次。区别很容易分辨。

在结束有关循环的内容之前,我们来介绍两个有用的分支语句。我们在讲述 switch 语句时已经见识过 break了。它在循环中具有一个类似的效果:停止循环。另一方面,continue 语句停止循环的当前迭代并转向下一个迭代。下面是一个简单的例子:

for (int i = 0; i < 3; i++) {
	if (i < 2) {
		System.out.println("Haven't hit 2 yet...");
		continue;
	}
	
	if (i == 2) {
		System.out.println("Hit 2...");
		break;
	}
}

如果将该代码放入您的 main() 方法中并运行,将会得到像下面这样的输出:

Haven't hit 2 yet...
Haven't hit 2 yet...
Hit 2...

前两次通过循环,i 小于 2,所以输出 "Haven't hit 2 yet...",然后 continue,即转到循环的下一个迭代。当 i 等于 2 时,第一个 if 的代码块不执行。我们跳转到第二个 if,输出 "Hit 2...",然后 break 出循环。

在下一节中,通过谈论如何处理事物的集合,我们将提高我们可以添加的行为的丰富性。

集合

介绍

大多数现实世界中的软件应用程序都会处理事物(文件、变量、文件行,等等)的集合。通常,OO 程序处理对象集合。Java 语言具有一个完善的集合框架(Collections Framework),它允许您创建和管理各种类型的对象集合。该框架自己就可以作为一个教程来讲解,所以这里没法完全介绍它。相反,我们将介绍一个非常常用的集合,以及有关使用它的一些技巧。这些技巧适用于 Java 语言中可用的大多数集合。

数组

大多数编程语言包含用于保存事物集合的数组的概念,Java 语言也不例外。数组其实就是相同类型的元素 的集合。

有两种声明数组的方式:

  • 以特定大小创建数组,该大小是永远固定不变的。
  • 以一组初始值创建数组。这组初始值的个数确定了数组的大小 —— 它将刚好能够容纳这些值。同样,该大小也是永远固定不变的。

一般来说,您像下面这样声明一个数组:

new elementType[arraySize]

要创建一个包含 5 个元素的整型数组,应该做下面两件事之一:

int[] integers = new int[5];
int[] integers = new int[] { 1, 2, 3, 4, 5 };

第一条语句创建一个 5 个元素大小的空数组。第二条语句是初始化数组的一种快捷方式。它让您在花括号之间指定一个由逗号分隔的初始值的列表。注意,我们没有在方括号中包含大小 —— 初始化块中的数据项个数指出了大小为 5 个元素。这比像下面这样先创建数组然后再编写一个循环将值放进去要容易一些:

int[] integers = new int[5];
for (int i = 1; i <= integers.length; i++) {
	integers[i] = i;
	System.out.print(integers[i] + " ");					
}

该代码也声明包含 5 个元素的数组。如果试图将 5 个以上的元素放到数组中,运行代码时就会遇到问题。为了装入数组,我们从整数 1 循环到数组长度,数组长度通过在数组上访问 length() 得到。每次通过循环,我们都将一个整数放入数组中。当到达 5 时,循环停止。

一旦装入了数组,我们就可以利用一个类似的循环来访问数组中的元素:

for (int i = 0; i < integers.length; i++) {
	System.out.print(integers[i] + " ");					
}

把数组看作是一系列的桶。数组中的每个元素位于其中一个桶中,在创建数组时会为每个桶分配一个下标 编号。通过编写以下语句来访问特定桶中的元素:

arrayName[elementIndex]

数组的下标是基于 0 的,意味着第一个下标是 0。所以现在我们的循环就有意义了。因为数组是基于 0 的,所以我们从 0 开始循环,并循环通过数组的每个元素,输出每个下标处的值。

什么是集合?

数组是好,但是处理它们有点笨拙。装入它们要花精力,并且一旦声明了一个数组,就只能装入那一类型的数据,并且只能装入该数组所能容纳的那么多数据项。数组无疑不怎么 OO。实际上,从 OO 编程时代之前开始,Java 语言拥有数组的主要原因只是为了当作延期保存(holdover)。数组普遍存在于软件中,所以语言如果没有数组的话就很难在现实中存在下去,特别是在需要与使用了数组的其他系统交互时更是如此。但是 Java 语言提供许多用于处理事物集合的工具。这些工具都非常 OO。

集合的概念并不很难理解。当需要固定个数相同类型的元素时,可以使用数组。当需要各种类型的元素,或者需要动态更改元素个数时,就使用 Java 集合。

ArrayList

在本教程中,我们只介绍一种类型的集合,叫做 ArrayList。期间,您将了解到许多 OO 纯化论者为什么对 Java 语言挑刺的另一个原因。

为了在代码中使用 ArrayList,必须在类中为它添加一条 import 语句:

import java.util.ArrayList;

像下面这样声明一个空的 ArrayList

ArrayList referenceVariableName = new ArrayList();

对链表进行添加和删除东西很直观。有多个方法可以做这两件事,但是下面这两个方法是最常用的:

someArrayList.add(someObject);
Object removedObject = someArrayList.remove(someObject);

对基本类型进行装箱和拆箱

Java 集合中保存着对象,而不是基本类型。数组中这两者都可以保存,但是大多数时候,数组没有我们想要的那么 OO。如果想要在列表中保存 Object 的子类型,只要调用 ArrayList 上的各种方法中的一个就可以了。最简单的是:

referenceVariableName.add(someObject);

这将添加的对象附加在列表的末端。到目前为止,一切尚好。但是如果想要添加一个基本类型到列表中,那会怎么样?不能直接添加。相反,必须将基本类型包装(wrap) 在对象中。每个基本类型都有一个包装器类:

  • Boolean 用于 boolean 类型。
  • Byte 用于 byte 类型。
  • Character 用于 char 类型。
  • Integer 用于 int 类型。
  • Short 用于 short 类型。
  • Long 用于 long 类型。
  • Float 用于 float 类型。
  • Double 用于 double 类型。

例如,要将一个 int 基本类型放到 ArrayList 中,我们必须使用像下面这样的代码:

Integer boxedInt = new Integer(1);
someArrayList.add(boxedInt);

将基本类型包装在包装器实例中也叫做对基本类型进行装箱。要将基本类型取出来,就必须将它进行拆箱。包装器类上有大量有用的方法,但是必须要具有这些方法的事实,真正地使得大多数程序员感到苦恼,因为要与集合一起使用基本类型,需要做大量额外的工作。Java 5.0 支持自动装箱(autoboxing)/拆箱(unboxing),从而减轻了这一痛苦。

使用集合

现实世界中的大多数成年人(adult)一般身上都带有一些钱。我们假设每个 Adult 都有一个钱包用来放钱。对于本教程,我们做以下假设:

  • 钱只用钞票(bill)来表示。
  • 钞票的面值(作为一个整数)标识每一张钞票。
  • 钱包中所有的钱都是美元。
  • 每个 Adult 一开始的程序“生活”都是身无分文。

还记得我们的整型数组吗?让我们建立一个 ArrayList。为 ArrayList 添加一条 import 语句,然后将 ArrayList 添加到 Adult 类中其他实例变量列表的末尾:

protected ArrayList wallet = new ArrayList();

我们创建了 ArrayList 并将它实例化为一个空列表,因为我们的 Adult 身无分文。我们也可以添加一些 wallet 存取器:

public ArrayList getWallet() {
	return wallet;
}
public void setWallet(ArrayList aWallet) {
	wallet = aWallet;
}

到底提供哪些存取器是一个难以回答的问题,但是在本例中,我们将具有一些最典型的存取器。毫无疑问我们不能像 resetWallet() 或者甚至 goBankrupt() 这样来调用 setWallet(),因为我们会将它重新设置为空 ArrayList。另一个对象应该能够用一个新的 wallet 来重新设置我们的 wallet 吗?这同样是一个难以回答的问题。这就是 OOD 所面临的问题!

现在我们已经准备好添加一些方法,用于让我们与 wallet 交互:

public void addMoney(int bill) {
	Integer boxedBill = new Integer(bill);
	wallet.add(boxedBill);		
}
public void spendMoney(int bill) {
	Integer boxedBill = new Integer(bill);
	boolean haveThatBill = wallet.contains(boxedBill);
	
	if(haveThatBill) { 
		wallet.remove(boxedBill);
	} else {
		System.out.println("I don't have that bill.");
	}
}

我们将在接下来的几屏中更加详细地介绍这些方法。

与集合交互

addMoney() 方法用于向钱包中插入一张钞票。记得我们的“钞票”都只是整数。要向集合添加钞票,我们将 int 包装在 Integer 中。

spendMoney() 方法再次执行装箱操作,以通过调用 contains() 来检查 wallet 中的钞票。如果有这样的钞票,我们就调用 remove() 将之取出。如果没有,我们就说没有。

让我们在 main() 中使用这些方法。用以下代码取代 main() 的当前内容:

public static void main(String[] args) {
	Adult myAdult = new Adult();
	
	myAdult.addMoney(5);
	myAdult.addMoney(1);
	myAdult.addMoney(10);
	
	StringBuffer bills = new StringBuffer();
	Iterator iterator = myAdult.getWallet().iterator();
	while (iterator.hasNext()) {
		Integer boxedInteger = (Integer) iterator.next();
		bills.append(boxedInteger);			
	}
	System.out.println(bills.toString());
}

我们的 main() 方法中组合了我们到目前已经学过的很多内容。首先,我们调用 addMoney() 几次,以将钱放入 wallet。然后我们在 wallet 的内容中循环,以输出 wallet 中有什么。我们使用一个 while 循环来做这件事,但是我们需要做一些额外的工作。我们必须:

  • 为列表获得一个 Iterator,用于访问列表中的元素。
  • 在作为循环的布尔表达式的 Iterator 上调用 hasNext(),看我们是否还有元素需要处理。
  • 每次通过循环都在 Iterator 上调用 next(),以获得下一个元素。
  • 将返回的对象强制类型转换(Typecast 或者 cast) 为我们知道列表中已有的类型(在本例中是 Integer)。

这是 Java 语言中关于循环通过集合的标准方言。另一种方法是,我们也可以在集合上调用 toArray() 并获得一个数组,然后我们会使用一个 for 循环,像在 while 循环中所做的那样,循环通过该数组。更 OO 的方式是利用 Java 集合框架的威力。

这里惟一的新概念是强制类型转换 的思想。这是什么意思呢?正如我们已经知道的,Java 语言中的对象具有一个类型或者类。如果来看 next() 方法的签名,会看到它返回一个 Object,而不是 Object 的一个特定子类。Java 编程世界中的所有对象都是 Object 的子类,但是 Java 语言需要知道一个对象的特定类型是什么,以便您能够调用那些特定于您想要处理的类型的方法。如果不进行强制类型转换,您就只能使用 Object 上可用的方法,而这样的方法非常少。在这个特定的例子中,我们不需要在从列表中取出的 weInteger 上调用任何方法,但是如果调用的话,就得首先进行强制类型转换。

增强对象

介绍

现在,我们的 Adult 已经相当有用了,但是还没有尽可能地有用。在本节中,我们将增强对象,以使之更加易于使用和更加有用。这将涉及:

  • 创建一些有用的构造函数。
  • 重载(overload) 一些方法,以创建一个更加方便的公共接口。
  • 添加代码以支持 Adult 之间的比较。
  • 添加代码以使得调试使用了 Adult 的代码更加容易。

期间,我们将会学习一些重构(refactoring) 技巧,并了解如何处理一些在我们运行代码时可能会出错的东西。

创建构造函数

我们前面已经谈到过构造函数。您可能还记得,Java 代码中的每个对象都自动获得一个默认的无参构造函数。您不必定义它,并且在您的代码中也见不到它。事实上,我们在 Adult 类中已经充分利用了这一好处。这里您看不到构造函数。

然而在实践中,定义自己的构造函数是明智之举。如果定义了自己的构造函数,就可以绝对确信检查您的类的人知道以您预期的方式构造类。所以让我们来定义自己的无参构造函数。回想一下构造函数的基本结构:

accessSpecifier ClassName( arguments ) {
	constructor statement(s)
}

Adult 定义一个无参构造函数很简单:

public Adult {
}

我们已经完成了。我们的无参构造函数实际上除了创建一个 Adult 之外不做任何事情。现在当调用 new 来创建 Adult 时,我们将使用无参构造函数而不是默认的构造函数。但是如果我们想要构造函数做一些事情,那该怎么办?在 Adult 例子中,如果能够将 first name 和 last name 作为 String 传递进来,并让构造函数将我们的实例变量设置为这些初始值,那么将会非常方便。这同样非常简单:

public Adult(String aFirstname, String aLastname) {
	firstname = aFirstname;
	lastname = aLastname;
}

该构造函数接受两个参数,并将我们的实例变量设置成这两个值。我们现在有了两个构造函数。我们实际上并不需要第一个构造函数,但是保留它也没什么错。这给该类的用户带来了选择。用户可以创建具有默认姓名的 Adult,也可以创建一个具有其提供的特定姓名的 Adult

我们刚才所做的,即使您可能还不知道,其实就是重载 一个方法。在下一屏中,我们将更加详细地讨论这一概念。

方法的重载

当创建两个具有相同名称、但是带有不同数量(或者不同类型)的参数的方法时,就重载 了该方法。这是关于对象的强大功能之一。Java 语言运行时将根据传递的参数来确定调用方法的哪个版本。在我们的构造函数例子中,如果我们不传递任何参数,JRE 将使用无参构造函数。如果我们传递两个 String,运行时将使用接受两个 String 参数的版本。如果我们传递两个不同类型的参数(或者只传递一个 String 参数),运行时将会抱怨没有接受这些类型的构造函数可用。

您可以重载任何方法,而不只是构造函数,这使得容易为类的用户创建一个方便的接口。下面让我们通过添加 addMoney() 方法的另一个版本来尝试这一点。此时,该方法接受一个 int 参数。这没问题,但是如果我们想要给 Adult 的资金增加 $100 的话,该怎么办?我们必须调用方法许多次,以增加总计为 $100 的钞票的特定集合。这非常不方便。如果能够传递一个表示钞票集合的 int 数组,那就会好得多。所以就让我们来重载该方法,以接受一个数组参数。下面是我们现在具有的方法:

public void addMoney(int bill) {
	Integer boxedBill = new Integer(bill);
	wallet.add(boxedBill);		
}

下面是已重载的版本:

public void addMoney(int[] bills) {
	for (int i = 0; i < bills.length; i++) {
		int bill = bills[i];
		Integer boxedBill = new Integer(bill);
		wallet.add(boxedBill);			
	}
}

该方法看起来非常类似于我们的另一个 addMoney() 方法,但是它接受一个数组参数。通过将 Adult 上的 main() 方法更改为下面这样,让我们来尝试使用该方法:

public static void main(String[] args) {
	Adult myAdult = new Adult();

	myAdult.addMoney(new int[] { 1, 5, 10 });
	System.out.println(myAdult);
}

当运行代码时,我们可以看到,Adult 现在具有了一个其中有 $16 的 wallet。这是一个好得多的接口。但是还没有大功告成。记住,我们是专业的程序员,我们要保持代码的整洁。在这两个方法中,您看到任何代码重复了吗?第一版中的两行代码逐字地出现在第二版中。如果想要更改加钱时的操作,就必须在两个地方更改代码,这不怎么理想。如果添加方法的另一个版本,以接受 ArrayList 而不是数组作为参数,就得在三个地方更改代码。这很快就会变得不可接受。相反,我们可以重构(refactor) 代码,以删除重复的部分。在下一屏中,我们将进行重构,通过调用 Extract Method 来完成该工作。

在增强的同时进行重构

重构 就是在不改变代码功能的情况下改变现有代码结构的过程。在重构过程之后,应用程序应该产生相同的输出,但是代码应该更加整洁并且更少重复。在添加特性之前和之后重构都很方便,前一种情况使得添加更加容易,或者使得添加在哪里更加明显,后一种情况则清除在添加过程中产生的任何重复代码。在本例中,我们添加了一个新的方法,并且看到一些重复的代码。该进行重构了!

首先,需要创建一个方法,用于捕获两行重复的代码。我们将它叫做 addToWallet()

protected void addToWallet(int bill) {
	Integer boxedBill = new Integer(bill);
	wallet.add(boxedBill);		
}

我们使该方法为 protected 的,因为它实际上是我们自己的内部助手方法,而不是我们的类的公共接口的一部分。现在我们用对这个新方法的调用来取代那几个版本的方法中的重复代码行:

public void addMoney(int bill) {
	addToWallet(bill);
}

下面是已重构的版本:

public void addMoney(int[] bills) {
	for (int i = 0; i < bills.length; i++) {
		int bill = bills[i];
		addToWallet(bill);
	}
}

如果重新运行该代码,应该会看到相同的结果。进行这种重构应该成为一种习惯,并且 Eclipse 包含许多自动的重构,使得这项工作更加容易。探讨重构的细节超出了本教程的范围,但是您可以体验重构。如果我们已经选择了 addMoney() 的第一版中的那两行重复代码,就应该在所选的代码上右击,并选择 Refactor>Extract Method。然后 Eclipse 就会带领我们进行重构。这是 IDE 最强大的特性之一。

类成员

我们在 Adult 上具有的变量和方法是实例变量和实例方法。回想一下,每个实例都具有这些变量和方法。

类本身也可以具有变量和方法,总称为类成员,可以用 static 关键字声明它们。类成员与实例变量之间的区别有:

  • 类的每个实例共享一个类变量的单个副本。
  • 可以在类本身上面调用类方法,而不用具有一个实例。
  • 实例方法可以访问类变量,但是类方法不能访问实例变量。
  • 类方法只可以访问类变量。

何时添加类变量或类方法有意义?最好的经验法则就是尽量少这样做,以便不会过多使用类变量或类方法。一些典型用途是:

  • 声明类的任何实例都可以使用的常量。
  • 跟踪类的实例的“计数器”。
  • 在一个带有从来不需要实例的实用方法(比如 Collections.sort() 方法)的类上是有用的。

类变量

要创建类变量,可以在声明它的时候使用 static 关键字:

accessSpecifier static variableName	[= initialValue];

JRE 为类的每个实例创建该类的实例变量的一个副本。它只在第一次遇到程序中的类时创建每个类变量的单个副本,而不管有多少个实例。所有实例将共享(并潜在地修改)这个副本。这使得类变量非常适合于所有实例都可以使用的常量

例如,我们前面使用整数来描述 Adultwallet 中的“钞票”。这完全是可以接受的,但是如果给整数值命名,以便我们在阅读代码时容易明白数字所表示的意思,那可能会更好。让我们在为类声明实例变量的地方声明一些常量来做这项工作:

protected static final int ONE_DOLLAR_BILL = 1;
protected static final int FIVE_DOLLAR_BILL = 5;
protected static final int TEN_DOLLAR_BILL = 10;
protected static final int TWENTY_DOLLAR_BILL = 20;
protected static final int FIFTY_DOLLAR_BILL = 30;
protected static final int ONE_HUNDRED_DOLLAR_BILL = 40;

按照约定,类常量的名称全部由大写字母组成,其中单词之间用下划线分隔。我们使用 static 将它们声明为类变量,并添加 final 关键词确保它们不能被任何实例更改(也就是说,使它们成为常量)。现在我们可以将 main() 更改为使用这些新命名的常量来给 Adult 添加一些钱:

public static void main(String[] args) {
	Adult myAdult = new Adult();		
	myAdult.addMoney(new int[] { Adult.ONE_DOLLAR_BILL, Adult.FIVE_DOLLAR_BILL });
	System.out.println(myAdult);
}

阅读该代码,很容易知道我们向 wallet 添加什么。

类方法

正如我们已经看到过的,我们像下面这样调用实例方法:

variableWithInstance.methodName();

我们是在一个保存类的实例的指定变量上调用方法。当调用类方法时,像下面这样进行调用:

ClassName.methodName();

调用该方法不需要实例,我们就在类本身上面调用它。我们所使用的 main() 方法就是类方法。看看它的签名。注意,它被声明为 public static。我们前面已经看到过访问说明符。static 关键词标识这是一个类方法,这也是这些方法有时被称为静态方法 的原因。调用 main() 不需要具有 Adult 的一个实例。

如果愿意的话,我们也可以为 Adult 创建类方法,尽管对于该例来说,根本没有理由这样做。但是为了演示如何创建,让我们来添加一个简单的类方法:

public static void doSomething() {
	System.out.println("Did something");
}

注释掉 main() 中当前的代码行并添加下面的代码行:

Adult.doSomething();
Adult myAdult = new Adult();
myAdult.doSomething();

当运行该代码时,应该会在控制台中两次看到适当的消息。第一次调用 doSomething() 是调用类方法的典型方式。也可以通过一个类的实例来调用它们,如第三行代码所示。但是这实际上不是一种好的形式。Eclipse 提示了这一点,它给这行代码下面加上黄色波浪线,并建议应该以“静态方式(static way)”访问该方法,意味着在类上而不是实例上访问该方法。

利用 == 比较对象

Java 语言中有两种比较对象的方式:

  • == 运算符
  • equals() 运算符

第一种,也是最基本的一种,比较对象的对象相等性。换句话说,下面这一语句:

a == b

当且仅当 ab 指向一个类的同一个实例(即同一对象)时返回 true。对于基本类型是个例外。当用 == 比较两个基本类型时,Java 语言运行时将比较它们的值(记住,基本类型不是真正的对象)。在 main() 中尝试这种体验,并在控制台中查看结果:

int int1 = 1;
int int2 = 1;
Integer integer1 = new Integer(1);
Integer integer2 = new Integer(1);
Adult adult1 = new Adult();
Adult adult2 = new Adult();

System.out.println(int1 == int2);
System.out.println(integer1 == integer2);
integer2 = integer1;
System.out.println(integer1 == integer2);
System.out.println(adult1 == adult2);

第一个比较返回 true,因为我们比较的是具有相同值的基本类型。第二个返回 false,因为两个变量不是指向同一对象实例。第三个返回 true,因为现在两个变量指向同一实例。用我们自己的类进行尝试,我们也是得到 false,因为 adult1adult2 不指向同一实例。

利用 equals() 比较对象

像下面这样在一个对象上调用 equals()

a.equals(b);

equals() 方法是在 Object 类型上进行调用的,而 Object 是 Java 语言中每个类的双亲。这意味着您创建的任何类都将从 Object 继承基本的 equals() 行为。这一基本行为与 == 运算符没有什么区别。换句话说,默认情况下,下面这两条语句都使用 == 并返回 false

a == b;
a.equals(b);

再来看 Adult 上的 spendMoney() 方法。当我们在 wallet 上调用 contains() 时,幕后发生了什么情况?Java 语言使用 == 运算符将列表中的对象与我们所要求的对象进行比较。如果找到一个匹配,方法就会返回 true,否则返回 false。因为我们是在比较基本类型,所以基于整数的值,它可以找到一个匹配(记住,== 基于值来比较基本类型)。

这对于基本类型来¯´很好,但是如果我们想要比较对象的内容,那该怎么办?== 运算符无法做到。要比较对象的内容,我们必须在 a 是它的一个实例的类上覆盖(override)equals() 方法。这意味着您必须在您的其中一个父类中,创建一个与该方法具有完全相同的签名的方法,但是却不同地实现该方法。如果创建了这样的方法,就可以比较两个对象的内容,看它们是否相等,而不只是看两个变量是否指向同一实例。

main() 中尝试这一体验,并在控制台中查看结果:

Adult adult1 = new Adult();
Adult adult2 = new Adult();

System.out.println(adult1 == adult2);
System.out.println(adult1.equals(adult2));

Integer integer1 = new Integer(1);
Integer integer2 = new Integer(1);

System.out.println(integer1 == integer2);
System.out.println(integer1.equals(integer2));

第一个比较返回 false,因为 adult1adult2 指向 Adult 的不同实例。第二个也返回 false,因为 equals() 的默认实现只比较两个变量是否指向同一实例。但是 equals() 的默认行为通常不是我们想要的。我们要比较两个 Adult内容,看它们是否相同。我们可以覆盖 equals() 来做这件事。正如从上面例子中的最后两个比较所看到的,Integer 类覆盖这个 == 返回 false 的方法,但是 equals() 比较已包装的 int 值的相等性。在下一屏中,我们将对 Adult 做一些类似的事情。

覆盖 equals()

覆盖 equals() 以比较对象实际上需要我们覆盖两个方法:

public boolean equals(Object other) {
	if (this == other)
		return true;

	if ( !(other instanceof Adult) )
		return false;
	
	Adult otherAdult = (Adult)other;
	if (this.getAge() == otherAdult.getAge() &&
		this.getName().equals(otherAdult.getName()) &&
		this.getRace().equals(otherAdult.getRace()) &&
		this.getGender().equals(otherAdult.getGender()) &&
		this.getProgress() == otherAdult.getProgress() &&
		this.getWallet().equals(otherAdult.getWallet()))
		return true;
	else
		return false;
}

public int hashCode() {
	return firstname.hashCode() + lastname.hashCode();
}

我们以下面的方式覆盖 equals(),这其实是一个典型的 Java 方言:

  • 如果将要比较的对象与这个是同一对象,二者显然相等,所以返回 true
  • 进行检查以确保将要比较的对象是 Adult 的一个实例(如果不是的话,二者显然不是同一个对象)。
  • 将传入的对象强制类型转换为一个 Adult,以便可以在它上面调用熟悉的方法。
  • 比较两个 Adult 的内容,如果两个对象“相等”,这两个 Adult 就应该是同一个人(不管我们使用的什么样的相等定义)。
  • 如果这些情况中有任何一处不相等,我们就返回 false,否则就返回 true

注意,可以用 == 比较每个人的 age,因为它是一个基本类型值。我们使用 equals() 来比较 String,因为该类覆盖 equals() 来比较 String 的内容(如果使用 ==,我们每次都会得到 false,因为两个 String 永远不会是同一对象)。我们对 ArrayList 做了相同的事情,因为它覆盖 equals() 来检查两个列表具有相同顺序的相同元素,这对于我们的简单例子来说已经足够好了。

每当覆盖 equals() 时,也应该覆盖 hashCode()。具体原因超出了本教程的范围,现在只要知道 Java 语言使用该方法返回的值来将您的类的实例放到集合中,而集合使用散列算法(比如 HashMap)来间隔对象。关于 hashCode() 返回什么(不是说它必须返回一个整数)的惟一绝对的原则是, 它必须返回:

  • 同一对象的相同值。
  • 相等对象的相等值。

一般来说,返回一个对象的一些或所有实例变量的散列码(hashcode)值对于计算一个散列码来说已经是一个足够好的方式了。另一个选择是将变量转换为 String,组合它们,然后返回结果的 String 的散列码。还有一个选择是将一个或多个数值变量与一些常量相乘,以达到进一步的惟一性,但是这通常太过火了。

覆盖 toString()

Object 类具有一个 toString() 方法,您创建的每个类都会继承该方法。它返回您的对象的一个 String 表示,并且对于调试非常有帮助。为了来看 toString() 的默认实现做些什么,请在 main() 中尝试这一体验:

public static void main(String[] args) {
	Adult myAdult = new Adult();
	
	myAdult.addMoney(1);
	myAdult.addmoney(5);
	
	System.out.println(myAdult);
}

我们将会在控制台中得到的结果看起来像下面这样:

intro.core.Adult@b108475c

println() 方法在传递给它的对象上调用 toString()。因为还没有覆盖 toString(),所以得到默认的输出,即一个对象 ID。每个对象都有一个 ID,但是这并没有告诉您关于对象的太多信息。如果我们覆盖了 toString(),以便给我们提供关于 Adult 的信息的一个格式良好的形象描述,那么将会更好一些:

public String toString() {
	StringBuffer buffer = new StringBuffer();

	buffer.append("And Adult with: " + "\n");
	buffer.append("Age: " + age + "\n");
	buffer.append("Name: " + getName() + "\n");
	buffer.append("Race: " + getRace() + "\n");
	buffer.append("Gender: " + getGender() + "\n");
	buffer.append("Progress: " + getProgress() + "\n");
	buffer.append("Wallet: " + getWallet());
	
	return buffer.toString();
}

创建一个 StringBuffer 以构建对象的一个 String 表示,然后返回该 String。当重新运行时,控制台应该显示一些像下面这样的好的输出:

An Adult with: 
Age: 25
Name: firstname lastname
Race: inuit
Gender: male
Progress: 0
Wallet: [1, 5]

这比一个隐含的对象 ID 要方便且有用得多。

异常

如果我们的代码从不出问题那当然很好,但是这几乎是不可能的。有时,事情并不像我们预期的那样,有时,事情则比只是产生不想要的结果还要糟糕。当这样的情况发生时,JRE 就抛出异常。Java 语言包含一些特殊的语句,让您捕获 异常并进行适当的处理。下面是这些语句的一般格式:

try {
	statement(s)
} catch (exceptionTypename) {
	statement(s)
} finally {
	statement(s)
}

try 语句包围可能会抛出异常的代码。如果这样的话,执行立即下落到 catch 块(也叫做异常处理器)。当完成所有的 try 和 catch 时,执行继续到 finally 块,不管是否抛出异常。当捕获到一个异常时,您可以尝试从它恢复,也可以优雅地退出程序(或方法)。

处理异常

main() 中尝试该体验:

public static void main(String[] args) {
	Adult myAdult = new Adult();

	myAdult.addMoney(1);
	String wontWork = (String) myAdult.getWallet().get(0);
}

运行该代码会得到一个异常。控制台将会显示像下面这样的一些东西:

java.lang.ClassCastException
	at intro.core.Adult.main(Adult.java:19)
Exception in thread "main"

堆栈跟踪(stack trace) 报告异常的类型以及发生异常的行号。记住,从集合删除 Object 时,必须进行强制类型转换。我们具有一个由 Integer 组成的集合,但是我们试图用 get(0)(其中 0 是指列表中第一个元素的下标,因为列表是基于 0 的,就跟数组一样)获得第一个元素,并将它强制类型转换为 String 类型。Java 语言运行时会发出抱怨。此时,程序只是死了。我们通过处理异常来让程序比较优雅地关闭:

try {
	String wontWork = (String) myAdult.getWallet().get(0);
} catch (ClassCastException e) {
	System.out.println("You can't cast that way.");
}

这里,我们捕获异常并输出一条消息。另外,我们也可以不在 catch 块中做任何事情,并在 finally 块中输出消息,但是这不是必要的。在有些情况下,异常对象(一般来说,而不是必定,叫做 eex)可以给予您关于错误的更多信息,这有助于您报告更完善的信息或者优雅地恢复。

异常的层次结构

Java 语言合并一个整个的异常层次结构,这意味着存在许多类型的异常。在最高层次,一些异常由编译器检查,而有些叫做 RuntimeException,不由编译器检查。语言规则是,您必须捕获或者指定 您的异常。如果一个方法可以抛出非 RuntimeException,那么该方法要么必须处理该异常,要么必须指定调用的方法来处理异常。用方法签名中的 throws 表达式来做这件事。例如:

protected void someMethod() throws IOException

在您的代码中,如果您调用一个方法,指定代码抛出一种或多种类型的异常,您就必须以某种方式处理它,或者添加一个 throws 到您的方法签名,以将它沿着调用堆栈 向上传递到调用您的代码的方法。在异常的事件当中,如果在抛出异常的地方没有异常处理器,Java 语言运行时就将在堆栈中向上搜索异常处理器。如果一直到达堆栈顶端都没有找到一个异常处理器,它就会突然终止程序。

好消息是,大多数 IDE(Eclipse 无疑是其中之一)将会告诉您,您的代码是否需要捕获一个由您调用的方法抛出的异常。然后您就可以决定如何处理它。

当然,异常处理还有更多内容,但是这超出了本教程的范围。希望我们这里介绍的内容将有助于您知道哪些是需要知道的东西。

Java 应用程序

应用程序是什么

我们已经看到过应用程序了,虽然是一个非常小的。我们的 Adult 类从一开始就具有一个 main() 方法。那是必需的,因为需要这样一个方法,才能让 Java 语言运行时执行您的代码。但是一般来说,域对象不会具有 main() 方法。Java 应用程序一般由以下部分组成:

  • 一个类,其最前面是一个 main() 方法。
  • 很多其他的类,用于完成工作。

为了演示这是如何工作的,我们需要向我们的应用程序添加另一个类。该类可以说是“驱动器”。

创建驱动器类

我们的驱动器类可以非常简单:

package intro.core;

public class CommunityApplication {	
	public static void main(String[] args) {
	}
}

执行以下步骤,创建一个类并真正使之成为我们的程序的“驱动器”:

  • 使用我们在 声明类 一节中用于创建 Adult 的相同的 New Java Class 工具条按钮,在 Eclipse 中创建一个类。
  • 将该类命名为 CommunityApplication,并确保选中了向该类添加一个 main() 方法的选项。Eclipse 为您生成这个类,其中包含 main()
  • Adult 类删除 main() 方法。

所有剩下的事情就是将一些内容放入新的 main() 中:

package intro.core;

public class CommunityApplication {
	public static void main(String[] args) {
		Adult myAdult = new Adult();				
		System.out.println(myAdult.walk(10));
	}
}

在 Eclipse 中创建一个新的启动配置,就像我们在 在 Eclipse 中执行代码 一节中为 Adult 类所做的一样,并运行它。您应该看到我们的对象走了 10 步。

现在我们有了一个简单的应用程序,它以 CommunityApplication.main() 开始,并使用我们的 Adult 域对象。当然,应用程序可以比这更复杂,但是基本的思想是相同的。具有几百个类的 Java 应用程序并不罕见。一旦主要的驱动器类开始运行,程序就通过让类相互协作以完成工作这样运行起来。如果您习惯于从开头启动并运行到结尾的过程化程序,那么程序的以下执行可能让您迷惑,但是随着实践会变得比较容易理解。

JAR 文件

如何将一个 Java 应用程序打包以便其他人可以使用它,或者给他们提供可以用在他们自己的程序中的代码(比如有用对象的库,或者框架)?创建一个将您的代码打包的 Java Archive (JAR) 文件,以便其他程序员可以在 Eclipse 中将它包含在他们的 Java Build Path 中,或者放在他们的类路径上(如果他们使用命令行工具的话)。同样,Eclipse 会减轻您的负担。在 Eclipse(和许多其他 IDE)中创建 JAR 文件很简单:

  1. 在工作空间中,右击 intro.core 包并选择 Export
  2. Export 对话框中选择 JAR file,然后单击 Next
  3. 浏览到您想要放置 JAR 文件的位置,并给该文件取一个任意您喜欢的名称,给它一个 .jar 文件扩展名。
  4. 单击 Finish

这样就应该在您指定的位置看到 JAR 文件。一旦有了一个 JAR 文件(自己的或者来自其他来源),如果将它放在 Eclipse 中的 Java Build Path 上,您就可以在自己的代码中使用其中的类了。这也不难做到。此时还不需要向我们的路径添加代码,但是让我们来遵循您这样做将会采取的步骤:

  1. 在工作空间中右击 Intro 项目,然后选择 Properties
  2. 在 Properties 对话框中,选择 Libraries 选项卡。
  3. 这里可以看到 Add JARs...Add External JARs... 按钮,它们用于将 JAR 放到 Java Build Path 上。

一旦 JAR 文件中的代码(即类文件)位于 Java Build Path 上,您就可以在 Java 代码中使用这些类,而不会出现编译错误。如果 JAR 文件包含源代码,那么您可以将这些源文件与您的路径上的类文件相关联。然后您就可以获得弹出式代码帮助,甚至打开并查看代码。

编写优良的 Java 代码

介绍

您现在已经了解了很多 Java 语法,但是这并不是专业编程的真正主题。什么造就“优良的”Java 程序呢?

也许有多少专业 Java 程序员,这个问题就有多少种答案。但是我有一些建议,我相信大多数专业 Java 程序员都赞同,可以改善他们日常处理的 Java 代码的质量。开诚布公地说,我推崇像极限编程(Extreme Programming,XP)这样的灵巧方法,所以我的许多关于“优良”代码的观点一般是受灵活社团(agile community )的鼓动,尤其是受 XP 的影响。我依然认为大多数有经验的专业 Java 程序员会赞同我将在本节中提出的观点。

保持类最小

在本教程中,我们创建了一个简单的 Adult 类。即使在将 main() 方法移动到另一个类之后,Adult 还剩下 100 多行代码。它具有 20 多个方法,但是将它与您可能看到(和创建)的许多类从专业的角度来进行比较,它其实并没有做多少事情。这是一个 类。具有 50 到 100 个方法的类并不少见。还有什么比拥有较少的类更坏的事情吗?可能不会再有了。当然,最好还是有满足自己需要的类。如果您需要几个本质上完成相同事情但带有不同参数的助手方法(比如 addMoney() 方法),那就是最好的选择。只是一定要限制需要的方法列表,除此之外,其他都按需行事。

一般来说,一个带有大量方法的类总是具有一些不属于这里的方法,因为这个庞大的对象所做的事情太多了。Martin Fowler 在他的 Refactoring 一书中(参见 参考资料 中的链接), 将这称为 Foreign Method 代码味道。如果您有一个带有 100 个方法的对象,就应该好好想想,这个对象是否应该拆成多个对象。大类通常在大学里大行其道。Java 代码与之一样。

保持方法最小

小方法就与小类一样可取,并且原因也类似。

很多有经验的 OO 程序员对 Java 语言具有的苦恼之一就是,它提供大量的 OO 能力,但是却没有教他们如何做好 OO。换句话说,它给了他们很多绳子去捆绑自己(尽管至少没有 C++ 给他们的多)。能看到这一点的一个常见地方是 main() 方法离得很远的类,或者一个单个的名叫 doIt() 的方法。仅仅因为能够 将所有的代码放在类中的单个方法中,并不意味着就应该 如此。Java 语言比很多其他 OO 语言具有更多的语法糖,所以一定的罗嗦是必要的,但是不要过了度。

思考一下这些超长的方法。滚动十屏代码去了解代码所做的工作是很艰难的。该方法做什么工作?您需要泡上一杯大大的咖啡花几个小时去研究才能知道。一个小的,甚至微小的方法是一个容易看懂的代码块。运行时效率不是要具有小方法的原因,可读性才是真正的目标。这将使得代码更加容易维护,并且在需要添加功能时更加容易更改。

将每个方法局限于执行一项工作。

给方法取好名称

我曾经见到过的最佳编码模式(忘了是在哪里见到的了)叫做揭示意图的方法名。下面两个方法名中哪个更容易一眼看出意图?

  • a()
  • computeCommission()

答案很明显。出于某些原因,程序员似乎讨厌长的方法名。过长的方法名无疑是不方便的,但是长到足够明了通常不是过长。我觉得像 aReallyLongMethodNameThatIsAbsolutelyClear() 这样的方法名是可以接受的。如果在凌晨 3 点,我试图找出程序为什么不工作时,遇到一个名叫 a() 的方法,那我真想揍人。

额外花几分钟想出一个非常具有描述性的方法名;如果可能,考虑以这样的方式命名方法,即让代码阅读起来更像英文,即使这意味着添加附加的助手方法来做这项工作。例如,考虑添加一个助手方法以使得代码更加可读:

if (myAdult.getWallet().isEmpty()) {
	do something
}

ArrayList 上的 isEmpty() 方法本身是有帮助的,但是我们的 if 语句中的布尔条件应该像下面这样得益于 Adult 上一个叫做 hasMoney() 的方法:

public boolean hasMoney() {
	return !getWallet().isEmpty();
}

然后我们的 if 语句读起来更像英文:

if (myAdult.hasMoney()) {
	do something
}

这一技巧很简单,也许在本例中是微不足道的,但是当代码变得越来越复杂时,它就会显露出惊人的威力。

保持类的数量最少

XP 中关于简单设计的其中一个指导方针是,用尽可能少的类完成一个目标。如果您需要另一个类,尽管添加就是了。如果另一个类将使得代码更简单或者简化您的意图表达,那么就添加这个类吧。但是没有理由只是为了具有而具有类。当然,通常项目早期比结束时具有的类要少,但是一般将代码重构成更多的类比组合类更容易。如果您有一个具有大量方法的类,那么分析一下,看是否有另一个对象陷入在其中,正在等待出去。如果有的话,就创建一个新的对象。

在我经历的几乎所有 Java 项目上,没有人害怕创建类,但是我们也总是试图在不降低意图清晰度的情况下,减少类的数量。

保持注释的数量最少

我过去常常在代码中编写很多的注释,读起来就像一本书。后来我变得聪明一些了。

每一个计算机科学程序、每一本编程书籍和我知道的很多程序员,都要您给代码编写注释。在有些情况下,注释是有帮助的。在许多情况下,注释使得代码维护更加困难。想想您更改代码时必须做什么。有注释吗?如果有的话,您最好更改注释,否则它会可怕地过期,甚至随着时间的推移,根本就不再能够描述代码。依我的经验,几乎会使维护时间加倍。

我的经验法则是:如果代码太难阅读和理解而需要注释,我就需要使它足够清晰,从而不需要注释。代码可能会太长,或者做太多的事情。如果这样的话,我就简化它。代码可能太隐晦。如果这样的话,我就添加助手方法,使之清晰。实际上,在与同一团队的其他成员一起进行 Java 编程的三年当中,我所编写的注释屈指可数。保持代码清晰!如果您需要系统或者某个特定组件的全景描述,就编写一个简短的注释来描述。

罗嗦的注释一般比较难维护,通常不及一个小的、编写良好的方法那么好地表达意图,并且很快就会过期。根本不要过分依赖注释。

使用一致的风格

编码风格实际上是您的环境中必然的并且可接受的东西。我甚至不知道哪一种风格可以称为“典型的”。这通常是个人品味问题。例如,下面是一些让我看到会难受的代码:

public void myMethod()
{
	if (this.a == this.b)
	{
		statements
	}
}

为什么这会烦扰我呢?因为我个人偏向于反对那些添加我认为不必要的代码行的编码风格。Java 编译器认为下面的代码与刚才的代码完全相同,而我减少了几行:

public void myMethod() {
	if (this.a == this.b)
		statements
}

二者没有“对”或“错”之分,只是一个比另一个短一些。那么当我与某个喜欢第一种形式的人一起编码时会怎么样?我们进行协商,挑选出一种来,然后一直坚持用这一种。惟一绝对的风格规则是一致性。如果一个项目上的每个人都用不同的风格,那么阅读代码将变得很困难。挑选一种风格并且不要改变。

避免 switch

一些 Java 程序员对 switch 语句情有独钟。我曾经认为它们很好,但是后来我认识到了,一个 switch 实际上就是几个 if,并且通常意味着条件逻辑出现在代码中的多个地方。这是代码重复,是应该禁忌的。为什么?因为在多处具有相同的代码使得代码比较难更改。如果我在三处具有相同的 switch,并且想要更改对某个 case 的处理,我就得在三处更改代码。

现在,如果您可以重构具有单个 switch 语句的代码,那会怎么样呢?很好!我不相信使用它有什么坏处。在有些情况下,它比嵌套的 if 更清晰。但是如果您看到它出现在很多地方,就有了应该解决的问题了。防止这一问题的一个容易的方法是,避免 switch,除非它对于该工作是最佳的工具。依我的经验,它很少是最佳的。

是 public 的

我把最具争议的建议放在最后。做一下深呼吸。

我相信您会反对让所有的方法都是 public 的。实例变量应该是 protected 的。

当然,许多专业程序员会害怕这种想法,因为如果任何东西都是公共的,那么任何人都可以更改它,也许会以未授权的方式更改。在任何东西都是公共可访问的世界中,您就不得不依赖于程序员纪律,以确保人们在其不应该访问时不会访问其不应该访问的东西。但是在编程生活当中,很少有什么事情比想要访问一个不可见的变量或方法更受挫的了。如果您对代码中您设想其他人不应该访问的东西限制访问,您就是在设想自己无所不知。这在大多数时候是一个危险的假设。

当使用其他人的代码时,这种受挫感会经常出现。您会看到一个方法刚好做您想要做的工作,但是它不是公共可访问的。有时,这有一个很好的原因,并且使得限制可访问性有意义。但是有时,不 public 的惟一原因是,编写代码的人这样想“没有人需要访问该代码”,或者他们这样想“没有人应该访问该代码,因为……”,于是他们并没有很好的理由。许多时候,人们使用 private 是因为它可用。不要这样做。

使方法是 public 的,变量是 protected 的,直到您有一个很好的理由限制访问。

追随 Fowler

现在您知道了如何创建优良的 Java 代码,以及如何保持它优良。

关于该主题,业界最好的书籍是 Martin Fowler 编著的 Refactoring (参见 参考资料 中的链接)。它读起来甚至很有趣。重构(refactoring) 的意思是在不改变代码结果的情况下,更改现有代码的设计。Fowler 谈论了请求重构的“代码气味”,并且详细介绍了用于改变“代码气味”的各种技巧(或者“重构”)。依我的观点,重构和编写一次通过测试的代码的能力(参阅 参考资料)是新手程序员要学习的最重要的技能。如果每个人都擅长于这两点,那将彻底改变行业当前的局面。如果 擅长于这两点,就会比较容易找到工作,因为您能比其他人产生更好的结果。

编写 Java 代码相当简单。编写优良的 Java 代码则是一门手艺。倾力成为一个手艺人。

结束语

在本教程中,学习了 OOP 和用于创建有用对象的 Java 语法,并了解了一个有助于控制开发环境的 IDE。您可以创建做很多事情的对象,尽管不是您能想像到的每一件事情。但是您可以用几种方式扩展自己的知识,包括通过其他 developerWorks 教程来研究 Java API 和探索 Java 语言的其他功能。请参见 参考资料 中的链接。

Java 语言无疑不是完美的;每种语言都有它的特点,而每个程序员都有自己的喜好。但是 Java 平台是一个很好的工具,可以帮助您编写非常好的专业程序。


相关主题

    • 官方的 Java Technology 主页 中,具有到与 Java 平台相关的任何方面的链接。在这里可以找到您需要的每一种“官方” Java 语言资源,包括语言规范和 API 文档。
    • 可以从 Eclipse Web 站点 下载 Eclipse。
    • 学习有关 Java 附带的 命令行工具 的更多知识。
    • 访问 Java 文档页面,其中有到每种 SDK 版本的 API 文档的链接。
    • John Zukowski 在 developerWorks 上的 驯服 Tiger 专栏,提供了对最新版 J2SE 平台的简要介绍。
    • javadoc 主页 介绍了 javadoc 的所有细节,包括如何使用命令行工具,以及如何编写自己的 DocletDoclet 用于为自己的文档创建定制的格式。
    • Sun Java 教程 是一个优秀的参考资料。它对 Java 语言做了一般介绍,并且包含比本教程更多的内容,包括到其他教程的链接。这些教程更加详细地介绍了 Java 语言的各个方面。
    • Martin Fowler 等人编著的 Refactoring 一书(Addison-Wesley, 1999),对于改善现有代码是一个优秀的参考资料。
    • New to Java technology 页面developerWorks 为初级 Java 开发人员准备的参考资料的集散地,其中包括到教程和认证资源的链接。
    • 在 developerWorks 的 Java 技术专区,可以找到关于 Java 编程每个方面的文章。
    • 访问 Developer Bookstore 获得技术书籍的完整列表,包括数百本 Java 相关书籍
    • 还请参阅 Java 技术专区教程页面,获得 developerWorks 的免费 Java 教程的完整列表。

评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=85167
ArticleTitle=Java 编程介绍
publish-date=12132004