了解有望从本教程中获得哪些收获,以及如何从本教程中获得最大的收获。
这个分两部分的 Java 编程简介教程适用于不熟悉 Java 技术的软件开发人员。学完这两个部分后,即可掌握并运行基于 Java 语言和平台执行的对象编程 (OOP) 和实际应用程序开发。
第 1 部分是使用 Java 语言进行 OOP 的一个分步介绍。本教程首先将概述 Java 平台和语言,随后指导您设置一个由 Java 开发工具包 (JDK) 和 Eclipse IDE 组成的开发环境。在了解开发环境的组成部分之后,将开始着手学习基本的 Java 语法。
第 2 部分将介绍更高级的语言特性,包括正则表达式、泛型、I/O 和序列化。第 2 部分中的编程示例使用第 1 部分中开始开发的 Person
对象作为基础。
完成第 1 部分后,您将熟悉基本的 Java 语言语法,并能够编写简单的 Java 程序。随后的“Java 编程简介,第 2 部分:实际应用程序的构造”将以此为基础。
本教程适用于没有体验过 Java 代码或 Java 平台的软件开发人员。教程中包含 OOP 概念的概述。
要完成本教程中的练习,需要安装并设置一个开发环境,其中包含:
教程中包含二者的下载和安装说明。
推荐的系统配置是:
Java 技术用于为各种环境开发应用程序,从用户设备到异构的企业系统。在这一节中,将获得 Java 平台及其组件的高级视图。
大部分 Java 开发人员经常查阅官方在线 Java API 文档—(也称为 Javadoc)。在默认情况下,会在 Javadoc 中看到三个窗格。左上方的窗格显示了 API 中的所有包,左下方的窗格显示了每个包中的类。右边的主窗格显示了当前选中的包或类的详细信息。例如,如果单击左上方窗格中的 java.util
包,然后单击它下方列出的 ArrayList
类,就会在右边窗格中看到有关 ArrayList
的详细信息,包括它的用途描述、如何使用它和它的方法。
像任何编程语言一样,Java 语言也有自己的结构、语法规则和编程范例。Java 语言的编程范例基于 OOP 的概念,该语言的特性支持此概念。
Java 语言是 C 语言的一种衍生语言,所以它的语法规则与 C 语言非常相似。例如,代码块被模块化到方法中并用括号({
和}
)分隔,在使用变量之前要声明它们。
在结构上,Java 语言是从包开始的。包是 Java 语言的命名空间机制。包中包含类,而类中则包含方法、变量、常量,等等。您将在本教程中了解 Java 语言的各个部分。
为 Java 平台编写程序时,会在 .java 文件中编写源代码,然后编译它们。编译器对照语言的语法规则来检查您的代码,然后将字节码写至 .class 文件中。字节码是一组用于在 Java 虚拟机 (JVM) 上运行的指令。Java 编译器与其他语言编译器不同,除了此抽象级别之外,它还会写出适用于将在其上运行程序的 CPU 芯片集的指令。
在运行时,JVM 读取并解释 .class 文件,在为其编写 JVM 的本机硬件平台上执行程序的指令。JVM 会解释字节码,就像 CPU 会解释汇编语言指令一样。不同之处在于,JVM 是一个专为特殊平台而编写的软件。JVM 是 Java 语言的“编写一次,随处运行”原则的核心。您的代码可以在为其提供了适当的 JVM 实现的任何芯片集上运行。JVM 可用于 Linux 和 Windows 等主要平台,在用于移动电话和爱好者芯片的 JVM 中已经实现了 Java 语言的子集。
Java 平台不会强迫您时刻关注内存分配(或使用第三方库完成此工作),它提供了开箱即用的内存管理。当 Java 应用程序在运行时创建了一个对象实例的时候,JVM 会自动从堆中为该对象分配内存空间 — 一个专门留出供您的程序使用的内存池。Java 垃圾收集器在后台运行,跟踪应用程序不再需要哪些对象并从它们那里回收内存。这种内存处理方法称为隐式内存管理,因为它不要求您编写任何内存处理代码。垃圾收集是 Java 平台性能的基本特征之一。
下载 Java Development Kit (JDK) 后,您会获得 — 除了编译器和其他工具之外 — 一个包含预构建实用程序的完整的类库,它可以帮助您完成大部分常见的应用程序开发任务。了解 JDK 包和库的范围的最佳方法是查阅 JDK API 文档。
Java Runtime Environment(JRE;也称为 Java 运行时)包括 JVM、代码库以及运行用 Java 语言编写的程序所需的组件。JRE 可用于多种平台。您可以根据 JRE 许可条款,对您的应用程序自由重新分发 JRE,以便为应用程序的用户提供一个运行您的软件的平台。JRE 包含在 JDK 中。
在这一节中,将会下载并安装 JDK 和 Eclipse IDE 的当前版本,还将设置您的 Eclipse 开发环境。
如果已经安装了 JDK 和 Eclipse IDE,您可能想要跳到“Eclipse 入门”部分,或者它后面的“面向对象编程的概念”部分。
JDK 包含一组用于编译和运行 Java 代码的命令行工具,包括 JRE 的一个完整副本。尽管您可以使用这些工具来开发您的应用程序,但大部分开发人员还喜欢 IDE 的其他功能、任务管理和可视界面。
Eclipse 是一个用于 Java 开发的流行的开源 IDE。Eclipse 处理基本任务,比如代码编译和调试,让您可以集中精力编写和测试代码。此外,还可以使用 Eclipse 将源代码文件组织到项目中,编译和测试这些项目,并将项目文件存储在任意数量的源代码存储库中。您需要一个已安装的 JDK,才能使用 Eclipse 进行 Java 开发。如果您下载一个 Eclipse bundle,其中已包含 JDK。
按照这些步骤下载和安装 JDK:
/usr/libexec/java_home -1.8
来查看 JDK 8 在 Mac 上的位置。路径的显示类似于 /Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home。 参阅 JDK 8 和 JRE 8 安装了解更多的信息。
现在,您的计算机上有了一个 Java 环境。接下来,您将安装 Eclipse IDE。
要下载并安装 Eclipse,请遵循这些步骤:
Eclipse IDE 作为一种有用的抽象而位于 JDK 之上,但它仍需要访问 JDK 及其各种工具。在可以使用 Eclipse 编写 Java 代码之前,必须告诉它 JDK 所在的位置。
要设置您的 Eclipse 开发环境:
Eclipse 现在已经设置好了,并为您创建项目以及编译和运行 Java 代码做好了准备。您将在下一节中熟悉 Eclipse。
Eclipse 不仅仅是一个 IDE;它还是一个完整的开发生态系统。这一节将简要介绍实际使用 Eclipse 进行 Java 开发。
Eclipse 开发环境有 4 个主要组件:
Eclipse 中的主要组织单元是工作区。一个工作区包含您所有的项目。透视图是一种查看每个项目的方式(由此命名为"透视图"),一个透视图中包含一个或多个视图。
图 2 显示了 Java 透视图,它是 Eclipse 的默认透视图。在启动 Eclipse 时会看到这个透视图。
Java 透视图包含开始编写 Java 应用程序所需的工具。图 2 中显示的每个选项卡式窗口都是 Java 透视图的一个视图。Package Explorer 和 Outline 是两个特别有用的视图。
Eclipse 环境是高度可配置的。每个视图都可停靠,所以您可以在 Java 透视图中移动它,将它放在您想要放置它的位置。但就目前而言,我们将保持默认的透视图和视图设置。
按照这些步骤创建一个新的 Java 项目:
Tutorial
作为项目名称,单击 Finish。 您现在已经创建了一个新的 Eclipse Java 项目和源代码文件夹。您的开发环境已经准备好大显身手。但是,理解 OOP 范例 — 将在本教程接下来的两个小节中介绍 — 至关重要。如果您熟悉 OOP 概念和原则,那么您可能想要跳到“Java 语言入门”部分。
Java 语言(基本上)是面向对象的。如果您之前未使用过面向对象的语言,OOP 概念初看起来可能很陌生。这一节将简要介绍 OOP 语言概念,并使用结构化编程作为一个对比点。
结构化编程语言(比如 C 和 COBOL)遵循一种不同于面向对象编程的编程范例。结构化编程范例是高度面向数据的:您拥有数据结构,然后程序指令会处理该数据。面向对象的语言(比如 Java 语言)将数据和程序指令组合到对象中。
对象是一个自包含 (self-contained) 的实体,它仅包含属性和行为,不包含其他任何内容。在面向对象的语言中,数据和程序逻辑被组合在一起,而不是通过字段(属性)提供一个数据结构,并将该结构传递到处理它的所有程序逻辑(行为)。这种组合可能出现在明显不同的粒度级别上,从细粒度的对象(比如 Number
)到粗粒度的对象(比如一个大型银行应用程序中的 FundsTransfer
服务)。
父对象被用作派生更复杂的子对象的结构基础。子对象看起来类似其父对象,但更加特殊化。在面向对象的范例中,可以重用父对象的通用属性和行为,向其子对象添加不同的属性和行为。(您将在本教程的下一节中了解有关继承的更多信息。)
对象通过发送消息(在 Java 用语中称为方法调用)与其他对象进行通信。此外,在面向对象的应用程序中,程序代码会协调对象之间的活动,以便在给定应用程序域的上下文中执行任务。
一个精心编写的对象:
实质上,对象是一个离散的实体,它仅在其他对象上拥有必要的依赖关系来执行其任务。
现在您了解了对象是什么样的。
Person
对象我从一个基于一种常见应用程序开发场景的示例开始:一个通过 Person
对象表示的人。
返回到对象的定义,您已经知道了一个对象有两个主要元素:属性和行为。我将展示这两个元素如何应用到 Person
对象。
一个人可能有哪些属性?一些常见的属性包括:
您可能想到更多的属性(而且以后您可以随时添加更多的属性),但此列表是一个不错的开端。
一个真正的人可以做各种各样的事,但对象行为通常与某种应用程序上下文相关。例如,在一个业务应用程序上下文中,您可能想询问您的 Person
对象,“您的年龄是多少?”作为响应,Person
会告诉您它的 Age
属性的值。
更复杂的逻辑可能隐藏在 Person
对象内部 — 比如为一个健康应用程序计算某个人的身体质量指数 (BMI) — 但就现在而言,假设 Person
拥有回答以下问题的行为:
状态是 OOP 中的一个重要概念。对象的状态在任何时刻都由它的属性的值来表示。
对于 Person
,它的状态是通过姓名、年龄、身高和体重等属性来定义的。如果想要表示多个属性,可以使用一个 String
类来实现此目的,本教程的后面部分会更详细地介绍这个类。
结合使用状态和字符串的概念,就可以对 Person
说,“通过向我提供您的属性列表(或 String
)来告诉我您是谁。”
如果您拥有结构化编程背景,那么您可能还不清楚 OOP 的价值主张。不过,一个人的属性以及检索(并转换)这些值的任何逻辑都可以使用 C 或 COBOL 进行编写。这一节将通过解释 OOP 范例的定义原则来解释它的好处:封装、继承和多态性。
回想一下,对象是离散的,或者是自包含的。此特征是封装的工作原理。隐藏是另一个有时用来表达对象的自包含、受保护性质的术语。
无论采用哪个术语,重要的是对象在其状态和行为与外部世界之间维护了一条界线。像真实世界中的对象一样,计算机编程中使用的对象与使用它们的应用程序中的不同类别的对象有着各种各样的关系。
在 Java 平台上,可以使用访问修饰符(将在本教程后面部分介绍)来改变对象关系性质,从公共更改为私有。公共访问是完全开放的,而私有访问则意味着对象的属性只能在对象自身内访问。
公共/私有边界采用了面向对象的封装原则。在 Java 平台上,您可以依靠一个信任系统,逐个对象地改变该边界的强度。封装是 Java 语言的一个强大特性。
在结构化编程中,常常会复制一个结构,为它提供一个新名称,并添加或修改属性,使新的实体(比如一个 Account
记录)不同于其原始来源。随着时间的推移,此方法会生成大量重复代码,这可能带来维护问题。
OOP 引入了继承的概念,特殊化的类 — 无需额外的代码 — 可以“复制”它们要特殊化的来源类的属性和行为。如果需要更改其中一些属性或行为,可以覆盖它们。您更改的唯一源代码是用于创建特殊化的类所需的代码。正如您从“面向对象编程的概念”部分所了解的那样,来源对象称为父对象,新的特殊化对象称为子对象。
假设您在编写一个人力资源应用程序,并且想使用 Person
类作为一个名为 Employee
的新类的基类(称为超类)。作为Person
的子类,Employee
将拥有 Person
类的所有属性,以及更多的属性,比如:
继承使得创建新的 Employee
类变得很容易,不需要手动复制所有 Person
代码。
您会在本教程的后面部分看到 Java 编程中的大量继承的示例,尤其是在第 2 部分中。
多态性是一种比封装和继承更难掌握的概念。实质上,它表示属于某个分层结构的相同分支的对象,在发送相同消息时(即在被告知做同一件事时),可采用不同的方式表明该行为。
要理解如何将多态性应用到业务应用程序上下文中,可以返回查看 Person
示例。记得告诉 Person
将它的属性格式化成一个 String
吗?多态性使得 Person
可以根据其 Person
类型,以各种不同的方式表示其属性。
多态性是在 Java 平台上的 OOP 中将会遇到的更复杂概念之一,不属于介绍性教程的讨论范围。
使用 Java 语言,您可以创建一级对象,但不是该语言中的所有内容都是对象。有两种特质可以区分 Java 语言与纯粹的面向对象的语言(比如 Smalltalk)。首先,Java 语言是对象和原语类型的一种混合。其次,使用 Java,您可以编写代码,将一个对象的内部工作向使用它的其他所有对象公开。
Java 语言为您提供了必要的工具来遵循合理的 OOP 原则,并生成合理的面向对象的代码。因为 Java 不是纯粹面向对象的,所以您必须在编写代码的方式上运用一些规则 — 该语言不会强制要求您做正确的事情,所以您必须自我约束。(本教程的最后一节“编写良好的 Java 代码”提供了一些技巧。)
一篇教程不可能介绍完所有的 Java 语言语法。第 1 部分的剩余内容将重点介绍该语言的基础知识,为您提供足够的知识和练习来编写简单程序。OOP 与对象密切相关,所以这一节会先介绍两个与 Java 语言如何处理它们紧密相关的主题:保留字和 Java 对象的结构。
像任何编程语言一样,Java 语言指定了一些编译器认为具有特殊含义的字。出于这个原因,不允许您使用它们来命名您的 Java 构造。保留字列表非常短:
abstract
assert
boolean
break
byte
case
catch
char
class
const
continue
default
do
double
else
enum
extends
final
finally
float
for
goto
if
implements
import
instanceof
int
interface
long
native
new
package
private
protected
public
return
short
static
strictfp
super
switch
synchronized
this
throw
throws
transient
try
void
volatile
while
请注意,true
、false
和 null
从技术上讲并不是保留字。尽管它们是文字,但我在此列表中包含它们是因为您不能使用它们来命名 Java 构造。
使用 IDE 进行编程的一个优势是,它可以对保留字使用语法颜色,本教程后面部分将会介绍。
类是一个包含属性和行为的离散实体(对象)的蓝图。类定义了对象的基本结构,在运行时,应用程序会创建一个对象实例。对象拥有清晰的边界和状态,可以在被正确请求时执行操作。每种面向对象的语言都拥有关于如何定义类的规则。
在 Java 语言中,类的定义如清单 1 所示:
package packageName; import ClassNameToImport; accessSpecifier class ClassName { accessSpecifier dataType variableName [= initialValue]; accessSpecifier ClassName([argumentList]) { constructorStatement(s) } accessSpecifier returnType methodName ([argumentList]) { methodStatement(s) } // This is a comment /* This is a comment too */ /* This is a multiline comment */ }
清单 1 包含各种类型的构造,我使用字体格式对它们进行了区分。加粗显示的构造(将在保留字列表中找到)是文字;在任何对象定义中,它们都必须与清单中的形式完全相同。我为其他构造提供的名称描述了它们表示的概念。本节的其余部分将会详细介绍所有构造。
备注:在清单 1 和本节的其他一些代码示例中,方括号表示它们之中的构造不是必需的。方括号本身(不同于 {
和 }
)不是 Java 语法的一部分。
请注意,清单 1 中还包含一些注释行:
// This is a comment /* This is a comment too */ /* This is a multiline comment */
对于每种编程语言,程序员都可以添加注释来帮助解释代码。Java 语法允许使用单行和多行注释。单行注释必须包含在一行上,但您可以使用邻近的单行注释来形成一个注释块。多行注释以 /*
开头,必须以 */
终止,多行注释可以分布在任意数量的行上。
在阅读本教程的“编写良好的 Java 代码”部分时,可以了解有关注释的更多信息。
在 Java 语言中,可以为您的类选择名称,比如 Account
、Person
或 LizardMan
。有时,可能最终使用相同名称来表达两个稍微不同的概念。这种情况称为名称冲突,这种情况经常发生。Java 语言使用包来解决这些冲突。
Java 包是一种提供命名空间的机制 — 命名空间是一个区域,名称在该区域内是唯一的,但在该区域外部可能不是唯一的。要唯一地标识一个构造,必须通过包含其命名空间来完全限定它。
包还提供了一种很好的方法,用于构建包含离散的功能单元的复杂多元应用程序。
要定义包,可以使用 package
关键字,后跟一个合法的包名称,并以一个分号结尾。包名称通常用点分隔,并遵循这种实际标准模式:
package orgType.orgName.appName.compName;
这个包的定义可以这样分解:
orgType
是组织类型,比如 com
、org
或 net
。 orgName
是组织的域的名称,比如 makotojava
、oracle
或 ibm
。 appName
是应用程序的缩写名称。 compName
是组件的名称。 Java 语言不会强制要求您遵循这种包约定。事实上,根本不需要指定一个包,在这种情况下,所有类都必须具有唯一名称且位于默认包中。作为一个最佳实践,建议您在按照这里描述的方式命名的包中定义所有的 Java 类。您在本教程中遵循了该约定。
在 Eclipse 编辑器中编写代码时,可以键入想要使用的某个类的名称,然后按下 Ctrl+Shift+O。Eclipse 会确定您需要哪些导入并自动添加它们。如果 Eclipse 找到两个具有相同名称的类,它会显示一个对话框,询问您想要为哪个类添加导入。
类定义中的下一部分(返回参考清单 1)是导入语句。导入语句告诉 Java 编译器在何处查找您代码中引用的类。任何重要的类都会使用其他类来实现某种功能,导入语句是您告诉 Java 编译器该功能的方式。
导入语句通常类似于以下语句:
import ClassNameToImport;
您指定 import
关键字,后跟您想要导入的类,然后是一个分号。类名称应该是完全限定的,这意味着它应该包含它的包。
要导入一个包中的所有类,可以将 .*
放在包名称的后面。例如,这条语句导入了 com.makotojava
包中的每个类:
import com.makotojava.*;
但是,导入整个包可能会降低代码的可读性,所以建议您使用完全限定名称仅导入您需要的类。
要在 Java 语言中定义一个对象,必须声明一个类。此外,可以将类看作一个对象的模板,就像一个饼干模子。
清单 1 包含这个类的声明:
accessSpecifier class ClassName { accessSpecifier dataType variableName [= initialValue]; accessSpecifier ClassName([argumentList]) { constructorStatement(s) } accessSpecifier returnType methodName([argumentList]) { methodStatement(s) } }
类的 accessSpecifier
可以拥有多个值,但该值通常是 public
。您很快会看到其他 accessSpecifier
值。
您几乎可以采用任何您想要的方式来命名类,但该约定将会使用驼峰式大小写:以一个大写字母开头,将串联的每个单词的第一个字母大写,将其他所有字母小写。类名称应该仅包含字母和数字。坚持这些准则,确保您的代码更容易供其他遵循相同约定的人访问。
类可以拥有两种类型的成员:变量和方法。
一个给定类的变量的值将区分该类的每个实例并定义它的状态。这些值通常称为实例变量。一个变量有:
accessSpecifier
dataType
variableName
initialValue
可能的 accessSpecifier
值是:
使用公共变量绝不是一个好主意,但在极少的情况下,有必要这么做,所以存在了这种选择。Java 平台不会限制您的用例,所以使用良好的编码约定取决于您的自律,即使您采取了别的选择。
public
:任何包中的任何对象都可以看到该变量。(不要使用此值;请参阅公共变量边栏。) protected
:相同包中定义的任何对象,或者(在任何包中定义的)一个子类,都可以看到该变量。 private
:只有包含该变量的类能够看到它。 变量的 dataType
取决于该变量是什么 — 它可能是一种原语类型或另一种类类型(同样地,稍后会更详细地介绍此内容)。
variableName
由您自己决定,但根据约定,变量名使用了我在“类命名约定”中描述的驼峰式大小写约定,但它们以小写字母开头。(这种样式有时称为小驼峰式大小写。)
现在不用担心 initialValue
,只需知道您在声明一个实例变量时可以初始化它。(否则,编译器会为您生成一个在实例化该类时设置的默认值。)
Person
的类定义在继续学习方法之前,这里有一个总结了您目前为止所学到的知识的示例。清单 2 是 Person
的类定义。
Person
的基类定义package com.makotojava.intro; public class Person { private String name; private int age; private int height; private int weight; private String eyeColor; private String gender; }
Person
的基类定义目前不是很有用,因为它只定义了它的属性(而且还是私有属性)。
为了更加有趣,Person
类需要行为 — 也就是方法。
类的方法定义了它的行为。
方法可分为两种主要类别:构造函数或其他所有方法,这些方法有许多类型。构造函数方法仅用于创建类的实例。其他类型的方法可用于几乎任何应用程序行为。
清单 1 中的类定义显示了定义某个方法的结构的方式,该方法包括一些元素,比如:
accessSpecifier
returnType
methodName
argumentList
某个方法的定义中的这些结构元素的组合称为该方法的签名。
接下来,我们会更详细地介绍这两种类型的方法,首先介绍构造函数。
您可以使用构造函数指定如何实例化一个类。清单 1 以抽象形式显示了构造函数声明语法;这里再次显示了它:
accessSpecifier ClassName([argumentList]) { constructorStatement(s) }
如果您没有提供构造函数,编译器会为您提供一个,该构造函数称为默认(或无参数)构造函数。但是,如果您提供了一个不同于无参数(或 no-arg)构造函数的构造函数,那么编译器就不会自动为您生成一个构造函数。
构造函数的 accessSpecifier
与用于变量的相同。构造函数的名称必须与类的名称相匹配。所以如果您将您的类命名为 Person
,那么构造函数的名称必须也是 Person
。
对于默认构造函数以外的其他任何构造函数,需要传递一个 argumentList
,它是一个或多个:
argumentType argumentName
argumentList
中的参数用逗号分隔,而且任何两个参数都不能具有相同的名称。argumentType
是一种原语类型或者另一种类类型(与变量类型相同)。
现在看看在以两种方式添加功能来创建 Person
对象时会发生什么:使用 no-arg 构造函数和初始化一个部分属性列表。
清单 3 显示了如何创建构造函数,以及如何使用 argumentList
:
Person
类定义 package com.makotojava.intro; public class Person { private String name; private int age; private int height; private int weight; private String eyeColor; private String gender; public Person() { // Nothing to do... } public Person(String name, int age, int height, int weight String eyeColor, String gender) { this.name = name; this.age = age; this.height = height; this.weight = weight; this.eyeColor = eyeColor; this.gender = gender; } }
请注意,清单 3 中使用了 this
关键字来实现变量赋值。this
关键字是 "this object" 的 Java 简写形式,在引用两个具有相同名称的变量时必须使用它。在本例中,age
既是一个构造函数参数,也是一个类变量,所以 this
关键字可以帮助编译器消除引用的歧义。
Person
对象变得更加有趣,但它需要更多的行为。为此,您需要更多的方法。
构造函数是包含特殊函数的一种特殊类型的方法。类似地,其他许多类型的方法执行了 Java 程序中的特殊函数。从本节开始,将探索其他方法类型,一直到整个教程结束。
返回清单 1 中,我展示了如何声明一个方法:
accessSpecifier returnType methodName ([argumentList]) { methodStatement(s) }
其他方法看起来非常类似于构造函数,但有两个例外。首先,可以为其他方法提供您喜欢的任何名称(当然,要遵守某些规则)。我推荐以下约定:
第二,不同于构造函数,其他方法有一个可选的返回类型。
Person
的其他方法 有了这些基本信息,您就可以在清单 4 中看到在向 Person
对象添加一些方法时会发生什么。(为了简便起见,我省略了构造函数。)
Person
package com.makotojava.intro; public class Person { private String name; private int age; private int height; private int weight; private String eyeColor; private String gender; public String getName() { return name; } public void setName(String value) { name = value; } // Other getter/setter combinations... }
请注意清单 4 中有关 "tter/setter combinations" 的注释。本教程的后面部分会更多地使用 getter 和 setter。就现在而言,您只需知道 getter 是一个用来检索属性值的方法,setter 是一个用来修改该值的方法。清单 4 只显示了一种 getter/setter 组合(针对 Name
属性),但您可以采用类似的方式定义更多的组合。
请注意,在清单 4 中,如果某个方法没有返回值,则必须通过在该方法的签名中指定 void
返回类型来告诉编译器。
通常,会使用两种类型的(非构造函数)方法:实例方法和静态方法。实例方法依赖于某个特定对象实例的状态来执行其行为。静态方法有时也称为类方法,因为它们的行为不依赖于任何单个对象的状态。静态方法的行为在类级别上发生。
静态方法主要用于实用程序;可以将它们视为全局方法(类似于 C),但将方法的代码与定义它的类放在一起。
例如,在整个教程中,都使用 JDK Logger
类将信息输出到控制台。要创建一个 Logger
类实例,不必实例化一个 Logger
类;而是可以调用一个名为 getLogger()
的静态方法。
在类上调用静态方法所用的语法不同于在对象上调用某个方法所用的语法。您还会使用包含该静态方法的类的名称,如这个调用中所示:
Logger l = Logger.getLogger("NewLogger");
在这个示例中,Logger
是类的名称,getLogger(...)
是方法的名称。所以,要调用一个静态方法,无需使用对象实例,只需使用类的名称。
是时候将前面几节中学到的知识融会贯通并开始编写一些代码了。这一节将指导您使用 Eclipse Package Explorer 声明一个类,并向它添加变量和方法。学习如何使用 Logger
类密切关注应用程序的行为,以及如何使用 main()
方法作为测试平台。
如果您未在(Java 透视图中的)Package Explorer 视图中,可以在 Eclipse 中通过 Window > Perspective > Open Perspective 进入该视图。您将执行设置来创建您的第一个 Java 类。第一步是创建一个放置该类的位置。包是命名空间构造,它们还可以方便地直接映射到文件系统的目录结构。
您可以不使用默认包(通常是一个坏主意),而是专门为您正在编写的代码创建一个包。单击 File > New > Package 启动 Java Package 向导,如图 4 所示:
将 com.makotojava.intro
键入至 Name 文本框中并单击 Finish。您会在 Package Explorer 中看到创建的新包。
您可以采用多种方法使用 Package Explorer 创建一个类,但最简单的方法是右键单击您刚刚创建的包并选择 New > Class...。New Class 对话框将会打开。
在 Name 文本框中,键入 Person
,然后单击 Finish。
新类将会显示在编辑器窗口中。建议关闭在首次打开 Java 透视图时其中默认处于打开状态的一些视图(Problems、Javadoc 等),以便更容易查看您的源代码。(在您下次打开 Eclipse 并转到 Java 透视图时,Eclipse 会记住您不想看到这些视图。)图 5 显示了一个打开了必要视图的工作区。
Eclipse 为您生成了一个 shell 类,并在顶部包含 package
语句。您现在只需充实该类。可以通过 Window > Preferences > Java > Code Style > Code Templates 配置 Eclipse 如何生成新类。为了简便起见,我们使用了 Eclipse 的开箱即用的代码生成。
在图 5 中,请注意新的源代码文件名称旁边的星号 (*),它表示我已经进行了修改。另请注意,该代码未保存。接下来请注意,我在声明 Name
属性时犯了一个错误:我将 Name
的类型声明为 Strin
。编译器无法找到对这样一个类的引用,并将它标记为一个编译器错误(也就是 Strin
下面的红色波浪线)。当然,我可以通过在 Strin
的末尾处添加一个 g
来修复我的错误。这是对使用 IDE 而不是命令行工具进行软件开发的一个功能小示范。通过将类型更改为 String
来更正该错误。
在清单 3 中,您已经开始充实 Person
类,但我没有过多地解释语法。现在,我将正式定义如何添加类变量。
回想一下,变量有一个 accessSpecifier
、一个 dataType
、一个 variableName
和一个可选的 initialValue
。早些时候,您简要了解了如何定义 accessSpecifier
和 variableName
。现在,将了解一个变量可以拥有的 dataType
。
dataType
可以是原语类型,或是对另一个对象的引用。例如,请注意,Age
是一个 int
(原语类型),Name
是一个 String
(一个对象)。JDK 带来了大量有用的类,比如 java.lang.String
,而且 java.lang
包中的类不需要导入(Java 编译器的一种简写形式)。但是,无论 dataType
是 JDK 类(比如 String
)还是用户定义的类,语法基本上是相同的。
表 1 给出了您可能经常看到的 8 种原语数据类型,包括没有显式初始化某个成员变量的值时这些原语采用的默认值。
类型 | 大小 | 默认值 | 值的范围 |
---|---|---|---|
boolean | 不适用 | false | true 或 false |
byte | 8 位 | 0 | -128 到 127 |
char | 16 位 | (无符号) | /u0000' /u0000' 到 /uffff' 或 0 到 65535 |
short | 16 位 | 0 | -32768 到 32767 |
int | 32 位 | 0 | -2147483648 到 2147483647 |
long | 64 位 | 0 | -9223372036854775808 到 9223372036854775807 |
float | 32 位 | 0.0 | 1.17549435e-38 到 3.4028235e+38 |
double | 64 位 | 0.0 | 4.9e-324 到 1.7976931348623157e+308 |
在进一步学习编码之前,您需要知道您的程序如何告诉您它们正在做什么。
Java 平台包含 java.util.logging
包,这是一种能够以可读形式收集程序信息的内置记录机制。Logger 是您通过对 Logger
类的一个静态方法调用来创建的指定实体:
import java.util.logging.Logger; //... Logger l = Logger.getLogger(getClass().getName());
在调用 getLogger()
方法时,您向它传递了一个 String
。就现在而言,只需养成传递您正编写的代码所在的类的名称的习惯。在任何常规(即非静态)方法中,前面的代码始终引用该类的名称并将它传递给 Logger
。
如果在静态方法中执行一个 Logger
调用,可以引用您所在的类的名称:
Logger l = Logger.getLogger(Person.class.getName());
在这个示例中,您所在的代码是 Person
类,所以引用了一个名为 class
的特殊文字代码,它检索 Class
对象(后面将会更详细地介绍)并获取其 Name
属性。
本教程的“编写良好的 Java 代码”部分包含一个关于如何不进行记录的技巧。
在开始执行测试之前,首先进入 Eclipse 源代码编辑器来编辑 Person
,将此代码添加到清单 3 中的 public class Person {
的后面,使其看起来像这样:
package com.makotojava.intro; public class Person { private String name; private int age; private int height; private int weight; private String eyeColor; private String gender; }
Eclipse 有一个用来生成 getter 和 setter(以及其他内容)的方便的代码生成器。 要尝试使用代码生成器,可以将鼠标光标放在 Person
类定义上(也就是说,放在类定义中的单词 Person
上),并单击 Source > Generate Getters and Setters...。在对话框打开时,单击 Select All,如图 6 所示。
对于插入点,可以选择 Last member 并单击 OK。
现在,通过将清单 5 中的代码键入到源代码窗口中类定义顶部的下面(public class Person ()
下方紧接的一行),将一个构造函数添加到 Person
中。
Person
构造函数 public Person(String name, int age, int height, int weight, String eyeColor, String gender) { this.name = name; this.age = age; this.height = height; this.weight = weight; this.eyeColor = eyeColor; this.gender = gender; }
确保没有指示编译错误的波浪线。
main()
作为测试平台 main()
是一个特殊方法,它可以包含在任何类中,以便 JRE 可以执行其代码。一个类不需要有 main()
方法 — 事实上,大部分类从未包含该方法 — 而且一个类最多可以有一个 main()
方法。 main()
是一个值得拥有的方便方法,因为它为类提供了一个快速测试平台。在企业开发中,将会使用测试库,比如 JUnit,但使用 main()
作为测试平台可以快速直接地创建测试平台。
现在生成一个 JUnit 测试案例,您将在其中使用清单 5 中的构造函数实例化一个 Person
,然后将该对象的状态打印到控制台。从这种意义上讲,“测试”可确保构造函数调用上的属性顺序是正确的(也就是说,它们被设置成了正确的属性)。
在 Package Explorer 中,右键单击 Person
类,然后单击 New > JUnit Test Case。New JUnit Test Case 向导的第一页将会打开,如图 7 所示。
单击 Next 接受默认值。您会看到 Test Methods 对话框,如图 8 所示。
在这个对话框中,选择了希望向导为其构建测试的一个或多个方法。在本例中,仅选择了构造函数,如图 8 所示。单击 Finish,Eclipse 生成了 JUnit 测试案例。
接下来,打开 PersonTest
,进入 testPerson()
方法,使其看起来类似于清单 6。
testPerson()
方法 @Test public void testPerson() { Person p = new Person("Joe Q Author", 42, 173, 82, "Brown", "MALE"); Logger l = Logger.getLogger(Person.class.getName()); l.info("Name:" + p.getName()); l.info("Age:"+ p.getAge()); l.info("Height (cm):"+ p.getHeight()); l.info("Weight (kg):"+ p.getWeight()); l.info("Eye Color:"+ p.getEyeColor()); l.info("Gender:"+ p.getGender()); assertEquals("Joe Q Author", p.getName()); assertEquals(42, p.getAge()); assertEquals(173, p.getHeight()); assertEquals(82, p.getWeight()); assertEquals("Brown", p.getEyeColor()); assertEquals("MALE", p.getGender()); }
暂时不用担心 Logger
类。只需输入您在清单 6 中看到的代码。您现在已经准备好运行您的第一个 Java 程序(和 JUnit 测试案例)。
要在 Eclipse 中运行 Java 应用程序,可以选择您想要运行的类,然后单击 Run 图标(它显示为绿色,有一个指向右边的小三角形箭头)。Eclipse 能够非常聪明地识别出您想要运行的类是一个 JUnit 测试案例,因此它会启动 JUnit。现在,休息一下,观看它运行。图 9 显示了发生的情况。
Person
的运行 Person
的运行 Console 视图将会自动打开并显示 Logger
输出。JUnit 视图也会打开,以显示测试的结果。
Person
目前看起来还不错,但可以使用一些额外的行为让它变得更有趣。创建行为意味着添加方法。这一节会更详细地介绍访问器方法— 也就是您已经看到过的 getter 和 setter。
要封装一个类的来自其他对象的数据,需要将它的变量声明为 private
,然后提供访问器方法。如您所见,getter 是一个用于检索属性值的访问器方法;setter 是一个用于修改该值的访问器方法。访问器的命名遵循一个称为 JavaBeans 模式的严格约定,其中任何属性 Foo
都有一个名为 getFoo()
的 getter 和一个名为 setFoo()
的 setter。
JavaBeans 模式非常常见,以至于 Eclipse IDE 中内置了对它的支持。您甚至已经看到了它的实际运用 — 在上一节中为 Person
生成 getter 和 setter 时。
访问器遵循以下准则:
private
访问进行声明。public
。 目前为止,声明访问器的最简单方法是让 Eclipse 为您声明它,如图 6 所示。但您还应该知道如何手动编写一个 getter 和 setter 对。假设您有一个属性 Foo
,它的类型为 java.lang.String
。它的完整声明(遵循访问器准则)将为:
private String foo; public String getFoo() { return foo; } public void setFoo(String value) { foo = value; }
您可能立刻会注意到,传递给 setter 的参数值的命名方式与通过 Eclipse 生成它时所用的命名方式不同。该命名遵循我自己的约定,也就是我向其他开发人员推荐的约定。我仅在少数情况下会手动编写 setter,这是将始终使用名称 value
作为 setter 的参数值。这个吸引眼球的词汇提醒我,我已经手动编写了 setter。因为我通常允许 Eclipse 为我生成 getter 和 setter,所以在我不这样做时,就有了一个很好的理由。使用 value
作为 setter 的参数值会提醒我这个 setter 很特殊。(代码注释也可以提醒我。)
调用方法很简单。您在清单 6 中已经了解了如何调用 Person
的各种 getter 来返回它们的值。现在,我将正式介绍执行方法调用的机制。
要在对象上调用某个方法,需要使用对该对象的引用。方法调用语法包含对象引用、一个文本点、方法名称和需要传递的所有参数:
objectReference.someMethod(); objectReference.someOtherMethod(parameter);
这是一个不含参数的方法调用:
Person p = /*obtain somehow */; p.getName();
这是一个包含参数的方法调用(访问 Person
的 Name
属性):
Person p = new Person("Joe Q Author", 42, 173, 82, "Brown", "MALE");
请记住,构造函数也是方法。而且您可以用空格和换行符来分隔参数。Java 编译器并不关心分隔方法。接下来的两个方法调用是等效的:
new Person("Joe Q Author", 42, 173, 82, "Brown", "MALE");
new Person("Joe Q Author",// Name 42, // Age 173, // Height in cm 82, // Weight in kg "Brown",// Eye Color "MALE");// Gender
请注意,第二个构造函数调用中的注释可以让下一位开发人员容易理解和阅读该调用。该开发人员一眼就能看出每个参数的用途。
方法调用也可以嵌套:
Logger l = Logger.getLogger(Person.class.getName()); l.info("Name:" + p.getName());
在这里,将 Person.class.getName()
的返回值传递给 getLogger()
方法。请记住,getLogger()
方法调用是一个静态方法调用,所以它的语法稍有不同。(您不需要使用一个 Logger
引用来实现该调用;而是可以使用类的名称作为调用的左边。)
这实际上就是方法调用。
本教程目前为止介绍了多个 String
类型的变量,但没有进行太多的解释。您在这一节中会进一步了解字符串,还会了解何时和如何使用运算符。
在 C 中处理字符串需要耗费大量精力,因为它们是您必须操作的 8 位字符的以 null 结尾的数组。在 Java 语言中,字符串是 String
类型的一级对象,包含帮助您处理它们的方法。(在字符串方面,与 C 语言最接近的 Java 代码是 char
原语数据类型,它可以保存单个 Unicode 字符,比如 a。)
您已经了解了如何实例化一个 String
对象并设置它的值(返回查阅清单 4),但您还有其他许多方法来实现此目的。这是创建一个具有 hello
值的 String
实例的两种方法:
String greeting = "hello";
greeting = new String("hello");
因为 String
是 Java 语言中的一级对象,所以可以使用 new
来实例化它们。设置一个 String
类型的变量会获得相同的结果,因为 Java 语言会创建一个 String
对象来保存文字,然后将该对象分配给实例变量。
您可以使用 String
完成许多事情,该类有许多很有帮助的方法。甚至还没有使用方法,您就已经通过串联或组合两个 String
完成了一些有趣的事情:
l.info("Name:" + p.getName());
加号 (+
) 在 Java 语言中是串联 String
的简写形式。(在循环内进行这种串联会影响性能,但就现在而言,无需担心这个问题。)
现在,可以尝试在 Person
类中串联 String
。 此刻,您有一个 name
实例变量,但有一个 firstName
和 lastName
也很不错。然后,您可以在另一个对象请求 Person
的全名时串联它们。
您需要做的第一件事是添加新的实例变量(在源代码中目前定义 name
的相同位置):
//private String name; private String firstName; private String lastName;
您不再需要 name
;您已经将它替换为 firstName
和 lastName
。
现在,您可以为 firstName
和 lastName
生成 getter 和 setter(如图 6 所示),删除 setName()
方法,将 getName()
更改为以下这样:
public String getName() { return firstName.concat(" ").concat(lastName); }
此代码演示了方法调用的链接 (chaining)。链接是一种经常用于不可变对象(比如 String
)的技术,其中对不可变对象的修改总是返回修改内容(但不会更改原始对象)。然后,您可以在返回的、已更改的值上进行操作。
正如您可能期望的,Java 语言可以执行算术运算,您已经了解了如何为变量赋值。现在,我将向您简要介绍随着您技术的提升而需要的一些 Java 语言运算符。Java 语言使用两种类型的运算符:
表 2 中总结了 Java 语言的算术运算符。
运算符 | 用法 | 描述 |
---|---|---|
+ | a + b | 将 a 和 b 相加 |
+ | +a | 如果 a 为 byte 、short 或 char ,则将它升级为 int |
- | a - b | 从 a 中减去 b |
- | -a | 求 a 的算术上的负数 |
* | a * b | 将 a 和 b 相乘 |
/ | a / b | 将 a 除以 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 的简写形式 |
除了表 2 中的运算符,您还了解了在 Java 语言中称为运算符的其他一些符号,包括:
.
),它限定了包的名称并调用方法 ()
),它限定了某个方法的用逗号分隔的参数列表 new
,(后跟一个构造函数名称时)它实例化了一个对象 Java 语言语法还包含一些专用于条件编程的运算符 — 也就是说,根据不同的输入提供不同的响应的程序。下一节将介绍这些运算符。
在这一节中,将了解可用来告诉 Java 程序,您希望它们根据不同的输入执行何种操作的各种语句和运算符。
Java 语言为您提供了运算符和控制语句,您可以在代码中使用它们来制定决策。代码中的决策是以一个 Boolean 表达式开始的(即一个计算结果为 true 或 false 的表达式)。这些表达式使用了关系运算符(它们将一个操作数或表达式与另一个进行对比)和条件运算符。
表 3 列出了 Java 语言的关系运算符和条件运算符。
运算符 | 用法 | 返回 true 的条件…… |
---|---|---|
> | a > b | a 大于 b |
>= | a >= b | a 大于或等于 b |
< | a < b | a 小于 b |
<= | a <= b | a 小于或等于 b |
== | a == b | a 等于 b |
!= | a != b | a 不等于 b |
&& | a && b | 如果 a 和 b 都为 true,则有条件地计算 b (如果 a 为 false,则不计算 b ) |
|| | a || b | 如果 a 或 b 为 true,则有条件地计算 b (如果 a 为 true,则不计算 b ) |
! | !a | a 为 false |
& | a & b | 如果 a 和 b 都为 true,则始终计算 b |
| | a | b | 如果 a 或 b 为 true,则始终计算 b |
^ | a ^ b | a 和 b 不同 |
if
语句 有了大量运算符后,是时候使用它们了。此代码显示了在将某种逻辑添加到 Person
对象的 getHeight()
访问器时发生的情况:
public int getHeight() { int ret = height; // If locale of the machine this code is running on is U.S., if (Locale.getDefault().equals(Locale.US)) ret /= 2.54;// convert from cm to inches return ret; }
如果当前地区是在美国(未使用公制),那么将 height
(以厘米为单位)的内部值转换为英寸可能更有意义。这个(人为设计的)示例演示了 if
语句的用法,该语句计算圆括号中的一个 Boolean 表达式。如果该表达式的计算结果为 true,那么它将执行下一条语句。
在本例中,只在运行代码的机器的 Locale
是 Locale.US
时,才需要执行一条语句。如果需要执行多条语句,可以使用花括号来形成一个复合语句。复合语句将许多语句组成一个语句 — 而且复合语句还可以包含其他复合语句。
Java 应用程序中的每个变量都拥有范围(或局部化的命名空间),在该范围中,您可以在代码中通过名称访问这些变量。在该空间外,该变量就位于范围之外,如果尝试访问它,则会获得编译器错误。Java 语言中的范围级别是通过变量的声明位置来定义的,如清单 7 所示。
public class SomeClass { private String someClassVariable; public void someMethod(String someParameter) { String someLocalVariable = "Hello"; if (true) { String someOtherLocalVariable = "Howdy"; } someClassVariable = someParameter; // legal someLocalVariable = someClassVariable; // also legal someOtherLocalVariable = someLocalVariable;// Variable out of scope! } public void someOtherMethod() { someLocalVariable = "Hello there";// That variable is out of scope! } }
在 SomeClass
中,someClassVariable
可以通过所有实例(即非静态)方法进行访问。在 someMethod
中,someParameter
是可见的,但在该方法外,它是不可见的,而且此规则也适用于 someLocalVariable
。在 if
代码块中,声明了 someOtherLocalVariable
,在 if
代码块以外的地方,它就超出了范围。出于这个原因,我们可以说 Java 拥有代码块范围,因为代码块(由 {
和 }
限定)定义了范围边界。
范围有许多规则,但清单 7 显示了最常用的规则。请花几分钟时间熟悉一下它们。
else
语句 有时,在程序的控制流中,您想仅在某个特殊表达式的计算结果不为 true 时执行操作。这时使用 else
很方便:
public int getHeight() { int ret; if (gender.equals("MALE")) ret = height + 2; else { ret = height; Logger.getLogger("Person").info("Being honest about height..."); } return ret; }
else
语句的工作原理与 if
相同,因为它只执行遇到的下一条语句。在本例中,两条语句分组到一条复合语句中(请注意花括号),然后程序会执行此复合语句。
还可以使用 else
执行一次额外的 if
检查:
if (conditional) { // Block 1 } else if (conditional2) { // Block 2 } else if (conditional3) { // Block 3 } else { // Block 4 } // End
如果 conditional
的计算结果为 true,则执行 Block 1
,并且程序跳到最后一个花括号(用 // End
表示)之后的下一条语句。如果 conditional
的计算结果不 为 true,则计算 conditional2
。如果 conditional2
为 true,则计算 Block 2
,并且程序跳到最后一个花括号之后的下一条语句。如果 conditional2
不为 true,则程序前进到 conditional3
,以此类推。仅在所有三个条件都失败时,才会执行 Block 4
。
Java 语言为执行简单的 if
/ else
语句检查提供了一个方便的运算符。它的语法是:
(conditional) ?statementIfTrue :statementIfFalse;
如果 conditional
的计算结果为 true,则执行 statementIfTrue
;否则执行 statementIfFalse
。每条语句都不允许使用复合语句。
当您知道在条件计算的结果为 true 时,需要执行一条语句,如果不为 true,则需要执行另一条语句,此时使用三元运算符就很方便。三元运算符常常用于初始化某个变量(比如一个返回值),像这样:
public int getHeight() { return (gender.equals("MALE")) ?(height + 2) : height; }
问号后面的圆括号不是必需的,但使用它们会让代码更容易阅读。
除了能够对程序应用条件,并且基于各种 if
/then
场景得到不同的结果,有时您可能希望代码反复执行相同操作,直到作业完成。在这一节中,将了解用于迭代代码或多次执行它的两个构造:for
循环和 while
循环。
循环是一种编程构造,它会在满足某个条件(或某组条件)时反复执行。例如,您可能要求某个程序读取所有记录,直到文件结束,或者在一个数组中的所有元素上循环,并处理每个元素。(您可以在本教程的“Java Collection”部分了解数组。)
for
循环 Java 语言中的基本循环结构是 for
语句,您可以使用它在某个值范围上进行迭代,以确定执行某个循环多少次。for
循环的抽象语法是:
for (initialization; loopWhileTrue; executeAtBottomOfEachLoop) { statementsToExecute }
在循环的开头,将会执行 initialization 语句(多个 initialization 语句可用逗号分隔)。只要 loopWhileTrue
(一个计算结果必须为 true 或 false 的 Java 条件表达式)为 true,就会执行该循环。在循环的底部,将会执行 executeAtBottomOfEachLoop
。
for
循环的示例 如果想要将一个 main()
方法改为执行三次,可以使用一个 for
循环,如清单 8 所示。
for
循环public static void main(String[] args) { Logger l = Logger.getLogger(Person.class.getName()); for (int aa = 0; aa < 3; aa++) { Person p = new Person("Joe Q Author", 42, 173, 82, "Brown", "MALE"); l.info("Loop executing iteration# " + aa); l.info("Name:" + p.getName()); l.info("Age:"+ p.getAge()); l.info("Height (cm):"+ p.getHeight()); l.info("Weight (kg):"+ p.getWeight()); l.info("Eye Color:"+ p.getEyeColor()); l.info("Gender:"+ p.getGender()); } }
在清单 8 的开头处,局部变量 aa
被初始化为 0。该语句仅在初始化循环时执行一次。然后,循环连续执行三次,每次都将 aa
递增 1。
如稍后将会看到的,可以使用一种替代性的 for
循环语法在实现 Iterable
接口的构造(比如数组和其他 Java 实用程序类)上执行循环。就现在而言,只需注意清单 8 中的 for
循环语法的用法。
while
循环while
循环的语法是:
while (condition) { statementsToExecute }
正如您猜想的那样,如果 while condition
的计算结果为 true,则执行该循环。在每个迭代的顶部(即在执行任何语句之前),都会计算该条件。如果该条件的计算结果为 true,则执行循环。因此,如果 while
循环的条件表达式的计算结果至少有一次不是 true,则永远不可能执行该循环。
再次查看清单 8 中的 for
循环。为了进行比较,清单 9 使用了一个 while
循环来获取相同的结果。
while
循环public static void main(String[] args) { Logger l = Logger.getLogger(Person.class.getName()); int aa = 0; while (aa < 3) { Person p = new Person("Joe Q Author", 42, 173, 82, "Brown", "MALE"); l.info("Loop executing iteration# " + aa); l.info("Name:" + p.getName()); l.info("Age:"+ p.getAge()); l.info("Height (cm):"+ p.getHeight()); l.info("Weight (kg):"+ p.getWeight()); l.info("Eye Color:"+ p.getEyeColor()); l.info("Gender:"+ p.getGender()); aa++; } }
如您所见,while
循环需要做比 for
循环更多的工作。您必须初始化 aa
变量,还要记得在循环的底部递增它。
do...while
循环 如果您想要一个始终执行一次并随后检查其条件表达式的循环,可以尝试使用 do...while
循环,如清单 10 所示。
do...while
循环int aa = 0; do { Person p = new Person("Joe Q Author", 42, 173, 82, "Brown", "MALE"); l.info("Loop executing iteration# " + aa); l.info("Name:" + p.getName()); l.info("Age:"+ p.getAge()); l.info("Height (cm):"+ p.getHeight()); l.info("Weight (kg):"+ p.getWeight()); l.info("Eye Color:"+ p.getEyeColor()); l.info("Gender:"+ p.getGender()); aa++; } while (aa < 3);
条件表达式 (aa < 3
) 在循环结束后才会检查。
有时,您需要在条件表达式的计算结果为 false 之前跳出一个循环。如果在一个 String
数组中搜索某个特殊的值,而且一旦找到它,就不关心数组的其他元素,那么有可能出现这种情况。在您想要跳出循环的时候,Java 语言提供了 break
语句,如清单 11 所示。
break
语句public static void main(String[] args) { Logger l = Logger.getLogger(Person.class.getName()); int aa = 0; while (aa < 3) { if (aa == 1) break; Person p = new Person("Joe Q Author", 42, 173, 82, "Brown", "MALE"); l.info("Loop executing iteration# " + aa); l.info("Name:" + p.getName()); l.info("Age:"+ p.getAge()); l.info("Height (cm):"+ p.getHeight()); l.info("Weight (kg):"+ p.getWeight()); l.info("Eye Color:"+ p.getEyeColor()); l.info("Gender:"+ p.getGender()); aa++; } }
break
语句会将您带到它所在的循环外的下一条可执行语句。
在清单 11 中的(简化的)示例中,您只希望执行循环一次并跳出。还可以跳过循环的单次迭代并继续执行循环。为实现此目的,需要使用 continue
语句,如清单 12 所示。
continue
语句public static void main(String[] args) { Logger l = Logger.getLogger(Person.class.getName()); int aa = 0; while (aa < 3) { if (aa == 1) continue; else aa++; Person p = new Person("Joe Q Author", 42, 173, 82, "Brown", "MALE"); l.info("Loop executing iteration# " + aa); l.info("Name:" + p.getName()); l.info("Age:"+ p.getAge()); l.info("Height (cm):"+ p.getHeight()); l.info("Weight (kg):"+ p.getWeight()); l.info("Eye Color:"+ p.getEyeColor()); l.info("Gender:"+ p.getGender()); } }
在清单 12 中,跳过了循环的第二次迭代,但继续执行第三次迭代。例如,在处理记录并遇到一条您完全不想处理的记录时,continue
很方便。您可以跳过该记录并前进到下一条记录。
大多数真实应用程序都会处理像文件、变量、来自文件的记录或数据库结果集这样的集合。Java 语言有一个复杂的 Collections Framework,它使您能够创建和管理各种类型的对象集合。这一节不会介绍 Java Collection 的每个细节,而将介绍最常用的集合类,并让您开始使用它们。
大多数编程语言都包含数组的概念,用数组来保存一组元素,Java 语言也不例外。数组只是相同类型的元素的集合。
备注:这一节的代码示例中的方括号是 Java Collection 所需的语法的一部分,不是可选元素的指示符。
您可以通过两种方式声明一个数组:
通常,像这样声明一个数组:
new elementType [arraySize]
您可以通过两种方式创建一个整数元素数组。这条语句创建了一个具有容纳 5 个元素的空间的数组,但它是空的:
// creates an empty array of 5 elements: int[] integers = new int[5];
这条语句一次性创建数组并将其初始化:
// creates an array of 5 elements with values: int[] integers = new int[] { 1, 2, 3, 4, 5 };
初始值放在花括号中,用逗号分隔。
创建数组的另一种方式是首先创建它,然后编写一个循环来初始化它:
int[] integers = new int[5]; for (int aa = 0; aa < integers.length; aa++) { integers[aa] = aa+1; }
前面的代码声明了一个可容纳 5 个元素的整数数组。如果尝试在该数组中放入 5 个以上的元素,Java 运行时会抛出一个异常。您将在第 2 部分 中了解异常以及如何处理它们。
要加载数组,您需要循环从 1 一直到数组长度的整数(可以在数组上调用 .length
来获取数组长度 — 稍后会更详细地介绍这一点)。在本例中,循环在到达 5 时停止。
在加载数组后,可以像以前一样访问它:
Logger l = Logger.getLogger("Test"); for (int aa = 0; aa < integers.length; aa++) { l.info("This little integer's value is:" + integers[aa]); }
这种较新的(从 JDK 5 起开始提供)语法也有效:
Logger l = Logger.getLogger("Test"); for (int i : integers) { l.info("This little integer's value is:" + i); }
我发现较新的语法更容易使用,而且我会在这一节中使用它。
可以将数组视为一系列的桶,每个桶中放入一个具有某种类型的元素。每个桶使用一个索引进行访问:
element = arrayName [elementIndex];
要访问某个元素,需要使用数组的引用(它的名称)和您想要的元素所在地方的索引。
length
方法 正如已经看到的,length
是一个方便的方法。它是一个内置方法,所以它的语法不包含常见的圆括号。只需键入单词 length
,它就会返回 — 您所期望的 — 数组的大小。
Java 语言中的数组是从 0 开始的。所以,对于某个名为 array
的数组,数组中的第一个元素始终位于 array[0]
,最后一个元素位于 array[array.length - 1]
。
您已经了解了数组如何保存原语类型,但值得一提的是,它们还可以保存对象。从这个意义上讲,数组是 Java 语言的最实用的集合。
创建一个 java.lang.Integer
对象数组与创建一个原语类型数组没有太大区别。同样地,您有两种方式来创建它:
// creates an empty array of 5 elements: Integer[] integers = new Integer[5];
// creates an array of 5 elements with values: Integer[] integers = new Integer[] { Integer.valueOf(1), Integer.valueOf(2) Integer.valueOf(3) Integer.valueOf(4) Integer.valueOf(5));
Java 语言中的每种原语类型都有一个对应的 JDK 类,可以在表 4 中看到它们。
原语 | 对应的 JDK 类 |
---|---|
boolean | java.lang.Boolean |
byte | java.lang.Byte |
char | java.lang.Character |
short | java.lang.Short |
int | java.lang.Integer |
long | java.lang.Long |
float | java.lang.Float |
double | java.lang.Double |
每个 JDK 类都提供了方法来解析其内部表示,并将它转换为相应的原语类型。例如,下面这段代码将十进制值 238 转换为一个 Integer
:
int value = 238; Integer boxedValue = Integer.valueOf(value);
这项技术称为装箱,因为您正将原语放在一个包装器(或箱子)中。
类似地,要将 Integer
表示转换回它的 int
对应类,可以对它执行拆箱:
Integer boxedValue = Integer.valueOf(238); int intValue = boxedValue.intValue();
严格地讲,您不需要显式装箱和拆箱原语。而是可以使用 Java 语言的自动装箱和自动拆箱特性:
int intValue = 238; Integer boxedValue = intValue; // intValue = boxedValue;
但是,建议您避免使用自动装箱和自动拆箱,因为它们可能导致代码可读性问题。装箱和拆箱代码段中的代码比自动装箱的代码更明显,因此更容易阅读;我相信为此投入额外的工作是值得的。
您已经了解了如何获取一个装箱的类型,但如何将一个您怀疑具有装箱类型的 String
解析到它的正确箱子中?JDK 包装器类也有实现此目的的方法:
String characterNumeric = "238"; Integer convertedValue = Integer.parseInt(characterNumeric);
您还可以将 JDK 包装器类型的内容转换为 String
:
Integer boxedValue = Integer.valueOf(238); String characterNumeric = boxedValue.toString();
请注意,当在 String
表达式中使用串联运算符时(您已经在对 Logger
的调用中了解过它),原语类型已自动装箱,而且包装器类型会自动在它们之上调用 toString()
。非常方便。
List
是一种集合构造,根据定义,它是一种有序集合,也称为序列。因为 List
是有序的,所以您能够完全控制将列表项放入 List
中的何处。Java List
集合只能保存对象,而且它为其行为方式定义了严格的契约。
List
是一个接口,所以您不能直接实例化它。您将使用它的最常用的实现 ArrayList
。可以通过两种方式来实现此声明。首先,可以使用显式语法:
List<String> listOfStrings = new ArrayList<String>();
其次,可以使用(JDK 7 中引入的)“菱形”运算符:
List<String> listOfStrings = new ArrayList<>();
请注意,ArrayList
实例化中没有指定对象类型。这是因为表达式右边的类的类型必须与左边的类的类型匹配。在本教程的剩余部分中,两种类型都会用到,因为在实际使用中可能看到这两种用法。
请注意,我将 ArrayList
对象分配给了一个 List
类型的变量。在 Java 编程中,可以将某种类型的变量分配给另一种类型的变量,只要被分配值的变量是分配值变量所实现的超类或接口。可以在第 2 部分中的“继承”部分进一步了解变量分配会受到哪些影响。
前面代码段中的 <Object>
被称为正式类型。<Object>
告诉编译器,这个 List
包含一个 Object
类型的集合,这意味着您可以将自己喜欢的任何东西放在 List
中。
如果您想对能够放入或不能放入 List
中的东西严加约束,可以采用不同的方式定义正式类型:
List<Person> listOfPersons = new ArrayList<Person>();
现在,您的 List
只能保存 Person
实例。
List
使用 List
非常容易,通常像使用 Java 集合一样。以下是您可以使用 List
执行的一些操作:
List
中。 List
它目前有多大。 List
中获取东西。 现在,您可以尝试其中一些操作。您已经了解了如何通过实例化 List
的 ArrayList
实现类型来创建一个 List 实例,现在您可以开始操作了。
要将一些东西放入 List
中,可以调用 add()
方法:
List<Integer> listOfIntegers = new ArrayList<>(); listOfIntegers.add(Integer.valueOf(238));
add()
方法将该元素添加到 List
的末尾处。
要询问 List
它有多大,可以调用 size()
:
List<Integer> listOfIntegers = new ArrayList<>(); listOfIntegers.add(Integer.valueOf(238)); Logger l = Logger.getLogger("Test"); l.info("Current List size:" + listOfIntegers.size());
要从 List
中检索某一项,可以调用 get()
并向它传递您想要获得的项的索引:
List<Integer> listOfIntegers = new ArrayList<>(); listOfIntegers.add(Integer.valueOf(238)); Logger l = Logger.getLogger("Test"); l.info("Item at index 0 is:" listOfIntegers.get(0));
在真实的应用程序中,List
将包含记录(或业务对象),您可能想在处理过程中查看所有这些对象。如何以通用方式实现此目的?您想要迭代该集合,您可以这么做是因为 List
实现了 java.lang.Iterable
接口。(您将在第 2 部分中了解接口。)
可迭代
如果一个集合实现了 java.lang.Iterable
,那么该集合被称为可迭代集合。您可以从一端开始,逐项地处理集合,直到处理完所有项。
您已经在“循环”部分了解了迭代实现 Iterable
接口的集合的特殊语法。这里再次使用了它:
for (objectType varName : collectionReference) { // Start using objectType (via varName) right away... }
List
前面的示例是抽象的;现在,这里有一个更加实际的示例:
List<Integer> listOfIntegers = obtainSomehow(); Logger l = Logger.getLogger("Test"); for (Integer i : listOfIntegers) { l.info("Integer value is :" + i); }
这个小代码段所做的事情与下面这个长代码段所做的事情相同:
List<Integer> listOfIntegers = obtainSomehow(); Logger l = Logger.getLogger("Test"); for (int aa = 0; aa < listOfIntegers.size(); aa++) { Integer I = listOfIntegers.get(aa); l.info("Integer value is :" + i); }
第一个代码段使用了简写形式的语法:没有 index
变量(在本例中为 aa
)要初始化,也没有调用 List
的 get()
方法。
因为 List
扩展了 java.util.Collection
(它实现 Iterable
),所以您可以使用简写形式的语法来迭代任何 List
。
Set
是一种集合构造,根据定义,它包含唯一的元素 — 也即是说,没有重复。List
可以包含同一个对象数百次,而 Set
只能包含某个给定实例一次。Java Set
集合只能保存对象,而且它为其行为方式定义了严格的契约。
因为 Set
是一个接口,所以您不能直接将其实例化,所以这是我最喜欢的实现之一:HashSet
。HashSet
很容易使用,并且类似于 List
。
以下是您可以使用 Set
执行的一些操作:
Set
中。 Set
它目前有多大。 Set
中获取东西。 Set
Set
的一个特有属性是,它可保证其元素中的唯一性,但不关心元素的顺序。考虑以下代码:
Set<Integer> setOfIntegers = new HashSet<Integer>(); setOfIntegers.add(Integer.valueOf(10)); setOfIntegers.add(Integer.valueOf(11)); setOfIntegers.add(Integer.valueOf(10)); for (Integer i : setOfIntegers) { l.info("Integer value is:" + i); }
您可能已料到,该 Set
中应该有三个元素,但它只有两个,因为包含值 10
的 Integer
对象只被添加了一次。
请在迭代 Set
时注意此行为,像这样:
Set<Integer> setOfIntegers = new HashSet(); setOfIntegers.add(Integer.valueOf(10)); setOfIntegers.add(Integer.valueOf(20)); setOfIntegers.add(Integer.valueOf(30)); setOfIntegers.add(Integer.valueOf(40)); setOfIntegers.add(Integer.valueOf(50)); Logger l = Logger.getLogger("Test"); for (Integer i : setOfIntegers) { l.info("Integer value is :" + i); }
对象的打印顺序可能不同于添加它们的顺序,因为 Set
只保证唯一性,不保证顺序。如果将前面的代码粘贴到 Person
类的 main()
方法中并运行它,就可以看到这种情况。
Map
是一种方便的集合构造,可以使用它将一个对象(键)与另一个对象(值)相关联。您可以想象,Map
的键必须是唯一的,而且它被用于在以后的时间检索值。Java Map
集合只能保存对象,而且它为其行为方式定义了严格的契约。
因为 Map
是一个接口,您无法直接实例化它,所以这是我最喜欢的实现之一:HashMap
。
以下是您可以使用 Map
执行的一些操作:
Map
中。 Map
中获取东西。 Map
的键的 Set
— 用于迭代它。 Map
要将东西放入 Map
中,需要拥有一个表示它的键的对象和一个表示它的值的对象:
public Map<String, Integer> createMapOfIntegers() { Map<String, Integer> mapOfIntegers = new HashMap<>(); mapOfIntegers.put("1", Integer.valueOf(1)); mapOfIntegers.put("2", Integer.valueOf(2)); mapOfIntegers.put("3", Integer.valueOf(3)); //... mapOfIntegers.put("168", Integer.valueOf(168)); }
在这个示例中,Map
包含 Integer
,使用一个 String
作为键,这恰好是它们的 String
表示。要检索某个特殊的 Integer
值,您需要它的 String
表示:
mapOfIntegers = createMapOfIntegers(); Integer oneHundred68 = mapOfIntegers.get("168");
Set
和 Map
有时,您可能发现自己有一个对 Map
的引用,而且您想要遍历它的整个内容集合。在这种情况下,需要 Map
的键的 Set
:
Set<String> keys = mapOfIntegers.keySet(); Logger l = Logger.getLogger("Test"); for (String key : keys) { Integer value = mapOfIntegers.get(key); l.info("Value keyed by '" + key + "' is '" + value + "'"); }
请注意,在用于 Logger
调用时,会自动调用从 Map
中检索到的 Integer
的 toString()
方法。Map
没有返回它的键的 List
,因为 Map
是有键的,而且每个键都是唯一的。唯一性是 Set
的显著特征。
现在,您已经了解了如何编写 Java 应用程序,您可能想知道如何打包它们,以便其他开发人员可以使用它们,或者想知道如何将其他开发人员的代码导入您的应用程序中。这一节将介绍如何做。
JDK 附带了一个称为 JAR 的工具,它代表 Java Archive(Java 归档文件)。您可以使用此工具来创建 JAR 文件。在将代码打包到 JAR 文件中后,其他开发人员可以将该 AJR 文件放入其项目中,并配置他们的项目来使用您的代码。
在 Eclipse 中创建 JAR 文件很容易。在工作区中,右键单击 com.makotojava.intro
包并单击 File > Export。您会看到如图 10 所示的对话框。选择 Java > JAR file 并单击 Next。
在打开下一个对话框时,浏览到您想要存储 JAR 文件的地方,并将该文件命名为您喜欢的名称。.jar 扩展名是默认扩展名,我推荐使用它。单击 Finish。
您会在您选择的位置中看到您的 JAR 文件。如果将 JAR 放在 Eclipse 中的构建路径中,那么可以通过您的代码使用该文件中的类。正如您接下来将会看到的,这么做也很容易。
随着您越来越熟悉编写 Java 应用程序,您可能想使用越来越多的第三方应用程序来支持您的代码。尽管 JDK 非常不错,但它无法执行编写优秀的 Jave 代码所需的一切操作。Java 开源社区提供了许多库来帮助填补这些空白。例如,假设您想要使用 Commons Lang,这是一个用于操作核心 Java 类的 JDK 替换库。Commons Lang 提供的类可以帮助您操作数组,创建随机数,并执行字符串操作。
我们假设您已经下载了 Commons Lang,它存储在一个 JAR 文件中。要使用这些类,第一步操作是在项目中创建一个 lib 目录并将 JAR 文件放入其中:
lib
。 新文件夹显示在与 src 相同的级别上。现在将 Commons Lang JAR 文件复制到新的 lib 目录中。对于此示例,该文件名为 commons-lang3.3.4.jar。(我们通常会在 JAR 文件的命名中包含版本编号,在本例中为 3.4。)
现在,您只需告诉 Eclipse 将 commons-lang3.3.4.jar 文件中的类包含在您的项目中:
在 Eclipse 已经处理了 JAR 文件中的代码(即类文件)后,就可以在您的 Java 代码中引用(导入)这些代码。在 Project Explorer 中可以注意到,有一个称为 Referenced Libraries 的新文件夹,它包含 commons-lang3.3.4.jar 文件。
您已经掌握了编写 Java 程序所需的足够多的 Jave 语法,这意味着本教程的第一部分即将结束。最后一节将提出一些最佳实践,它们可以帮助您编写更干净、更容易维护的 Java 代码。
您在本教程中创建了一些类。甚至为少量(根据真实 Java 类的标准)属性生成 getter/setter 对后,Person
类有 150 行代码。这是一个小型的类。我们通常会看到包含 50 或 100 个方法和数千行源代码(或更多)的类。对方法而言,关键的一点是仅保留您需要的方法。如果您需要几个实质上执行相同操作但接受不同参数的帮助器方法(比如 printAudit()
方法),这是一个不错的选择。只需确保将方法列表限制为您需要的方法,不需要太多的方法。
通常,类表示应用程序中的某个概念实体,它们的大小应该只反映执行该实体所需的操作的功能。它们应该密切关注少量操作的执行并做好它们。
对于方法名称,一种不错的编码模式是意图揭示方法命名模式。通过一个简单的示例,很容易理解这种模式。乍一看,以下哪种方法名称更容易理解?
a()
computeInterest()
答案应该很明显,但出于某种原因,程序员倾向于为方法(在这里还包括变量)提供简短的名称。当然,过于长的名称可能不太方便,但传达某个方法的用途的名称不需要过于长。在编写大量代码 6 个月后,您可能已经不记得某个名为 compInt()
的方法的用途,但名为 computeInterest()
的方法的用途就很明显,可能用于计算利息。
与简短的类一样,简短的方法更受欢迎,原因是类似的。我尝试遵循的一条原则是,将方法的大小限制为一页,因为我可以在我的屏幕上看到它。这使我的应用程序类更容易维护。
如果某个方法的长度超过一页,我会重构它。重构会更改现有代码的设计,但不会更改它的结果。Eclipse 有一组非常强大的重构工具。通常,一个长方法包含一些聚集在一起的功能的子组。获得该功能并将它移动到另一个方法中(并为其提供相应的名称),然后根据需要传入参数。
将每个方法限定到单个作业中。我发现,一个方法做好一件事通常只需不超过 30 行的代码。
请使用注释。长期关注于您的人(甚至是六个月后的您自己)会感谢您。您可能听过一句格言代码写得好可以自述其身,谁还需要注释?我会给出两个理由表明这句格言是错误的:
所以,请注释您的代码。就这么简单。
编码风格是一种个人偏好,但建议您使用标准的 Java 括号语法:
public static void main(String[] args) { }
不要使用此风格:
public static void main(String[] args) { }
或这种风格:
public static void main(String[] args) { }
为什么?因为它是标准的,所以您遇到的大部分代码(比如,您没有编写却要为其支付维护费的代码)很可能都是采用这种方式编写的。尽管如此,Eclipse 确实允许您以自己喜欢的任何方式定义代码风格以及格式化您的代码。重点在于您挑选一种风格并坚持采用这种风格。
在 Java 1.4 引入内置记录之前,确定您的程序在执行何种操作的权威方式是执行一次类似这样的系统调用:
public void someMethod() { // Do some stuff... // Now tell all about it System.out.println("Telling you all about it:"); // Etc... }
Java 语言的内置记录工具(请参阅“您的第一个 Java 对象”)是一个更好的替代工具。我从不在我的代码中使用 System.out.println()
,而且我建议您也不要使用它。另一种替代方法是常用的 log4j 替换库,它是 Apache umbrella 项目的一部分。
(在我看来,而且不止我一个人这么看)业界最优秀的图书是 Martin Fowler 等人合著的重构:改进现有代码的设计。这本书读起来很有趣。作者们谈论了需要重构的“代码异味 (code smell)”,他们详细介绍了修复它们的各种技术。
重构和编写测试优先代码的能力是新程序员要学习的最重要技能。如果每个人都擅长这两种能力,业界将发生革命性变化。如果您擅长这两种能力,您最终会生成比许多同行更干净的代码和功能更强的应用程序。
在本教程中,学习了面向对象编程,了解了可用来创建有用对象的 Java 语法,还熟悉了可以帮助您控制开发环境的 IDE。您知道如何创建和运行可执行大量操作的 Java 对象,包括根据不同的输入执行不同的操作。您还知道了如何创建您的应用程序的 JAR,供其他开发人员在其程序中使用,您还掌握了一些基本的 Java 编程实践。
在本教程的后半部分中,将开始学习 Java 编程的一些更高级的构造,但整体讨论仍是介绍性的。该教程中涵盖的 Java 编程主题包括:
参阅“Java 编程介绍,第 2 部分:真实应用程序的构造。”