面向 Android 开发的 Java 学习手册(一)

原文:Learn Java for Android development

协议:CC BY-NC-SA 4.0

零、简介

智能手机和平板电脑如今风靡一时。它们的受欢迎程度很大程度上是由于它们运行应用的能力。尽管拥有越来越多基于 Objective-C 的应用的 iPhone 和 iPad 占据了先机,但事实证明,拥有越来越多基于 Java 的应用的基于 Android 的智能手机和平板电脑是一个强有力的竞争对手。

不仅许多 iPhone/iPad 应用开发人员通过销售他们的应用赚钱,许多 Android 应用开发人员也通过销售类似的应用赚钱。据科技网站如 The Register(【www.theregister.co.uk/】)报道,一些安卓应用开发者正在赚大钱(【www.theregister.co.uk/2010/03/02/android_app_profit/】)。

在当今充满挑战的经济环境下,也许你想尝试开发 Android 应用并赚些钱。如果你有好的想法、毅力和一些艺术天赋(或者可能认识一些有天赋的人),你已经是实现这个目标的一部分了。

提示相比 iPhone/iPad 应用开发,考虑 Android 应用开发的一个很好的理由是使用 Android 会产生更低的启动成本。比如,你不需要购买 Mac 来开发 Android 应用(开发 iPhone/iPad 应用需要 Mac);您现有的 Windows、Linux 或 Unix 机器会做得很好。

最重要的是,在进入 Android 之前,您需要对 Java 语言和基础应用编程接口(API)有一个坚实的理解。毕竟,Android 应用是用 Java 编写的,并且与许多标准的 Java APIs(例如,线程和输入/输出 API)交互。

我写了Learn Java for Android Development来给你一个坚实的 Java 基础,你以后可以用 Android 架构、API 和工具细节的知识来扩展它。这本书将让你对 Java 语言和许多重要的 API 有很强的掌握,这些 API 是 Android 应用和其他 Java 应用的基础。它还将向您介绍关键的开发工具。

图书组织

这本书的第一版分为十章和一个附录。第二版由 14 章和三个附录组成。每一版的每一章都提供了一组练习,你应该完成这些练习才能从内容中获得最大的益处。他们的解决方案在附录中给出。

在第一章中,我首先通过关注 Java 的双重性(语言和平台)来介绍 Java。然后我简单介绍一下 Oracle 的 Java SE、Java EE、Java ME 版的 Java 开发软件,以及 Google 的 Android 版。接下来,您将学习如何下载和安装 Java SE 开发工具包(JDK ),并通过开发和使用两个简单的 Java 应用来学习一些 Java 基础知识。在简要介绍了 Eclipse IDE 之后,您将对本书中涉及的各种 API 有一个概述。

在第二章中,我将重点介绍语言基础,让你开始一次深入的 Java 语言之旅。您将了解注释、标识符(和保留字)、类型、变量、表达式(和文字)以及语句。

在第三章中,我继续关注类和对象。您将学习如何声明一个类并从该类中实例化对象,如何在该类中声明字段并访问这些字段,如何在该类中声明方法并调用它们,如何初始化类和对象,以及如何在不再需要对象时将其删除。你还会学到更多关于数组的知识,数组在第二章中首次介绍。

在第四章中,我通过向你介绍带你从基于对象的应用到面向对象的应用的语言特性,增加了第三章的基于对象的知识库。具体来说,您将了解与继承、多态性和接口相关的特性。在探索继承的过程中,您了解了 Java 的终极超类。此外,在探索接口时,您会发现为什么它们包含在 Java 语言中;接口不仅仅是 Java 缺乏对多实现继承支持的一种变通方法,它还有更高的目的。

在第五章中,我向你介绍了四类高级语言特性:嵌套类型、包、静态导入和异常。

在第六章中,我向你介绍了四个额外的高级语言特性类别:断言、注释、泛型和枚举。

在第七章中,我开始关注 API 多于语言特性的趋势。在本章中,我首先向您介绍 Java 的许多面向数学的类型(例如, Math、StrictMath、BigDecimal 和 BigInteger ),然后向您介绍它的面向字符串的类型(例如, String、StringBuffer 和 StringBuilder )。最后,您将探索用于获取包信息的包类。

在第八章中,我继续通过关注基本类型包装类、线程和面向系统的 API 来探索 Java 的基本 API。

在第九章中,我专门关注 Java 的集合框架,它为你提供了一个在列表、集合、队列和映射中组织对象的解决方案。您还将了解面向集合的工具类,并回顾 Java 的遗留工具类型。

在第十章中,我继续探索 Java 的工具 API,向您介绍并发工具、日期类(用于表示时间)、格式化程序类(用于格式化数据项)、随机类(用于生成随机数)、定时器和定时器任务类(用于偶尔或重复执行任务),以及用于处理 ZIP 和 JAR 文件的 API。

第十一章是关于传统的输入/输出(I/O)的,主要是从文件的角度来看。在这一章中,您将从文件类、随机访问文件类、各种流类和各种写/读类的角度探索经典的 I/O。我对流 I/O 的讨论包括 Java 的对象序列化和反序列化机制。

在第十二章中,我继续通过关注网络来探索传统的 I/O。您将了解到 Socket、ServerSocket、DatagramSocket 和 MulticastSocket 类以及相关类型。您还将了解到用于在更高层次上实现网络 I/O 的 URL 类。在了解了底层的 NetworkInterface 和 InterfaceAddress 类之后,您将根据 CookieHandler 和 CookieManager 类以及 CookiePolicy 和 CookieStore 接口来探索 cookie 管理。

在第十三章中,我向你介绍了新的 I/O。你将在这一章中学习缓冲区、通道和正则表达式。我也很想介绍选择器和字符集,但是由于空间不足,我无法这样做。为了讨论选择器,我还必须讨论套接字通道,但是我只能讨论文件通道。然而,第十一章确实给了你一点儿字符集的味道。

在第十四章中,我通过关注数据库来总结这本书的章节部分。您将首先了解 Java DB 和 SQLite 数据库产品,然后探索与通过这些产品创建的数据库进行通信的 JDBC。

在附录 A 中,我给出了第一章到第十四章中所有练习的答案。

在附录 B 中,我将向您介绍在四张牌背景下的应用开发,这是一款基于控制台的纸牌游戏。

在附录 C 中,这本书的代码附带了一个单独的 PDF 文件,我向您介绍了高级 API(如反射和引用)以及在 Android 应用环境中可能不太有用的 API(如首选项——Android 提供了自己的解决方案)。)

注意你可以下载这本书的源代码,方法是将你的网络浏览器指向【www.apress.com/book/view/1430257226】,点击源代码标签,然后点击立即下载链接。

第一版对第二版

这本书的第一版于 2010 年 9 月出版。总的来说,我对第一版很满意,我感谢所有购买它的人。然而,正如有人多次向我指出的那样,第一版是有缺陷的。除了一些小的技术错误,在这本书的开发过程中,我还遇到了一些组织和其他方面的问题。

首先,我不应该在《??》第一章中介绍四张同类型的纸牌游戏。对于许多读者来说,这一点太复杂了。因此,我把游戏移到了附录 B ,以免让 Java 初学者不知所措。

此外,我试图在同一章中用类和对象的基础知识来涵盖语言基础知识(例如,语句和表达式)。虽然有些人欣赏这种方法,但它对初学者来说太令人困惑了;我向有这种感觉的读者道歉。在第二版中,我将 Java 语言的这些方面分开来(希望)解决这个问题。在第二章中,我关注于语句、表达式和其他非类/非对象的基础知识;在第三章中,我关注类和对象。

另一个问题是包含了复杂的 API,这些 API 要么在开发 Android 应用时很少使用,要么与 Android 开发者无关。示例包括引用、反射、首选项和国际化。我把这些 API 移到了附录 C,这样我就可以涵盖更简单(也可能更有用)的 API,比如 ZIP 和 Timer。(我还在附录 c 中增加了新内容。)

在编写第一版时,我计划更进一步,介绍 Java 对网络和数据库访问(通过 JDBC)、安全性、XML、新 I/O 等的支持。我愚蠢地提出了一个写六个免费章节的计划,但是只完成了三个章节的一部分。

不幸的是,我最初的六个免费章节的计划是有缺陷的。例如,我计划在关于新 I/O 的免费章节之后写一个关于网络的免费章节。这不是一个好的组织,因为新 I/O 包括套接字通道,所以网络章节应该在新 I/O 章节之前。

此外,我还了解到(通过各种关于 Android 和安全的博客)Java 的安全特性在 Android 环境中并不必要。因为这本书在一定程度上侧重于展示最有用的 Java APIs,以供随后在 Android 环境中使用,所以对 Java 安全 API 的介绍可能并不重要(尽管我可能是错的)。

注意第二版没有免费章节补充。然而,附录 C 是免费的。此外,我可能最终会在我的网站上提供一些额外的材料(也许是关于套接字通道、选择器和字符集的内容)。

接下来是什么?

在你完成这本书之后,我建议你去买一本格兰特·艾伦(2012 年出版)的《Android 4 入门》,开始学习如何开发 Android 应用。在那本书里,你学习了 Android 基础知识,以及如何为 Android 4 移动设备创建创新的、可扩展的应用。没有给出 Android 开发的一些肤浅的细节,而是专注于教你 Java 语言和 API,比如你将需要在你的应用中使用的集合。如果不先了解 Java,怎么着手了解 Android 呢?

我还推荐你去看看第二版的安卓食谱(见【www.apress.com/9781430246145】)。虽然那本书的内容很大程度上包含了学习 Android 各种东西的独立食谱,第一章包含了对 Android 应用开发的总结和快速介绍。通过阅读那一章,你会学到很多关于 Android 的基础知识。

感谢您购买我的书。我希望你会发现这是一个有益的准备,我希望你能成功地成为一名令人满意且利润丰厚的 Android 应用开发人员。

一、Java 入门

Android 是谷歌用于移动设备的软件栈。这个堆栈由应用(或通常所说的应用)、运行应用的虚拟机(基于软件的处理器和相关环境)、中间件(位于操作系统之上并为虚拟机及其应用提供各种服务的软件),以及基于 Linux 的操作系统组成。

Android 应用是用 Java 编写的,使用各种 Java 应用接口(API)。因为你想写你自己的应用,但可能不熟悉 Java 语言和这些 API,这本书教你 Java,作为应用开发的第一步。它为您提供了开发应用时有用的 Java 语言和 Java APIs 的基础知识。

注意这本书通过非 Android Java 应用阐述了 Java 概念。新手掌握这些应用比对应的安卓应用更容易。

API 是指应用代码用来与其他代码通信的接口,通常存储在某种软件库中。关于这个术语的更多信息,可以查看维基百科的“应用编程接口”主题(en . Wikipedia . org/wiki/Application _ programming _ interface)。

这一章为你在开始 Android 应用开发生涯之前需要理解的基本 Java 概念奠定了基础。我首先回答“什么是 Java?”问题。接下来,我将向您展示如何安装 Java SE 开发工具包(JDK),并向您介绍用于编译和运行 Java 应用的 JDK 工具。

在向您展示了如何安装和使用开源 Eclipse IDE(集成开发环境)以便您可以更容易(更快速)地开发 Java 应用(以及最终的 Android 应用)之后,我将为您提供各种 Java APIs 的高级概述,您可以从您的 Java 应用和 Android 应用中访问这些 API。在后续章节中,您将更详细地探索这些和其他有用的 API。

注意 第一章简短而紧凑,介绍了许多你将在本书中更详细遇到的概念。如果您是 Java 新手,您可能会发现自己对这些概念有点不知所措。然而,当你继续阅读余下的章节时,任何迷雾都将消散。如果您仍然感到有些困惑,请联系我(Jeff @ tutortutor . ca),我会尽力帮助您。

Java 是什么?

Java 是 Sun Microsystems 首创的语言和平台。在这一节中,我将简要描述这种语言,并揭示它对 Java 作为一个平台意味着什么。为了满足各种需求,Sun 将 Java 组织成三个主要版本:Java SE、Java EE 和 Java ME。本节还简要探讨了这些版本以及 Android。

注意 Java 有一段有趣的历史,可以追溯到 1990 年 12 月。当时,詹姆斯·高斯林、帕特里克·诺顿和迈克·谢里丹(都是太阳微系统公司的雇员)被赋予了弄清计算领域下一个主要趋势的任务。他们的结论是,一个趋势将涉及计算设备和智能消费电器的融合。由此诞生了 绿色工程

绿色的果实是 Star7 ,这是一款手持无线设备,配有 5 英寸彩色液晶显示屏、SPARC 处理器、复杂的图形功能和 Unix 版本;以及由詹姆斯·高斯林开发的用于编写在 Star7 上运行的应用的语言 Oak ,他以生长在 Sun 公司他办公室窗外的一棵橡树命名了 star 7。为了避免与另一种同名语言的冲突,Gosling 博士将这种语言的名称改为 Java。

Sun Microsystems 随后发展了 Java 语言和平台,直到甲骨文在 2010 年初收购了 Sun。查看oracle.com/technetwork/java/index.html了解来自甲骨文的最新 Java 新闻。

Java 是一种语言

Java 是开发者表达源代码 (程序文本)的语言。Java 的语法(将符号组合成语言特性的规则)部分模仿了 C 和 C++ 语言,以缩短 C/C++开发人员的学习曲线。

下面列出了 Java 和 C/C++之间的一些相似之处:

  • Java 和 C/C++共享相同的单行和多行注释样式。注释让您记录源代码。
  • Java 的许多保留字与它们的 C/C++对应物是相同的(表示、 if】、 switch 和而是示例)和 C++对应物( catch 、 class 、 public 和 try 是示例)。
  • Java 支持字符、双精度浮点、浮点、整数、长整型和短整型原语类型,并通过相同的 char 、 double 、 float 、 int 、 long 和 short 保留字。
  • Java 支持许多相同的运算符,包括算术运算符( + 、 – 、 * 、 / 、 % )和条件运算符(?:)运算符。
  • Java 使用大括号字符( { 和 } )来分隔语句块。

下面列出了 Java 和 C/C++之间的一些差异:

  • Java 支持另一种称为 Javadoc 的注释风格。(我在第二章简单介绍一下 Javadoc。)
  • Java 提供了 C/C++中没有的保留字(扩展、 strictfp 、同步和瞬态就是例子)。
  • Java 不需要特定于机器的知识。支持字节整数类型(见en . Wikipedia . org/wiki/Integer _(computer _ science));不提供字符类型的有符号版本;并且不提供整数、长整数和短整数的无符号版本。此外,所有 Java 的基本类型都有保证的实现大小,这是实现可移植性的重要部分(稍后讨论)。在 C 和 C++中,等价的基本类型就不一样了。
  • Java 提供了 C/C++中没有的运算符。这些运算符包括的实例和 > > > (无符号右移)。
  • Java 提供了 C/C++中没有的带标签的 break 和 continue 语句。

你将在第二章中学习单行和多行注释。此外,在那一章中,您将学习保留字、基本类型、操作符、块和语句(包括标记为 break 和 continue 的语句)。

Java 被设计成比 C/C++更安全的语言。它实现安全性的部分原因是不让您重载操作符,并省略了 C/C++特性,如 指针(包含地址的变量—参见)http://en . Wikipedia . org/wiki/Pointer _(computer _ programming)。

Java 还通过修改某些 C/C++特性来实现安全性。例如,循环必须由布尔表达式控制,而不是由整数表达式控制,其中 0 为假,非零值为真。(在第二章的中有关于循环和表达式的讨论。)

假设您必须编写一个重复不超过 10 次的 C/C++ while 循环。累了,你指定 while(x)x++;(假设 x 是一个初始化为 0 的基于整数的变量——我在第二章中讨论变量)其中 x++ 给 x 的值加 1。当 x 达到 10 时,该循环不停止;你引入了一个错误。

这个问题在 Java 中不太可能出现,因为它在看到 while (x) 时会报错。这个投诉要求你重新检查你的表达,然后你很可能指定 while (x!= 10)。不仅提高了安全性(不能只指定 x ),还明确了含义: while (x!= 10) 比更有意义而(x) 。

这些和其他基本的语言特性支持类、对象、继承、多态和接口。Java 还提供了与嵌套类型、包、静态导入、异常、断言、注释、泛型、枚举等相关的高级特性。后续章节将探讨这些语言的大部分特性。

Java 是一个平台

Java 是一个由虚拟机和执行环境组成的平台。虚拟机是一个基于软件的处理器,提供一个指令集。执行环境由运行程序和与底层操作系统交互的库组成。

执行环境包括一个巨大的预建类文件库,这些类文件执行常见任务,如数学运算(例如,三角学)和网络通信。这个库通常被称为标准类库

一个被称为 Java 编译器的特殊 Java 程序将源代码翻译成由虚拟机执行的指令(和相关数据)。这些指令被称为字节码

编译器将程序的字节码和数据存储在带有的文件中。类扩展。这些文件被称为类文件 ,因为它们通常存储编译后的等价类,这是一个在第三章中讨论的语言特性。

Java 程序通过工具(例如, java )执行,该工具加载并启动虚拟机,并将程序的主类文件传递给机器。虚拟机使用一个类加载器 (一个虚拟机或执行环境组件)来加载类文件。

加载类文件后,虚拟机的字节码验证器组件确保类文件的字节码是有效的,并且不会危及安全性。当验证器发现字节码有问题时,它就终止虚拟机。

假设类文件的字节码一切正常,虚拟机的解释器一次解释一条指令。 解释包括识别字节码指令和执行等价的本机指令。

原生指令(也称原生代码)是底层平台物理处理器理解的指令。

当解释器得知字节码指令序列被重复执行时,它通知虚拟机的即时(JIT)编译器将这些指令编译成本机代码。

对于给定的字节码指令序列,JIT 编译只执行一次。因为执行的是本机指令,而不是相关的字节码指令序列,所以程序执行起来要快得多。

在执行过程中,解释器可能会遇到执行另一个类文件的字节码的请求。当发生这种情况时,它会要求类加载器加载类文件和字节码验证器,以便在执行字节码之前验证字节码。

Java 的平台端通过提供底层平台的抽象来提升 的可移植性。因此,相同的字节码在基于 Windows、基于 Linux、基于 Mac OS X 和其他平台上运行时没有变化。

注意 Java 推出时的口号是“一次编写,随处运行”尽管 Java 不遗余力地加强可移植性(例如,一个整数总是 32 位二进制数字,一个长整数总是 64 位——参见【http://en.wikipedia.org/wiki/Bit】了解二进制数字),但它并不总是成功的。例如,尽管大部分是独立于平台的,Java 的某些部分(例如,线程的调度,在第八章中讨论)在不同的基础平台上是不同的。

Java 的平台端也通过提供一个代码执行的安全环境(例如,字节码验证器)来提升 安全性。目标是防止恶意代码破坏底层平台(并可能窃取敏感信息)。

Java SE ,Java EE ,Java ME ,安卓

开发者使用 Java 平台的不同版本来创建运行在台式计算机、网络浏览器、网络服务器、移动信息设备(例如,功能电话)和嵌入式设备(例如,电视机顶盒)上的 Java 程序:

  • Java 平台,标准版*(Java SE**):开发应用的 Java 平台,这些应用是运行在桌面上的独立程序。Java SE 也用于开发小程序*,这是在网络浏览器中运行的程序。
  • Java 平台, 企业版(Java EE ) :开发面向企业应用的 Java 平台和Servlet,是符合 Java EE 的 Servlet API 的服务器程序。Java EE 构建在 Java SE 之上。
  • Java 平台、 微型版(Java ME ) :开发MIDlets的 Java 平台,midlet 是运行在移动信息设备上的程序,xlet 是运行在嵌入式设备上的程序。

开发人员还使用谷歌创建的 Java 平台的特殊版本(见【http://developer.android.com/index.html】)来创建在支持 Android 的设备上运行的 Android 应用。这个版本被称为 安卓平台

谷歌的 Android 平台展示了一个 Dalvik 虚拟机,它运行在一个特别修改的 Linux 内核之上。一个 Android 应用的 Java 源代码被编译成 Java 类文件,然后被翻译成一个特殊的文件供 Dalvik 执行。

通过维基百科的“Android(操作系统)”词条(en . Wikipedia . org/wiki/Android _(operating _ system))了解更多关于 Android OS 的信息,通过维基百科的“Dalvik(软件)”词条(en . Wikipedia . org/wiki/Dalvik _(软件) )了解关于 dal vik 虚拟机的信息。

在本书中,我将介绍 Java 语言(Java SE 和 Android 支持)以及 Android 也支持的各种 Java SE APIs。我通过 Java 版本 5 关注语言特性,通过 Java 5 关注 Java APIs,还有少量 Java 6。

注意谷歌的 Android 平台是基于 Java 5T3 的开源版本。它没有正式识别比 Java 5 更新的语言特性,尽管有可能增加这种支持(参见 www.informit.com/articles/article.aspx?p=1966024T5)。关于 API,这个平台支持 Java 6 和以前 Java 版本的 API。此外,它还提供了自己独特的 API。

安装和探索 JDK

Java 运行时环境(JRE) 实现了 Java SE 平台,使得运行 Java 程序成为可能。公共 JRE 可以从 Oracle 的 Java SE 下载页面下载(Oracle . com/tech network/Java/javase/Downloads/index . html)。

然而,公共 JRE 并没有使开发 Java 程序成为可能。对于那个任务,你需要下载并安装 Java SE 开发包(JDK ) ,它包含开发工具(包括 Java 编译器)和一个私有的 JRE 。

注意 JDK 1.0 是第一个发布的 JDK(1995 年 5 月)。在 JDK 6 到来之前,JDK 代表 Java 开发工具包(SE 不是这个名称的一部分)。多年来,已经发布了许多 JDK,在撰写本文时,JDK 7 是最新的。

每个 JDK 版本号标识一个 Java 版本。比如 JDK 1.0 识别 Java 1.0,JDK 5 识别 Java 5.0。JDK 5 是第一个也提供内部版本号 1.5.0 的 JDK。

谷歌不提供 JDK。它确实提供了类似于 JRE 的功能,但是有一个 Android 焦点。

Java SE 下载页面还提供了对当前 JDK 的访问,在撰写本文时是 JDK 7 Update 9。点击下载 JDK 链接(在页面上的 处 http://Oracle . com/tech network/Java/javase/downloads/index . html)下载适用于您平台的最新 JDK 安装程序。

JDK 安装程序将 JDK 放在主目录中。(它也可以将公共 JRE 安装在另一个目录中。)在我的 Windows 7 平台上,主目录是 C: Program Files Java JDK 1 . 7 . 0 _ 06。(我目前使用的是 JDK 7 更新 6。)

提示安装完 JDK 后,你应该将 bin 子目录添加到你平台的 PATH 环境变量中(参见java.com/en/download/help/path.xml),这样你就可以从任何目录执行 JDK 工具了。另外,您可能希望在 JDK 的主目录下创建一个项目子目录来组织您的 Java 项目,并在项目中为每个项目创建一个单独的子目录。

主目录包含各种文件(例如,README.html,提供关于 JDK 的信息, src.zip ,提供标准类库源代码)和子目录,包括以下三个重要的子目录:

  • bin :该子目录包含各种 JDK 工具。在本书中,您将只使用其中的几个工具,主要是 javac (Java 编译器)、 java (Java 应用启动器)、 jar (Java 归档创建器、更新器和提取器),以及 javadoc (Java 文档生成器)。
  • jre :这个子目录包含了 JDK 的 jre 的私有副本,可以让你运行 Java 程序,而不必下载并安装公共的 JRE。
  • lib :这个子目录包含 JDK 工具使用的库文件。例如, tools.jar 包含 Java 编译器的类文件——编译器是用 Java 编写的。

注意 javac 不是 Java 编译器。它是一个加载并启动虚拟机的工具,将编译器的主类文件(位于 tools.jar 中)标识给虚拟机,并将正在编译的源文件的名称传递给编译器的主类文件。

您在命令行执行 JDK 工具,将命令行参数传递给工具。您可以通过维基百科的“命令行界面”条目()了解命令行和参数。

现在您已经安装了 JDK,并对其工具有所了解,您已经准备好探索一个小型的 DumpArgs 应用,它将其命令行参数输出到标准输出流。

注意标准输出流是标准 I/O(??【http://en.wikipedia.org/wiki/Standard_streams)的一部分,它也由标准输入和标准错误流组成,起源于 Unix 操作系统。标准 I/O 使得从不同来源(键盘或文件)读取文本和将文本写入不同目的地(屏幕或文件)成为可能。

文本从标准输入流中读取,标准输入流默认为键盘,但可以重定向到文件。文本被写入标准输出流,该输出流默认显示在屏幕上,但可以重定向到一个文件。错误消息文本被写入标准错误流,该错误流默认显示在屏幕上,但可以重定向到不同于标准输出文件的文件。

清单 1-1 展示了 DumpArgs 应用源代码。

清单 1-1 。通过 main()的 args 数组将命令行参数转储到标准输出流中

public class DumpArgs
{
   public static void main (String[] args )
   {
      System.out.println("Passed arguments:");
      for (int i = 0; i < args .length; i++)
         System.out.println(args[i]);
   }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

清单 1-1 的 DumpArgs 应用由一个名为 DumpArgs 的类和该类中一个名为 main() 的方法组成,该方法是应用的入口点并提供要执行的代码。(你会在第三章中了解到类和方法。)

main() 方法包括一个标识该方法的头和一段位于左大括号( { )和右大括号( } )之间的代码。除了命名此方法之外,标头还提供了以下信息:

  • public :这个保留字使得 main() 对调用这个方法的启动代码可见。如果 public 不存在,编译器会输出一条错误消息,指出它找不到 main() 方法。(我在第二章讨论保留字。)
  • 静态:这个保留字使这个方法与这个类相关联,而不是与从这个类创建的任何对象相关联(在第三章中讨论)。因为调用 main() 的启动代码没有从类中创建一个对象来调用这个方法,所以它要求这个方法被声明为 static 。虽然当 static 丢失时编译器不会报错,但是运行 DumpArgs 将是不可能的,当正确的 main() 方法不存在时,它将不是一个应用。
  • void :这个保留字表示该方法不返回值。如果你把 void 改成一个类型的保留字(如 int ,然后插入一个返回这个类型的值的语句(如 return 0;),编译器不会报错。然而,您将无法运行 DumpArgs ,因为正确的 main() 方法将不存在。(我在第二章中讨论类型。)
  • (String[] args) :该参数列表由一个名为 args 的参数组成,类型为 String[] 。启动代码将一系列命令行参数传递给 args ,这使得这些参数可供在 main() 中执行的代码使用。您将在第三章中了解参数和自变量。

main() 用一组字符串(字符序列)调用,这些字符串标识应用的命令行参数。这些字符串存储在基于字符串的数组变量 args 中。(我在第二章和第三章中讨论了方法调用、数组和变量。)虽然数组变量命名为 args ,但是这个名字并没有什么特别之处。您可以为此变量选择另一个名称。

该代码块首先执行 System.out.println(“传递的参数:”);,用传递的参数:字符串调用 System.out 的 println() 方法。该字符串被写入标准输出流。

从左起写, System 标识系统工具的标准类别; out 标识位于系统中的一个对象变量,它的方法让你可以输出各种类型的值,后面可选地跟一个换行符到标准输出流;println 标识一个方法,该方法将其参数和一个换行符一起输出到标准输出中;和传递的参数:是一个字符串(一个由双引号字符分隔的字符序列,被视为一个单元),它作为参数传递给 println 并写入标准输出(起始 ” 和结束 ” 双引号字符未被写入);这些字符分隔但不是字符串的一部分)。

注意 System.out 提供对一系列 println() 方法和一系列 print() 方法的访问,用于输出不同种类的数据(例如,字符和整数序列)。与 println() 方法不同, print() 方法不终止当前行;后续输出在当前行继续。

每个 println() 方法通过输出行分隔符字符串来终止一行,该字符串由系统属性 line.separator 定义,并且不一定是单个换行符(在源代码中通过字符文字 ‘
’ )来标识)。(我在第八章中讨论系统属性,在第十一章中讨论行分隔符,在第二章中讨论字符文字。)例如,在 Windows 平台上,行分隔符字符串是一个回车符(其整数代码为 13),后跟一个换行符(其整数代码为 10)。

代码块接下来使用 for 循环重复执行 system . out . println(args[I]);。循环执行 args.length 次,或者对存储在 args 中的每个字符串执行一次。(我讨论 for 循环和。第二章中的长度。)

system . out . println(args[I]);方法调用读取存储在 args 数组的第 i 个条目中的字符串——第一个条目位于索引(位置)0;最后一个条目存储在索引 args.length – 1 处。这个方法调用然后将这个字符串输出到标准输出。

假设您熟悉您的平台的命令行界面,并且在命令行中,将 DumpArgs 作为您的当前目录,并将清单 1-1 复制到一个名为【DumpArgs.java】的文件。然后通过下面的命令行编译这个源文件:

javac DumpArgs.java

  • 1
  • 2

假设您已经包括了。java 扩展名,这是 javac 所需要的,DumpArgs.java 编译的,你应该会在当前目录下发现一个名为 DumpArgs.class 的文件。通过以下命令行运行该应用:

java DumpArgs

  • 1
  • 2

如果一切顺利,您应该会在屏幕上看到以下输出行:

Passed arguments:

  • 1
  • 2

为了获得更有趣的输出,您需要将命令行参数传递给 DumpArgs 。例如,执行下面的命令行,它指定卷毛、莫伊和拉里作为传递给 DumpArgs 的三个参数:

java DumpArgs Curly Moe Larry

  • 1
  • 2

这一次,您应该在屏幕上看到以下扩展输出:

Passed arguments:
Curly
Moe
Larry

  • 1
  • 2
  • 3
  • 4
  • 5

您可以通过指定后跟文件名的大于号尖括号( > )将输出目标重定向到文件。例如,Java DumpArgs Curly Moe Larry>out . txt 将 DumpArgs 应用的输出存储在一个名为 out.txt 的文件中。

注意不要指定 System.out.println() ,可以指定 System.err.println() 将字符输出到标准错误流。( System.err 提供了与 System.out 相同系列的 println() 和 print() 方法。)但是,即使标准输出被重定向到一个文件,当您需要输出一个错误消息以便错误消息显示在屏幕上时,您也应该只从 System.out 切换到 System.err 。

祝贺您成功编译了您的第一个应用源文件并运行了该应用!清单 1-2 将源代码呈现给第二个应用,它将从标准输入流获得的文本回显到标准输出流。

清单 1-2 。将从标准输入读取的文本回显到标准输出

public class EchoText
{
   public static void main(String[] args) throws java.io.IOException
   {
      System.out.println("Please enter some text and press Enter!");
      int ch;
      while ((ch = System.in.read() ) != −1)
         System.out.print((char) ch);
      System.out.println();
   }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

在输出一条提示用户输入一些文本的消息后, main() 引入了 int 变量 ch 来存储每个字符的整数表示。(你会在第二章中了解到 int 和 integer。)

main() 现在进入 while 循环(在第二章中讨论)来读取和回显字符。循环首先调用 System.in.read() 读取一个字符,并将其整数值赋给 ch 。当该值等于 1 时(不再有输入数据可用),环路结束。

注意当标准输入被重定向到一个文件时, System.in.read() 从文件中读取每个字符(该文件随后被转换成一个整数),直到不再有字符需要读取。此时,该方法返回 1。然而,当标准输入没有被重定向时,循环不会结束,因为永远看不到 1。在这种情况下,一行文本的结尾由回车符(整数值 13)后跟一个换行符(整数值 10)来表示(在 Windows 平台上)。确切的终止顺序取决于平台。您必须在 Windows 上同时按下 Ctrl 和 C 键(或者在非 Windows 平台上的等效键)来终止循环。

对于 ch 中的任何其他值,这个值通过 (char) 转换成一个字符,这是 Java 的 cast 运算符的一个例子(在第二章中讨论)。然后通过 System.out.print() 输出字符,这也不会终止当前行。最后的 system . out . println(); call 终止当前行,不输出任何内容。

注意当标准输入被重定向到一个文件,而 System.in.read() 无法从该文件中读取文本时(可能该文件存储在一个可移动存储设备上,而该设备在读取操作之前已经被移除), System.in.read() 失败,抛出一个描述该问题的对象。我通过在 main() 方法头的末尾添加 throws Java . io . io exception 来承认这种可能性。我在第五章的中讨论抛出,在第十一章的中讨论 java.io.IOException 。

通过 javac EchoText.java 编译清单 1-2 ,并通过 java EchoText 运行应用。系统会提示您输入一些文本。输入此文本并按 Enter 后,文本将被发送到标准输出。例如,考虑以下输出:

Please enter some text and press Enter!
Hello Java
Hello Java

  • 1
  • 2
  • 3
  • 4

通过指定小于尖括号( < )后跟文件名,可以将输入源重定向到文件。例如,Java echo text<EchoText.java 从 EchoText.java 读取文本,并将文本输出到屏幕上。

ANDROID 应用入口点

DumpArgs 和 EchoText 应用演示了 public static void main(String[]args)作为 Java 应用的入口点。这是应用开始执行的地方。相比之下,Android 应用不需要这个方法作为其入口点,因为应用的架构非常不同。

Android 应用基于交互组件的联盟,这些组件被称为活动、服务、广播接收器和内容供应器。活动提供用户界面屏幕,服务支持后台处理,广播接收器响应系统范围的广播,内容供应器提供便携式数据访问。

考虑活动。该组件被实现为一个类,该类从 Android 的 android.app.Activity 类继承生命周期方法,并有机会覆盖它们。(我在第三章的中讨论方法,在第四章的中讨论继承和重写。)例如,它可以覆盖 void onCreate(Bundle savedInstanceState)方法,以便在 Android 调用该方法时构造用户界面屏幕。

在这本书里,我用 public static void main(String[]args)方法呈现 Java 应用。我这样做是因为这本书的重点是学习 Java,作为进入 Android 应用开发的准备步骤。

除了下载和安装 JDK,您还需要访问 JDK 文档,特别是为了探索 Java APIs。您可以浏览两组文档:

  • 甲骨文的 JDK 7 文档()
  • 谷歌的 Java API 文档(developer.android.com/reference/packages.html)

Oracle 的 JDK 7 文档提供了许多 Android 不支持的 API。此外,它没有涵盖特定于 Android 的 API。这本书只关注 Google 文档中包含的 Java APIs。

安装和探索 Eclipse IDE

对于小项目来说,在命令行中使用 JDK 工具可能没问题。但是,对于大型项目,不推荐这种做法,因为没有 IDE 的帮助,大型项目很难管理。

IDE 由用于管理项目文件的项目管理器、用于输入和编辑源代码的文本编辑器、用于定位错误的调试器以及其他功能组成。Eclipse 是 Google 支持的用于开发 Android 应用的流行 IDE。

注意为了方便起见,除了我讨论和演示 Eclipse IDE 的这一节之外,我在整本书中都使用了 JDK 工具。

Eclipse IDE 是一个开源 IDE,用于开发 Java 和其他语言的程序(如 C、COBOL、PHP、Perl 和 Python)。Eclipse Classic 是这个 IDE 的一个发行版,可以下载;4.2.1 版是撰写本文时的最新版本。

您应该下载并安装 Eclipse Classic,以遵循本节的面向 Eclipse 的示例。首先将浏览器指向,并完成以下任务:

  1. 向下滚动页面,直到看到一个 Eclipse Classic 条目。(可能指的是 4.2.1 或者更新的版本。)
  2. 单击此项右侧的一个平台链接(例如,Windows 32 位)。
  3. 从随后显示的页面中选择一个下载镜像,并继续下载发行版的归档文件。

我为我的 Windows 7 平台下载了大约 183 MB 的 eclipse-SDK-4 . 2 . 1-win32-x86 _ 64 . zip 归档文件,解压缩这个文件,将生成的 eclipse 主目录移动到另一个位置,并创建了该目录的【eclipse.exe】文件的快捷方式。

安装完 Eclipse Classic 之后,运行这个应用。您应该会发现一个标识该 IDE 的闪屏和一个让您选择存储项目的工作区位置的对话框,然后是一个主窗口,如图图 1-1 所示。

面向 Android 开发的 Java 学习手册(一)

图 1-1 保留默认工作空间或选择另一个工作空间

单击 OK 按钮,您将被带到 Eclipse 的主窗口。参见图 1-2 。

面向 Android 开发的 Java 学习手册(一)

图 1-2 主窗口最初显示一个欢迎选项卡

主窗口最初显示一个欢迎选项卡,从中可以了解关于 Eclipse 的更多信息。单击该选项卡的 X 图标关闭该选项卡;您可以通过从菜单栏的帮助菜单中选择欢迎来恢复欢迎选项卡。

Eclipse 用户界面基于一个主窗口,该窗口由菜单栏、工具栏、工作台区域和状态栏组成。工作台提供了用于组织 Eclipse 项目、编辑源文件、查看消息等的窗口。

为了帮助您熟悉 Eclipse 用户界面,我将向您展示如何创建一个 DumpArgs 项目,其中包含一个带有清单 1-1 源代码的 DumpArgs.java 源文件。您还将学习如何编译和运行这个应用。

完成以下步骤来创建 DumpArgs 项目:

  1. 从“文件”菜单中选择“新建”,从出现的弹出菜单中选择“Java 项目”。
  2. 在生成的新 Java 项目对话框中,在项目名称文本字段中输入 DumpArgs 。保留所有其他默认值,然后单击 Finish 按钮。

第二步之后,你会看到一个类似于图 1-3 所示的工作台。

面向 Android 开发的 Java 学习手册(一)

图 1-3 一个 DumpArgs 条目出现在工作台的包浏览器中

在工作台的左侧,您会看到一个名为 Package Explorer 的窗口。该窗口以包的形式标识工作区的项目(在第五章中讨论)。目前,只有一个 DumpArgs 条目出现在该窗口中。

单击 DumpArgs 左侧的三角形图标可以展开此项,显示 src 和 JRE 系统库项目。src 项存储了 DumpArgs 项目的源文件,JRE 系统库标识了用于运行该应用的各种 JRE 文件。

现在,您将向 src 添加一个名为 DumpArgs.java 的新文件,如下所示:

  1. 突出显示 src,并从“文件”菜单中选择“新建”,然后从弹出菜单中选择“文件”。
  2. 在出现的新文件对话框中,在文件名文本字段中输入 DumpArgs.java,并点击完成按钮。

Eclipse 通过显示一个名为 DumpArgs.java 的编辑器窗口做出响应。将清单 1-1 的内容复制到该窗口。然后通过从 run 菜单中选择 Run 来编译并运行这个应用。(如果看到“保存并启动”对话框,请单击“确定”关闭该对话框。)图 1-4 显示了结果。

面向 Android 开发的 Java 学习手册(一)

图 1-4 。工作台底部的 Console 选项卡显示 DumpArgs 应用的输出

您必须向 DumpArgs 传递命令行参数,以查看该应用的附加输出。按如下方式完成此任务:

  1. 从运行菜单中选择运行配置。
  2. 在产生的“运行配置”对话框中,选择“参数”选项卡。
  3. 在程序参数文本区输入卷毛莫拉里 并点击关闭按钮。

再次从 Run 菜单中选择 Run 来运行 DumpArgs 应用。这一次,控制台选项卡在“传递的参数:”下面的单独行中显示 Curly、Moe 和 Larry。

这就是我对 Eclipse IDE 要说的全部内容。有关更多信息,请通过“欢迎”选项卡学习教程,通过“帮助”菜单访问 IDE 帮助,并在www.eclipse.org/documentation/浏览 Eclipse 文档。

Java APIs 概述

Oracle 将其标准类库 API 组织成包(见第五章),类似于文件夹。同样,Google 将其面向 Android 的标准类库 API 组织成包。在这一节中,我将概述 Oracle 和 Google 通用的各种 Java APIs。此外,我(在本书中)只讨论那些位于两个库中的 API。通过将我的讨论限制在通用 API,我避免了讨论在创建 Android 应用时不能使用的 Java APIs。

语言支持和其他面向语言的 API

Java 依靠几个 API 来支持基本的语言特性,比如字符串(见第七章)、异常(见第五章)和线程(见第八章)。例如, java.lang 包提供了支持字符串的 String 类,支持异常的 Throwable 类,以及支持线程的 Thread 类和 Runnable 接口。

Java 也提供 API 来完成面向语言的任务。例如, java.lang 提供了一个 StringBuffer 类(参见第七章)用于创建可变字符串,一个 Math 类(参见第七章)用于执行三角函数和其他基本数学运算,一个 Package 类(参见第七章)用于获取面向包的信息。

面向集合的 API

Java 的设计者开发了一个强大的 集合框架来组织对象(见第九章)。这个框架位于 java.util 包中,它基于接口,并允许您将对象存储在列表、队列、集合(排序或未排序)和映射(排序或未排序)中。这些接口与各种实现类相关联(例如, ArrayList )。

集合框架还提供了集合和数组类。这些工具类(由静态 [class]方法组成的类)提供了对集合和数组执行常见操作的各种方法。例如,收藏让您方便地搜索或排序收藏;并且数组可以让你方便地搜索、排序、复制或填充一个数组。

其他工具 API

Java 的设计者还开发了一个强大的 并发工具框架,它提供了低级线程的高级替代方案(见第十章)。这个框架的 API 被组织成 java.util.concurrent 、Java . util . concurrent . atomic 和 Java . util . concurrent . locks 包。第一个包中 API 的例子包括 Executor 接口和 CyclicBarrier 类。

位于 java.util 包中的其他工具 API 包括用于处理日期的 Date 类、用于格式化数据项(例如整数和字符串)的 Formatter 类、用于实现复杂随机数生成的 Random 类,以及用于将输入字符流解析为整数、字符串和其他值的 Scanner 类。我在第十章中讨论了这些 API。

最后, java.util.zip 包提供了从现有 zip 存档中提取信息和创建新 ZIP 存档的能力(参见第十章)。此外,相关的 java.util.jar 包通过提供 jar 文件所需的额外功能扩展了 java.util.zip ,具体来说,就是从 JAR 文件的清单中读取属性和向其中写入属性(参见第十章)。

经典输入/输出应用编程接口

输入和输出信息的能力对 Java 来说一直很重要。你已经发现了标准的 I/O,但是在第十一章中还有更多要探索的。例如, java.io 包提供了用于执行面向文件的操作(例如,列出一个目录的文件)的文件类,还提供了用于执行 I/O(通常涉及文件)的流/写/读类。

网络应用编程接口

尽管许多 I/O 发生在文件系统的环境中,Java 也提供了通过位于其 java.net 包中的各种类型在网络上执行 I/O 的能力(参见第十二章)。例如,您可以使用套接字和服务器套接字类来创建网络通信链接的客户端和服务器端。

套接字提供了一种通过网络进行通信的底层方法。在某些情况下,您将使用更高级的 URL 类通过 web 进行通信,也许是为了获得一个 Web 页面。当你与网络交互时,你会遇到可以通过额外的 java.net 接口和类来管理的 cookies,比如 CookiePolicy 和 CookieManager。

新的 I/O API

现代操作系统引入了复杂的 I/O 机制,如内存映射文件和就绪选择。Java 通过缓冲区、通道、选择器以及在 java.nio 和相关包中找到的相关类型来支持这些新的 I/O 机制。另外, java.util.regex 通过提供高性能的字符串操作来支持新的 I/O。参见第十三章。

数据库 API

数据库存储信息,关系数据库将这些信息存储在表中,这些表可以通过特殊的键列相互关联。Java 通过 java.sql 和 javax.sql 包支持数据库访问。前一个包包含了 DriverManager 和 ResultSet 等类和接口;后一个包提供了数据源、行集等等。参见第十四章中的。

练习

以下练习旨在测试您对第一章内容的理解:

  1. Java 是什么?
  2. 什么是虚拟机?
  3. Java 编译器的目的是什么?
  4. 是非判断:类文件的指令通常被称为字节码。
  5. 当虚拟机的解释器得知一个字节码指令序列被重复执行时,它会做什么?
  6. Java 平台如何促进可移植性?
  7. Java 平台如何提升安全性?
  8. 是非判断:Java SE 是用于开发 servlets 的 Java 平台。
  9. 什么是 JRE?
  10. 公有和私有 JREs 有什么区别?
  11. 什么是 JDK?
  12. 用哪个 JDK 工具编译 Java 源代码?
  13. 哪个 JDK 工具用于运行 Java 应用?
  14. 什么是标准输入输出?
  15. 如何指定 main() 方法的头?
  16. 什么是 IDE?确定 Google 支持开发 Android 应用的 IDE。

摘要

Java 是一种语言,也是一个平台。该语言部分模仿 C 和 C++语言,以缩短 C/C++开发人员的学习曲线。该平台由一个虚拟机和相关的执行环境组成。

开发人员使用不同版本的 Java 平台来创建运行在桌面计算机、web 浏览器、web 服务器、移动信息设备和嵌入式设备上的 Java 程序。这些版本被称为 Java SE、Java EE 和 Java ME。

开发人员还使用谷歌创建的 Java 平台的特殊版本来创建在支持 Android 的设备上运行的 Android 应用。这个版本被称为 Android 平台,它展示了一个 Dalvik 虚拟机,运行在一个经过特殊修改的 Linux 内核之上。

公共 JRE 实现了 Java SE 平台,并使运行 Java 程序成为可能。JDK 提供了开发 Java 程序的工具(包括 Java 编译器),还包括 JRE 的私有副本。

对于大型项目,不推荐在命令行使用 JDK 的工具,如果没有集成开发环境的帮助,大型项目很难管理。Eclipse 是 Google 支持的用于开发 Android 应用的流行 IDE。

Oracle 将其标准类库 API 组织成包,类似于文件夹。同样,Google 将其面向 Android 的标准类库 API 组织成包。在这一章中,我还概述了一些打包的 API。

第二章通过关注 Java 语言的基础知识,开始向您介绍 Java 语言。您将了解注释、标识符、类型、变量、表达式、语句等等。

二、学习语言基础

有抱负的 Android 应用开发人员需要了解 Java 语言,Java 语言用于表达应用的源代码。在第二章第一节,我开始通过关注这门语言的基础来介绍它。具体来说,您将了解注释、标识符(和保留字)、类型、变量、表达式(和文字)和语句。

注意美国信息交换标准码(ASCII) 传统上被用来编码程序的源代码。因为 ASCII 仅限于英语,所以开发了 Unicode(【http://unicode.org/】)作为替代。 Unicode 是一种计算行业标准,用于一致地编码、表示和处理世界上大多数书写系统中表达的文本。因为 Java 支持 Unicode,所以非面向英语的符号可以集成到 Java 源代码中或从 Java 源代码中访问。你会在这一章看到例子。

学习评论

源代码需要被文档化,以便你(和任何其他必须维护它的人)现在和以后都能理解它。源代码应该在编写和修改的时候被记录下来。如果这些修改影响现有文档,则必须更新文档,以便准确解释代码。

Java 提供了在源代码中嵌入文档的注释特性。编译源代码时,Java 编译器会忽略所有注释——不生成字节码。支持单行、多行和 Javadoc 注释。

单行注释

一个单行注释 占用一行源代码的全部或部分。该注释以 // 字符序列开始,并以解释文本继续。编译器忽略从 // 到出现 // 的行尾的所有内容。以下示例显示了单行注释:

System.out.println(Math.sqrt(10 * 10 + 20 * 20)); // Output distance from (0, 0) to (10, 20).

  • 1
  • 2

此示例计算笛卡尔 x/y 平面中(0,0)原点和点(10,20)之间的距离。对于这个任务,它使用公式距离=平方根(xx+yy) ,其中 x 是 10, y 是 20。Java 提供了一个 Math 类,其 sqrt() 方法 返回其单个数值参数的平方根。(我在第七章的中讨论数学,在第三章的中讨论论点。)

注意单行注释对于在代码中插入简短但有意义的源代码解释非常有用。不要用它们来插入无用的信息。比如在声明一个变量的时候,不要插入一个无意义的注释比如 //这个变量存储的是整数值。

多行注释

一个多行注释 占用一行或多行源代码。该注释以 /* 字符序列开始,以说明性文本继续,以 / 字符序列结束。编译器会忽略从 / 到 */ 的所有内容。以下示例演示了多行注释:

/*
   A year is a leap year if it is divisible by 400, or divisible by 4 and
   not also divisible by 100.
*/
return (year % 400 == 0 || (year % 4 == 0 && year % 100 != 0));

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这个例子引入了一个 return 语句(在第三章的中讨论过)来确定一年(存储在一个名为 year 的变量中);我将在本章后面讨论变量)是否是闰年。这段代码中需要掌握的重要部分是多行注释,它阐明了决定年的值是否代表闰年的表达式(稍后讨论)。

注意不能将一个多行注释放在另一个多行注释中。例如, //嵌套多行注释是非法的!// 不是有效的多行注释。

Javadoc 注释

一个 Javadoc 注释 占用一行或多行源代码。该注释以 /** 字符序列开始,以说明性文本继续,以 / 字符序列结束。编译器会忽略从 /* 到 */ 的所有内容。以下示例演示了 Javadoc 注释:

/**
 *  Application entry point
 *
 *  @param args array of command-line arguments passed to this method
 */
public static void main(String[] args)
{
   // TODO code application logic here
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这个例子以一个 Javadoc 注释开始,描述了 main() 方法 ,我在第一章中讨论过。夹在 /** 和 */ 之间的是方法的描述和 @param Javadoc 标签(一个@-Javadoc 工具的前缀指令)。

下表列出了几种常用的标签:

  • @author 标识源代码的作者。
  • @deprecated 标识不应再使用的源代码实体(如方法)。
  • @param 标识方法的一个参数。
  • @see 提供了另见参考。
  • @因为标识了实体最初起源的软件版本。
  • @return 标识该方法返回的值的种类。
  • @throws 记录从方法中抛出的异常。我在第五章中讨论了异常。

清单 2-1 展示了第一章的 DumpArgs 应用源代码,以及描述 DumpArgs 类及其 main() 方法的 Javadoc 注释。

清单 2-1 。记录应用类及其 main()方法

/**
   Dump all command-line arguments to standard output.

   @author Jeff Friesen
*/

public class DumpArgs
{
   /**
      Application entry point.

      @param args array of command-line arguments.
   */

   public static void main(String[] args)
   {
      System.out.println("Passed arguments:");
      for (int i = 0; i < args.length; i++)
         System.out.println(args[i]);
   }
}

面向 Android 开发的 Java 学习手册(一)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

您可以使用 JDK 的 javadoc 工具将这些文档注释提取到一组 HTML 文件中,如下所示:

javadoc DumpArgs.java

  • 1
  • 2

javadoc 通过输出以下消息进行响应:

Loading source file DumpArgs.java...
Constructing Javadoc information...
Standard Doclet version 1.7.0_06
Building tree for all the packages and classes...
Generating DumpArgs.html...
Generating package-frame.html...
Generating package-summary.html...
Generating package-tree.html...
Generating constant-values.html...
Building index for all the packages and classes...
Generating overview-tree.html...
Generating index-all.html...
Generating deprecated-list.html...
Building index for all classes...
Generating allclasses-frame.html...
Generating allclasses-noframe.html...
Generating index.html...
Generating help-doc.html...

面向 Android 开发的 Java 学习手册(一)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

它还生成几个文件,包括 index.html 文档入口点文件。将你的浏览器指向这个文件,你应该会看到一个类似于图 2-1 所示的页面。

面向 Android 开发的 Java 学习手册(一)

图 2-1 。DumpArgs 文档的入口点页面描述了这个类

注意 附录 B 提供了另一个(更广泛的)例子,涉及 Javadoc 注释和 javadoc 工具。

学习标识符

类和方法等源代码实体需要命名,以便可以从代码中的其他地方引用它们。Java 为此提供了标识符特性。

一个标识符由字母(A-Z,A-Z,或其他人类字母表中的等价大写/小写字母)、数字(0-9 或其他人类字母表中的等价数字)、连接标点符号(如下划线)和货币符号(如美元符号$)组成。该名称必须以字母、货币符号或连接标点符号开头。并且它的长度不能超过它所在的行。

有效标识符的例子包括π(一些编辑可能对这种符号有问题)、 i 、计数器、 j2 、 first$name、和 _for 。无效标识符的示例包括 1name (以数字开头)和 first#name ( # 不是有效的标识符符号)。

注意 Java 是一种区分大小写的语言,这意味着只有大小写不同的标识符被认为是单独的标识符。例如,温度和温度是独立的标识符。

几乎可以选择任何有效的标识符来命名类、方法或其他源代码实体。然而,一些标识符是为特殊目的而保留的;它们被称为保留字。Java 保留了以下标识符:抽象,断言,布尔,断符,字节,大小写,捕捉,字符,类,常量,继续,默认,做 最后,浮动,为,转,如果,实现,导入,实例化, int ,接口,长,原生, 静态,严格,超,切换,同步,本,投,投,瞬变,真,试, 当您试图在使用上下文之外使用这些保留字时,编译器会输出一条错误消息。

Java 的大部分保留字也被称为关键词。三个例外是假、空和真,它们是字面量(逐字指定的值)的示例。

学习类型

应用处理不同类型的值,如整数、浮点值、字符和字符串。类型标识一组值(以及它们在内存中的表示)和一组将这些值转换成该组中其他值的操作。例如,整数类型标识没有小数部分和面向整数的数学运算的数值,例如将两个整数相加得到另一个整数。

注意 Java 是一种强类型语言,这意味着每一个表达式、变量等等都有一个编译器已知的类型。这种能力有助于编译器在编译时检测与类型相关的错误,而不是让这些错误在运行时显示出来。表达式和变量将在本章后面讨论。

Java 将类型分为基本类型类型、用户定义类型和数组类型。

原始类型

原始类型 是由语言定义的类型,其值不是对象。Java 支持布尔、字符、字节整数、短整数、整数、长整数、浮点和双精度浮点原语类型。在表 2-1 中对它们进行了描述。

表 2-1 。原始类型

面向 Android 开发的 Java 学习手册(一)

表 2-1 描述了每个原语类型的保留字、大小、最小值和最大值。“-”条目表示它所在的列不适用于该条目的行中描述的基元类型。

size 列根据该类型的值在内存中所占的(二进制数字—每个数字为 0 或 1)的数量来标识每个原始类型的大小。除了 Boolean(其大小取决于实现——一个 Java 实现可能用一位存储一个布尔值,而另一个实现为了提高性能可能需要一个 8 位的字节),每个原语类型的实现都有一个特定的大小。

最小值和最大值列标识每种类型可以表示的最小和最大值。除了 Boolean(其值只有 true 和 false)之外,每个基本类型都有一个最小值和一个最大值。

字符类型的最小值和最大值指的是 Unicode。 Unicode 0 是“第一个 Unicode 码位”的简写——码位是一个整数,表示一个符号(比如 A)或一个控制字符(比如 newline 或 tab)或者与其他码位组合形成一个符号。

注意字符类型的限制意味着该类型是无符号的(所有字符值都是正的)。相比之下,每个数值类型都有符号(它支持正值和负值)。

字节整数、短整数、整数和长整数类型的最小值和最大值表明负值比正值多一个(0 通常不被视为正值)。这种不平衡的原因与整数的表示方式有关。

Java 将一个整数值表示为一个符号位(最左边的位—0 表示正值,1 表示负值)和幅度位(符号位右边的所有剩余位)的组合。当符号位为 0 时,幅度直接存储。然而,当符号位为 1 时,幅度使用二进制补码表示法存储,其中所有 1 翻转为 0,所有 0 翻转为 1,并且减号后面的数字加 1。使用二进制补码是为了让负整数可以自然地与正整数共存。例如,将 1 的表示与+1 相加得到 0。图 2-2 显示了字节整数 2 的直接表示和字节整数 2 的二进制补码表示。

面向 Android 开发的 Java 学习手册(一)

图 2-2 两个字节整数值的二进制表示以符号位开始

浮点和双精度浮点类型的最小值和最大值参考电气和电子工程师协会( IEEE) 754 ,这是一个在内存中表示浮点值的标准。查看维基百科的“IEEE 754-2008”条目(【http://en.wikipedia.org/wiki/IEEE_754】)来了解关于这个标准的更多信息。

那些认为 Java 应该只支持对象的开发人员对于在语言中包含原始类型并不满意。然而,Java 被设计成包括基本类型,以克服 20 世纪 90 年代早期设备的速度和内存限制,这也是 Java 最初的目标。

用户定义的类型

一个用户定义的类型 是一个经常被用来模拟现实世界概念的类型(例如,一种颜色或一个银行账户)。它由开发人员使用类、接口、枚举或注释类型来定义;它的值是对象。(我在第三章讨论类,在第四章讨论接口,在第六章讨论枚举和注释类型。)

例如,您可以创建一个 Color 类来模拟颜色;它的值可以将颜色描述为红/绿/蓝分量值。还有,Java 的 String 类定义了字符串自定义类型;它的值描述字符串,它的方法执行各种字符串操作,比如将两个字符串连接在一起。(我在第三章中讨论方法。)

用户定义的类型也被称为引用类型,因为该类型的变量存储了对存储该类型对象的内存区域的引用(内存地址或其他标识符)。相反,基本类型的变量直接存储值;它们不存储对这些值的引用。

数组类型

一个数组类型 是一个特殊的引用类型,表示一个数组,一个在大小相等且连续的槽中存储值的内存区域,通常被称为元素

这种类型由元素类型(原始类型或用户定义的类型)和一对或多对方括号组成,这些方括号表示维度(范围)的数量。一对括号表示一个一维数组(一个向量),两对括号表示一个二维数组(一个表),三对括号表示一个二维数组的一维数组(一个表的向量),以此类推。例如, int[] 表示一维数组(元素类型为 int ),而 double[][] 表示二维数组(元素类型为 double )。

学习变量

应用操作存储在内存中的值,这些值通过使用变量特性在源代码中象征性地表示出来。一个变量是一个存储某种类型值的命名内存位置。存储引用的变量通常被称为引用变量

变量必须在使用前声明。一个声明至少包含一个类型名,可选地后跟一系列方括号对,后跟一个名称,可选地后跟一系列方括号对,并以分号()结束;)。考虑下面的例子:

int counter;
double temperature;
String firstName;
int[] ages;
char gradeLetters[];
float[][] matrix;
double π;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

第一个示例声明一个名为 counter 的整数变量,第二个示例声明一个名为 temperature 的(双精度浮点类型)变量,第三个示例声明一个名为 firstName 的字符串变量,第四个示例声明一个名为 ages 的一维整数数组变量,第五个示例声明一个名为 gradeLetters 的一维字符数组变量,第六个示例声明一个名为 matrix 的二维浮点数组变量,以及还没有字符串与名字关联,也没有数组与年龄、年级字母和矩阵关联。

注意方括号可以出现在类型名之后,也可以出现在变量名之后,但不能同时出现在这两个地方。比如编译器遇到 int[] x[],就报错;。通常的做法是在类型名后面加上方括号(如 int[]ages;)而不是变量名后(如 char grade letters[];),除非数组是在上下文中声明的,比如 int x,y[],z;。

您可以在一行中声明多个变量,方法是用逗号将每个变量与其前一个变量分隔开,如以下示例所示:

int x, y[], z;

  • 1
  • 2

这个例子声明了三个名为 x 、 y 和 z 的变量。每个变量共享相同的类型,恰好是整数。与存储单个整数值的 x 和 z 不同, y[] 表示元素类型为 integer 的一维数组——每个元素存储一个整数值。还没有数组与 y 相关联。

当数组与其他变量声明在同一行时,方括号必须出现在变量名之后。如果你把方括号放在变量名前,比如 int x,[]y,z;,编译器报错。如果将方括号放在类型名之后,如 int[] x,y,z;,这三个变量都表示一维整数数组。

学习表达

先前声明的变量没有显式初始化为任何值。因此,根据它们出现的上下文(在类内声明或在方法内声明),它们要么被初始化为默认值(比如 0 代表 int ,0.0 代表 double ),要么保持未初始化状态。在第三章中,我从字段、局部变量和参数的角度讨论了变量上下文。

Java 为初始化变量和其他目的提供了表达式特性。一个表达式是文字、变量名、方法调用和操作符的组合。在运行时,它计算出一个值,该值的类型称为表达式的类型。如果表达式被赋值给一个变量,表达式的类型必须与变量的类型一致;否则,编译器会报告错误。

Java 将表达式分为简单表达式和复合表达式。

简单表达

一个简单表达式 是一个文字(一个一字不差表达的值),一个变量的名字(包含一个值),或者一个方法调用(返回值)。Java 支持几种文字:字符串、布尔值 true 和 false 、字符、整数、浮点和 null 。

注意不返回值的方法调用——被调用的方法被称为 void 方法——是一种特殊的简单表达式;比如 System.out.println(“Hello,World!”);。此独立表达式不能赋给变量。尝试这样做(如在 int I = system . out . println(” X “);)导致编译器报告错误。

一个字符串文字 由一对双引号括起来的 Unicode 字符序列组成;比如“敏捷的棕色狐狸跳过懒狗。”它还可能包含转义序列,这是一种特殊的语法,用于表示某些可打印和不可打印的字符,否则这些字符不会出现在文本中。比如“那只敏捷的棕色“狐狸”跳过了那只懒狗。”使用 ” 转义序列将 fox 用双引号括起来。

表 2-2 描述了所有支持的转义序列。

表 2-2 。转义序列

转义语法 描述
反斜线符号
双引号
单引号
退格
f 换页
换行符(也称为换行)
回车
横表

最后,字符串文字可能包含 Unicode 转义序列,这是表示 Unicode 字符的特殊语法。Unicode 转义序列以 u 开始,以四个十六进制数字(0–9、A–F、A–F)继续,中间没有空格。例如, u0041 代表大写字母 A, u20ac 代表欧盟的欧元货币符号。

一个布尔文字 由保留字真或保留字假组成。

一个字符字面量 由一个 Unicode 字符和一对单引号组成(例如‘A’)。您还可以将转义序列(例如, ‘’’ )或 Unicode 转义序列(例如, ‘u0041’ )表示为字符文字。

一个整数文字 由一系列数字组成。如果文字要表示一个长整型值,那么它的后缀必须是大写的 L 或者小写的 l ( L 更容易阅读)。如果没有后缀,文字表示 32 位整数(一个 int )。

整数可以用十进制、十六进制和八进制格式指定:

  • 十进制格式是默认格式;比如 127 。
  • 十六进制格式要求文字以 0x 或 0X 开头,以十六进制数字(0-9、A-F、A-F)继续;比如 0x7F 。
  • 八进制格式要求文字以 0 为前缀,以八进制数字(0–7)继续;比如 0177 。

一个浮点字面值由一个整数部分的、一个小数点(用句点字符[ 表示)组成。)、小数部分、指数(以字母 E 或 e 开头)、类型后缀(字母 D 、 d 、 F 或 f )。大多数部分是可选的,但是必须有足够的信息来区分浮点文字和整数文字。例子包括 0.1 (双精度浮点)、 89F (浮点)、 600D (双精度浮点)、以及 13.08E+23 (双精度浮点)。

最后,空值被赋给一个引用变量,表示该变量不引用对象。

以下示例使用文本来初始化前面提供的变量:

int counter = 10;
double temperature = 98.6; // Assume Fahrenheit scale.
String firstName = "Mark";
int[] ages = { 52, 28, 93, 16 };
char gradeLetters[] = { 'A', 'B', 'C', 'D', 'F' };
float[][] matrix = { { 1.0F, 2.0F, 3.0F }, { 4.0F, 5.0F, 6.0F }};
int x = 1, y[] = { 1, 2, 3 }, z = 3;
double π = 3.14159;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

第四至第七个示例使用数组初始值设定项来初始化年龄、年级字母、矩阵和 y 数组。一个数组初始化器由一个用括号和逗号分隔的表达式列表组成,这些表达式(如矩阵示例所示)本身可能就是数组初始化器。矩阵示例生成如下所示的表格:

1.0F 2.0F 3.0F
4.0F 5.0F 6.0F

  • 1
  • 2
  • 3

在内存中组织变量

也许你对变量在内存中的组织方式很好奇。图 2-3 展示了一个可能的高层组织,用于计数器、年龄和矩阵变量,以及分配给年龄和矩阵的数组。

面向 Android 开发的 Java 学习手册(一)

图 2-3 counter 变量存储一个 4 字节的整数值,而 ages 和 matrix 存储对各自数组的 4 字节引用

Figure 2-3 reveals that each of counter, ages, and matrix is stored at a memory address (starting at a fictitious 20001000 value in this example) and divisible by 4 (each variable stores a 4-byte value); that counter’s 4-byte value is stored at this address; and that each of the ages and matrix 4-byte memory locations stores the 32-bit address of its respective array (64-bit addresses would most likely be used on 64-bit virtual machines). Also, a one-dimensional array is stored as a list of values, whereas a two-dimensional array is stored as a one-dimensional row array of addresses, where each address identifies a one-dimensional column array of values for that row.Although Figure 2-3 implies that array addresses are stored in ages and matrix, which equates references with addresses, a Java implementation might equate references with handles (integer values that identify slots in a list). This alternative is presented in Figure 2-4 for ages and its referenced array.

面向 Android 开发的 Java 学习手册(一)

图 2-4 一个句柄存储在 ages 中,由这个句柄标识的列表条目存储相关数组的地址

句柄使得在垃圾收集期间在内存区域中移动变得容易(在第三章中讨论过)。如果多个变量通过同一个地址引用同一个数组,那么当数组移动时,每个变量的地址值都必须更新。但是,如果多个变量通过同一个句柄引用数组,那么只需要更新句柄的列表条目。使用句柄的一个缺点是,通过句柄访问内存比通过地址直接访问内存要慢。不管引用是如何实现的,这个实现细节对 Java 开发人员是隐藏的,以提高可移植性。

以下示例显示了一个简单表达式,其中一个文本被分配给一个变量,后面是一个简单表达式,其中一个变量被分配给另一个变量的值:

int counter1 = 1;
int counter2 = counter1;

  • 1
  • 2
  • 3

最后,下面的例子展示了一个简单的表达式,它将方法调用的结果赋给一个名为 isLeap 的变量:

boolean isLeap = isLeapYear(2012);

  • 1
  • 2

前面的例子假设只有那些类型与它们正在初始化的变量的类型相同的表达式才能被赋给那些变量。然而,在某些情况下,有可能分配一个不同类型的表达式。例如,Java 允许你给短整型变量赋值特定的整型文字,如 short s = 20;,并将一个短整型表达式赋给一个整型变量,如 int I = s;。

Java 允许前一种赋值,因为 20 可以表示为一个短整数(不会丢失任何信息)。相比之下,Java 会抱怨短 s = 40000 因为整数字面量 40000 不能表示为短整数(32767 是短整数变量中可以存储的最大正整数)。Java 允许后一种赋值,因为当 Java 从一个值集较小的类型转换到一个值集较大的类型时,不会丢失任何信息。

Java 通过扩展转换规则支持以下原始类型转换:

  • 字节整数到短整数、整数、长整数、浮点或双精度浮点
  • 短整数到整数、长整数、浮点或双精度浮点
  • 字符转换为整数、长整数、浮点或双精度浮点
  • 整数到长整数、浮点或双精度浮点
  • 长整数到浮点或双精度浮点
  • 浮点到双精度浮点

注意从小整数转换到大整数时,Java 会将小整数的符号位复制到大整数的多余位。

在第四章的中,我讨论了在用户定义和数组类型的上下文中执行类型转换的扩展转换规则。

复合表达式

一个复合表达式是一系列简单的表达式和操作符,其中一个操作符(源代码中象征性表示的一系列指令)将其操作数表达式值转换为另一个值。例如, -6 是由运算符 – 和整数文字 6 作为操作数组成的复合表达式。这个表达式将 6 转换成它的负等价物。同样, x + 5 是一个复合表达式,由变量名 x ,整数文字量 5 ,以及夹在这些操作数之间的运算符 + 组成。当这个表达式被求值时,变量 x 的值被取出并加到 5 中。总和成为表达式的值。

注意当 x 的类型为字节整数或短整数时,该变量的值被加宽为整数。然而,当 x 的类型为长整型、浮点型或双精度浮点型时, 5 被加宽为适当的类型。加法运算在扩大转换发生后执行。

Java 提供了许多操作符,这些操作符是根据它们接受的操作数的数量来分类的。一个一元运算符 只取一个操作数(一元减[-]为例),一个二元运算符取两个操作数(加法[ + ]为例),Java 的单个三元运算符 (条件[ ?: ])采用三个操作数。

运算符也分为前缀、后缀和中缀。前缀运算符是位于其操作数之前的一元运算符(如在 6 中),而后缀运算符 是位于其操作数之后的一元运算符(如在 x++ 中),而中缀运算符 是夹在二元运算符的两个或三个操作数之间的二元或三元运算符(如在 x + 5 中)。

表 2-3 从符号、描述和优先级的角度展示了所有支持的操作符——我在这一节的最后讨论了优先级的概念。各种操作符描述都提到了“整数类型”,这是指定任何字节整数、短整数、整数或长整数的简写,除非“整数类型被限定为 32 位整数。此外,“数字类型”是指除浮点和双精度浮点之外的任何整数类型。

表 2-3。操作员

面向 Android 开发的 Java 学习手册(一)
面向 Android 开发的 Java 学习手册(一)
面向 Android 开发的 Java 学习手册(一)
面向 Android 开发的 Java 学习手册(一)
面向 Android 开发的 Java 学习手册(一)
面向 Android 开发的 Java 学习手册(一)

表 2-3 的运算符可分为加法、数组索引、赋值、按位、转换、条件、等式、逻辑、成员访问、方法调用、乘法、对象创建、关系、移位和一元减/加。

加法运算符

加法运算符由加法( + )、减法(-)、后减量(-)、后增量(+)、前增量(-)、前增量(+)和字符串串联( + )组成。加法返回其操作数之和(例如, 6 + 4 返回 10),减法返回其操作数之差(例如,6–4 返回 2,而 4–6 返回 2),后减量从其变量操作数中减去 1,并返回变量的前一个值(例如,x–),后增量相加 predecrement 从其变量操作数中减去 1 并返回变量的新值(例如,-x),preincrement 向其变量操作数加 1 并返回变量的新值(例如, ++x ),string concatenation 合并其字符串操作数并返回合并后的字符串(例如, “A” + “B” 返回 “AB” )。

加法、减法、后减量、后增量、前增量和前增量运算符可以生成溢出或下溢结果值类型限制的值。例如,将两个大的 32 位正整数值相加会产生一个无法表示为 32 位整数值的值。结果据说溢出来了。Java 不检测溢出和下溢。

Java 提供了一种特殊的扩大转换规则,用于字符串操作数和字符串连接运算符。当任一操作数不是字符串时,操作数在字符串连接之前首先转换为字符串。例如,当呈现为 “A” + 5 时,编译器生成的代码首先将 5 转换为 “5” ,然后执行字符串串联操作,得到 “A5” 。

数组索引运算符

数组索引运算符 ( [] )通过将数组元素的位置表示为整数索引来访问该元素。该运算符在数组变量的名称后指定,例如, ages[0] 。

索引是相对于 0 的,这意味着 ages[0] 访问第一个元素,而 ages[6] 访问第七个元素。索引必须大于或等于 0,并且小于数组的长度;否则,虚拟机抛出 ArrayIndexOutOfBoundsException(参考第五章了解异常)。

通过追加“”返回数组的长度。数组变量的长度。例如, ages.length 返回 ages 引用的数组的长度(元素个数)。类似地, matrix.length 返回矩阵二维数组中行元素的个数,而 matrix[0]。length 返回分配给该数组第一行元素的列元素个数。

赋值运算符

赋值运算符 ( = )将表达式的结果赋给一个变量(如 int x = 4;)。变量和表达式的类型必须一致;否则,编译器会报告错误。

Java 还支持几个复合赋值操作符,它们执行一个特定的操作并将结果赋给一个变量。例如, += 运算符计算右边的数值表达式,并将结果添加到左边的变量内容中。其他复合赋值运算符的行为方式类似。

按位运算符

按位运算符由按位 AND ( & )、按位补码(∾)、按位异或(【^】)和按位异或( | )组成。这些运算符设计用于处理字符或整数操作数的二进制表示。因为如果您以前没有在另一种语言中使用过这些运算符,这个概念可能很难理解,所以下面来自一个假设应用的输出演示了这些运算符:

∼00000000000000000000000010110101 results in 11111111111111111111111101001010
00011010 & 10110111 results in 00000000000000000000000000010010
00011010 ^ 10110111 results in 00000000000000000000000010101101
00011010 | 10110111 results in 00000000000000000000000010111111

  • 1
  • 2
  • 3
  • 4
  • 5

最后三行中的 & 、【^】和 | 运算符在执行运算之前,首先将其字节整数操作数转换为 32 位整数值(通过符号位扩展,将符号位的值复制到额外的位中)。

铸造操作符

cast 运算符— ( 类型)—试图将其操作数的类型转换为类型。此运算符的存在是因为编译器不允许您将一个值从一种类型转换为另一种类型,如果不指定您的意图,信息将会丢失(通过 cast 运算符)。比如用短 s = 1.65 + 3 呈现时;,编译器会报告一个错误,因为试图将双精度浮点值转换为短整型会导致分数丢失。65 — s 将包含 4 而不是 4.65。

认识到信息丢失可能并不总是一个问题,Java 允许您通过强制转换到目标类型来明确表达您的意图。例如,short s =(short)1.65+3;告诉编译器你希望 1.65 + 3 被转换成一个短整数,并且你意识到这个分数会消失。

下面的示例提供了需要强制转换运算符的另一个示例:

char c = 'A';
byte b = c;

  • 1
  • 2
  • 3

当遇到字节 b = c 时,编译器报告一个关于精度损失的错误;。原因是 c 可以表示从 0 到 65535 的任何无符号整数值,而 b 只能表示从 128 到+127 的有符号整数值。即使 ‘A’ 等于+65,这可以在 b 的范围内,但是 c 可能很容易被初始化为 ‘u0323’ ,这是不合适的。

这个问题的解决方案是引入一个 (byte) 强制转换,如下所示,这使得编译器生成代码将 c 的字符类型强制转换为字节整数:

byte b = (byte) c;

  • 1
  • 2

Java 通过强制转换操作符支持以下基本类型转换:

  • 字节整数到字符
  • 短整数到字节整数或字符
  • 字符到字节整数或短整数
  • 整数到字节整数、短整数或字符
  • 长整数到字节整数、短整数、字符或整数
  • 浮点到字节整数、短整数、字符、整数或长整数
  • 双精度浮点到字节整数、短整数、字符、整数、长整数或浮点

当从更多位转换到更少位并且没有发生数据丢失时,并不总是需要转换运算符。比如当它遇到字节 b = 100,编译器生成代码将整数 100 赋给字节整数变量 b ,因为 100 可以很容易地放入赋给这个变量的 8 位存储位置。

条件运算符

条件运算符由条件与( & & )、条件或( || )和条件(?😃。前两个运算符总是计算其左操作数(计算结果为 true 或 false 的布尔表达式),并有条件地计算其右操作数(另一个布尔表达式)。第三个运算符基于第三个布尔操作数计算两个操作数之一。

条件,并且总是计算其左操作数,并且仅当其左操作数的计算结果为 true 时,才计算其右操作数。比如年龄>64&还在工作先评估年龄> 64 。如果该子表达式为真,则对 stillWorking 求值,其真或假值( stillWorking 为布尔变量)作为整个表达式的值。如果年龄> 64 为假,则仍在工作不被评估。

条件 OR 始终计算其左操作数,仅当其左操作数的计算结果为 false 时,才计算其右操作数。例如,值< 20 ||值> 40 先求值值< 20 。如果该子表达式为假,则评估值> 40 ,其真值或假值作为整个表达式的值。如果值< 20 为真,则值> 40 不求值。

条件 AND 和条件 OR 通过防止不必要的子表达式求值来提高性能,这被称为短路。例如,如果其左操作数为 false,则条件 and 的右操作数无法改变整个表达式的计算结果为 false 的事实。

如果不小心,短路可能会阻止副作用(子表达式求值后持续存在的子表达式的结果)的执行。例如,年龄>64&&++人数> 5 只对年龄大于 64 岁的员工递增人数。递增 numEmployees 是副作用的一个例子,因为 numEmployees 中的值在子表达式 ++numEmployees > 5 求值后仍然存在。

条件运算符通过根据第三个操作数的值计算并返回两个操作数中的一个来做出决策,这非常有用。以下示例将布尔值转换为其等效的整数(1 表示真,0 表示假):

boolean b = true;
int i = b ? 1 : 0; // 1 assigns to i

  • 1
  • 2
  • 3

等式运算符

等式运算符由等式( == )和不等式()组成!= )。这些运算符比较它们的操作数,以确定它们是否相等。前一个运算符在相等时返回 true 后一个运算符在不相等时返回 true。比如 2 == 2 和 2 的每一个!= 3 评估为真,而 2 == 4 和 4!= 4 评估为假。

当涉及到对象操作数时(在第三章的中讨论),这些操作符不比较它们的内容。比如“ABC”= =“XYZ”不比较 a 和 x 。相反,因为字符串文字实际上是 String 对象(在第七章中我进一步讨论了这个概念), == 比较对这些对象的引用。

逻辑运算符

逻辑运算符由逻辑与( & )、逻辑补码(!)、逻辑异或( ^ )、逻辑异或( | )。虽然这些运算符与按位运算符相似,它们的操作数必须是整数/字符,但传递给逻辑运算符的操作数必须是布尔型的。比如!false 返回 true。此外,当遇到年龄> 64 &仍在工作时,逻辑 AND 会评估两个子表达式。这种模式同样适用于逻辑异或和逻辑包含或。

成员访问操作员

成员访问操作员 ( )。用于访问一个类的成员或一个对象的成员。比如 String s = ” Hello “;int len = s . length();返回分配给变量 s 的字符串长度。它通过调用 String 类的 length() 方法成员来实现。在第三章中,我将更详细地讨论成员访问。

数组是特殊的对象,它只有一个长度的成员。当您指定一个数组变量,后跟成员访问操作符,再后跟长度时,结果表达式将数组中的元素数作为 32 位整数返回。例如, ages.length 返回 ages 引用的数组的长度(元素个数)。

方法调用运算符

方法调用操作符—()—用于表示一个方法(在第三章的中讨论)正在被调用。此外,它还标识了传递给方法的参数的数量、顺序和类型,这些参数将由方法的参数选取。system . out . println(” Hello “);就是一个例子。

乘法运算符

乘法运算符由乘法( * )、除法( / )和余数( % )组成。乘法返回其操作数的乘积(例如, 6*4 返回 24),除法返回其左操作数除以其右操作数的商(例如, 6/4 返回 1),余数返回其左操作数除以其右操作数的余数(例如, 6%4 返回 2)。

乘法、除法和余数运算符可以生成溢出或下溢结果值类型限制的值。例如,将两个大的 32 位正整数值相乘会产生一个无法用 32 位整数值表示的值。结果据说溢出来了。Java 不检测溢出和下溢。

将数值除以 0(通过除法或余数运算符)也会产生有趣的行为。将一个整数值除以整数 0 会导致操作符抛出一个算术异常对象(我会在第五章的中讨论异常)。将浮点/双精度浮点值除以 0 会导致运算符返回+无穷大或-无穷大,具体取决于被除数是正数还是负数。最后,将浮点 0 除以 0 会导致运算符返回 NaN(不是数字)。

对象创建操作符

对象创建操作符 ( new )从一个类创建一个对象,也从一个初始化器创建一个数组。我将在第三章中讨论这些话题。

关系运算符

关系运算符 由大于( > )、大于等于( > = )、小于( < )、小于等于( < = )和类型检查()组成。前四个运算符比较它们的操作数,当左操作数分别大于、大于或等于、小于或小于或等于右操作数时,返回 true。例如, 5.0 > 3 、 2 > = 2 、 16.1 < 303.3 、 54.0 < = 54.0 中的每一个都求值为真。

类型检查运算符用于确定对象是否属于特定类型。我在第四章中讨论了这个话题。

移位运算符

移位运算符由左移( < < )、有符号右移( > > )和无符号右移( > > > )组成。左移将左操作数的二进制表示向左移动右操作数指定的位数。每次移位相当于乘以 2。比如 2<3 将 2 的二进制表示左移 3 位;结果相当于 2 乘以 8。

每个有符号和无符号右移都将其左操作数的二进制表示向右移动由其右操作数指定的位置数。每次移位相当于除以 2。比如 16>3 将 16 的二进制表示右移 3 位;结果相当于 16 除以 8。

有符号右移和无符号右移的区别在于移位过程中符号位的变化。有符号右移位包括移位中的符号位,而无符号右移位忽略符号位。因此,有符号右移保留负数,但无符号右移不保留负数。例如,-4>>1(相当于 4 / 2 )的计算结果为 2,而 4 > > > 1 的计算结果为 2147483646。

提示移位运算符比乘以或除以 2 的幂要快。

一元减/加运算符

一元减号()和一元加号( + )是所有运算符中最简单的。一元减返回其操作数的负数(如 5 返回 5 和 – 5 返回 5 ),而一元加则逐字返回其操作数(如 +5 返回 5 和±5 返回 5 )。一元加号不常用,但为了完整性而出现。

优先级和结合性和

当计算一个复合表达式时,Java 会考虑每个操作符的优先级(重要性级别),以确保表达式的计算符合预期。例如,当用表达式 60 + 3 * 6 表示时,您期望乘法在加法之前执行(乘法的优先级高于加法),最终结果是 78。你不会期望加法首先发生,产生 378 的结果。

表 2-3 最右边的一栏显示了一个值,表示一个操作符的优先级:数字越大,优先级越高。例如,加法的优先级别是 10,乘法的优先级别是 11,这意味着乘法在加法之前执行。

可以通过在表达式中引入左括号和右括号 ( 和 ) 来规避优先级,其中首先计算最里面的一对嵌套括号。例如,计算 2 * ((60 + 3) * 6) 会导致首先计算 (60 + 3) ,然后计算 (60 + 3) * 6 ,最后计算整体表达式。类似地,在表达式 60/(3–6)中,在除法之前执行减法。

在求值过程中,具有相同优先级的操作符(如加法和减法,都具有 10 级)根据它们的结合性进行处理(这是一个属性,确定当缺少括号时,具有相同优先级的操作符如何分组)。

例如,因为 * 和 / 是从左到右的关联运算符,所以表达式 9 * 4 / 3 被视为 (9 * 4) / 3 。相比之下,表达式 x = y = z = 100 被求值,就好像它是 x =(y =(z = 100))—100 被赋给 z , z 的新值(100)被赋给 y,y 的新值(100)被赋给

Java 的大多数操作符都是从左到右关联的。从右到左的关联运算符包括赋值、按位补码、强制转换、复合赋值、条件、逻辑补码、对象创建、预递增、预递增、一元减号和一元加号。

注意与 C++等语言不同,Java 不允许重载操作符。然而,Java 重载了 + 、 ++ 和 – 运算符。

学习语句 s

语句是一个程序的主力。它们给变量赋值,通过决策和/或重复执行其他语句来控制程序流程,并执行其他任务。一个语句可以用简单语句或复合语句来表达:

  • 简单语句 是用于执行某些任务的单个独立源代码指令;它以分号结束。
  • 复合语句 是夹在左大括号和右大括号之间的简单语句和其他复合语句的(可能为空)序列——分隔符是标记某个部分的开始或结束的字符。方法体(如 main() 方法体)就是一个例子。复合语句可以出现在简单语句出现的任何地方,或者被称为块*。*

*在这一节中,我将向您介绍 Java 的许多语句。其他声明将在后面的章节中介绍。例如,在第三章中,我讨论了 return 语句。

赋值语句

赋值 语句是给变量赋值的表达式。该语句以变量名开始,以赋值操作符( = )或复合赋值操作符(如 += )继续,以赋值兼容表达式和分号结束。以下是三个例子:

x = 10;
ages[0] = 25;
counter += 10;

  • 1
  • 2
  • 3
  • 4

第一个例子将整数 10 赋给变量 x ,它大概也是整数类型。第二个示例将整数 25 分配给年龄数组的第一个元素。第三个示例将 10 加到存储在计数器中的值,并将总和存储在计数器中。

注意在变量声明中初始化一个变量(比如 int counter = 1;)可以认为是赋值语句的一种特殊形式。

决策声明

前面描述的条件运算符(?:)对于在两个表达式之间选择求值是有用的,不能用于在两个语句之间选择。为此,Java 提供了三个决策语句:if、if-else 和 switch。

If 语句

if 语句评估一个布尔表达式,并在该表达式评估为真时执行另一个语句。该语句具有以下语法:

if (*Boolean expression*)
   *statement*

  • 1
  • 2
  • 3

If 由保留字 if 组成,后面是圆括号中的布尔表达式,后面是当布尔表达式评估为真时执行的语句

以下示例演示了该语句:

if (numMonthlySales > 100)
   wage += bonus;

  • 1
  • 2
  • 3

如果月销售额超过 100, numMonthlySales > 100 计算为真,工资+=奖金;赋值语句执行。否则,这个赋值语句不会执行。

注意有些人喜欢用大括号将一条语句括起来,以防止出错。因此,他们会按如下方式编写前面的示例:

if (numMonthlySales > 100){
   wage += bonus;
}

  • 1
  • 2
  • 3
  • 4

我不会对单个语句这样做,因为我认为多余的大括号是不必要的混乱。然而,你可能会有不同的感受。使用任何让你最舒服的方法。

If-Else 语句

if-else 语句该语句具有以下语法:

if (*Boolean expression*)
   *statement1*
else
 *statement2*

  • 1
  • 2
  • 3
  • 4
  • 5

If-else 由保留字 if 组成,后面是圆括号中的布尔表达式,后面是当布尔表达式评估为真时执行的语句 1 ,后面是当布尔表达式评估为假时执行的语句 2

以下示例演示了该语句:

if ((n & 1) == 1)
   System.out.println("odd");
else
   System.out.println("even");

  • 1
  • 2
  • 3
  • 4
  • 5

这个例子假设存在一个名为 n 的 int 变量,它已经被初始化为一个整数。然后,它继续确定该整数是奇数(不能被 2 整除)还是偶数(能被 2 整除)。

布尔表达式首先对 n & 1 求值,然后将 n 的值与 1 进行位与运算。然后将结果与 1 进行比较。如果相等,则输出一条消息,说明 n 的值为奇数;否则,会输出一条消息,说明 n 的值为偶数。

括号是必需的,因为 == 的优先级高于 & 。如果没有这些括号,表达式的求值顺序将变为首先对 1 == 1 求值,然后尝试对布尔结果与 n 的整数值进行位与运算。由于类型不匹配,这种顺序会导致编译器错误信息:不能将整数与布尔值进行位 AND 运算。

您可以重写这个 if-else 语句示例以使用条件运算符,如下所示:system . out . println((n&1)= = 1?“奇”:“偶”);。但是,在以下示例中,您无法做到这一点:

if ((n & 1) == 1)
   odd();
else
   even();

  • 1
  • 2
  • 3
  • 4
  • 5

本例假设存在不返回任何内容的 odd() 和 even() 方法。因为条件运算符要求其第二个和第三个操作数的计算结果都是一个值,所以当试图编译 (n & 1) == 1 时,编译器会报告一个错误?奇数() :偶数()。

您可以将多个 if-else 语句链接在一起,产生以下语法:

if (*Boolean expression1*)
   *statement1*
else
if (*Boolean expression2*)
 *statement2*
else
   ...
else
   *statementN*

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

如果布尔表达式 1 评估为真,则语句 1 执行。否则,如果布尔表达式 2 评估为真,则语句 2 执行。这种模式一直持续到这些表达式中的一个计算为真并且其相应的语句执行,或者到达最后的 else 并且语句 N (默认语句)执行。

以下示例演示了这种链接:

if (testMark >= 90)
{
   gradeLetter = 'A';
   System.out.println("You aced the test.");
}
else
if (testMark >= 80)
{
   gradeLetter = 'B';
   System.out.println("You did very well on this test.");
}
else
if (testMark >= 70)
{
   gradeLetter = 'C';
   System.out.println("Not bad, but you need to study more for future tests.");
}
else
if (testMark >= 60)
{
   gradeLetter = 'D';
   System.out.println("Your test result suggests that you need a tutor.");
}
else
{
   gradeLetter = 'F';
   System.out.println("Your test result is pathetic; you need summer school.");
}

面向 Android 开发的 Java 学习手册(一)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

悬挂-否则问题

当 if 和 if-else 一起使用时,如果源代码没有正确缩进,就很难确定哪个 if 与 else 相关联。例如,请参见以下内容:

if (car.door.isOpen())

if (car.key.isPresent())

car . start();

else car . door . open();

开发人员是否打算让 else 与内部 if 匹配,但却错误地格式化了代码,使其看起来不匹配?例如,请参见以下内容:

if (car.door.isOpen())

if (car.key.isPresent())

car . start();

其他

car . door . open();

如果 car.door.isOpen() 和 car.key.isPresent() 各自返回 true, car.start() 执行。如果 car.door.isOpen() 返回 true, car.key.isPresent() 返回 false,car . door . open();执行。试图打开一扇敞开的门毫无意义。

开发人员肯定希望 else 匹配外部 if,但是忘记了 else 匹配最近的 if。这个问题可以通过用大括号将内部 if 括起来来解决,如下所示:

if (car.door.isOpen())

{

if (car.key.isPresent())

car . start();

}

其他

car . door . open();

当 car.door.isOpen() 返回 true 时,复合语句执行。当此方法返回 false 时,car . door . open();执行,有道理。

忘记 else 匹配最近的 if 并使用糟糕的缩进来掩盖这一事实被称为悬空-else 问题

开关语句

与等效的链式 if-else 语句相比, switch 语句 允许您以更有效的方式从几个执行路径中进行选择。该语句具有以下语法:

switch (*selector expression* )
{
   case*value1* :*statement1* [break;]
   case*value2* :*statement2* [break;]
   ...
   case*valueN* :*statementN* [break;]
   [default:*statement* ]
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

Switch 由保留字 switch 组成,后面是圆括号中的选择器表达式,后面是案例体。选择器表达式是任何计算结果为整数或字符值的表达式。例如,它可能计算 32 位整数或 16 位字符。

每个 case 以保留字 case 开头;以一个文字值和一个冒号字符( : )继续;继续执行一条语句;并且可选地以 break 语句结束,这使得执行在 switch 语句之后继续。

在对选择器表达式求值后,switch 将该值与每个事例的值进行比较,直到找到匹配项。当有匹配时,执行 case 语句。例如,当选择器表达式的值与值 1 匹配时,语句 1 执行。

可选的 break 语句(方括号中的任何内容都是可选的),由保留字 break 后跟一个分号组成,阻止执行流继续执行下一个 case 语句。而是继续执行 switch 后面的第一条语句。

注意你通常会在案件陈述后放置一个中断陈述。忘记包含 break 会导致一个很难发现的 bug。但是,有些情况下,您希望将几个案例组合在一起,并让它们执行相同的代码。在这种情况下,您可以从参与案例中省略 break 语句。

如果没有一个案例的值与选择器表达式的值匹配,并且如果存在一个默认案例(由后面跟有冒号的默认保留字表示),则执行默认案例的语句。

以下示例演示了该语句:

switch (direction)
{
   case  0: System.out.println("You are travelling north."); break;
   case  1: System.out.println("You are travelling east."); break;
   case  2: System.out.println("You are travelling south."); break;
   case  3: System.out.println("You are travelling west."); break;
   default: System.out.println("You are lost.");
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这个例子假设方向存储一个整数值。当该值在 0-3 范围内时,输出适当的方向信息;否则,输出关于丢失的消息。

Strong 这个例子硬编码了值 0、1、2 和 3,这在实践中并不是一个好主意。相反,应该使用常数。在第三章中,我会向你介绍常数。

循环语句

经常需要重复执行一条语句,这种重复执行被称为循环。Java 提供了三种循环语句:for、while 和 do-while。在本节中,我首先讨论这些陈述。然后,我研究了空语句循环的主题。最后,我讨论了 break、标记为 break、continue 和标记为 continue 的语句,这些语句用于提前结束全部或部分循环。

对于语句

语句 的可以让你循环一个语句特定的次数,甚至是无限的。该语句具有以下语法:

for ([*initialize*]; [*test*]; [*update*])
   *statement*

  • 1
  • 2
  • 3

For 由用于的保留字组成,后面是括号中的头,后面是要执行的语句。头部由可选的初始化部分、可选的测试部分、可选的更新部分组成。一个非可选的分号将前两个部分与下一个部分分开。

initialize 部分由逗号分隔的变量声明或变量赋值列表组成。这些变量中的一些或全部通常用于控制循环的持续时间,被称为循环控制变量

测试部分由一个布尔表达式组成,它决定了循环执行的时间。只要该表达式的计算结果为 true,执行就会继续。

最后,更新部分由逗号分隔的表达式列表组成,这些表达式通常修改循环控制变量。

For 非常适合于在一个数组上迭代(循环)。每次迭代(循环执行)通过一个数组 [ 索引 ] 表达式访问数组的一个元素,其中数组是被访问元素的数组,索引是被访问元素的从零开始的位置。

以下示例使用 for 语句迭代传递给 main() 方法的命令行参数数组:

public static void main(String[] args)
{
   for (int i = 0; i < args.length; i++)
      System.out.println(args[i]);
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

For 的初始化部分声明变量 i 用于控制循环,其测试部分将 i 的当前值与 args 数组的长度进行比较,以确保该值小于数组的长度,其更新部分将 i 递增 1。循环继续,直到 i 的值等于数组的长度。

每次迭代通过 args[i] 表达式访问数组的一个值。这个表达式返回这个数组的第 i 个值(在这个例子中恰好是一个字符串对象)。第一个值存储在 args[0] 中。

注意虽然我已经将包含命令行参数的数组命名为 args ,但是这个名称并不是强制性的。我可以很容易地把它命名为论据(或者甚至其他名称)。

以下示例使用 for 输出先前声明的矩阵数组的内容,为了方便起见,在此重新声明:

float[][] matrix = { { 1.0F, 2.0F, 3.0F }, { 4.0F, 5.0F, 6.0F }};
for (int row = 0; row < matrix.length; row++)
{
   for (int col = 0; col < matrix[row].length; col++)
      System.out.print(matrix[row][col] + " ");
   System.out.print("
");
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

表达式 matrix.length 返回该表格数组中的行数。对于每一行,表达式矩阵【行】。length 返回该行的列数。后一个表达式表明每一行可以有不同的列数,尽管在本例中每一行都有相同的列数。

System.out.print() 与 System.out.println() 密切相关。与后一种方法不同, System.out.print() 输出它的参数时不带尾随换行符。

此示例生成以下输出:

1.0 2.0 3.0
4.0 5.0 6.0

  • 1
  • 2
  • 3

而声明

while 语句重复执行另一个语句,同时其布尔表达式的值为 true。该语句具有以下语法:

while (*Boolean expression*)
   *statement*

  • 1
  • 2
  • 3

While 由保留字 while 组成,后面是带括号的布尔表达式,后面是要重复执行的语句

while 语句首先评估布尔表达式。如果为真,则执行另一个语句。再次对布尔表达式求值。如果仍然为真,则重新执行语句。这种循环模式继续下去。

提示用户输入特定字符是 while 有用的一种情况。例如,假设您希望提示用户输入一个特定的大写字母或其小写等效字母。以下示例提供了一个演示:

int ch = 0;
while (ch != 'C' && ch != 'c')
{
   System.out.println("Press C or c to continue.");
   ch = System.in.read();
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这个例子从初始化变量 ch 开始。此变量必须初始化;否则,当编译器试图在 while 语句的布尔表达式中读取 ch 的值时,它将报告一个未初始化的变量。

该表达式使用条件 AND 运算符( & & )来测试 ch 的值。这个操作符首先计算它的左操作数,恰好是表达式 ch!= ‘C’ 。(第!= 运算符在比较之前将 ‘C’ 从 16 位无符号 char 类型转换为 32 位有符号 int 类型。)

如果 ch 不包含 C (此时不包含— 0 刚刚被赋值给 ch ),那么这个表达式的计算结果为 true。

& & 运算符接下来计算其右操作数,恰好是表达式 ch!= ‘c’ 。因为该表达式的计算结果也为 true,所以条件表达式返回 true,而 while 执行复合语句。

复合语句首先通过 System.out.println() 方法调用输出一条消息,提示用户在有或没有 Shift 键的情况下按 C 键。接下来它通过 System.in.read() 读取输入的按键,将其整数值保存在 ch 中。

从左到右,系统标识系统工具的标准类,中的标识位于系统中的对象,该对象提供从标准输入设备输入 1 个或更多字节的方法, read() 返回下一个字节(或当没有更多的字节时返回 1)。

在这个赋值之后,复合语句结束,while 重新计算它的布尔表达式。

假设 ch 包含 C 的整数值。有条件和评价 ch!= ‘C’ ,计算结果为假。看到表达式已经为 false,条件 AND 通过不计算其右操作数来缩短其计算,并返回 false。while 语句随后检测到该值并终止。

假设 ch 包含 c 的整数值。有条件和评价 ch!= ‘C’ ,计算结果为真。眼见表情为真,有条件的又评价 ch!= ‘c’ ,计算结果为假。while 语句再次终止。

注意for 语句可以编码为 while 语句。例如,

for (int i = 0; i < 10; i++)
   System.out.println(i);

  • 1
  • 2
  • 3

相当于

int i = 0;
while (i < 10)
{
   System.out.println(i);
   i++;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

Do-While 语句

do-while 语句 重复执行一个布尔表达式为真的语句。与在循环顶部计算布尔表达式的 while 语句不同,do-while 在循环底部计算布尔表达式。该语句具有以下语法:

do
  *statement*
while (*Boolean expression* );

  • 1
  • 2
  • 3
  • 4

Do-while 由 do 保留字组成,后面是要重复执行的语句,后面是 while 保留字,后面是带括号的布尔表达式,后面是分号。

do-while 语句首先执行另一个语句。然后它评估布尔表达式的*。如果为真,do-while 执行另一个语句。再次对布尔表达式求值。如果仍然为真,do-while 重新执行语句。这种循环模式继续下去。*

下列范例示范 do-while 提示使用者输入特定的大写字母或其小写对等字母:

int ch;
do
{
   System.out.println("Press C or c to continue.");
   ch = System.in.read();
}
while (ch != 'C' && ch != 'c');

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这个例子与其前身相似。因为在测试之前不再执行复合语句,所以不再需要在布尔表达式求值之前初始化 ch — ch 被赋予 System.in.read() 的返回值。

循环空语句

Java 引用一个分号字符作为空语句。循环语句重复执行空语句有时很方便。loop 语句执行的实际工作发生在语句头中。考虑以下示例:

for (String line; (line = readLine()) != null; System.out.println(line));

  • 1
  • 2

此示例使用 for 来呈现一种编程习惯用法,用于将从某个源读取的文本行(在此示例中通过虚构的 readLine() 方法)复制到某个目的地(在此示例中通过 System.out.println() )。复制继续进行,直到 readLine() 返回空值。注意行尾的分号(空语句)。

小心空语句,因为它会给你的代码带来微妙的错误。例如,下面的循环应该在 10 行中输出字符串 Hello 。相反,只输出该字符串的一个实例,因为它是空语句,而不是执行了 10 次的 System.out.println() :

for (int i = 0; i < 10; i++); // this ; represents the empty statement
   System.out.println("Hello");

  • 1
  • 2
  • 3

Break 和标注 Break 的语句和

做什么用(;;);、 while(真实);和做;while(真);有什么共同点?这些循环语句中的每一个都代表了一个无限循环(一个永不结束的循环)的极端例子。无限循环是应该避免的,因为它的无休止执行会导致应用挂起,从应用用户的角度来看,这是不可取的。

注意由于许多浮点值具有不精确的内部表示,通过等式或不等式运算符将浮点值与非零值进行比较的循环布尔表达式也会产生无限循环。例如,下面的例子永远不会结束,因为 0.1 没有确切的内部表示:

for (double d = 0.0; d != 1.0; d += 0.1)
   System.out.println(d);

  • 1
  • 2
  • 3

然而,有时通过使用上述编程习惯用法之一,可以方便地编写一个循环,就好像它是无限的一样。例如,您可以编写一个 while (true) 循环,重复提示特定的击键,直到按下正确的键。当按下正确的键时,循环必须结束。Java 为此提供了 break 语句。

break 语句将执行转移到 switch 语句(如前所述)或循环之后的第一条语句。在任一场景中,该语句由保留字 break 后跟一个分号组成。

以下示例使用 break with if decision 语句在用户按下 C 或 C 键时退出基于 while (true) 的无限循环:

int ch;
while (true)
{
   System.out.println("Press C or c to continue.");
   ch = System.in.read();
   if (ch == 'C' || ch == 'c')
      break;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

break 语句在有限循环的上下文中也很有用。例如,考虑这样一个场景,在一个值数组中搜索一个特定的值,当找到这个值时,您希望退出循环。以下示例揭示了这种情况:

int[] employeeIDs = { 123, 854, 567, 912, 224 };
int employeeSearchID = 912;
boolean found = false;
for (int i = 0; i < employeeIDs.length; i++)
   if (employeeSearchID == employeeIDs[i])
   {
      found = true;
      break;
   }
System.out.println((found) ? "employee " + employeeSearchID + " exists"
                           : "no employee ID matches " + employeeSearchID);

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

该示例使用 for 和 if 来搜索雇员 ID 数组,以确定特定的雇员 ID 是否存在。如果找到这个 ID,If 的复合语句将 true 赋给 found 。因为继续搜索没有意义,所以它使用 break 退出循环。

标记的 break 语句将执行转移到循环后面的第一条语句,该语句前面有一个标签(一个标识符,后跟一个冒号)。它由保留字 break 组成,后跟一个匹配标签必须存在的标识符。此外,标签必须紧接在循环语句之前。

标签 break 对于跳出嵌套循环(循环中的循环)很有用。以下示例显示了标记为 break 的语句将执行转移到外部 for 循环后面的第一条语句:

outer:
for (int i = 0; i < 3; i++)
   for (int j = 0; j < 3; j++)
      if (i == 1 && j == 1)
         break outer;
      else
         System.out.println("i=" + i + ", j=" + j);
System.out.println("Both loops terminated.");

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

当 i 的值为 1, j 的值为 1 时,破外;执行以终止两个 for 循环。该语句将执行转移到外层 for 循环之后的第一条语句,恰好是 System.out.println(“两个循环都终止了”));。

将生成以下输出:

i=0, j=0
i=0, j=1
i=0, j=2
i=1, j=0
Both loops terminated.

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

继续和标注继续的语句

continue 语句跳过当前循环迭代的剩余部分,重新计算循环的布尔表达式,并执行另一次迭代(如果为真)或终止循环(如果为假)。继续由保留字继续后跟分号组成。

考虑一个 while 循环,它从源中读取行,并以某种方式处理非空行。因为它不应处理空行,而在检测到空行时会跳过当前迭代,如以下示例所示:

String line;
while ((line = readLine()) != null)
{
   if (isBlank(line))
      continue;
   processLine(line);
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这个例子使用了一个虚构的 isBlank() 方法 来确定当前读取的行是否是空白的。如果此方法返回 true,则执行 continue 语句以跳过当前迭代的剩余部分,并在检测到空行时读取下一行。否则,将调用虚构的 processLine() 方法来处理该行的内容。

仔细看看这个例子,你应该意识到 continue 语句是不需要的。相反,这个清单可以通过重构(重写源代码以提高其可读性、组织性或可重用性)来缩短,如下例所示:

String line;
while ((line = readLine()) != null)
{
   if (!isBlank(line))
      processLine(line);
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

本例的重构将 if 的布尔表达式修改为使用逻辑补码运算符(!)。每当 isBlank() 返回 false 时,该运算符将该值翻转为 true,并执行 processLine() 。虽然在这个例子中 continue 不是必需的,但是您会发现在重构不容易执行的更复杂的代码中使用这个语句很方便。

标记为的 continue 语句跳过一个或多个嵌套循环的剩余迭代,并将执行转移到标记的循环。它由保留字 continue 组成,后跟一个标识符,必须存在与之匹配的标签。此外,标签必须紧接在循环语句之前。

带标签的 continue 对于在继续执行带标签的循环的同时跳出嵌套循环非常有用。以下示例显示了终止内部 for 循环迭代的带标签的 continue 语句:

outer:
for (int i = 0; i < 3; i++)
   for (int j = 0; j < 3; j++)
      if (i == 1 && j == 1)
         continue outer;
      else
         System.out.println("i=" + i + ", j=" + j);
System.out.println("Both loops terminated.");

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

当 i 的值为 1, j 的值为 1,继续外层;执行以终止内部 for 循环,并在其下一个值 i 处继续外部 for 循环。两个循环都继续,直到结束。

将生成以下输出:

i=0, j=0
i=0, j=1
i=0, j=2
i=1, j=0
i=2, j=0
i=2, j=1
i=2, j=2
Both loops terminated.

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

练习

以下练习旨在测试您对第二章内容的理解:

  1. 什么是 Unicode?
  2. 什么是评论?
  3. 识别 Java 支持的三种注释。
  4. 什么是标识符?
  5. 是非判断:Java 是一种不区分大小写的语言。
  6. 什么是类型?
  7. 定义原始类型。
  8. 识别 Java 的所有基本类型。
  9. 定义用户定义的类型。
  10. 定义数组类型。
  11. 什么是变量?
  12. 什么是表达式?
  13. 识别两种表达式类别。
  14. 什么是文字?
  15. 是字符串文字“敏捷的棕色狐狸‘跳过’懒狗。”合法还是非法?为什么呢?
  16. 什么是运营商?
  17. 识别前缀运算符和后缀运算符之间的区别。
  18. 强制转换运算符的目的是什么?
  19. 什么是优先?
  20. 是非判断:大多数 Java 操作符都是从左到右关联的。
  21. 什么是陈述?
  22. while 和 do-while 语句的区别是什么?
  23. break 和 continue 语句的区别是什么?
  24. 编写一个 OutputGradeLetter 应用(类名为 OutputGradeLetter ),其 main()方法执行前面讨论 if-else 语句时给出的等级字母代码序列。使用清单 2-1 中的 DumpArgs 应用作为该应用的基础。(您不需要包含 Javadoc 注释,但是如果您愿意,您可以这样做。)
  25. Create a Triangle application whose Triangle class’s main() method uses a pair of nested for statements along with System.out.print() to output a 10-row triangle of asterisks, where each row contains an odd number of asterisks (1, 3, 5, 7, and so on), as shown following:
![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Flearn-java-andr-dev%2Fimg%2Fstar.jpg&pos_id=img-6GPFJbTA-1724344234023)

  • 1

编译并运行该应用。

摘要

源代码需要被文档化,这样你(和任何维护它的人)就可以理解它,无论是现在还是将来。Java 提供了在源代码中嵌入文档的注释特性。支持单行、多行和文档注释。

一个单行注释占据了一行源代码的全部或者部分。该注释以 // 字符序列开始,并以解释文本继续。编译器忽略从 // 到出现 // 的行尾的所有内容。

一个多行注释占据了一行或多行源代码。该注释以 /* 字符序列开始,以说明性文本继续,以 / 字符序列结束。编译器会忽略从 / 到 */ 的所有内容。

一个 Javadoc 注释占据了一行或多行源代码。该注释以 /** 字符序列开始,以说明性文本继续,以 / 字符序列结束。编译器会忽略从 /* 到 */ 的所有内容。

标识符用于命名类、方法和其他源代码实体。一个标识符由字母(A-Z,A-Z,或其他人类字母表中的等价大写/小写字母)、数字(0-9 或其他人类字母表中的等价数字)、连接标点符号(如下划线)和货币符号(如美元符号$)组成。该名称必须以字母、货币符号或连接标点符号开头。并且它的长度不能超过它所在的行。有些标识符是 Java 保留的。例子包括摘要和案例。

应用处理不同类型的值,如整数、浮点值、字符和字符串。类型标识一组值(以及它们在内存中的表示)和一组将这些值转换成该组中其他值的操作。

原始类型是由语言定义的类型,其值不是对象。Java 支持布尔、字符、字节整数、短整数、整数、长整数、浮点和双精度浮点原语类型。

用户定义类型是由开发人员使用类、接口、枚举或注释类型定义的类型,其值是对象。用户定义类型也称为引用类型。

一个数组类型是一个引用类型,表示一个数组,一个在大小相等的连续槽中存储值的内存区域,通常被称为元素。这种类型由元素类型和一对或多对方括号组成,表示尺寸的数量。

应用操作存储在内存中的值,这些值通过使用变量特性在源代码中象征性地表示出来。一个变量是一个存储某种类型值的命名内存位置。

Java 为初始化变量和其他目的提供了表达式特性。一个表达式组合了文字、变量名、方法调用和操作符的某种排列。在运行时,它计算出一个值,该值的类型称为表达式的类型。

一个简单表达式是一个文字(一个逐字指定的值),一个变量名(包含一个值),或者一个方法调用(返回值)。Java 支持几种文字:字符串、布尔真和假,字符、整数、浮点和空。

一个复合表达式是一系列简单的表达式和操作符,其中一个操作符(源代码中象征性表示的一系列指令)将其操作数表达式值转换为另一个值。

Java 提供了许多操作符,这些操作符是根据它们接受的操作数的数量来分类的。一个一元运算符只取一个操作数,一个二元运算符取两个操作数,Java 的单个三元运算符取三个操作数。

运算符也分为前缀、后缀和中缀。一个前缀运算符是位于其操作数之前的一元运算符,后缀运算符是位于其操作数之后的一元运算符,中缀运算符是夹在其操作数之间的二元或三元运算符。

语句是程序的核心。它们给变量赋值,通过决策和/或重复执行其他语句来控制程序流程,并执行其他任务。一个语句可以用简单语句或复合语句来表达。

在第三章中,我继续通过检查 Java 语言对类和对象的支持来探索 Java 语言。您还会学到更多关于数组的知识。*

三、探索类和对象

在第二章中,我向你介绍了 Java 语言的基础。您现在知道了如何通过将语句插入到类的 main() 方法中来编写简单的应用。然而,当您试图以这种方式开发复杂的应用时,您一定会发现开发是乏味的、缓慢的,并且容易出错。类和对象通过简化应用架构来解决这些问题。

在第三章中,我向你介绍了 Java 对类和对象的支持。您将学习如何声明一个类并从该类中实例化对象,如何在该类中声明字段并访问这些字段,如何在该类中声明方法并调用它们,如何初始化类和对象,以及如何在不再需要对象时将其删除。

在第二章讨论变量的时候,我向你介绍了数组。您学习了数组变量,并发现了一种创建数组的简单方法。然而,Java 也提供了一种更强大、更灵活的方法来创建数组,这有点类似于创建对象的方式。本章还通过向您介绍这一功能扩展了第二章的的阵列覆盖范围。

声明类和实例化对象

在涉及类和对象的现代编程方法之前,应用遵循结构化编程,其中数据结构被创建来组织和存储数据项,而函数(返回值的命名代码序列)和过程(不返回值的命名代码序列)被用于操纵数据结构内容。这种数据与代码的分离使得建模真实世界的实体(比如银行账户和雇员)变得困难,并且经常导致复杂应用的维护问题。

比雅尼·斯特劳斯特鲁普(C++编程语言的创始人)等计算机科学家发现,通过将数据结构与函数和过程合并成称为类的离散单元,可以简化这种复杂性。这些类可以描述真实世界的实体并被实例化。结果对象被证明是建模这些实体的有效方法。

您首先学习如何声明一个类,然后学习如何在 new 操作符和构造函数的帮助下从这个类创建对象。最后,您将了解构造函数参数以及如何指定它们来初始化对象,了解局部变量以及如何指定它们来帮助控制构造函数中的代码流。

声明类

一个是制造对象(代码和数据的命名分组)的模板,也称为类实例,或简称实例。类概括了现实世界中的实体,而对象是这些实体在应用级别的具体表现。您可能会认为类是 cookie cutter,对象是 cookie cutter 创建的 cookie。

因为不能实例化不存在的类中的对象,所以必须首先声明该类。声明由一个标题和一个正文组成。至少,头文件由保留字 class 组成,后跟一个标识该类的名称(以便可以从源代码中的其他地方引用它)。正文以左括号字符( { )开始,以右括号( } )结束。夹在这些分隔符之间的是各种声明。考虑清单 3-1 。

清单 3-1 。声明骨架图像类

class Image
{
   // various member declarations
}

  • 1
  • 2
  • 3
  • 4
  • 5

清单 3-1 声明了一个名为 Image 的类,它大概描述了某种在屏幕上显示的图像。按照惯例,类名以大写字母开头。此外,多单词类名中每个后续单词的第一个字母都要大写。这就是所谓的驼绒

用 New 运算符和构造函数实例化对象

Image 是一个用户定义类型的例子,可以从该类型创建对象。通过使用带有构造函数的 new 操作符来创建这些对象,如下所示:

Image image = new Image();

  • 1
  • 2

new 操作符分配内存来存储由 new 的唯一操作数指定类型的对象,在本例中恰好是 Image() 。对象存储在一个叫做的内存区域中。

图像后面的括号(圆括号)表示一个构造器,它是一段代码,通过以某种方式初始化来构造一个对象。 new 操作符在分配内存存储对象后立即调用(调用)构造函数。

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

注意 new 返回的引用在源代码中用关键字 this 表示。无论这个出现在哪里,它都代表当前对象。同样,存储引用的变量被称为引用变量

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

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

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

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

清单 3-2 增强了清单 3-1 的图像类,声明了三个构造函数,它们带有声明零个、一个或两个参数的参数列表和一个用于测试该类的 main() 方法。

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

public class Image
{
   Image()
   {
      System.out.println("Image() called");
   }

   Image(String filename)
   {
      this(filename, null);
      System.out.println("Image(String filename) called");
   }

   Image(String filename, String imageType)
   {
      System.out.println("Image(String filename, String imageType) called");
      if (filename != null)
      {
         System.out.println("reading " + filename);
         if (imageType != null)
            System.out.println("interpreting " + filename + " as storing a " +
                               imageType + " image");
      }
      // Perform other initialization here.
   }

   public static void main(String[] args)
   {
      Image image = new Image();
      System.out.println();
      image = new Image("image.png");
      System.out.println();
      image = new Image("image.png", "PNG");
   }
}

面向 Android 开发的 Java 学习手册(一)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

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

Image 接下来声明一个 Image(String filename) 构造函数,它的参数列表由一个参数声明组成——一个变量的类型后跟变量名。 java.lang.String 参数命名为 filename ,表示该构造函数从文件中获取图像内容。

注意在本章和其余章节中,我通常会在第一次使用预定义类型(如字符串)之前加上存储该类型的包层次结构。例如,字符串存储在 java 包的 lang 子包中。我这样做是为了帮助您了解类型存储在哪里,以便您可以更容易地指定将这些类型导入到源代码中的导入语句(而不必首先搜索类型的包)——您不必导入存储在 java.lang 包中的类型,但是为了完整起见,我仍然将 java.lang 包作为类型名的前缀。在第五章中,我会对包和导入声明有更多的说明。

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

虽然看起来不是这样,但是构造函数没有名字(但是,通常通过指定类名和参数列表来引用构造函数)。一个构造函数通过使用关键字 this 和圆括号分隔的逗号分隔的参数列表来调用另一个构造函数。例如, Image(字符串文件名)执行 this(文件名,空);执行图像(字符串文件名,字符串图像类型)。

注意你必须使用关键字 this 来调用另一个构造函数——你不能使用类名,就像在 Image() 中一样。 this() 构造函数调用(如果存在的话)必须是在构造函数中执行的第一个代码——该规则防止您在同一个构造函数中指定多个 this() 构造函数调用。最后,您不能在方法中指定 this()——构造函数只能由其他构造函数调用,并且只能在对象创建期间调用。(我将在本章后面讨论方法。)

如果存在,构造函数调用必须是构造函数中指定的第一个代码;否则,编译器会报告错误。因此,调用另一个构造函数的构造函数只有在另一个构造函数完成后才能执行额外的工作。例如, Image(字符串文件名)执行 System.out.println(“Image(字符串文件名)调用”);被调用的图像(字符串文件名,字符串图像类型)构造函数完成后。

Image(String filename,String imageType) 构造函数声明了一个 imageType 参数,该参数表示存储在文件中的图像类型——例如,可移植网络图形(PNG ) 图像。大概,构造函数使用 imageType 通过不检查文件内容来学习图像格式来加速处理。当 null 被传递给 imageType 时,正如 Image(String filename) 构造函数所发生的那样, Image(String filename,String imageType) 检查文件内容以学习格式。如果 null 也被传递给 filename , Image(String filename,String imageType) 不会读取该文件,但可能会通知试图创建 Image 对象的代码出现错误情况。

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

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

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

main() 的出现将图像从仅仅一个类变成了一个应用。您通常将 main() 放在用于创建对象的类中,以测试这些类。当构建一个供他人使用的应用时,通常在一个类中声明 main() ,其目的是运行一个应用,而不是从那个类创建一个对象——然后应用只从那个类运行。参见第一章的 DumpArgs 和 EchoText 类的例子。

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

Image() called

Image(String filename, String imageType) called
reading image.png
Image(String filename) called

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

第一行输出表明 noargument 构造函数已经被调用。随后的输出行表明已经调用了第二个和第三个构造函数。

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

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

面向 Android 开发的 Java 学习手册(一)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

与文件名和图像类型参数一样,文件是构造函数的局部变量,称为局部变量以区别于参数。虽然这三个变量都是构造函数的局部变量,但是参数和局部变量之间有两个关键的区别:

  • 文件名和图像类型参数在构造函数开始执行时存在,并一直存在到执行离开构造函数。相比之下,文件在其声明点出现并继续存在,直到声明它的块被终止(通过一个右括号字符)。参数或局部变量的这个属性被称为生存期
  • 可以从构造函数的任何地方访问文件名和图像类型参数。相比之下,文件只能从它的声明点到声明它的块的末尾被访问。不能在声明前或声明块后访问局部变量,但嵌套子块可以访问局部变量。参数或局部变量的这个属性被称为范围

注意生存期和范围(也称为可见性)属性也适用于类、对象和字段(稍后讨论)。当加载到内存中时,类就存在了,当从内存中卸载时,类就不存在了,通常是在应用退出时。此外,加载的类通常对其他类可见。

一个对象的生命周期从通过 new 操作符创建它开始,直到它被垃圾收集器从内存中删除(在本章后面讨论)。它的范围取决于各种因素,例如当它的引用被赋给局部变量或字段时。我将在本章后面讨论字段。

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

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

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

封装状态和行为

类从模板的角度模拟现实世界的实体,例如汽车和储蓄账户。对象表示特定的实体,例如,John 的红色 Toyota Camry(汽车实例)和 Cuifen 的结余为两万美元的储蓄帐户(储蓄帐户实例)。

实体有属性,比如颜色红色,制造丰田,型号凯美瑞,余额两万美元。一个实体的属性集合被称为其状态。实体也有行为,如开门、开车、显示油耗、存款、取款、显示账户余额。

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

在这一节中,我首先向您介绍 Java 表示状态的语言特性,然后向您介绍它表示行为的语言特性。因为一些状态和行为支持类的内部架构,并且不应该对那些想要使用该类的人可见,所以我通过介绍信息隐藏的重要概念来结束这一节。

通过字段表示状态

Java 让你通过字段来表示状态,这些字段是在类体内声明的变量。实体属性通过实例字段描述。因为 Java 也支持与类而不是对象相关联的状态,所以 Java 提供了类字段来描述这个类状态。

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

声明和访问实例字段

您可以通过最低限度地指定类型名,后跟命名字段的标识符,再跟一个分号字符()来声明实例字段。)。清单 3-3 展示了一个汽车类,它有三个实例字段声明。

清单 3-3 。用声明一个汽车类制造、型号和 numDoors 实例字段

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

当创建一个对象时,实例字段被初始化为缺省的零值,您在源代码级别将它解释为文字值 false 、’ u 0000’、 0 、 0L 、 0.0 、 0.0F 或 null (取决于元素类型)。例如,如果您要执行 Car Car = new Car();、 make 和 model 将被初始化为 null 和 numDoors 将被初始化为 0 。

您可以使用成员访问运算符()为对象的实例字段赋值或从中读取值。);左操作数指定对象的引用,右操作数指定要访问的实例字段。清单 3-4 使用这个操作符来初始化一个汽车对象的制造、型号和 numDoors 实例字段。

清单 3-4 。初始化汽车对象的实例字段

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

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

清单 3-4 展示了一个 main() 方法,它实例化了 Car 。 car 实例的 make 实例字段被赋予 “Toyota” 字符串,其 model 实例字段被赋予 “Camry” 字符串,其 numDoors 实例字段被赋予整数文字 4 。(字符串的双引号分隔字符串的字符序列,但不是字符串的一部分。)

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

清单 3-5 。将汽车的 numDoors 实例字段初始化为默认非零值

public class Car
{
   String make;
   String model;
   int numDoors = 4;

   Car()
   {
   }

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

面向 Android 开发的 Java 学习手册(一)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

清单 3-5 显式初始化 numDoors 到 4 ,因为开发者已经假设这个类建模的大多数汽车有四个门。当 Car 通过 Car() 构造函数初始化时,开发者只需要初始化那些有四个门的汽车的 make 和 model 实例字段。

直接初始化一个对象的实例字段通常不是一个好主意,当我讨论信息隐藏(在本章的后面)的时候你会知道为什么。相反,你应该在类的构造函数中执行这个初始化——参见清单 3-6 。

清单 3-6 。通过构造函数初始化汽车的实例字段

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

   Car(String make, String model)
   {
      this(make, model, 4);
   }

   Car(String make, String model, int nDoors)
   {
      this.make = make;
      this.model = model;
      numDoors = nDoors;
   }

   public static void main(String[] args)
   {
      Car myCar = new Car("Toyota", "Camry");
      Car yourCar = new Car("Mazda", "RX-8", 2);
   }
}

面向 Android 开发的 Java 学习手册(一)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

清单 3-6 的 Car 类声明 Car(String make,String model) 和 Car(String make,String model,int nDoors) 构造函数。第一个构造函数让您指定品牌和型号,而第二个构造函数让您指定三个实例字段的值。

第一个构造函数执行 this(make,model,4);将它的 make 和 model 参数的值连同默认值 4 一起传递给第二个构造函数。这样做展示了一种显式初始化实例字段的替代方法,从代码维护的角度来看,这种方法更可取。

Car(String make,String model,int numDoors) 构造函数演示了关键字 this 的另一种用法。具体来说,它演示了构造函数参数与类的实例字段同名的场景。在变量名前加上这个前缀。”使 Java 编译器创建访问实例字段的字节码。比如 this . make = make;将 make 参数的字符串对象引用分配给这个(当前) Car 对象的 make 实例字段。ifmake = make;相反,如果指定了,那么通过将 make 的值赋给自身,它将一事无成;Java 编译器可能不会生成代码来执行不必要的赋值。与此相反,“这个。”对于 numDoors = nDoors 来说是不必要的;赋值,从 nDoors 参数值初始化 numDoors 域。

注意以最小化错误(通过忘记在字段名前加上“ this。”),最好保持字段名和参数名的不同(例如 numDoors 和 nDoors )。或者,您可以在字段名前面加上下划线(例如,_ n 或者)。无论哪种方式,你都不用担心这个。”前缀(又忘了指定)。

声明和访问类字段

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

例如,假设您想要跟踪已经创建的 Car 对象的数量,并在该类中引入一个计数器实例字段(初始化为 0)。您还可以在该类的构造函数中放置代码,当创建一个对象时,该代码会将计数器的值增加 1。然而,因为每个对象都有自己的计数器实例字段的副本,所以这个字段的值永远不会超过 1。清单 3-7 通过用静态关键字作为字段声明的前缀,将计数器声明为类字段,解决了这个问题。

清单 3-7 。给汽车增加一个计数器类字段

public class Car
{
   String make;
   String model;
   int numDoors;
   static int counter;

   Car(String make, String model)
   {
      this(make, model, 4);
   }

   Car(String make, String model, int numDoors)
   {
      this.make = make;
      this.model = model;
      this.numDoors = numDoors;
      counter++; // This code is unsafe because counter can be accessed directly.
   }

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

面向 Android 开发的 Java 学习手册(一)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

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

与实例字段不同,类字段通常通过成员访问操作符直接访问。虽然可以通过对象引用访问类字段(如在 myCar.counter 中),但通常使用类名访问类字段,如在 Car.counter 中。(也更容易看出代码正在访问一个类字段。)

注意因为 main() 方法是清单 3-7 的 Car 类的成员,你可以直接访问 counter ,就像在 System.out.println(counter)中一样;。然而,要在另一个类的 main() 方法的上下文中访问 counter ,您必须指定 Car.counter 。

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

声明只读实例和类字段

先前声明的字段既可以写入也可以读取。但是,您可能希望声明一个只读字段,例如,一个以 pi (3.14159…)等常数值命名的字段。Java 通过提供保留字 final 让你完成这个任务。

每个对象都接收自己的只读实例字段副本。此字段必须作为字段声明的一部分或在类的构造函数中初始化。当在构造函数中初始化时,只读实例字段被称为空白 final ,因为它没有值,直到在构造函数中给它赋值。因为构造函数可能会给每个对象的 blank final 赋予不同的值,所以这些只读变量并不是真正的常量。

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

清单 3-8 显示了如何声明一个只读的类字段。

清单 3-8 。在雇员类中声明一个真常数

class Employee
{
   final static int RETIREMENT_AGE = 65;
}

  • 1
  • 2
  • 3
  • 4
  • 5

清单 3-8 的 RETIREMENT_AGE 声明是编译时间常数的一个例子。因为它的值只有一个副本(由于静态关键字),并且因为这个值永远不会改变(由于 final 关键字),编译器可以通过将常数值插入到所有使用它的计算中来自由地优化编译后的代码。代码运行得更快,因为它不必访问只读的类字段。

查看字段访问规则

前面的字段访问示例可能看起来有些混乱,因为有时您可以直接指定字段的名称,而在其他时候您需要在字段名称前面加上对象引用或类名以及成员访问操作符。以下规则通过指导您如何从各种上下文中访问字段来消除这种混淆:

  • 从与类字段声明相同的类中的任意位置指定类字段的名称。示例:计数器
  • 指定类字段的类的名称,后跟成员访问运算符,再后跟该类外部的类字段的名称。示例: Car.counter
  • 将实例字段的名称指定为与实例字段声明相同的类中的任何实例方法、构造函数或实例初始值设定项(稍后讨论)。示例: numDoors
  • 指定一个对象引用,后面是成员访问操作符,再后面是实例字段的名称,该实例字段来自与实例字段声明相同的类中的任何类方法或类初始化器(稍后讨论),或者来自该类之外。例: Car car =新车();car . numdoors = 2;

尽管最终的规则似乎暗示您可以从类上下文中访问实例字段,但事实并非如此。相反,您是从对象上下文中访问该字段。

前面的访问规则并不详尽,因为还有两种字段访问场景需要考虑:声明一个与实例字段或类字段同名的局部变量(甚至是参数)。在任一场景中,局部变量/参数被称为隐藏(隐藏或屏蔽)字段。

如果您发现您声明了一个隐藏字段的局部变量或参数,您可以重命名该局部变量/参数,或者您可以使用带有保留字 this (实例字段)或类名(类字段)的成员访问运算符来显式标识该字段。例如,清单 3-6 的 Car(String make,String model,int nDoors) 构造函数通过指定诸如 this . make = make;区分实例字段和同名参数。

通过方法表现行为

Java 让你通过方法来表现行为,这些方法是在类的主体中声明的代码块。实体行为通过实例方法来描述。因为 Java 也支持与类而不是对象相关联的行为,所以 Java 提供了类方法来描述这些类行为。

首先学习如何声明和调用实例方法,然后学习如何创建实例方法调用链。接下来,您将了解如何声明和调用类方法,了解关于向方法传递参数的更多细节,并探索 Java 的 return 语句。在学习了如何递归调用方法作为迭代的替代方法,以及如何重载方法之后,您将回顾从不同上下文调用方法的规则。

声明和调用实例方法

您可以通过以下方式声明实例方法:最低限度地指定一个返回类型名称,后跟一个命名该方法的标识符,再跟一个参数列表,最后跟一个大括号分隔的主体。清单 3-9 展示了一个带有 printDetails() 实例方法的 Car 类。

清单 3-9 。在 Car 类中声明一个 printDetails() 实例方法

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

   Car(String make, String model)
   {
      this(make, model, 4);
   }

   Car(String make, String model, int numDoors)
   {
      this.make = make;
      this.model = model;
      this.numDoors = numDoors;
   }

   void printDetails()
   {
      System.out.println("Make = " + make);
      System.out.println("Model = " + model);
      System.out.println("Number of doors = " + numDoors);
      System.out.println();
   }

   public static void main(String[] args)
   {
      Car myCar = new Car("Toyota", "Camry");
      myCar.printDetails();
      Car yourCar = new Car("Mazda", "RX-8", 2);
      yourCar.printDetails();
   }
}

面向 Android 开发的 Java 学习手册(一)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

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

方法类似于构造函数,因为它们有参数列表。当您呼叫方法时,会将引数传递给这些参数。因为 printDetails() 不接受参数,所以它的参数列表是空的。

注意一个方法的名字和它的参数的数量、类型和顺序被称为它的签名

当一个方法被调用时,其主体中的代码被执行。在 printDetails() 的情况下,该方法的主体执行一系列 System.out.println() 方法调用,以输出其 make 、 model 和 numDoors 实例字段的值。

与构造函数不同,方法被声明为具有返回类型。返回类型标识该方法返回的值的种类(例如, int count() 返回 32 位整数)。当一个方法不返回值时(并且 printDetails() 也不返回值),它的返回类型被替换为关键字 void ,如 void printDetails() 所示。

注意构造函数没有返回类型,因为它们不能返回值。如果一个构造函数可以返回一个任意值,那么 Java 将如何返回这个值呢?毕竟, new 操作符返回一个对象的引用; new 怎么也能返回一个构造函数的值呢?

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

编译清单 3-9(【Car.java】)并运行这个应用( java Car )。您应该观察到以下输出,其不同的实例字段值证明了 printDetails() 与一个对象相关联:

Make = Toyota
Model = Camry
Number of doors = 4

Make = Mazda
Model = RX-8
Number of doors = 2

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

当实例方法被调用时,Java 将一个隐藏的参数传递给该方法(作为参数列表中最左边的参数)。该参数是对调用该方法的对象的引用。它在源代码级别通过保留字 this 表示。不需要在实例字段名前面加上“ this。”因为 Java 编译器会确保使用隐藏参数来访问实例字段,所以每当您试图访问实例字段名称而该名称又不是参数名称时,都会从方法内部调用。

方法调用堆栈

方法调用需要一个方法调用栈(也称为方法调用栈)来跟踪执行必须返回的语句。把方法调用栈想象成自助餐厅中一堆干净托盘的模拟——你从这堆托盘的顶部弹出(移除)干净托盘,洗碗机将把下一个干净托盘推到这堆托盘的顶部。

当一个方法被调用时,虚拟机将它的参数和第一条语句的地址推送到方法调用堆栈上。虚拟机还为方法的局部变量分配堆栈空间。当方法返回时,虚拟机移除局部变量空间,从堆栈中弹出地址和参数,并将执行转移到该地址处的语句。

将实例方法调用链接在一起

两个或多个实例方法调用可以通过成员访问操作符链接在一起,从而产生更紧凑的代码。为了完成实例方法调用链接,你需要稍微不同地重新构建你的实例方法,这在清单 3-10 中有所揭示。

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

public class SavingsAccount
{
   int balance;

   SavingsAccount deposit(int amount)
   {
      balance += amount;
      return this;
   }

   SavingsAccount printBalance()
   {
      System.out.println(balance);
      return this;
   }

   public static void main(String[] args)
   {
      new SavingsAccount().deposit(1000).printBalance();
   }
}

面向 Android 开发的 Java 学习手册(一)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

清单 3-10 显示你必须指定类名作为实例方法的返回类型。 deposit() 和 printBalance() 都必须指定 SavingsAccount 作为返回类型。另外,必须指定 return this;(返回当前对象的引用)作为最后一条语句——我稍后讨论 return 语句。

例如,新建 SavingsAccount()。押金(1000)。print balance();创建一个 SavingsAccount 对象,使用返回的 SavingsAccount 引用调用 SavingsAccount 的 deposit() 实例方法,向储蓄帐户添加一千美元(为了方便起见,我忽略了美分),最后使用 deposit() 返回的 SavingsAccount 引用(同 SavingsAccount 实例)调用 saving

声明和调用类方法

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

例如,假设您想引入一个工具类(一个由静态 [class]方法组成的类),它的类方法执行各种类型的转换(比如从摄氏度转换到华氏度)。您不希望从该类创建一个对象来执行转换。相反,您只是想调用一个方法并获得它的结果。清单 3-11 通过提供一个带有一对类方法的转换类来解决这个需求。这些方法可以被调用,而不必创建一个转换对象。

清单 3-11 。一个带有一对类方法的转换工具类

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

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

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

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

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

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

向方法传递参数

方法调用包括传递给该方法的一系列(零个或多个)参数。Java 通过一种称为按值传递的参数传递方式将参数传递给方法,下面的示例将演示这种方式:

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

  • 1
  • 2
  • 3
  • 4
  • 5

按值传递将变量的值(例如,存储在 emp 中的引用值或存储在 recommended annual salary increase 中的 1000 值)或一些其他表达式的值(例如, new Employee(“Cuifen”) 或 1500 )传递给该方法。

由于传递值,您不能通过此参数的 printReport() 参数从 printReport() 内部将不同的 Employee 对象的引用分配给 emp 。毕竟,您只是将 emp 值的一个副本传递给了该方法。

许多方法和构造函数要求您在调用它们时传递固定数量的参数。然而,Java 也提供了传递可变数量的参数的能力——这样的方法/构造函数通常被称为 varargs 方法 / 构造函数 。若要声明采用可变数量的参数的方法或构造函数,请在该方法/构造函数的最右侧参数的类型名称后指定三个连续的句点。以下示例展示了一个接受可变数量参数的 sum() 方法:

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

sum() 的实现合计传递给该方法的参数个数,例如 sum(10.0,20.0) 或 sum(30.0,40.0,50.0) 。(在后台,这些参数存储在一个一维数组中,如 values.length 和 values[i] 所示。)在对这些值求和之后,通过 return 语句返回这个总数。

通过 Return 语句从一个方法返回

不返回值(其返回类型设置为 void )的方法中语句的执行从第一条语句流向最后一条语句。然而,Java 的 return 语句让方法或构造函数在到达最后一个语句之前退出。如清单 3-12 所示,这种形式的 return 语句由保留字 return 后跟一个分号组成。

清单 3-12 。使用 Return 语句从方法中提前返回

public class Employee
{
   String name;

   Employee(String name)
   {
      setName(name);
   }

   void setName(String name)
   {
      if (name == null)
      {
         System.out.println("name cannot be null");
         return;
      }
      else
         this.name = name;
   }

   public static void main(String[] args)
   {
      Employee john = new Employee(null);
   }
}

面向 Android 开发的 Java 学习手册(一)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

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

注意当从同一个类中的构造函数或方法调用一个类的实例或类方法时,只需指定方法的名称。不要用成员访问操作符和对象引用或类名作为方法调用的前缀。

setName() 使用 if 语句来检测将空引用分配给名称字段的企图。当检测到这种尝试时,它输出“名称不能为空”错误消息,并提前从该方法返回,以便不能分配空值(并替换先前分配的名称)。

注意使用 return 语句时,您可能会遇到编译器报告“不可达代码”错误信息的情况。当它检测到永远不会执行的代码并不必要地占用内存时,它就会这样做。您可能会在 switch 语句中遇到这个问题。比如,假设你指定 case 2:printUsageInstructions();返回;打破;作为本声明的一部分。编译器在检测到 return 语句后面的 break 语句时会报告一个错误,因为 break 语句是不可访问的;它永远不会被执行。

return 语句的前一种形式在返回值的方法中是非法的。对于这样的方法,Java 提供了 return 的替代版本,允许方法返回值(其类型必须与方法的返回类型相匹配)。以下示例演示了此版本:

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

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

注意不能在构造函数中使用这种形式的 return 语句,因为构造函数没有返回类型。

递归调用方法

一个方法通常执行可能包含对其他方法的调用的语句,比如 printDetails() 调用 System.out.println() 。然而,偶尔有一个方法调用本身是很方便的。这个场景被称为递归

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

编写此方法的第一种方法可能由以下示例中的代码组成:

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

虽然这段代码完成了它的任务(通过迭代), factorial() 也可以按照下面例子的递归方式编写:

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

递归方法利用了能够用更简单的术语来表达问题的优势。根据这个例子,最简单的问题,也就是大家熟知的基数问题,是 1!(1).

当一个大于 1 的参数被传递给 factorial() 时,该方法通过用下一个更小的参数值调用自己,将问题分解成一个更简单的问题。最终会达到基数问题。

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

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

  • 1
  • 2
  • 3
  • 4

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

  • 2 阶乘(1) 现在变成 21 (2)
  • 3 阶乘(2) 现在变成了 32 (6)
  • 4 阶乘(3) 现在变成了 46 (24)

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

注意递归消耗堆栈空间,所以要确保你的递归最终以一个基本问题结束;否则,您将耗尽堆栈空间,您的应用将被迫终止。

重载方法

Java 允许您将名称相同但参数列表不同的方法引入到同一个类中。这个特性被称为方法重载。当编译器遇到方法调用表达式时,它会将被调用方法的参数列表与每个重载方法的参数列表进行比较,以寻找要调用的正确方法。

当两个同名方法的参数列表在参数的数量或顺序上不同时,它们会被重载。比如 Java 的 String 类提供了重载的 int indexOf(int ch) 和 int indexOf(int ch,int fromIndex) 方法。这些方法在参数计数上有所不同。(我在第七章的中探索字符串。)

当至少有一个参数的类型不同时,两个同名的方法被重载。比如 Java 的 java.lang.Math 类提供了重载的静态双 abs(双 a) 和静态 int abs(int a) 方法。一个方法的参数是一个 double;另一个方法的参数是一个 int (我在第七章的中探索数学。)

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

查看方法调用规则

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

  • 从与类方法相同的类中的任意位置指定类方法的名称。例:c2f(37.0);
  • 指定类方法的类的名称,后跟成员访问运算符,再后跟该类外部的类方法的名称。示例:conversions . c2f(37.0);(你也可以通过一个对象实例调用一个类方法,但是这被认为是不好的形式,因为它隐藏了一个类方法被调用的事实。)
  • 指定实例方法的名称,该名称来自与实例方法在同一类中的任何实例方法、构造函数或实例初始值设定项。例: setName(名称);
  • 指定一个对象引用,后面是成员访问操作符,再后面是实例方法的名称,该实例方法来自与实例方法相同的类内或来自类外的任何类方法或类初始值设定项。例: Car 汽车=新车(“丰田”、“凯美瑞”);car . print details();

尽管后一条规则似乎意味着您可以从类上下文中调用实例方法,但事实并非如此。相反,您可以从对象上下文中调用该方法。

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

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

隐藏信息

每个类 X 都公开了一个接口(一个由构造函数、方法和【可能】字段组成的协议,它们可供从其他类创建的对象使用,用于创建和与 X 的对象通信)。

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

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

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

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

  • Public :声明为 public 的字段、方法或构造函数可以从任何地方访问。类也可以被声明为公共的。
  • Protected :声明为 protected 的字段、方法或构造函数可以从与成员类相同的包中的所有类以及该类的子类中访问,而不考虑包。(我在第五章中讨论了包。)
  • 私有:声明为私有的字段、方法或构造函数不能从声明它的类之外访问。
  • Package-private :在没有访问控制保留字的情况下,一个字段、方法或构造函数只能被与成员类相同的包中的类访问。非公立班也是如此。公共、保护或私有的缺失意味着包私有。

注意声明为 public 的类必须存储在一个同名的文件中。例如,一个公共图像类必须存储在 Image.java 中。一个源文件只能声明一个公共顶级类。(也可以声明 public 的嵌套类,你将在第五章中学习如何这么做。)

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

清单 3-13 。接口与实现的分离

public class Employee
{
   private String name;

   public Employee(String name)
   {
      setName(name);
   }

   public void setName(String empName)
   {
      name = empName; // Assign the empName argument to the name field.
   }

   public String getName()
   {
      return name;
   }
}

面向 Android 开发的 Java 学习手册(一)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

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

当您可以简单地省略 private 并直接访问 name 字段时,这么做似乎毫无意义。但是,假设您被告知要引入一个新的构造函数,它接受单独的姓和名参数,并引入新的方法,将雇员的姓和名设置/获取到这个类中。此外,假设已经确定名字和姓氏将比整个名字被更频繁地访问。清单 3-14 揭示了这些变化。

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

public class Employee
{
   private String firstName;
   private String lastName;

   public Employee(String name)
   {
      setName(name);
   }

   public Employee(String firstName, String lastName)
   {
      setName(firstName + " " + lastName);
   }

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

   public String getName()
   {
      return getFirstName() + " " + getLastName();
   }

   public void setFirstName(String empFirstName)
   {
      firstName = empFirstName;
   }

   public String getFirstName()
   {
      return firstName;
   }

   public void setLastName(String empLastName)
   {
      lastName = empLastName;
   }

   public String getLastName()
   {
      return lastName;
   }
}

面向 Android 开发的 Java 学习手册(一)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

清单 3-14 显示出姓名字段已经被删除,取而代之的是新的名字和姓氏字段,这两个字段是为了提高性能而添加的。因为 setFirstName() 和 setLastName() 将比 setName() 被更频繁地调用,并且因为 getFirstName() 和 getLastName() 将比 getName() 被更频繁地调用,所以(在每种情况下)让前两个方法 set/getfirst names 和 lastName 更高效

清单 3-14 还揭示了 setName() 调用 setFirstName() 和 setLastName() ,以及 getName() 调用 getFirstName() 和 getLastName() ,而不是直接访问 firstName 和 lastName 字段。虽然在这个例子中避免直接访问这些字段是不必要的,但是设想另一个实现变化,向 setFirstName() 、 setLastName() 、 getFirstName() 和 getLastName() 添加更多代码;不调用这些方法将导致新代码无法执行。

当雇员的实现从清单 3-13 变为清单 3-14 所示时,客户端代码(实例化并使用一个类的代码,如雇员)不会中断,因为原始接口保持不变,尽管接口已经被扩展。这种缺少破损是由于隐藏了清单 3-13 的实现,尤其是名称字段。

注意 setName() 调用 String 类的 indexOf() 和 substring() 方法。你将在第七章的中了解这些和其他字符串方法。

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

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

public class PrivateAccess
{
   private int x;

   PrivateAccess(int x)
   {
      this.x = x;
   }

   boolean equalTo(PrivateAccess pa)
   {
      return pa.x == x;
   }

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

面向 Android 开发的 Java 学习手册(一)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

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

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

我之前介绍了 Java 的四个访问控制级别,并介绍了以下关于私有访问控制级别的声明:“声明为 private 的字段、方法或构造函数不能从声明它的类之外访问。”当你仔细考虑这个声明并检查清单 3-15 时,你会意识到 x 没有被声明它的 PrivateAccess 类之外的类访问。因此,没有违反私有访问控制级别。

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

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

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

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

  • 1
  • 2
  • 3
  • 4
  • 5

技巧养成开发有用的接口同时隐藏实现的习惯,因为这将为你在维护你的类时省去很多麻烦。

初始化类和对象

类和对象在使用前需要正确初始化。你已经知道了类字段在类加载后被初始化为缺省的零值,随后可以通过类字段初始化器在声明中给它们赋值来初始化,例如,static int counter = 1;。类似地,当一个对象的内存通过 new 分配时,实例字段被初始化为默认值,随后可以通过实例字段初始化器在它们的声明中给它们赋值来初始化;例如,int num doors = 4;。

已经讨论过的初始化的另一个方面是构造函数,它用于初始化一个对象,通常是通过给各种实例字段赋值,但也能够执行任意代码,例如打开文件和读取文件内容的代码。

Java 提供了两个额外的初始化特性:类初始化器和实例初始化器。在本节向您介绍了这些特性之后,我将讨论所有 Java 初始化器执行工作的顺序。

类初始化器

构造函数执行对象的初始化任务。从类初始化的角度来看,它们的对应物是类初始化器。

一个类初始化器是一个静态前缀的块,它被引入到一个类体中。它用于通过一系列语句初始化一个加载的类。例如,我曾经使用一个类初始化器来加载一个定制的数据库驱动程序类。清单 3-16 显示了加载细节。

清单 3-16 。通过类初始化器加载数据库驱动程序

class JDBCFilterDriver implements Driver
{
   static private Driver d;

   static
   {
      // Attempt to load JDBC-ODBC Bridge Driver and register that
      // driver.
      try
      {
         Class c = Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
         d = (Driver) c.newInstance();
         DriverManager.registerDriver(new JDBCFilterDriver());
      }
      catch (Exception e)
      {
         System.out.println(e);
      }
   }
   //. . .
}

面向 Android 开发的 Java 学习手册(一)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

清单 3-16 的 JDBCFilterDriver 类使用其类初始化器来加载和实例化描述 Java 的 JDBC-ODBC 桥驱动程序的类,并向 Java 的数据库驱动程序注册一个 JDBCFilterDriver 实例。虽然这个清单中面向 JDBC 的代码现在对您来说可能毫无意义,但是这个清单展示了类初始化器的用处。(我在第十四章中讨论 JDBC。)

一个类可以声明类初始化器和类字段初始化器的混合,如清单 3-17 所示。

清单 3-17 。混合类初始值设定项和类字段初始值设定项

class C
{
   static
   {
      System.out.println("class initializer 1");
   }

   static int counter = 1;

   static
   {
      System.out.println("class initializer 2");
      System.out.println("counter = " + counter);
   }
}

面向 Android 开发的 Java 学习手册(一)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

清单 3-17 声明了一个名为 C 的类,它指定了两个类初始化器和一个类字段初始化器。当 Java 编译器将声明了至少一个类初始值设定项或类字段初始值设定项的类编译到类文件中时,它会创建一个特殊的 void < clinit > () 类方法,该方法按照所有类初始值设定项和类字段初始值设定项出现的顺序(从上到下)存储它们的字节码等价物。

注意 < clinit > 不是一个有效的 Java 方法名,但是从运行时的角度来看是一个有效的名称。尖括号被选作名称的一部分,以防止与您可能在该类中声明的任何 clinit() 方法发生名称冲突。

对于类 C ,clinit>()将首先包含等同于 System.out.println(“类初始化器 1 “)的字节码;,接下来它将包含相当于 static int counter = 1 的字节码;,它将最终包含与 system . out . println(” class initializer 2 “)等价的字节码;system . out . println(” counter = “+counter);。

当类 C 被加载到内存中时, < clinit > () 立即执行并生成以下输出:

class initializer 1
class initializer 2
counter = 1

  • 1
  • 2
  • 3
  • 4

实例初始化器

不是所有的类都可以有构造函数,当我介绍匿名类时,你会在第五章中发现。对于这些类,Java 提供了实例初始化器来处理实例初始化任务。

一个实例初始化器是一个块,它被引入到一个类主体中,而不是作为一个方法或构造函数的主体被引入。实例初始化器用于通过一系列语句初始化一个对象,如清单 3-18 所示。

清单 3-18 。通过实例初始化器初始化一对数组

class Graphics
{
   double[] sines;
   double[] cosines;

   {
      sines = new double[360];
      cosines = new double[sines.length];
      for (int degree = 0; degree < sines.length; degree++)
      {
         sines[degree] = Math.sin(Math.toRadians(degree));
         cosines[degree] = Math.cos(Math.toRadians(degree));
      }
   }
}

面向 Android 开发的 Java 学习手册(一)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

清单 3-18 的图形类使用一个实例初始化器来创建一个对象的正弦和余弦数组,并将这些数组的元素初始化为范围从 0 到 359 度的正弦和余弦。这样做是因为读取数组元素比在其他地方重复调用 Math.sin() 和 Math.cos() 要快;性能很重要。(在第七章我介绍了 Math.sin() 和 Math.cos() 。)

一个类可以声明实例初始化器和实例字段初始化器的混合,如清单 3-19 所示。

清单 3-19 。混合实例初始值设定项和实例字段初始值设定项

class C
{
   {
      System.out.println("instance initializer 1");
   }

   int counter = 1;

   {
      System.out.println("instance initializer 2");
      System.out.println("counter = " + counter);
   }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

清单 3-19 声明了一个名为 C 的类,它指定了两个实例初始化器和一个实例字段初始化器。当 Java 编译器将一个类编译成 classfile 时,会创建一个特殊的 void < init > () 方法,在没有显式声明构造函数时,代表默认的无参数构造函数;否则,它为每个遇到的构造函数创建一个 < init > () 方法。此外,它在每个 < init > () 方法中存储所有实例初始化器和实例字段初始化器的字节码等价物,按照它们出现的顺序(从上到下)存储在构造函数代码之前。

注意 < init > 不是一个有效的 Java 方法名,但是从运行时的角度来看是一个有效的名称。选择尖括号作为名称的一部分是为了防止与您可能在该类中声明的任何 init() 方法冲突。

对于类 C , < init > () 将首先包含等同于 System.out.println(“实例初始化器 1 “)的字节码;,接下来它将包含相当于 int counter = 1 的字节码;,它将最终包含与 System.out.println(“实例初始化器 2 “)等价的字节码;system . out . println(” counter = “+counter);。

当 new C() 执行时, < init > () 立即执行并生成以下输出:

instance initializer 1
instance initializer 2
counter = 1

  • 1
  • 2
  • 3
  • 4

注意你应该很少需要使用实例初始化器,这在业界并不常用。其他开发人员在浏览源代码时可能会错过实例初始化器,并且可能会感到困惑。

初始化顺序

类的主体可以包含类字段初始值设定项、类初始值设定项、实例字段初始值设定项、实例初始值设定项和构造函数的混合。(你应该更喜欢构造函数而不是实例字段初始值设定项,尽管我很抱歉没有始终如一地这样做,并且将实例初始值设定项的使用限制在匿名类中,这在第五章中讨论过。)此外,类字段和实例字段初始化为默认值。理解所有这些初始化发生的顺序对于防止混淆是必要的,所以查看清单 3-20 。

清单 3-20 。一个完整的初始化演示

public class InitDemo
{
   static double double1;
   double double2;
   static int int1;
   int int2;
   static String string1;
   String string2;

   static
   {
      System.out.println("[class] double1 = " + double1);
      System.out.println("[class] int1 = " + int1);
      System.out.println("[class] string1 = " + string1);
      System.out.println();
   }

   {
      System.out.println("[instance] double2 = " + double2);
      System.out.println("[instance] int2 = " + int2);
      System.out.println("[instance] string2 = " + string2);
      System.out.println();
   }

   static
   {
      double1 = 1.0;
      int1 = 1000000000;
      string1 = "abc";
   }

   {
      double2 = 1.0;
      int2 = 1000000000;
      string2 = "abc";
   }

   InitDemo()
   {
      System.out.println("InitDemo() called");
      System.out.println();
   }

   static double double3 = 10.0;
   double double4 = 10.0;

   static
   {
      System.out.println("[class] double3 = " + double3);
      System.out.println();
   }

   {
      System.out.println("[instance] double4 = " + double3);
      System.out.println();
   }

   public static void main(String[] args)
   {
      System.out.println ("main() started");
      System.out.println();
      System.out.println("[class] double1 = " + double1);
      System.out.println("[class] double3 = " + double3);
      System.out.println("[class] int1 = " + int1);
      System.out.println("[class] string1 = " + string1);
      System.out.println();
      for (int i = 0; i < 2; i++)
      {
         System.out.println("About to create InitDemo object");
         System.out.println();
         InitDemo id = new InitDemo();
         System.out.println("id created");
         System.out.println();
         System.out.println("[instance] id.double2 = " + id.double2);
         System.out.println("[instance] id.double4 = " + id.double4);
         System.out.println("[instance] id.int2 = " + id.int2);
         System.out.println("[instance] id.string2 = " + id.string2);
         System.out.println();
      }
   }
}

面向 Android 开发的 Java 学习手册(一)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82

清单 3-20 的 InitDemo 类声明了双精度浮点原始类型的两个类字段和两个实例字段,整数原始类型的一个类字段和一个实例字段,以及字符串引用类型的一个类字段和一个实例字段。它还引入了一个显式初始化的类字段、一个显式初始化的实例字段、三个类初始值设定项、三个实例初始值设定项和一个构造函数。如果您编译并运行此代码,您将会看到以下输出:

[class] double1 = 0.0
[class] int1 = 0
[class] string1 = null

[class] double3 = 10.0

main() started

[class] double1 = 1.0
[class] double3 = 10.0
[class] int1 = 1000000000
[class] string1 = abc

About to create InitDemo object

[instance] double2 = 0.0
[instance] int2 = 0
[instance] string2 = null

[instance] double4 = 10.0

InitDemo() called

id created

[instance] id.double2 = 1.0
[instance] id.double4 = 10.0
[instance] id.int2 = 1000000000
[instance] id.string2 = abc

About to create InitDemo object

[instance] double2 = 0.0
[instance] int2 = 0
[instance] string2 = null

[instance] double4 = 10.0

InitDemo() called

id created

[instance] id.double2 = 1.0
[instance] id.double4 = 10.0
[instance] id.int2 = 1000000000
[instance] id.string2 = abc

面向 Android 开发的 Java 学习手册(一)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

当您结合前面对类初始化器和实例初始化器的讨论来研究这个输出时,您会发现一些关于初始化的有趣事实:

  • 类字段在类加载后立即初始化为默认值或显式值。在一个类加载之后,所有的类字段都被归零为默认值。 < clinit > () 方法内的代码执行显式初始化。
  • 所有的类初始化都发生在 < clinit > () 方法返回之前。
  • 实例字段在对象创建期间初始化为默认值或显式值。当 new 为一个对象分配内存时,它将所有实例字段归零为默认值。一个 < init > () 方法内的代码执行显式初始化。
  • 所有实例初始化都发生在 < init > () 方法返回之前。

此外,因为初始化以自顶向下的方式发生,所以试图在声明类字段之前访问该字段的内容或者试图在声明实例字段之前访问该字段的内容会导致编译器报告非法前向引用

收集垃圾

对象是通过保留字 new 创建的,但是它们是如何被销毁的呢?如果没有销毁对象的方法,它们最终会填满堆的可用空间,应用将无法继续运行。Java 没有为开发人员提供从内存中删除它们的能力。相反,Java 通过提供一个垃圾收集器来处理这个任务,垃圾收集器是在后台运行的代码,偶尔会检查未被引用的对象。当垃圾收集器发现一个未被引用的对象(或者多个相互引用的对象,并且彼此之间没有其他引用——例如,只有 A 引用 B 并且只有 B 引用 A )时,它会将该对象从堆中移除,从而腾出更多的堆空间。

未引用对象是不能从应用内的任何地方访问的对象。例如,新员工(“约翰”、“多伊”);是一个未被引用的对象,因为 new 返回的员工引用被丢弃。相反,引用对象是应用存储至少一个引用的对象。例如, Employee emp =新员工(” John “,” Doe “);是一个被引用的对象,因为变量 emp 包含对 Employee 对象的引用。

当应用移除其最后存储的引用时,被引用的对象变得不被引用。例如,如果 emp 是一个局部变量,它包含了对一个雇员对象的唯一引用,那么当声明 emp 的方法返回时,这个对象就变得不被引用了。一个应用也可以通过将 null 赋给它的引用变量来删除一个存储的引用。例如,EMP = null;删除对先前存储在 emp 中的 Employee 对象的引用。

Java 的垃圾收集器消除了不依赖垃圾收集器的 C++实现中的一种内存泄漏。在这些 C++实现中,开发人员必须在动态创建的对象超出范围之前销毁它们。如果它们在毁灭前消失,它们将留在堆中。最终,堆填满,应用停止。

尽管这种形式的内存泄漏在 Java 中不是问题,但一种相关形式的泄漏却是有问题的:不断地创建对象而忘记删除对每个对象的一个引用会导致堆被填满,应用最终会停止运行。这种形式的内存泄漏通常发生在集合(存储对象的基于对象的数据结构)的上下文中,并且对于长时间运行的应用来说是一个主要问题——web 服务器就是一个例子。对于寿命较短的应用,您通常不会注意到这种形式的内存泄漏。

考虑清单 3-21 中的。

清单 3-21 。一个内存泄漏堆栈

public class Stack
{
   private Object[] elements;
   private int top;

   public Stack(int size)
   {
      elements = new Object[size];
      top = −1; // indicate that stack is empty
   }

   public void push(Object o)
   {
      if (top + 1 == elements.length)
      {
         System.out.println("stack is full");
         return;
     }
     elements[++top] = o;
   }

   public Object pop()
   {
      if (top == −1)
      {
         System.out.println("stack is empty");
         return null;
      }
      Object element = elements[top--];
//      elements[top + 1] = null;
      return element;
   }

   public static void main(String[] args)
   {
      Stack stack = new Stack(2);
      stack.push("A");
      stack.push("B");
      stack.push("C");
      System.out.println(stack.pop());
      System.out.println(stack.pop());
      System.out.println(stack.pop());
   }
}

面向 Android 开发的 Java 学习手册(一)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

清单 3-21 描述了一个被称为的集合,这是一个按照后进先出的顺序存储元素的数据结构。堆栈对于记忆东西很有用,比如当一个方法停止执行并且必须返回到它的调用者时返回的指令。

Stack 提供了一个 push() 方法,用于将任意对象推送到堆栈的顶部,还提供了一个 pop() 方法,用于按照对象被推的相反顺序将对象从堆栈顶部弹出。

在创建了一个最多可以存储两个对象的堆栈对象后, main() 调用 push() 三次,将三个字符串对象推送到堆栈上。因为堆栈的内部数组只能存储两个对象,所以当 main() 试图推“C”时, push() 会输出一个错误消息。

此时, main() 试图从堆栈中弹出三个对象,将每个对象输出到标准输出设备。前两个 pop() 方法调用成功,但最后一个方法调用失败并输出错误消息,因为调用时堆栈为空。

当您运行此应用时,它会生成以下输出:

stack is full
B
A
stack is empty
null

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

栈类有一个问题:它泄漏内存。当你将一个对象压入堆栈时,它的引用存储在内部的元素数组中。当您从堆栈中弹出一个对象时,将获得该对象的引用,并且减少 top ,但是该引用将保留在数组中(直到您调用 push() )。

想象一个场景,其中栈对象的引用被分配给一个类字段,这意味着栈对象在应用的生命周期内一直存在。此外,假设您已经将三个 50 兆字节的图像对象压入堆栈,然后将它们弹出堆栈。在使用这些对象之后,您将 null 分配给它们的引用变量,认为它们将在下一次垃圾收集器运行时被垃圾收集。然而,这种情况不会发生,因为堆栈对象仍然维护其对这些对象的引用,因此 150 兆字节的堆空间对应用不可用,并且应用可能会耗尽内存。

这个问题的解决方案是让 pop() 在返回引用之前将 null 显式分配给元素条目。只需取消对元素[top + 1] = null 的注释;清单 3-21 中的行使这种情况发生。

您可能会认为,当不再需要引用变量的被引用对象时,您应该总是将空值分配给引用变量。然而,经常这样做并不能提高性能或者释放大量的堆空间,并且在不小心的时候会导致抛出 Java . lang . nullpointerexception 类的实例。(我将在第五章关于 Java 面向异常的语言特性的中讨论 NullPointerException )。通常在管理自己内存的类中取消引用变量,比如前面提到的 Stack 类。

注意要了解更多关于 Java 5 环境中的垃圾收集,请查看 Oracle 的“Java HotSpot 虚拟机中的内存管理”白皮书(www . Oracle . com/tech network/Java/javase/tech/Memory Management-white paper-1-150020 . pdf)。

重访数组

在第二章的中,我向您介绍了数组,它是内存区域(具体来说,是堆),在大小相等且连续的槽中存储值,称为元素。我还举了几个例子,包括下面这个例子:

char gradeLetters[] = { 'A', 'B', 'C', 'D', 'F' };

  • 1
  • 2

这里有一个名为 gradeLetters 的数组变量,它存储了对一个五元素内存区域的引用,该内存区域将字符 A 、 B 、 C 、 D 和 F 存储在连续且大小相等(16 位)的内存位置中。

注意我已经把 [] 括号放在了等级字母之后。尽管这是合法的,但习惯上还是将这些括号放在类型名之后,如 char[] gradeLetters = { ‘A ‘,’ B ‘,’ C ‘,’ D ‘,’ F ’ };。在本节中,我将演示这两种方法。

您可以通过指定个等级字母[ x ],来访问一个元素,其中 x 是一个标识数组元素的整数,被称为索引;第一个数组元素总是位于索引 0 处。以下示例显示了如何输出和更改第一个元素的值:

System.out.println(gradeLetters[0]); // Output the first grade letter.
gradeLetters[0] = 'a'; // Perhaps you prefer lowercase grade letters.

  • 1
  • 2
  • 3

{ ‘A ‘,’ B ‘,’ C ‘,’ D ‘,’ F’ } 数组创建语法是语法糖的一个例子(简化语言的语法,使其使用起来更“甜蜜”)。在后台,用新的操作符创建数组,并初始化为这些值,如下所示:

char gradeLetters[] = new char[] { 'A', 'B', 'C', 'D', 'F' };

  • 1
  • 2

首先,分配一个五字符的内存区域。接下来,该区域的五个字符元素被初始化为 A 、 B 、 C 、 D 和 F 。最后,对这些元素的引用存储在数组变量 gradeLetters 中。

注意将整数值放在字符后面的方括号中是错误的。比如编译器在遇到 new char[5] { ‘A ‘,’ B ‘,’ C ‘,’ D ‘,’ F’ }中的 5 时报错;。

您可以将数组视为一种特殊的对象,尽管它不是与类实例是对象相同意义上的对象。这个伪对象有一个单独的只读长度字段,包含数组的大小(元素的数量)。例如, gradeLetters.length 返回 gradeLetters 数组中元素(5)的个数。

虽然您可以使用前两种方法中的任何一种来创建数组,但是您通常会指定第三种方法,这种方法不涉及显式的元素初始化,并且随后会初始化数组。下面的代码演示了这种方法:

char gradeLetters[] = new char[5];

  • 1
  • 2

您可以将元素的数量指定为方括号之间的正整数。运算符 new 将每个数组元素的存储位置中的位清零,您在源代码级别将其解释为文字值 false 、 ‘u0000’ 、 0 、 0L 、 0.0 、 0.0F 或 null (取决于元素类型)。

然后,您可以初始化数组,如下所示:

gradeLetters[0] = 'A';
gradeLetters[1] = 'B';
gradeLetters[2] = 'C';
gradeLetters[3] = 'D';
gradeLetters[4] = 'F';

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

但是,您可能会发现使用循环执行此任务更方便,如下所示:

for (int i = 0; i < gradeLetters.length; i++)
   gradeLetters[i] = 'A' + i;

  • 1
  • 2
  • 3

前面的例子着重于创建一个数组,它的值共享一个通用的原始类型(字符,由 char 关键字表示)。您还可以创建对象引用的数组。例如,您可以创建一个数组来存储三个图像对象引用,如下所示:

Image[] imArray = { new Image("image0.png"), new Image("image1.png"), new Image("image2.png") };

  • 1
  • 2

这里有一个名为 imArray 的数组变量,它存储一个对三元素内存区域的引用,其中每个元素存储一个对 Image 对象的引用。图像对象位于内存中的其他地方。

您可以通过指定 imArray[x]来访问一个图像元素。以下示例假设存在一个返回图像长度(以字节为单位)的 getLength() 方法,并在第一个 Image 对象上调用该方法以返回第一个图像的长度,该长度随后被输出:

System.out.println(imArray[0].getLength());

  • 1
  • 2

与前面的 gradeLetters 示例一样,您可以将 new 操作符与语法 sugar initializer 结合起来,如下所示:

Image[] imArray = new Image[] { new Image("image0.png"), new Image("image1.png"),
                                new Image("image2.png") };

  • 1
  • 2
  • 3

最后,您可以使用第三种方法,通过将每个元素中的所有位设置为 0,将每个对象引用初始化为空引用。这种方法演示如下:

Image[] imArray = new Image[3];

  • 1
  • 2

因为 new 将每个元素初始化为空引用,所以您必须显式初始化该数组,您可以方便地这样做,如下所示:

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

  • 1
  • 2
  • 3

“图像”+ i +”。png” 表达式使用字符串连接运算符( + )将图像与存储在变量 i 中的整数值的字符串等效项组合起来。png 。产生的字符串被传递给 Image 的 Image(字符串文件名)构造函数,产生的引用被存储在一个数组元素中。

注意根据循环的长度,在循环上下文中使用字符串连接操作符会导致大量不必要的字符串对象的创建。我会在第七章向你介绍 String 类的时候讨论这个话题。

前面的例子着重于创建一维数组。然而,你也可以创建多维数组(即二维或多维数组)。例如,考虑温度值的二维数组。

虽然您可以使用这三种方法中的任何一种来创建温度数组,但是当这些值变化很大时,第三种方法更可取。以下示例将此数组创建为一个三行两列的双精度浮点温度值表:

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

  • 1
  • 2

注意双和温度之间的两组方括号。这两组括号表示二维数组(表格)。还要注意新和双后面的两组方括号。每个集合包含一个正整数值,表示每行的行数( 3 )或列数( 2 )。

注意创建多维数组时,与数组变量相关联的方括号对的数量和 new 后面的方括号对的数量以及类型名必须相同。

创建数组后,可以用合适的值填充它的元素。以下示例通过 Math.random() 将每个 temperatures 元素初始化为随机生成的温度值,该元素被访问为 temperatures【row】【col】,我将在第七章中对此进行解释:

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

  • 1
  • 2
  • 3
  • 4

外部 for 循环选择从第 0 行到数组长度的每一行(确定数组中的行数)。内部 for 循环选择从 0 到当前行数组长度的每一列(确定该数组表示的列数)。本质上,您看到的是一个一维行数组,其中每个元素都引用一个一维列数组。

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

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这种数组被称为不规则数组 ,因为每行可以有不同数量的列;该阵列不是矩形的,而是参差不齐的。

注意当创建行数组时,你必须指定一对额外的空括号作为跟随 new 的表达式的一部分。(对于三维数组——表格的一维数组,其中该数组的元素引用行数组——您必须指定两对空括号作为跟随 new 的表达式的一部分。)

练习

以下练习旨在测试您对第三章内容的理解:

  1. 什么是课?
  2. 如何声明一个类?
  3. 什么是对象?
  4. 如何实例化一个对象?
  5. 什么是构造函数?
  6. 是非判断:当一个类没有声明构造函数时,Java 会创建一个默认的无参数构造函数。
  7. 什么是参数表,什么是参数?
  8. 什么是参数列表,什么是参数?
  9. 是非判断:通过指定类名后跟一个参数列表来调用另一个构造函数。
  10. 定义 arity。
  11. 什么是局部变量?
  12. 定义寿命。
  13. 定义范围。
  14. 什么是封装?
  15. 定义字段。
  16. 实例字段和类字段有什么区别?
  17. 什么是空终值,它与真常量有什么不同?
  18. 如何防止字段被隐藏?
  19. 定义方法。
  20. 实例方法和类方法有什么区别?
  21. 定义递归。
  22. 怎么霸王一个方法?
  23. 什么是类初始化器,什么是实例初始化器?
  24. 定义垃圾收集器。
  25. 是非:String[]letters = new String[2]{“A”,“B”};是正确的语法。
  26. 什么是参差不齐的数组?
  27. factorial() 方法提供了一个尾递归的例子,这是递归的一个特例,其中方法的最后一条语句包含一个递归调用,这被称为尾调用。提供另一个尾部递归的例子。
  28. 创建一个包含姓名、作者和国际标准书号(ISBN)字段的 Book 类。提供合适的构造函数和返回字段值的 getter 方法。在这个类中引入一个 main() 方法,该方法创建一个 Book 对象的数组,并遍历该数组,输出每本书的名称、作者和 ISBN。

摘要

类是制造对象的模板,这些对象被命名为代码和数据的集合。类概括了现实世界中的实体,而对象是这些实体在应用级别的具体表现。

new 操作符分配内存来存储由 new 的唯一操作数指定类型的对象。这个运算符后面是一个构造函数,它是一个用于初始化对象的代码块。 new 在分配内存存储对象后立即调用构造函数。

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

Java 允许你通过方法来表示一个实体的行为,这些方法是在一个类的主体中声明的命名代码块。实体行为通过实例方法来描述。因为 Java 也支持与类而不是对象相关联的行为,所以 Java 提供了类方法来描述这些类行为。

类和对象在使用前需要正确初始化。您已经了解了类字段在类加载后被初始化为缺省的零值,随后可以通过类字段初始化器在声明中给它们赋值来初始化。类似地,当对象的内存通过 new 分配时,实例字段被初始化为默认值,随后可以通过实例字段初始值设定项或构造函数在声明中给它们赋值来初始化。

Java 也支持这个任务的类初始化器和实例初始化器。一个类初始化器是一个被引入到类体中的带前缀的静态块。它用于通过一系列语句初始化一个加载的类。实例初始化器是引入到类体中的块,与作为方法或构造函数的体引入相对。实例初始化器用于通过一系列语句初始化一个对象。

对象是通过保留字 new 创建的,但是它们是如何被销毁的呢?如果没有销毁对象的方法,它们最终会填满堆的可用空间,应用将无法继续运行。Java 没有为开发人员提供从内存中删除它们的能力。相反,Java 通过提供垃圾收集器来处理这项任务,垃圾收集器是在后台运行的代码,偶尔会检查未引用的对象。

您可以将数组视为一种特殊的对象,尽管它不是与类实例是对象相同意义上的对象。这个伪对象有一个单独的只读长度字段,包含数组的大小(元素的数量)。

除了使用在第二章中首次出现的语法糖来创建数组之外,你还可以使用新的操作符来创建数组,可以使用也可以不使用语法糖。

在第四章中,我继续通过检查 Java 语言对继承、多态和接口的支持来探索 Java 语言。

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...