Java核心技术 卷Ⅰ 基础知识(原书第10版)

Java核心技术系列

Java核心技术

卷Ⅰ 基础知识

(原书第10版)

Core Java Volume I—Fundamentals (10th Edition)

[美] 凯S.霍斯特曼(Cay S. Horstmann) 著

周立新 陈 波 叶乃文 邝劲筠 杜永萍 译

图书在版编目(CIP)数据

Java核心技术 卷Ⅰ 基础知识(原书第10版) / (美)凯S. 霍斯特曼(Cay S. Horstmann)著;周立新等译. —北京:机械工业出版社,2016.8

(Java核心技术系列)

书名原文:Core Java Volume I—Fundamentals (Tenth Edition)

ISBN 978-7-111-54742-6

I. J… II. ①凯… ②周… III. JAVA语言-程序设计 IV. TP312.8

中国版本图书馆CIP数据核字(2016)第211440号

本书版权登记号:图字:01-2016-5145

Authorized translation from the English language edition, entitled Core Java Volume I—Fundamentals (Tenth Edition),9780134177304 by Cay S. Horstmann, published by Pearson Education, Inc., Copyright ? 2016 Oracle and /or its aff?iliates.

All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc.

Chinese Simplif?ied language edition published by Pearson Education Asia Ltd., and China Machine Press Copyright ? 2016.

本书中文简体字版由Pearson Education(培生教育出版集团)授权机械工业出版社在中华人民共和国境内(不包括香港、澳门特别行政区及台湾地区)独家出版发行。未经出版者书面许可,不得以任何方式抄袭、复制或节录本书中的任何部分。

本书封底贴有Pearson Education(培生教育出版集团)激光防伪标签,无标签者不得销售。

Java核心技术 卷Ⅰ 基础知识(原书第10版)

出版发行:机械工业出版社(北京市西城区百万庄大街22号 邮政编码:100037)

责任编辑:关 敏 责任校对:董纪丽

印  刷: 版  次:2016年9月第1版第1次印刷

开  本:186mm×240mm 1/16 印  张:45.5

书  号:ISBN 978-7-111-54742-6 定  价:119.00元

凡购本书,如有缺页、倒页、脱页,由本社发行部调换

客服热线:(010)88379426 88361066 投稿热线:(010)88379604

购书热线:(010)68326294 88379649 68995259 读者信箱:hzit@hzbook.com

版权所有 ? 侵权必究

封底无防伪标均为盗版

本书法律顾问:北京大成律师事务所 韩光/邹晓东

译 者 序

书写Java传奇的Sun Microsystems曾经堪称“日不落”帝国,但服务器市场的萎缩却让这个声名赫赫的庞大帝国从蓬勃走向落寞。在2009年被Oracle公司收购之后,Sun公司逐渐淡出了人们的视线,而与此同时,我们也在很长一段时间内没能看到Java当初活跃的身影。

Java就这样退出历史舞台了吗?当然不是!从Sun公司2006年12月发布Java 6后,经过5年多的不懈努力,终于在2011年7月底发布了Java 7正式版。3年后,被冠名为“跳票王”的Oracle终于发布了Java 8的正式版,但对于很多开发者来说,Java 8却比Java 7来得更漫长一些。主要是因为Oracle原本计划在2013年发布正式版Java 8,却因受困于安全性的问题经过了两次“跳票”。无论如何,如今Java 8来了,全新“革命”而不只是“进化”的功能将会让无数开发者动容。

值得一提的是,伴随着Java的成长,《Java核心技术》也从第1版到第9版一路走来,得到了广大Java程序设计人员的青睐,成为一本畅销不衰的Java经典图书。经过几年的蛰伏,针对Java 8打造的《Java核心技术》第10版终于问世,这一版有了大幅的修订和更新,以反映Java 8增补、删改的内容。它将续写从前的辉煌,使人们能及时跟上Java前进的脚步。

本书由周立新、陈波等主译,程芳、刘晓兵、张练达、陈峰、江健、谢连宝、张雷生、杨健康、张莹参与了全书的修改整理,并完善了关键部分的翻译。全体人员共同完成了本书的翻译工作。特别需要说明的是,按照出版社的要求,这一版的翻译在老版本基础上完成,因此尤其感谢之前版本的译者叶乃文、邝劲筠和杜永萍,他们的辛勤工作为新版本的翻译奠定了很好的基础。

书中文字与内容力求忠实原著,不过由于译者水平有限,译文肯定有不当之处,敬请批评指正。

译者

2016年6月于北京

前  言

致读者

1995年年底,Java语言在Internet舞台一亮相便名声大噪。其原因在于它将有望成为连接用户与信息的万能胶,而不论这些信息来自Web服务器、数据库、信息提供商,还是任何其他渠道。事实上,就发展前景而言,Java的地位是独一无二的。它是一种完全可信赖的程序设计语言,得到了除微软之外的所有厂家的认可。其固有的可靠性与安全性不仅令Java程序员放心,也令使用Java程序的用户放心。Java内建了对网络编程、数据库连接、多线程等高级程序设计任务的支持。

1995年以来,已经发布了Java开发工具包(Java Development Kit)的9个主要版本。在过去的20年中,应用程序编程接口(API)已经从200个类扩展到超过4000个类。现在这些API覆盖了用户界面构建、数据库管理、国际化、安全性以及XML处理等各个不同的领域。

本书是《Java核心技术》第10版的卷Ⅰ。自《Java核心技术》出版以来,每个新版本都尽可能快地跟上Java开发工具箱发展的步伐,而且每一版都重新改写了部分内容,以便适应Java的最新特性。在这一版中,已经反映了Java 标准版(Java SE 8)的特性。

与前几版一样,本版仍然将读者群定位在那些打算将Java应用到实际工程项目中的程序设计人员。本书假设读者是一名具有程序设计语言(除Java之外)坚实背景知识的程序设计人员,并且不希望书中充斥着玩具式的示例(诸如,烤面包机、动物园的动物或神经质的跳动文本)。这些内容绝对不会在本书中出现。本书的目标是让读者充分理解书中介绍的Java语言及Java类库的相关特性,而不会产生任何误解。

在本书中,我们选用大量的示例代码演示所讨论的每一个语言特性和类库特性。我们有意使用简单的示例程序以突出重点,然而,其中的大部分既不是赝品也没有偷工减料。它们将成为读者自己编写代码的良好开端。

我们假定读者愿意(甚至渴望)学习Java提供的所有高级特性。例如,本书将详细介绍下列内容:

面向对象程序设计

反射与代理

接口与内部类

异常处理

泛型程序设计

集合框架

事件监听器模型

使用Swing UI工具箱进行图形用户界面设计

并行操作

随着Java类库的爆炸式增长,一本书无法涵盖程序员需要了解的所有Java特性。因此,我们决定将本书分为两卷。卷I(本书)集中介绍Java语言的基本概念以及图形用户界面程序设计的基础知识。卷Ⅱ(高级特性)涉及企业特性以及高级的用户界面程序设计,其中详细讨论下列内容:

流API

文件处理与正则表达式

数据库

XML处理

注释

国际化

网络编程

高级GUI组件

高级图形

原生方法

本书中难免出现错误和不准确之处。我们很想知道这些错误,当然,也希望同一个问题只被告知一次。我们在网页http://horstmann.com/corejava中以列表的形式给出了常见的问题、bug修正和解决方法。在勘误页(建议先阅读一遍)最后附有用来报告bug并提出修改意见的表单。如果我们不能回答每一个问题或没有及时回复,请不要失望。我们会认真地阅读所有的来信,感谢您的建议使本书后续的版本更清晰、更有指导价值。

关于本书

第1章概述Java与其他程序设计语言不同的性能。解释这种语言的设计初衷,以及在哪些方面达到了预期的效果。然后,简要叙述Java诞生和发展的历史。

第2章详细论述如何下载和安装JDK以及本书的程序示例。然后,通过编译和运行3个典型的Java程序(一个控制台应用、一个图形应用、一个applet),指导读者使用简易的JDK、可启用Java的文本编辑器以及一个Java IDE。

第3章开始讨论Java 语言。这一章涉及的基础知识有变量、循环以及简单的函数。对于C或C++程序员来说,学习这一章的内容将会感觉一帆风顺,因为这些语言特性的语法本质上与C语言相同。对于没有C语言程序设计背景,但使用过其他程序设计语言(如Visual Basic)的程序员来说,仔细地阅读这一章是非常必要的。

面向对象程序设计(Object-Oriented Programming, OOP)是当今程序设计的主流,而Java是一种完全面向对象的语言。第4章将介绍面向对象两个基本成分中最重要的——封装,以及Java语言实现封装的机制,即类与方法。除了Java语言规则之外,还对如何完成合理的OOP设计给出了忠告。最后,介绍奇妙的javadoc工具,它将代码注释转换为一组包含超链接的网页。熟悉C++的程序员可以快速地浏览这一章,而没有面向对象程序设计背景的程序员应在进一步学习Java之前花一些时间了解OOP的有关概念。

类与封装仅仅是OOP中的一部分,第5章将介绍另一部分——继承。继承使程序员可以使用现有的类,并根据需要进行修改。这是Java程序设计中的一个基础技术。Java中的继承机制与C++的继承机制十分相似。C++程序员只需关注两种语言的不同之处即可。

第6章展示如何使用Java的接口。接口可以让你的理解超越第5章的简单继承模型。掌握接口可以充分获得Java的完全的面向对象程序设计能力。介绍接口之后,我们将转而介绍lambda表达式(lambda expression),这是一种简洁的方法,用来表述可以在以后某个时间点执行的代码块。本章还将介绍Java的一个有用的技术特性——内部类。

第7章讨论异常处理(exception handling),即Java的一种健壮机制,用于处理可正常运行程序可能出现意外的情况。异常提供了一种将正常处理代码与错误处理代码分开的有效手段。当然,即使程序能够处理所有异常条件,仍然有可能无法按照预计的方式工作。这一章的后半部分将给出大量实用的调试技巧。

第8章概要介绍泛型程序设计。泛型程序设计可以让程序更可读、更安全。我们会展示如何使用强类型机制,而舍弃不安全的强制类型转换,以及如何处理与旧版本Java兼容所带来的复杂问题。

第9章讨论的是Java平台的集合框架。如果希望收集多个对象并在以后获取这些对象,就应当使用集合,而不要简单地把这些元素放在一个数组中,这是这种情况下最适用的做法。这一章会介绍如何充分利用内建的标准集合。

第10章开始介绍GUI程序设计。我们会讨论如何建立窗口、如何在窗口中绘图、如何利用几何图形绘图、如何采用多种字体格式化文本,以及如何显示图像。

第11章将详细讨论抽象窗口工具包(abstract window toolkit,AWT)的事件模型。你会看到如何编写代码来响应事件,如鼠标点击事件或按键事件。同时,你还会看到如何处理基本的GUI元素,如按钮和面板。

第12章详细讨论Swing GUI工具包。Swing工具包允许建立跨平台的图像用户界面。在这里你会了解各种按钮、文本组件、边框、滑块、列表框、菜单以及对话框的有关内容。不过,一些更高级的组件会在卷II中讨论。

第13章介绍如何将程序部署为应用或applet。在这里我们会描述如何将程序打包在JAR文件中,以及如何使用Java Web Start和applet机制在Internet上发布应用。另外还会解释Java程序部署之后如何存储和获取配置信息。

第14章是本书的最后一章,这一章将讨论并发,并发能够让程序任务并行执行。在当今这个时代,大多数处理器都有多个内核,你往往希望这些内核都在工作,并发是Java技术的一个重要而且令人振奋的应用。

附录列出了Java语言的保留字。

约定

本书使用以下图标表示特殊内容。

注释:“注释”信息会用这样的“注释”图标标志。

提示:“提示”信息会用这样的“提示”图标标志。

警告:对于可能出现的危险,我们用一个“警告”图标做出警示。

C++注释:在本书中有许多用来解释Java与C++之间差别的C++注释。对于没有C++程序设计背景,或者不擅长C++程序设计、把它当做一场噩梦不愿再想起的程序员来说,可以跳过这些注释。

Java提供了一个很大的程序设计库,即应用程序编程接口。第一次使用API调用时,我们会在该节的结尾给出一个概要描述。这些描述十分通俗易懂,希望能够比联机API文档提供更多的信息。类、接口或方法名后面的编号是介绍该特性的JDK版本号,如下例所示:

应用程序编程接口1.2

程序(源代码见本书网站)以程序清单形式给出,例如:

程序清单1-1 InputTest/InputTest.java

示例代码

本书网站http://horstmann.com/corejava以压缩的形式提供了书中的所有示例代码。可以用熟悉的解压缩程序或者用Java开发包中的jar实用程序解压这个文件。有关安装Java开发包和示例代码的详细信息请参看第2章。

致  谢

写一本书需要投入大量的精力,改写一本书也并不像想象的那样轻松,尤其是Java技术一直在持续不断地更新。编著一本书让很多人耗费了很多心血,在此衷心地感谢《Java核心技术》编写小组的每一位成员。

Prentice Hall公司的许多人提供了非常有价值的帮助,却甘愿做幕后英雄。在此,我希望每一位都能够知道我对他们努力的感恩。与以往一样,我要真诚地感谢我的编辑,Prentice Hall公司的Greg Doench,从本书的写作到出版他一直在给予我们指导,同时感谢那些不知其姓名的为本书做出贡献的幕后人士。非常感谢Julie Nahil在图书制作方面给予的支持,还要感谢Dmitry Kirsanov和Alina Kirsanova完成手稿的编辑和排版工作。我还要感谢早期版本中我的合作者,Gary Cornell,他已经转向其他的事业。 

感谢早期版本的许多读者,他们指出了许多令人尴尬的错误并给出了许多具有建设性的修改意见。我还要特别感谢本书优秀的审阅小组,他们仔细地审阅我的手稿,使本书减少了许多错误。

本书及早期版本的审阅专家包括:Chuck Allison (Utah Valley大学)、Lance Andersen (Oracle)、Paul Anderson (Anderson Software Group)、Alec Beaton (IBM)、Cliff Berg、Andrew Binstock (Oracle)、Joshua Bloch、David Brown、Corky Cartwright、Frank Cohen (PushToTest)、Chris Crane (devXsolution)、Dr. Nicholas J. De Lillo (Manhattan学院)、Rakesh Dhoopar (Oracle)、David Geary (Clarity Training)、Jim Gish (Oracle)、Brian Goetz (Oracle)、Angela Gordon、Dan Gordon (Electric Cloud)、Rob Gordon、John Gray (Hartford大学)、Cameron Gregory (olabs.com)、Marty Hall (coreservlets.com公司)、Vincent Hardy (Adobe Systems)、Dan Harkey (San Jose州立大学)、William Higgins (IBM)、Vladimir Ivanovic (PointBase)、Jerry Jackson (CA Technologies)、Tim Kimmet (Walmart)、Chris Laffra、Charlie Lai (Apple)、Angelika Langer、Doug Langston、Hang Lau (McGill大学)、Mark Lawrence、Doug Lea (SUNY Oswego)、Gregory Longshore、Bob Lynch (Lynch Associates)、Philip Milne (consultant)、Mark Morrissey (Oregon研究院)、Mahesh Neelakanta (Florida Atlantic大学)、Hao Pham、Paul Philion、Blake Ragsdell、Stuart Reges (Arizona大学)、Rich Rosen (Interactive Data Corporation)、Peter Sanders (法国尼斯ESSI大学)、Dr. Paul Sanghera (San Jose州立大学Brooks学院)、Paul Sevinc (Teamup AG)、Devang Shah (Sun Microsystems)、Yoshiki Shibata、Bradley A. Smith、Steven Stelting (Oracle)、Christopher Taylor、Luke Taylor (Valtech)、George Thiruvathukal、Kim Topley (StreamingEdge)、Janet Traub、Paul Tyma (consultant)、Peter van der Linden、Christian Ullenboom、Burt Walsh、Dan Xu (Oracle)和John Zavgren (Oracle)。

Cay Horstmann

2015年11月于瑞士比尔

目  录

译者序

前言

致谢

第1章 Java程序设计概述  1

1.1 Java程序设计平台  1

1.2 Java“白皮书”的关键术语  2

1.2.1 简单性  2

1.2.2 面向对象  2

1.2.3 分布式  3

1.2.4 健壮性  3

1.2.5 安全性  3

1.2.6 体系结构中立  4

1.2.7 可移植性  4

1.2.8 解释型  5

1.2.9 高性能  5

1.2.10 多线程  5

1.2.11 动态性  5

1.3 Java applet与Internet  6

1.4 Java发展简史  7

1.5 关于Java的常见误解  9

第2章 Java程序设计环境  12

2.1 安装Java开发工具包  12

2.1.1 下载JDK  12

2.1.2 设置JDK  13

2.1.3 安装库源文件和文档  15

2.2 使用命令行工具  16

2.3 使用集成开发环境  18

2.4 运行图形化应用程序  21

2.5 构建并运行applet  23

第3章 Java的基本程序设计结构  28

3.1 一个简单的Java应用程序  28

3.2 注释  31

3.3 数据类型  32

3.3.1 整型  32

3.3.2 浮点类型  33

3.3.3 char类型  34

3.3.4 Unicode和char类型  35

3.3.5 boolean类型  35

3.4 变量  36

3.4.1 变量初始化  37

3.4.2 常量  37

3.5 运算符  38

3.5.1 数学函数与常量  39

3.5.2 数值类型之间的转换  40

3.5.3 强制类型转换  41

3.5.4 结合赋值和运算符  42

3.5.5 自增与自减运算符  42

3.5.6 关系和boolean运算符  42

3.5.7 位运算符  43

3.5.8 括号与运算符级别  44

3.5.9 枚举类型  45

3.6 字符串  45

3.6.1 子串  45

3.6.2 拼接  46

3.6.3 不可变字符串  46

3.6.4 检测字符串是否相等  47

3.6.5 空串与Null串  48

3.6.6 码点与代码单元  49

3.6.7 String API  50

3.6.8 阅读联机API文档  52

3.6.9 构建字符串  54

3.7 输入输出  55

3.7.1 读取输入  55

3.7.2 格式化输出  58

3.7.3 文件输入与输出  61

3.8 控制流程  63

3.8.1 块作用域  63

3.8.2 条件语句  63

3.8.3 循环  66

3.8.4 确定循环  69

3.8.5 多重选择:switch语句  72

3.8.6 中断控制流程语句  74

3.9 大数值  76

3.10 数组  78

3.10.1 for each循环  79

3.10.2 数组初始化以及匿名数组  80

3.10.3 数组拷贝  81

3.10.4 命令行参数  81

3.10.5 数组排序  82

3.10.6 多维数组  85

3.10.7 不规则数组  88

第4章 对象与类  91

4.1 面向对象程序设计概述  91

4.1.1 类  92

4.1.2 对象  93

4.1.3 识别类  93

4.1.4 类之间的关系  94

4.2 使用预定义类  95

4.2.1 对象与对象变量  95

4.2.2 Java类库中的LocalDate类  98

4.2.3 更改器方法与访问器方法  100

4.3 用户自定义类  103

4.3.1 Employee类  103

4.3.2 多个源文件的使用  105

4.3.3 剖析Employee类  106

4.3.4 从构造器开始  106

4.3.5 隐式参数与显式参数  108

4.3.6 封装的优点  109

4.3.7 基于类的访问权限  111

4.3.8 私有方法  111

4.3.9 f?inal实例域  112

4.4 静态域与静态方法  112

4.4.1 静态域  112

4.4.2 静态常量  113

4.4.3 静态方法  114

4.4.4 工厂方法  115

4.4.5 main方法  115

4.5 方法参数  118

4.6 对象构造  123

4.6.1 重载  123

4.6.2 默认域初始化  123

4.6.3 无参数的构造器  124

4.6.4 显式域初始化  125

4.6.5 参数名  125

4.6.6 调用另一个构造器  126

4.6.7 初始化块  127

4.6.8 对象析构与f?inalize方法  130

4.7 包  131

4.7.1 类的导入  131

4.7.2 静态导入  133

4.7.3 将类放入包中  133

4.7.4 包作用域  136

4.8 类路径  137

4.8.1 设置类路径  139

4.9 文档注释  140

4.9.1 注释的插入  140

4.9.2 类注释  140

4.9.3 方法注释  141

4.9.4 域注释  142

4.9.5 通用注释  142

4.9.6 包与概述注释  143

4.9.7 注释的抽取  143

4.10 类设计技巧  144

第5章 继承  147

5.1 类、超类和子类  147

5.1.1 定义子类  147

5.1.2 覆盖方法  149

5.1.3 子类构造器  150

5.1.4 继承层次  153

5.1.5 多态  154

5.1.6 理解方法调用  155

5.1.7 阻止继承:f?inal类和方法  157

5.1.8 强制类型转换  158

5.1.9 抽象类  160

5.1.10 受保护访问  165

5.2 Object:所有类的超类  166

5.2.1 equals方法  166

5.2.2 相等测试与继承  167

5.2.3 hashCode方法  170

5.2.4 toString方法  172

5.3 泛型数组列表  178

5.3.1 访问数组列表元素  180

5.3.2 类型化与原始数组列表的兼容性  183

5.4 对象包装器与自动装箱  184

5.5 参数数量可变的方法  187

5.6 枚举类  188

5.7 反射  190

5.7.1 Class类  190

5.7.2 捕获异常  192

5.7.3 利用反射分析类的能力  194

5.7.4 在运行时使用反射分析对象  198

5.7.5 使用反射编写泛型数组代码  202

5.7.6 调用任意方法  205

5.8 继承的设计技巧  208

第6章 接口、lambda表达式与内部类  211

6.1 接口  211

6.1.1 接口概念  211

6.1.2 接口的特性  217

6.1.3 接口与抽象类  218

6.1.4 静态方法  218

6.1.5 默认方法  219

6.1.6 解决默认方法冲突  220

6.2 接口示例  222

6.2.1 接口与回调  222

6.2.2 Comparator接口  224

6.2.3 对象克隆  225

6.3 lambda表达式  231

6.3.1 为什么引入lambda表达式  231

6.3.2 lambda表达式的语法  232

6.3.3 函数式接口  234

6.3.4 方法引用  235

6.3.5 构造器引用  237

6.3.6 变量作用域  237

6.3.7 处理lambda表达式  239

6.3.8 再谈Comparator  242

6.4 内部类  242

6.4.1 使用内部类访问对象状态  244

6.4.2 内部类的特殊语法规则  247

6.4.3 内部类是否有用、必要和安全  248

6.4.4 局部内部类  250

6.4.5 由外部方法访问变量  250

6.4.6 匿名内部类  252

6.4.7 静态内部类  255

6.5 代理  258

6.5.1 何时使用代理  259

6.5.2 创建代理对象  259

6.5.3 代理类的特性  262

第7章 异常、断言和日志  264

7.1 处理错误  264

7.1.1 异常分类  265

7.1.2 声明受查异常  267

7.1.3 如何抛出异常  269

7.1.4 创建异常类  270

7.2 捕获异常  271

7.2.1 捕获异常  271

7.2.2 捕获多个异常  273

7.2.3 再次抛出异常与异常链  274

7.2.4 f?inally子句  275

7.2.5 带资源的try语句  278

7.2.6 分析堆栈轨迹元素  280

7.3 使用异常机制的技巧  282

7.4 使用断言  285

7.4.1 断言的概念  285

7.4.2 启用和禁用断言  286

7.4.3 使用断言完成参数检查  287

7.4.4 为文档假设使用断言  288

7.5 记录日志  289

7.5.1 基本日志  289

7.5.2 高级日志  289

7.5.3 修改日志管理器配置  291

7.5.4 本地化  292

7.5.5 处理器  293

7.5.6 过滤器  296

7.5.7 格式化器  296

7.5.8 日志记录说明  296

7.6 调试技巧  304

第8章 泛型程序设计  309

8.1 为什么要使用泛型程序设计  309

8.1.1 类型参数的好处  309

8.1.2 谁想成为泛型程序员  310

8.2 定义简单泛型类  311

8.3 泛型方法  313

8.4 类型变量的限定  314

8.5 泛型代码和虚拟机  316

8.5.1 类型擦除  316

8.5.2 翻译泛型表达式  317

8.5.3 翻译泛型方法  318

8.5.4 调用遗留代码  319

8.6 约束与局限性  320

8.6.1 不能用基本类型实例化类型参数  320

8.6.2 运行时类型查询只适用于原始类型  321

8.6.3 不能创建参数化类型的数组  321

8.6.4 Varargs警告  322

8.6.5 不能实例化类型变量  323

8.6.6 不能构造泛型数组  323

8.6.7 泛型类的静态上下文中类型变量无效  325

8.6.8 不能抛出或捕获泛型类的实例  325

8.6.9 可以消除对受查异常的检查  326

8.6.10 注意擦除后的冲突  327

8.7 泛型类型的继承规则  328

8.8 通配符类型  330

8.8.1 通配符概念  330

8.8.2 通配符的超类型限定  331

8.8.3 无限定通配符  334

8.8.4 通配符捕获  334

8.9 反射和泛型  337

8.9.1 泛型Class类  337

8.9.2 使用Class<T>参数进行类型匹配  338

8.9.3 虚拟机中的泛型类型信息  338

第9章 集合  344

9.1 Java集合框架  344

9.1.1 将集合的接口与实现分离  344

9.1.2 Collection接口  346

9.1.3 迭代器  347

9.1.4 泛型实用方法  349

9.1.5 集合框架中的接口  352

9.2 具体的集合  353

9.2.1 链表  355

9.2.2 数组列表  362

9.2.3 散列集  363

9.2.4 树集  366

9.2.5 队列与双端队列  369

9.2.6 优先级队列  371

9.3 映射  372

9.3.1 基本映射操作  372

9.3.2 更新映射项  375

9.3.3 映射视图  376

9.3.4 弱散列映射  377

9.3.5 链接散列集与映射  378

9.3.6 枚举集与映射  379

9.3.7 标识散列映射  380

9.4 视图与包装器  381

9.4.1 轻量级集合包装器  382

9.4.2 子范围  382

9.4.3 不可修改的视图  383

9.4.4 同步视图  384

9.4.5 受查视图  384

9.4.6 关于可选操作的说明  385

9.5 算法  388

9.5.1 排序与混排  389

9.5.2 二分查找  391

9.5.3 简单算法  392

9.5.4 批操作  394

9.5.5 集合与数组的转换  394

9.5.6 编写自己的算法  395

9.6 遗留的集合  396

9.6.1 Hashtable类  397

9.6.2 枚举  397

9.6.3 属性映射  398

9.6.4 栈  399

9.6.5 位集  399

第10章 图形程序设计  403

10.1 Swing概述  403

10.2 创建框架  407

10.3 框架定位  409

10.3.1 框架属性  411

10.3.2 确定合适的框架大小  411

10.4 在组件中显示信息  415

10.5 处理2D图形  419

10.6 使用颜色  426

10.7 文本使用特殊字体  429

10.8 显示图像  435

第11章 事件处理  439

11.1 事件处理基础  439

11.1.1 实例:处理按钮点击事件  441

11.1.2 简洁地指定监听器  445

11.1.3 实例:改变观感  447

11.1.4 适配器类  450

11.2 动作  453

11.3 鼠标事件  459

11.4 AWT事件继承层次  465

11.4.1 语义事件和底层事件  466

第12章 Swing用户界面组件  469

12.1 Swing和模型–视图–控制器设计模式  469

12.1.1 设计模式  469

12.1.2 模型–视图–控制器模式  470

12.1.3 Swing按钮的模型–视图–控制器分析  473

12.2 布局管理概述  474

12.2.1 边框布局  477

12.2.2 网格布局  478

12.3 文本输入  481

12.3.1 文本域  482

12.3.2 标签和标签组件  483

12.3.3 密码域  484

12.3.4 文本区  485

12.3.5 滚动窗格  485

12.4 选择组件  488

12.4.1 复选框  488

12.4.2 单选钮  490

12.4.3 边框  493

12.4.4 组合框  496

12.4.5 滑动条  499

12.5 菜单  504

12.5.1 菜单创建  504

12.5.2 菜单项中的图标  507

12.5.3 复选框和单选钮菜单项  508

12.5.4 弹出菜单  508

12.5.5 快捷键和加速器  510

12.5.6 启用和禁用菜单项  511

12.5.7 工具栏  515

12.5.8 工具提示  516

12.6 复杂的布局管理  518

12.6.1 网格组布局  520

12.6.2 组布局  528

12.6.3 不使用布局管理器  537

12.6.4 定制布局管理器  537

12.6.5 遍历顺序  541

12.7 对话框  541

12.7.1 选项对话框  542

12.7.2 创建对话框  551

12.7.3 数据交换  554

12.7.4 文件对话框  559

12.7.5 颜色选择器  569

12.8 GUI程序排错  573

12.8.1 调试技巧  573

12.8.2 让AWT机器人完成工作  576

第13章 部署Java应用程序  580

13.1 JAR文件  580

13.1.1 创建JAR文件  580

13.1.2 清单文件  581

13.1.3 可执行JAR文件  582

13.1.4 资源  583

13.1.5 密封  585

13.2 应用首选项的存储  586

13.2.1 属性映射  586

13.2.2 首选项API  591

13.3 服务加载器  596

13.4 applet  598

13.4.1 一个简单的applet  599

13.4.2 applet HTML标记和属性  602

13.4.3 使用参数向applet传递信息  603

13.4.4 访问图像和音频文件  608

13.4.5 applet上下文  609

13.4.6 applet间通信  609

13.4.7 在浏览器中显示信息项  610

13.4.8 沙箱  611

13.4.9 签名代码  612

13.5 Java Web Start  614

13.5.1 发布Java Web Start应用  614

13.5.2 JNLP API  617

第14章 并发  624

14.1 什么是线程  624

14.1.1 使用线程给其他任务提供机会  629

14.2 中断线程  632

14.3 线程状态  635

14.3.1 新创建线程  635

14.3.2 可运行线程  635

14.3.3 被阻塞线程和等待线程  636

14.3.4 被终止的线程  636

14.4 线程属性  638

14.4.1 线程优先级  638

14.4.2 守护线程  639

14.4.3 未捕获异常处理器  639

14.5 同步  640

14.5.1 竞争条件的一个例子  641

14.5.2 竞争条件详解  644

14.5.3 锁对象  646

14.5.4 条件对象  648

14.5.5 synchronized关键字  653

14.5.6 同步阻塞  656

14.5.7 监视器概念  657

14.5.8 Volatile域  658

14.5.9 f?inal变量  659

14.5.10 原子性  659

14.5.11 死锁  661

14.5.12 线程局部变量  663

14.5.13 锁测试与超时  665

14.5.14 读/写锁  666

14.5.15 为什么弃用stop和suspend方法  667

14.6 阻塞队列  668

14.7 线程安全的集合  673

14.7.1 高效的映射、集和队列  674

14.7.2 映射条目的原子更新  675

14.7.3 对并发散列映射的批操作  676

14.7.4 并发集视图  678

14.7.5 写数组的拷贝  679

14.7.6 并行数组算法  679

14.7.7 较早的线程安全集合  680

14.8 Callable与Future  681

14.9 执行器  685

14.9.1 线程池  685

14.9.2 预定执行  689

14.9.3 控制任务组  690

14.9.4 Fork-Join框架  691

14.9.5 可完成Future  694

14.10 同步器  696

14.10.1 信号量  696

14.10.2 倒计时门栓  697

14.10.3 障栅  697

14.10.4 交换器  698

14.10.5 同步队列  698

14.11 线程与Swing  698

14.11.1 运行耗时的任务  699

14.11.2 使用Swing工作线程  703

14.11.3 单一线程规则  708

附录A Java关键字  710

第1章 Java程序设计概述

▲  Java程序设计平台 ▲  Java发展简史

▲  Java“白皮书”的关键术语 ▲  关于Java的常见误解

▲  Java applet与Internet

1996年Java第一次发布就引起了人们的极大兴趣。关注Java的人士不仅限于计算机出版界,还有诸如《纽约时报》《华盛顿邮报》《商业周刊》这样的主流媒体。Java是第一种也是唯一一种在National Public Radio上占用了10分钟时间来进行介绍的程序设计语言,并且还得到了$100 000 000的风险投资基金。这些基金全部用来支持用这种特别的计算机语言开发的产品。重温那些令人兴奋的日子是很有意思的。本章将简要地介绍一下Java语言的发展历史。

1.1 Java程序设计平台

本书的第1版是这样描写Java的:“作为一种计算机语言,Java的广告词确实有点夸大其辞。然而,Java的确是一种优秀的程序设计语言。作为一个名副其实的程序设计人员,使用Java无疑是一个好的选择。有人认为:Java将有望成为一种最优秀的程序设计语言,但还需要一个相当长的发展时期。一旦一种语言应用于某个领域,与现存代码的相容性问题就摆在了人们的面前。”

我们的编辑手中有许多这样的广告词。这是Sun公司高层的某位不愿透露姓名的人士提供的(Sun是原先开发Java的公司)。Java有许多非常优秀的语言特性,本章稍后将会详细地讨论这些特性。由于相容性这个严峻的问题确实存在于现实中,所以,或多或少地还是有一些“累赘”被加到语言中,这就导致Java并不如想象中的那么完美无瑕。

但是,正像我们在第1版中已经指出的那样,Java并不只是一种语言。在此之前出现的那么多种语言也没有能够引起那么大的轰动。Java是一个完整的平台,有一个庞大的库,其中包含了很多可重用的代码和一个提供诸如安全性、跨操作系统的可移植性以及自动垃圾收集等服务的执行环境。

作为一名程序设计人员,常常希望能够有一种语言,它具有令人赏心悦目的语法和易于理解的语义(C++不是这样的)。与许多其他的优秀语言一样,Java完全满足了这些要求。有些语言提供了可移植性、垃圾收集等,但是,没有提供一个大型的库。如果想要有奇特的绘图功能、网络连接功能和数据库存取功能就必须自己动手编写代码。Java具备所有这些特性,它是一种功能齐全的出色语言,是一个高质量的执行环境,还提供了一个庞大的库。正是因为它集多种优势于一身,所以对广大的程序设计人员有着不可抗拒的吸引力。

1.2 Java“白皮书”的关键术语

Java的设计者已经编写了颇有影响力的“白皮书”,用来解释设计的初衷以及完成的情况,并且发布了一个简短的摘要。这个摘要用下面11个关键术语进行组织:

1)简单性 7)可移植性

2)面向对象 8)解释型

3)分布式 9)高性能

4)健壮性 10)多线程

5)安全性 11)动态性

6)体系结构中立

本节将提供一个小结,给出白皮书中相关的说明,这是Java设计者对各个关键术语的论述,另外还会根据我们对Java当前版本的使用经验,给出对这些术语的理解。

注释:写这本书时,白皮书可以在www.oracle.com/technetwork/java/langenv-140151.html上找到。对于11个关键术语的论述请参看http://horstmann.com/corejava/java-an-overview/7Gosling.pdf。

1.2.1 简单性

人们希望构建一个无须深奥的专业训练就可以进行编程的系统,并且要符合当今的标准惯例。因此,尽管人们发现C++不太适用,但在设计Java的时候还是尽可能地接近C++,以便系统更易于理解。Java剔除了C++中许多很少使用、难以理解、易混淆的特性。在目前看来,这些特性带来的麻烦远远多于其带来的好处。

的确,Java语法是C++语法的一个“纯净”版本。这里没有头文件、指针运算(甚至指针语法)、结构、联合、操作符重载、虚基类等(请参阅本书各个章节给出的C++注释,其中比较详细地解释了Java与C++之间的区别)。然而,设计者并没有试图清除C++中所有不适当的特性。例如,switch语句的语法在Java中就没有改变。如果你了解C++就会发现可以轻而易举地转换到Java语法。

Java发布时,实际上C++并不是最常用的程序设计语言。很多开发人员都在使用Visual Basic和它的拖放式编程环境。这些开发人员并不觉得Java简单。很多年之后Java开发环境才迎头赶上。如今,Java开发环境已经远远超出大多数其他编程语言的开发环境。

简单的另一个方面是小。Java的目标之一是支持开发能够在小型机器上独立运行的软件。基本的解释器以及类支持大约仅为40KB;再加上基础的标准类库和对线程的支持(基本上是一个自包含的微内核)大约需要增加175KB。

在当时,这是一个了不起的成就。当然,由于不断的扩展,类库已经相当庞大了。现在有一个独立的具有较小类库的Java微型版(Java Micro Edition),这个版本适用于嵌入式设备。

1.2.2 面向对象

简单地讲,面向对象设计是一种程序设计技术。它将重点放在数据(即对象)和对象的接口上。用木匠打一个比方,一个“面向对象的”木匠始终关注的是所制作的椅子,第二位才是所使用的工具;一个“非面向对象的”木匠首先考虑的是所用的工具。在本质上,Java的面向对象能力与C++是一样的。

开发Java时面向对象技术已经相当成熟。Java的面向对象特性与C++旗鼓相当。Java与C++的主要不同点在于多重继承,在Java中,取而代之的是更简单的接口概念。与C++相比,Java提供了更丰富的运行时自省功能(有关这部分内容将在第5章中讨论)。

1.2.3 分布式

Java有一个丰富的例程库,用于处理像HTTP和FTP之类的TCP/IP协议。Java应用程序能够通过URL打开和访问网络上的对象,其便捷程度就好像访问本地文件一样。

如今,这一点已经得到认可,不过在1995年,主要还是从C++或Visual Basic程序连接Web服务器。

1.2.4 健壮性

Java的设计目标之一在于使得Java编写的程序具有多方面的可靠性。Java投入了大量的精力进行早期的问题检测、后期动态的(运行时)检测,并消除了容易出错的情况……Java和C++最大的不同在于Java采用的指针模型可以消除重写内存和损坏数据的可能性。

Java编译器能够检测许多在其他语言中仅在运行时才能够检测出来的问题。至于第二点,对于曾经花费几个小时来检查由于指针bug而引起内存冲突的人来说,一定很喜欢Java的这一特性。

1.2.5 安全性

Java适用于网络/分布式环境。为了达到这个目标,在安全方面投入了很大精力。使用Java可以构建防病毒、防篡改的系统。

从一开始,Java就设计成能够防范各种攻击,其中包括:

运行时堆栈溢出。如蠕虫和病毒常用的攻击手段。

破坏自己的进程空间之外的内存。

未经授权读写文件。

原先,Java对下载代码的态度是“尽管来吧!”。不可信代码在一个沙箱环境中执行,在这里它不会影响主系统。用户可以确信不会发生不好的事情,因为Java代码不论来自哪里,都不能脱离沙箱。

不过,Java的安全模型很复杂。Java开发包(Java Development Kit,JDK)的第一版发布之后不久,普林斯顿大学的一些安全专家就发现一些小bug会允许不可信的代码攻击主系统。

最初,安全bug可以快速修复。遗憾的是,经过一段时间之后,黑客已经很擅长找出安全体系结构实现中的小漏洞。Sun以及之后的Oracle为修复bug度过了一段很是艰难的日子。

遭遇多次高调攻击之后,浏览器开发商和Oracle都越来越谨慎。Java浏览器插件不再信任远程代码,除非代码有数字签名而且用户同意执行这个代码。

注释:现在看来,尽管Java安全模型没有原先预想的那么成功,但Java在那个时代确实相当超前。微软提供了一种与之竞争的代码传输机制,其安全性完全依赖于数字签名。显然这是不够的,因为微软自身产品的任何用户都可以证实,知名开发商的程序确实会崩溃并对系统产生危害。

1.2.6 体系结构中立

编译器生成一个体系结构中立的目标文件格式,这是一种编译过的代码,只要有Java运行时系统,这些编译后的代码可以在许多处理器上运行。Java编译器通过生成与特定的计算机体系结构无关的字节码指令来实现这一特性。精心设计的字节码不仅可以很容易地在任何机器上解释执行,而且还可以动态地翻译成本地机器代码。

当时,为“虚拟机”生成代码并不是一个新思路。诸如Lisp、Smalltalk和Pascal等编程语言多年前就已经采用了这种技术。

当然,解释虚拟机指令肯定会比全速运行机器指令慢很多。然而,虚拟机有一个选项,可以将执行最频繁的字节码序列翻译成机器码,这一过程被称为即时编译。

Java虚拟机还有一些其他的优点。它可以检测指令序列的行为,从而增强其安全性。

1.2.7 可移植性

与C和C++不同,Java规范中没有“依赖具体实现”的地方。基本数据类型的大小以及有关运算都做了明确的说明。

例如,Java中的int永远为32位的整数,而在C/C++中,int可能是16位整数、32位整数,也可能是编译器提供商指定的其他大小。唯一的限制只是int类型的大小不能低于short int,并且不能高于long int。在Java中,数据类型具有固定的大小,这消除了代码移植时令人头痛的主要问题。二进制数据以固定的格式进行存储和传输,消除了字节顺序的困扰。字符串是用标准的Unicode格式存储的。

作为系统组成部分的类库,定义了可移植的接口。例如,有一个抽象的Window类,并给出了在UNIX、Windows和Macintosh环境下的不同实现。

选择Window类作为例子可能并不太合适。凡是尝试过的人都知道,要编写一个在Windows、Macintosh和10种不同风格的UNIX上看起来都不错的程序有多么困难。Java 1.0就尝试着做了这么一个壮举,发布了一个将常用的用户界面元素映射到不同平台上的简单工具包。遗憾的是,花费了大量的心血,却构建了一个在各个平台上都难以让人接受的库。原先的用户界面工具包已经重写,而且后来又再次重写,不过跨平台的可移植性仍然是个问题。

不过,除了与用户界面有关的部分外,所有其他Java库都能很好地支持平台独立性。你可以处理文件、正则表达式、XML、日期和时间、数据库、网络连接、线程等,而不用操心底层操作系统。不仅程序是可移植的,Java API往往也比原生API质量更高。

1.2.8 解释型

Java解释器可以在任何移植了解释器的机器上执行Java字节码。由于链接是一个增量式且轻量级的过程,所以,开发过程也变得更加快捷,更加具有探索性。

这看上去很不错。用过Lisp、Smalltalk、Visual Basic、Python、R或Scala的人都知道“快捷而且具有探索性”的开发过程是怎样的。你可以做些尝试,然后就能立即看到结果。Java开发环境并没有将重点放在这种体验上。

1.2.9 高性能

尽管对解释后的字节码性能已经比较满意,但在有些场合下还需要更加高效的性能。字节码可以(在运行时刻)动态地翻译成对应运行这个应用的特定CPU的机器码。

使用Java的头几年,许多用户不同意这样的看法:性能就是“适用性更强”。然而,现在的即时编译器已经非常出色,以至于成了传统编译器的竞争对手。在某些情况下,甚至超越了传统编译器,原因是它们含有更多的可用信息。例如,即时编译器可以监控经常执行哪些代码并优化这些代码以提高速度。更为复杂的优化是消除函数调用(即“内联”)。即时编译器知道哪些类已经加载。基于当前加载的类集,如果特定的函数不会被覆盖,就可以使用内联。必要时,还可以撤销优化。

1.2.10 多线程

多线程可以带来更好的交互响应和实时行为。

如今,我们非常关注并发性,因为摩尔定律行将完结。我们不再追求更快的处理器,而是着眼于获得更多的处理器,而且要让它们一直保持工作。不过,可以看到,大多数编程语言对于这个问题并没有显示出足够的重视。

Java在当时很超前。它是第一个支持并发程序设计的主流语言。从白皮书中可以看到,它的出发点稍有些不同。当时,多核处理器还很神秘,而Web编程才刚刚起步,处理器要花很长时间等待服务器响应,需要并发程序设计来确保用户界面不会“冻住”。

并发程序设计绝非易事,不过Java在这方面表现很出色,可以很好地管理这个工作。

1.2.11 动态性

从各种角度看,Java与C或C++相比更加具有动态性。它能够适应不断发展的环境。库中可以自由地添加新方法和实例变量,而对客户端却没有任何影响。在Java中找出运行时类型信息十分简单。

当需要将某些代码添加到正在运行的程序中时,动态性将是一个非常重要的特性。一个很好的例子是:从Internet下载代码,然后在浏览器上运行。如果使用C或C++,这确实难度很大,不过Java设计者很清楚动态语言可以很容易地实现运行程序的演进。最终,他们将这一特性引入这个主流程序设计语言中。

注释:Java成功地推出后不久,微软就发布了一个叫做J++的产品,它与Java有几乎相同的编程语言以及虚拟机。现在,微软不再支持J++,取而代之的是另一种名为C#的语言。C#与Java有很多相似之处,然而使用的却是完全不同的虚拟机。本书不准备介绍J++或C#语言。

1.3 Java applet与Internet

这里的想法很简单:用户从Internet下载Java字节码,并在自己的机器上运行。在网页中运行的Java程序称为applet。要使用applet,需要启用Java的Web浏览器执行字节码。不需要安装任何软件。任何时候只要访问包含applet的网页都会得到程序的最新版本。最重要的是,要感谢虚拟机的安全性,它让我们不必再担心来自恶意代码的攻击。

在网页中插入一个applet就如同在网页中嵌入一幅图片。applet会成为页面的一部分。文本环绕着applet所占据的空间周围。关键的一点是这个图片是活动的。它可以对用户命令做出响应,改变外观,在运行它的计算机与提供它的计算机之间传递数据。

图1-1展示了一个很好的动态网页的例子。Jmol applet显示了分子结构,这将需要相当复杂的计算。在这个网页中,可以利用鼠标进行旋转,调整焦距等操作,以便更好地理解分子结构。用静态网页就无法实现这种直接的操作,而applet却可以达到此目的(可以在http://jmol.sourceforge.net上找到这个applet)。

图1-1 Jmol applet

当applet首次出现时,人们欣喜若狂。许多人相信applet的魅力将会导致Java迅速地流行起来。然而,初期的兴奋很快就淡化了。不同版本的Netscape与Internet Explorer运行不同版本的Java,其中有些早已过时。这种糟糕的情况导致更加难于利用Java的最新版本开发applet。实际上,为了在浏览器中得到动态效果,Adobe的Flash技术变得相当流行。后来,Java遭遇了严重的安全问题,浏览器和Java浏览器插件变得限制越来越多。如今,要在浏览器中使用applet,这不仅需要一定的水平,而且要付出努力。例如,如果访问Jmol网站,可能会看到一个消息,警告你要适当地配置浏览器允许运行applet。

1.4 Java发展简史

本节将介绍Java的发展简史。这些内容来自很多出版资料(最重要的是SunWorld的在线杂志1995年7月刊上对Java创建者的专访)。

Java的历史要追溯到1991年,由Patrick Naughton和James Gosling(一个全能的计算机奇才)带领的Sun公司的工程师小组想要设计一种小型的计算机语言,主要用于像有线电视转换盒这类的消费设备。由于这些消费设备的处理能力和内存都很有限,所以语言必须非常小且能够生成非常紧凑的代码。另外,由于不同的厂商会选择不同的中央处理器(CPU),因此这种语言的关键是不与任何特定的体系结构捆绑在一起。这个项目被命名为“Green”。

代码短小、紧凑且与平台无关,这些要求促使开发团队设计一个可移植的语言,可以为虚拟机生成中间代码。

不过,Sun公司的人都有UNIX的应用背景。因此,所开发的语言以C++为基础,而不是Lisp、Smalltalk或Pascal。不过,就像Gosling在专访中谈到的:“毕竟,语言只是实现目标的工具,而不是目标本身”。Gosling把这种语言称为“Oak”(这么起名的原因大概是因为他非常喜欢自己办公室外的橡树)。Sun公司的人后来发现Oak是一种已有的计算机语言的名字,于是,将其改名为Java。事实证明这是一个很有灵感的选择。

1992年,Green项目发布了它的第一个产品,称之为“*7”。这个产品具有非常智能的远程控制。遗憾的是,Sun公司对生产这个产品并不感兴趣,Green项目组的人员必须找出其他的方法来将他们的技术推向市场。然而,没有一个标准消费品电子公司对此感兴趣。于是,Green项目组竞标了一个提供视频点播等新型服务的有线电视盒的项目,但没有成功(有趣的是,得到这个项目的公司的领导恰恰是开创Netscape公司的Jim Clark。Netscape公司后来对Java的成功给予了很大的帮助)。

Green项目(这时换了一个新名字——“First Person公司”)花费了1993年一整年以及1994年的上半年,一直在苦苦寻求其技术的买家。然而,一个也没有找到(Patrick Naughton,项目组的创立人之一,也是完成此项目大多数市场工作的人,声称为了销售这项技术,累计飞行了300 000英里)。1994年First Person公司解散了。

当这一切在Sun公司发生的时候,Internet的万维网也在日渐发展壮大。万维网的关键是把超文本页面转换到屏幕上的浏览器。1994年大多数人都在使用Mosaic,这是一个1993年出自伊利诺斯大学超级计算中心的非商业化的Web浏览器(Mosaic的一部分是由Marc Andreessen编写的。当时,他作为一名参加半工半读项目的本科生,编写了这个软件,每小时的薪水只有6.85美元。他后来成了Netscape公司的创始人之一和技术总监,可谓名利双收)。

在接受SunWorld采访的时候,Gosling说在1994年中期,Java语言的开发者意识到:“我们能够建立一个相当酷的浏览器。我们已经拥有在客户机/服务器主流模型中所需要的体系结构中立、实时、可靠、安全——这些在工作站环境并不太重要,所以,我们决定开发浏览器。”

实际的浏览器是由Patrick Naughton和Jonathan Payne开发的,并演变为HotJava浏览器。为了炫耀Java语言超强的能力,HotJava浏览器采用Java编写。设计者让HotJava浏览器具有在网页中执行内嵌代码的能力。这一“技术印证”在1995年5月23日的SunWorld上得到展示,同时引发了人们延续至今的对Java的狂热追逐。

1996年年初,Sun发布了Java的第1个版本。人们很快地意识到Java1.0不能用来进行真正的应用开发。的确,可以使用Java 1.0来实现在画布上随机跳动的神经质的文本applet,但它却没有提供打印功能。坦率地说,Java 1.0的确没有为其黄金时期的到来做好准备。后来的Java 1.1弥补了其中的大多明显的缺陷,大大改进了反射能力,并为GUI编程增加了新的事件处理模型。不过它仍然具有很大的局限性。

1998年JavaOne会议的头号新闻是即将发布Java 1.2版。这个版本取代了早期玩具式的GUI,并且它的图形工具箱更加精细而具有可伸缩性,更加接近“一次编写,随处运行”的承诺。在1998年12月Java 1.2发布三天之后,Sun公司市场部将其名称改为更加吸引人的“Java 2标准版软件开发工具箱1.2版”。

除了“标准版”之外,Sun还推出了两个其他的版本:一个是用于手机等嵌入式设备的“微型版”;另一个是用于服务器端处理的“企业版”。本书主要讲述标准版。

标准版的1.3和1.4版本对最初的Java 2版本做出了某些改进,扩展了标准类库,提高系统性能。当然,还修正了一些bug。在此期间,Java applet采用低调姿态,并淡化了客户端的应用,但Java却成为服务器端应用的首选平台。

5.0版是自1.1版以来第一个对Java语言做出重大改进的版本(这一版本原来被命名为1.5版,在2004年的JavaOne会议之后,版本数字升至5.0)。经历了多年的研究,这个版本添加了泛型类型(generic type)(类似于C++的模板),其挑战性在于添加这一特性并没有对虚拟机做出任何修改。另外,还有几个受C#启发的很有用的语言特性:“for each”循环、自动装箱和注解。

版本6(没有后缀.0)于2006年年末发布。同样,这个版本没有对语言方面再进行改进。但是,改进了其他性能,并增强了类库。

随着数据中心越来越依赖于商业硬件而不是专用服务器,Sun Microsystems终于沦陷,于2009年被Oracle收购。Java的开发停滞了很长一段时间。直到2011年Oracle发布了Java的一个新版本,Java 7,其中只做了一些简单的改进。

2014年,Java 8终于发布,在近20年中这个版本有了最大的改变。Java 8提供了一种“函数式”编程方式,可以很容易地表述并发执行的计算。所有编程语言都必须与时俱进,Java在这方面显示出非凡的能力。

表1-1展示了Java语言以及类库的发展状况。可以看到,应用程序编程接口(API)的规模发生了惊人的变化。

表1-1 Java语言的发展状况

版  本 年  份 语言新特性 类与接口的数量

1.0 1996 语言本身 211

1.1 1997 内部类 477

1.2 1998 strictfp修饰符 1524

1.3 2000 无 1840

1.4 2002 断言 2723

5.0 2004 泛型类、“for each”循环、可变元参数、自动装箱、元数据、枚举、静态导入 3279

6 2006 无 3793

7 2011 基于字符串的switch、钻石操作符、二进制字面量、异常处理改进 4024

8 2014 lambda表达式,包含默认方法的接口,流和日期/时间库 4240

1.5 关于Java的常见误解

在结束本章之前,我们列出了一些关于Java的常见误解,同时给出了解释。

1.?Java是HTML的扩展

Java是一种程序设计语言;HTML是一种描述网页结构的方式。除了用于在网页上放置Java applet的HTML扩展之外,两者没有任何共同之处。

2.?使用XML,所以不需要Java

Java是一种程序设计语言;XML是一种描述数据的方式。可以使用任何一种程序设计语言处理XML数据,而Java API对XML处理提供了很好的支持。此外,许多重要的第三方XML工具采用Java编写。有关这方面更加详细的信息请参看卷Ⅱ。

3.?Java是一种非常容易学习的程序设计语言

像Java这种功能强大的语言大都不太容易学习。首先,必须将编写玩具式程序的轻松和开发实际项目的艰难区分开来。需要注意的是:本书只用了7章讨论Java语言。在两卷中,其他的章节介绍如何使用Java类库将Java语言应用到实际中去。Java类库包含了数千种类和接口以及数万个函数。幸运的是,并不需要知道它们中的每一个,然而,要想Java解决实际问题,还是需要了解不少内容的。

4.?Java将成为适用于所有平台的通用性编程语言

从理论上讲,这是完全有可能的。但在实际中,某些领域其他语言有更出色的表现,比如,Objective C和后来的Swift在iOS设备上就有着无可取代的地位。浏览器中的处理几乎完全由JavaScript掌控。Windows程序通常都用C++或C#编写。Java在服务器端编程和跨平台客户端应用领域则很有优势。

5.?Java只不过是另外一种程序设计语言

Java是一种很好的程序设计语言,很多程序设计人员喜欢Java胜过C、C++或C#。有上百种好的程序设计语言没有广泛地流行,而带有明显缺陷的语言,如:C++和Visual Basic却大行其道。

这是为什么呢?程序设计语言的成功更多地取决于其支撑系统的能力,而不是优美的语法。人们主要关注:是否提供了易于实现某些功能的易用、便捷和标准的库?是否有开发工具提供商能建立强大的编程和调试环境?语言和工具集是否能够与其他计算基础架构整合在一起?Java的成功源于其类库能够让人们轻松地完成原本有一定难度的事情。例如:联网Web应用和并发。Java减少了指针错误,这是一个额外的好处,因此使用Java编程的效率更高。但这些并不是Java成功的全部原因。

6.?Java是专用的,应该避免使用

最初创建Java时,Sun为销售者和最终用户提供了免费许可。尽管Sun对Java拥有最终的控制权,不过在语言版本的不断发展和新库的设计过程中还涉及很多其他公司。虚拟机和类库的源代码可以免费获得,不过仅限于查看,而不能修改和再发布。Java是“闭源的,不过可以很好地使用”。

这种状况在2007年发生了戏剧性的变化,Sun声称Java未来的版本将在General Public License(GPL)下提供。Linux使用的是同一个开放源代码许可。Oracle一直致力于保持Java开源。只有一点美中不足——专利。根据GPL,任何人都可以得到专利许可,允许其使用和修改Java,不过仅限于桌面和服务器平台。如果你想在嵌入式系统中使用Java,就需要另外一个不同的许可,这很可能需要付费。不过,这些专利在未来十年就会到期,那时Java就完全免费了。

7.?Java是解释型的,因此对于关键的应用程序速度太慢了

早期的Java是解释型的。现在Java虚拟机使用了即时编译器,因此采用Java编写的“热点”代码其运行速度与C++相差无几,有些情况下甚至更快。

对于Java桌面应用速度慢,人们已经抱怨很多年了。但是,今天的计算机速度远比人们发出抱怨的时候快了很多。一个较慢的Java程序与几年前相当快的C++程序相比还要快一些。

8.?所有的Java程序都是在网页中运行的

所有的Java applet都是在网页浏览器中运行的。这也恰恰是applet的定义,即一种在浏览器中运行的Java程序。然而,大多数Java程序是运行在Web浏览器之外的独立应用程序。实际上,很多Java程序都在Web服务器上运行并生成用于网页的代码。

9.?Java程序是主要的安全风险

对于早期的Java,有过关于安全系统失效的报道,曾经一度引起公众哗然。研究人员将这视为一种挑战,即努力找出Java的漏洞,对applet安全模型的强度和复杂度发起挑战。随后,人们很快就解决了引发问题的所有技术因素。后来又发现了更严重的漏洞,而Sun以及后来的Oracle反应却过于迟缓。浏览器制造商则有些反应过度,他们甚至默认禁用了Java。客观地来讲,可以想想针对Windows可执行文件和Word宏有数百万种病毒攻击,并造成了巨大的损害,不过奇怪的是却很少有人批评被攻击平台的脆弱。

有些系统管理员甚至在公司浏览器中禁用了Java,而同时却允许用户下载可执行文件和Word文档,实际上,这些带来的风险远甚于使用Java。尽管距离Java诞生已经20年之久,与其他常用的执行平台相比,Java还是安全得多。

10.?JavaScript是Java的简易版

JavaScript是一种在网页中使用的脚本语言,它是由Netscape发明的,原来的名字叫做LiveScript。JavaScript的语法类似Java,除此之外,两者无任何关系。当然,名字有些相像。JavaScript的一个子集已经标准化为ECMA-262。与Java applet相比,JavaScript更紧密地与浏览器集成在一起。特别是JavaScript程序可以修改正在显示的文档,而applet只能在有限的区域内控制外观。

11.?使用Java可以用廉价的Internet设备取代桌面计算机

当Java刚刚发布的时候,一些人打赌:肯定会有这样的好事情发生。一些公司已经生产出Java网络计算机的原型,不过用户还不打算放弃功能强大而便利的桌面计算机,而去使用没有本地存储而且功能有限的网络设备。当然,如今世界已经发生改变,对于大多数最终用户,常用的平台往往是手机或平板电脑。这些设备大多使用安卓平台,这是Java的衍生产物。学习Java编程肯定也对Android编程很有帮助。

第2章 Java程序设计环境

▲ 安装Java开发工具包 ▲ 运行图形化应用程序

▲ 使用命令行工具 ▲ 构建并运行applet

▲ 使用集成开发环境

本章主要介绍如何安装Java开发工具包(JDK)以及如何编译和运行不同类型的程序:控制台程序、图形化应用程序以及applet。运行JDK工具的方法是在终端窗口中键入命令。然而,很多程序员更喜欢使用集成开发环境。为此,将在稍后介绍如何使用免费的开发环境编译和运行Java程序。尽管学起来很容易,但集成开发环境需要吞噬大量资源,编写小型程序时也比较烦琐。一旦掌握了本章的技术,并选定了自己的开发工具,就可以学习第3章,开始研究Java程序设计语言。

2.1 安装Java开发工具包

Oracle公司为Linux、Mac OS X、Solaris和Windows提供了Java开发工具包(JDK)的最新、最完整的版本。用于很多其他平台的版本仍处于多种不同的开发状态中,不过,这些版本都由相应平台的开发商授权并分发。

2.1.1 下载JDK

要想下载Java开发工具包,可以访问Oracle网站:www.oracle.com/technetwork/java/javase/downloads,在得到所需的软件之前必须弄清楚大量专业术语。请看表2-1的总结。

表2-1 Java术语

术 语 名 缩写 解  释

Java Development Kit JDK 编写Java程序的程序员使用的软件

Java Runtime Environment JRE 运行Java程序的用户使用的软件

Server JRE — 在服务器上运行Java程序的软件

Standard Edition SE 用于桌面或简单服务器应用的Java平台

Enterprise Edition  EE 用于复杂服务器应用的Java平台

Micro Edition ME 用于手机和其他小型设备的Java平台

Java FX — 用于图形化用户界面的一个替代工具包,在Oracle的Java SE发布版本中提供

OpenJDK — Java SE的一个免费开源实现,不包含浏览器集成或JavaFX

Java 2 J2 一个过时的术语,用于描述1998年~2006年之间的Java版本

Software Development Kit SDK 一个过时的术语,用于描述1998年~2006年之间的JDK 

Update u Oracle的术语,表示bug修正版本

NetBeans — Oracle的集成开发环境

你已经看到,JDK是Java Development Kit的缩写。有点混乱的是:这个工具包的版本1.2~版本1.4被称为Java SDK(软件开发包,Software Development Kit)。在某些场合下,还可以看到这个过时的术语。另外,还有一个术语是Java运行时环境(JRE),它包含虚拟机但不包含编译器。这并不是开发者想要的环境,而是专门为不需要编译器的用户而提供。

接下来,Java SE会大量出现,相对于Java EE(Enterprise Edition)和Java ME(Micro Edition),它是Java的标准版。

Java 2这种提法始于1998年。当时Sun公司的销售人员感觉增加小数点后面的数值改变版本号并没有反映出JDK 1.2的重大改进。但是,由于在发布之后才意识到这个问题,所以决定开发工具包的版本号仍然沿用1.2,接下来的版本是1.3、1.4和5.0。但是,Java平台被重新命名为Java 2。因此,就有了Java 2 Standard Edition Software Development Kit(Java 2标准版软件开发包)的5.0版,即J2SE SDK 5.0。

幸运的是,2006年版本号得到简化。Java标准版的下一个版本取名为Java SE 6,后来又有了Java SE 7和Java SE 8。不过,“内部”版本号分别是1.6.0、1.7.0和1.8.0。

当Oracle为解决一些紧急问题做出某些微小的版本改变时,将其称为更新。例如:Java SE 8u31是Java SE 8的第31次更新,它的内部版本号是1.8.0_31。更新不需要安装在前一个版本上,它会包含整个JDK的最新版本。另外,并不是所有更新都公开发布,所以如果“更新31”之后没有“更新32”,你也不用惊慌。

对于Windows或Linux,需要在x86(32位)和x64(64位)版本之间做出选择。应当选择与你的操作系统体系结构匹配的版本。

对于Linux,还可以在RPM文件和.tar.gz文件之间做出选择。我们建议使用后者,可以在你希望的任何位置直接解压缩这个压缩包。

现在你已经了解了如何选择适当的JDK。下面做一个小结:

你需要的是JDK(Java SE开发包),而不是JRE。

Windows或Linux:32位选择x86,64位以x64。

Linux:选择.tar.gz版本。

接受许可协议,然后下载文件。

注释:Oracle提供了一个捆绑包,其中包含Java开发包(JDK)和NetBeans集成开发环境。建议现在不要安装任何捆绑包,而只需安装Java开发包。如果以后你打算使用NetBeans,可以再从http://netbeans.org下载。

2.1.2 设置JDK

下载JDK之后,需要安装这个开发包并明确要在哪里安装,后面还会需要这个信息。

在Windows上,启动安装程序。会询问你要在哪里安装JDK。最好不要接受路径名中包含空格的默认位置,如c:\Program Files\Java\jdk1.8.0_version。取出路径名中的Program Files部分就可以了。

在Mac上,运行安装程序。这会把软件安装到/Library/Java/JavaVirtualMachines/jdk1.8.0_version.jdk/Contents/Home。用Finder找到这个目录。

在Linux上,只需要把.tar.gz文件解压缩到你选择的某个位置,如你的主目录,或者/opt。如果从RPM文件安装,则要反复检查是否安装在/usr/java/jdk1.8.0_version。

在这本书中,安装目录用jdk表示。例如,谈到jdk/bin目录时,是指/opt/jdk1.8.0_31/bin或c:\Java\jdk1.8.0_31\bin目录。

在Windows或Linux上安装JDK时,还需要另外完成一个步骤:将jdk/bin目录增加到执行路径中——执行路径是操作系统查找可执行文件时所遍历的目录列表。

在Linux上,需要在?/.bashrc或?/.bash_prof?ile文件的最后增加这样一行:

一定要使用JDK的正确路径,如/opt/jdk1.8.0_31。

在Windows上,启动控制面板,选择“系统与安全”(System and Security),再选择“系统”(System),选择高级系统设置(Advanced System Settings)(参见图2-1)。在系统属性(System Properties)对话框中,点击“高级”(Advanced)标签页,然后点击“环境”(Environment)按钮。

图2-1 Windows 7中设置系统属性

滚动“系统变量”(System Variables)列表,直到找到名为Path的变量。点击“编辑”(Edit)按钮(参见图2-2)。将jdk\bin目录增加到路径最前面,并用一个分号分隔新增的这一项,如下所示:

图2-2 Windows 7中设置Path环境变量

注意要把jdk替换为具体的Java安装路径,如c:\Java\jdk1.8.0_31。如果忽视前面的建议,想要保留Program Files部分,则要把整个路径用双引号引起来:"c:\Program Files\Java\jdk1.8.0_31\bin";其他目录。

保存所做的设置。之后新打开的所有控制台窗口都会有正确的路径。

可以如下测试设置是否正确:打开一个终端窗口,键入:

然后按回车键。应该能看到显示以下信息:

如果得到诸如“javac: command not found”(javac::命令未找到)或“The name specif?ied is not recognized as an internal or external command, operable program or batch f?ile”(指定名不是一个内部或外部命令、可执行的程序或批文件),就需要退回去反复检查你的安装。

2.1.3 安装库源文件和文档

库源文件在JDK中以一个压缩文件src.zip的形式发布,必须将其解压缩后才能够访问源代码。建议按照下面所述的步骤进行操作。很简单:

1)确保JDK已经安装,并且jdk/bin目录在执行路径中。

2)在主目录中建立一个目录javasrc。如果愿意,可以在一个终端窗口完成这个步骤。

3)在jdk目录下找到文件src.zip。

4)将src.zip文件解压缩到javasrc目录。在一个终端窗口中,可以执行以下命令:

提示:src.zip文件中包含了所有公共类库的源代码。要想获得更多的源代码(例如:编译器、虚拟机、本地方法以及私有辅助类),请访问网站:http://jdk8.java.net。

文档包含在一个压缩文件中,它是一个独立于JDK的压缩文件。可以直接从网站http://www.oracle.com/technetwork/java/javase/downloads下载这个文档。操作步骤如下:

1)下载文档压缩文件。这个文件名为jdk-version-docs-all.zip,其中的version表示版本号,例如8u31。

2)解压缩这个文件,将doc目录重命名为一个更有描述性的名字,如javadoc。如果愿意,可以从命令行完成这个工作:

这里version是相应的版本号。

3)在浏览器中导航到javadoc/api/index.html,将这个页面增加到书签。

还要安装本书的程序示例。可以从http://horstmann.com/corejava下载示例。这些程序打包在一个zip文件corejava.zip中。可以将程序解压缩到你的主目录。它们会放在目录corejava中。如果愿意,可以从命令行完成这个工作:

2.2 使用命令行工具

如果在此之前有过使用Microsoft Visual Studio等开发环境编程的经验,你可能会习惯于有一个内置文本编辑器、用于编译和启动程序的菜单以及调试工具的系统。JDK完全没有这些功能。所有工作都要在终端窗口中键入命令来完成。这听起来很麻烦,不过确实是一个基本技能。第一次安装Java时,你可能希望在安装开发环境之前先检查Java的安装是否正确。另外,通过自己执行基本步骤,你可以更好地理解开发环境的后台工作。

不过,掌握了编译和运行Java程序的基本步骤之后,你可能就会希望使用专业的开发环境。下一节会介绍如何使用开发环境。

首先介绍较难的方法:从命令行编译并运行Java程序。

1)打开一个终端窗口。

2)进入corejava/v1ch02/Welcome目录(CoreJava是安装本书示例源代码的目录,请参看2.1.3节)。

3)键入下面的命令:

然后,将会在终端窗口中看到图2-3所示的输出。

图2-3 编译并运行Welcome.java

祝贺你!你已经编译并运行了第一个Java程序。

那么,刚才都进行了哪些操作呢?javac程序是一个Java编译器。它将文件Welcome.java编译成Welcome.class。java程序启动Java虚拟机。虚拟机执行编译器放在class文件中的字节码。

Welcome程序非常简单。它只是向控制台输出了一条消息。你可能想查看程序清单2-1的程序代码。(在下一章中,将解释它是如何工作的。)

程序清单2-1 Welcome/Welcome.java

在使用可视化开发环境的年代,许多程序员对于在终端窗口中运行程序已经很生疏了。常常会出现很多错误,最终导致令人沮丧的结果。

一定要注意以下几点:

如果手工输入源程序,一定要注意大小写。尤其是类名为Welcome,而不是welcome或WELCOME。

编译器需要一个文件名(Welcome.java),而运行程序时,只需要指定类名(Welcome),不要带扩展名.java或.class。

如果看到诸如Bad command or f?ile name或javac:command not found这类消息,就要返回去反复检查安装是否有问题,特别是执行路径的设置。

如果javac报告了一个错误,指出无法找到Welcome.java,就应该检查目录中是否存在这个文件。

在Linux环境下,检查Welcome.java是否以正确的大写字母开头。

在Windows环境下,使用命令dir,而不要使用图形浏览器工具。有些文本编辑器(特别是Notepad)在每个文件名后面要添加扩展名.txt。如果使用Notepad编辑Welcome.java就会存为Welcome.java.txt。对于默认的Windows设置,浏览器与Notepad都隐含.txt扩展名,这是因为这个扩展名属于“已知的文件类型”。此时,需要重新命名这个文件,使用命令ren,或是另存一次,为文件名加一对双引号,如:“Welcome.java”。

如果运行程序之后,收到关于java.lang.NoClassDefFoundError的错误消息,就应该仔细地检查出问题的类的名字。

如果收到关于welcome(w为小写)的错误消息,就应该重新执行命令:java Welcome(W为大写)。记住,Java区分大小写。

如果收到有关Welcome/java的错误信息,这说明你错误地键入了java Welcome.java,应该重新执行命令java Welcome。

如果键入java Welcome,而虚拟机没有找到Welcome类,就应该检查一下是否有人设置了系统的CLASSPATH环境变量(将这个变量设置为全局并不是一个提倡的做法,然而,Windows中有些比较差的软件安装程序就是这样做的)。可以像设置PATH环境变量一样设置CLASSPATH,不过这里将删除这个设置。

提示:在http://docs.oracle.com/javase/tutorial/getStarted/cupojava/上有一个很好的教程。其中提到了初学者经常容易犯的一些错误。

2.3 使用集成开发环境

上一节中,你已经了解了如何从命令行编译和运行一个Java程序。这是一个很有用的技能,不过对于大多数日常工作来说,都应当使用集成开发环境。这些环境非常强大,也很方便,不使用这些环境有些不合情理。我们可以免费得到一些很棒的开发环境,如Eclipse、NetBeans和IntelliJ IDEA程序。这一章中,我们将学习如何从Eclipse起步。当然,如果你喜欢其他开发环境,学习本书时也完全可以使用你喜欢的环境。

本节将介绍如何使用Eclipse编译一个程序。Eclipse是一个可以从网站http://eclipse.org/downloads上免费下载得到的集成开发环境。Eclipse已经有面向Linux、Mac OS X、Solaris和Windows的版本。访问下载网站时,选择“Eclipse IDE for Java Developers”。再根据你的操作系统选择32位或64位版本。

将Eclipse解压缩到你选择的位置,执行这个zip文件中的eclipse程序。

下面是用Eclipse编写程序的一般步骤。

1)启动Eclipse之后,从菜单选择File→ New→Project。

2)从向导对话框中选择Java Project(如图2-4所示)。

3)点击Next按钮,不选中“Use default location”复选框。点击Browse导航到corejava/v1ch02/Welcome目录(见图2-5)。

图2-5 配置Eclipse工程

4)点击Finish按钮。这个工程已经创建完成了。

5)点击工程窗口左边窗格中的三角,直到找到Welcome.java并双击。现在应该看到带有程序代码的窗口了(如图2-6所示)。

图2-6 使用Eclipse编辑源文件

6)用鼠标右键点击最左侧窗格中的工程名(Welcome),选择Run→Run As→Java Application。程序输出会显示在控制台窗格中。

可以假定,这个程序没有输入错误或bug(毕竟,这段代码只有几行)。为了说明问题,假定在代码中不小心出现了录入错误(或者甚至语法错误)。试着将原来的程序修改一下,让它包含一些录入错误,例如,将String的大小写弄错:

注意string下面的波折线。点击源代码下标签页中的Problems,展开小三角,会看到一个错误消息,指出有一个未知的string类型(见图2-7)。点击这个错误消息。光标会移到编辑窗口中相应的代码行,可以在这里纠正错误。利用这个特性可以快速地修正错误。

提示:通常,Eclipse错误报告会伴有一个灯泡图标。点击这个图标可以得到一个建议解决这个错误的方案列表。

图2-7 Eclipse中的错误消息

2.4 运行图形化应用程序

Welcome程序并不会引起人们的兴奋。接下来,给出一个图形化应用程序。这个程序是一个简单的图像文件查看器(viewer),它可以加载并显示一个图像。首先,由命令行编译并运行这个程序。

1)打开一个终端窗口。

2)进入corejava/v1ch02/ImageViewer。

3)输入:

将弹出一个标题栏为ImageViewer的新程序窗口(如图2-8所示)。

现在,选择File→Open,然后找到一个图像文件并打开它(我们在同一个目录下提供了两个示例文件)。要关闭这一程序,只需要点击标题栏中的关闭按钮或者从菜单中选择File→Exit。

下面快速地浏览一下源代码(程序清单2-2)。这个程序比第一个程序要长很多,但是只要想一想用C或C++编写同样功能的应用程序所需要的代码量,就不会觉得它太复杂了。本书将在第10章~第12章介绍如何编写像这样的图形化应用程序。

程序清单2-2 ImageViewer/ImageViewer.java

2.5 构建并运行applet

本书给出的前两个程序是Java应用程序。它们与所有本地程序一样,是独立的程序。然而,正如第1章提到的,有关Java的大量宣传都在炫耀Java在浏览器中运行applet的能力。如果你对“过去的记忆”感兴趣,可以继续阅读下面的内容来了解如何构建和运行一个applet,以及如何在Web浏览器中显示;如果你不感兴趣,完全可以跳过这个例子,直接转到第3章。

首先,打开终端窗口并转到CoreJava/v1ch02/RoadApplet,然后,输入下面的命令:

图2-9显示了在applet查看器窗口中显示的内容。这个applet图示显示了司机随意减速可能导致交通拥堵的情况。1996年,applet是创建这种可视化显示的绝佳工具。

第一条命令是大家已经非常熟悉的调用Java编译器的命令。它将RoadApplet.java源文件编译成字节码文件RoadApplet.class。

不过这一次不要运行java程序。首先,使用jar工具将类文件打包到一个“JAR文件”。然后调用appletviewer程序,这是JDK自带的一个工具,可以用来快速测试applet。需要为这个程序指定一个HTML文件名,而不是一个Java类文件名。RoadApplet.html文件的内容如本节最后的程序清单2-3所示。

程序清单2-3 RoadApplet/RoadApplet.html

如果熟悉HTML,你会注意这里的标准HTML标记和applet标签,这会告诉applet查看器加载applet,其代码存储在RoadApplet.jar中。applet会忽略除applet标签外的所有HTML标签。

当然,applet要在浏览器中查看。遗憾的是,现在很多浏览器并不提供Java支持,或者启用Java很困难。对此,最好使用Firefox。

如果使用Windows或Mac OS X,Firefox会自动启用计算机上安装的Java。在Linux上,则需要用下面的命令启用这个插件:

作为检查,可以在地址栏键入about:plugins,查找Java插件。确保使用这个插件的Java SE 8版本,为此要查找MIME类型application/x-java-applet;version=1.8。

接下来,将浏览器导航到http://horstmann.com/applets/RoadApplet/RoadApplet.html,对所有安全提示都选择接受,保证最后会显示applet。

遗憾的是,只是测试刚刚编译的applet还不够。horstmann.com服务器上的applet有数字签名。还必须再花一些工夫,让Java虚拟机信任的一个证书发行者信任我,为我提供一个证书,我再用这个证书为JAR文件签名。浏览器插件不再运行不信任的applet。与过去相比,这是一个很大的变化,原先在屏幕上绘制像素的简单applet会限制在“沙箱”中,即使没有签名也可以工作。可惜,即使是Oracle也不再相信沙箱的安全性了。

为了解决这个问题,可以临时将Java配置为信任本地文件系统的applet。首先,打开Java控制面板。

在Windows中,查看控制面板中的Programs(程序)部分。

在Mac上,打开System Preferences(系统首选项)。

在Linux上,运行jcontrol。

然后点击Security(安全)标签页和Edit Site List(编辑网站列表)按钮。再点击Add(增加),并键入f?ile:///。点击OK,接受下一个安全提示,然后再次点击OK(见图2-10)。

现在应该可以在浏览器中加载文件corejava/v1ch02/RoadApplet/RoadApplet.html,applet将随周围的文本一同显示。结果如图2-11所示。

最后,在程序清单2-4中给出了这个applet类的代码。现在,只需要简单看一下。在第13章中,还会再来介绍applet的编写。

图2-10 配置Java信任本地applet

图2-11 在浏览器中运行RoadApplet

程序清单2-4 RoadApplet/RoadApplet.java

在本章中,我们学习了有关编译和运行Java程序的机制。现在可以转到第3章开始学习Java语言了。

第3章 Java的基本程序设计结构

▲  一个简单的Java应用程序 ▲  字符串

▲  注释 ▲  输入输出

▲  数据类型 ▲  控制流

▲  变量 ▲  大数值

▲  运算符 ▲  数组

现在,假定已经成功地安装了JDK,并且能够运行第2章中给出的示例程序。我们从现在开始将介绍Java应用程序设计。本章主要介绍程序设计的基本概念(如数据类型、分支以及循环)在Java中的实现方式。

非常遗憾,需要告诫大家,使用Java编写GUI应用程序并不是一件很容易的事情,编程者需要掌握很多相关的知识才能够创建窗口、添加文本框以及能响应的按钮等。介绍基于GUI的Java应用程序设计技术与本章将要介绍的程序设计基本概念相差甚远,因此本章给出的所有示例都是为了说明一些相关概念而设计的“玩具式”程序,它们仅仅使用终端窗口提供输入输出。

最后需要说明,对于一个有C++编程经验的程序员来说,本章的内容只需要浏览一下,应该重点阅读散布在正文中的C/C++注释。对于具有使用Visual Basic等其他编程背景的程序员来说,可能会发现其中的绝大多数概念都很熟悉,但是在语法上有比较大的差异,因此,需要非常仔细地阅读本章的内容。

3.1 一个简单的Java应用程序

下面看一个最简单的Java应用程序,它只发送一条消息到控制台窗口中:

这个程序虽然很简单,但所有的Java应用程序都具有这种结构,还是值得花一些时间来研究。首先,Java区分大小写。如果出现了大小写拼写错误(例如,将main拼写成Main),程序将无法运行。

下面逐行地查看一下这段源代码。关键字public称为访问修饰符(access modif?ier),这些修饰符用于控制程序的其他部分对这段代码的访问级别。在第5章中将会更加详细地介绍访问修饰符的具体内容。关键字class表明Java程序中的全部内容都包含在类中。这里,只需要将类作为一个加载程序逻辑的容器,程序逻辑定义了应用程序的行为。在第4章中将会用大量的篇幅介绍Java类。正如第1章所述,类是构建所有Java应用程序和applet的构建块。Java应用程序中的全部内容都必须放置在类中。

关键字class后面紧跟类名。Java中定义类名的规则很宽松。名字必须以字母开头,后面可以跟字母和数字的任意组合。长度基本上没有限制。但是不能使用Java保留字(例如,public或class)作为类名(保留字列表请参看附录A)。

标准的命名规范为(类名FirstSample就遵循了这个规范):类名是以大写字母开头的名词。如果名字由多个单词组成,每个单词的第一个字母都应该大写(这种在一个单词中间使用大写字母的方式称为骆驼命名法。以其自身为例,应该写成CamelCase)。

源代码的文件名必须与公共类的名字相同,并用.java作为扩展名。因此,存储这段源代码的文件名必须为FirstSample.java(再次提醒大家注意,大小写是非常重要的,千万不能写成f?irstsample.java)。

如果已经正确地命名了这个文件,并且源代码中没有任何录入错误,在编译这段源代码之后就会得到一个包含这个类字节码的文件。Java编译器将字节码文件自动地命名为FirstSample. class,并与源文件存储在同一个目录下。最后,使用下面这行命令运行这个程序:

(请记住,不要添加.class扩展名。)程序执行之后,控制台上将会显示“We will not use ‘Hello,World’!”。

当使用

运行已编译的程序时,Java虚拟机将从指定类中的main方法开始执行(这里的“方法”就是Java中所说的“函数”),因此为了代码能够执行,在类的源文件中必须包含一个main方法。当然,也可以将用户自定义的方法添加到类中,并且在main方法中调用它们(第4章将讲述如何自定义方法)。

注释:根据Java语言规范,main方法必须声明为public(Java语言规范是描述Java语言的官方文档。可以从网站http://docs.oracle.com/javase/specs上阅读或下载)。

不过,当main方法不是public时,有些版本的Java解释器也可以执行Java应用程序。有个程序员报告了这个bug。如果感兴趣的话,可以在网站http://bugs.java.com/ bugdatabase/ index.jsp上输入bug号码4252539查看。这个bug被标明“关闭,不予修复。”Sun公司的工程师解释说:Java虚拟机规范(在http://docs.oracle.com/javase/specs/jvms/se8/html)并没有要求main方法一定是public,并且“修复这个bug有可能带来其他的隐患”。好在,这个问题最终得到了解决。在Java SE 1.4及以后的版本中强制main方法是public的。

从上面这段话可以发现一个问题的两个方面。一方面让质量保证工程师判断在bug报告中是否存在问题是一件很头痛的事情,这是因为其工作量很大,并且工程师对Java的所有细节也未必了解得很清楚。另一方面,Sun公司在Java开源很久以前就把bug报告及其解决方案放到网站上让所有人监督检查,这是一种非常了不起的举动。某些情况下,Sun甚至允许程序员为他们最厌恶的bug投票,并用投票结果来决定发布的下一个JDK版本将修复哪些bug。

需要注意源代码中的括号{ }。在Java中,像在C/C++中一样,用大括号划分程序的各个部分(通常称为块)。Java中任何方法的代码都用“{”开始,用“}”结束。

大括号的使用风格曾经引发过许多无意义的争论。我们的习惯是把匹配的大括号上下对齐。不过,由于空白符会被Java编译器忽略,所以可以选用自己喜欢的大括号风格。在下面讲述各种循环语句时,我们还会详细地介绍大括号的使用。

我们暂且不去理睬关键字static void,而仅把它们当作编译Java应用程序必要的部分就行了。在学习完第4章后,这些内容的作用就会揭晓。现在需要记住:每个Java应用程序都必须有一个main方法,其声明格式如下所示:

C++注释:作为一名C++程序员,一定知道类的概念。Java的类与C++的类很相似,但还是有些差异会使人感到困惑。例如,Java中的所有函数都属于某个类的方法(标准术语将其称为方法,而不是成员函数)。因此,Java中的main方法必须有一个外壳类。读者有可能对C++中的静态成员函数(static member functions)十分熟悉。这些成员函数定义在类的内部,并且不对对象进行操作。Java中的main方法必须是静态的。最后,与C/C++一样,关键字void表示这个方法没有返回值,所不同的是main方法没有为操作系统返回“退出代码”。如果main方法正常退出,那么Java应用程序的退出代码为0,表示成功地运行了程序。如果希望在终止程序时返回其他的代码,那就需要调用System.exit方法。

接下来,研究一下这段代码:

一对大括号表示方法体的开始与结束,在这个方法中只包含一条语句。与大多数程序设计语言一样,可以将Java语句看成是这种语言的句子。在Java中,每个句子必须用分号结束。特别需要说明,回车不是语句的结束标志,因此,如果需要可以将一条语句写在多行上。

在上面这个main方法体中只包含了一条语句,其功能是:将一个文本行输出到控制台上。

在这里,使用了System.out对象并调用了它的println方法。注意,点号(·)用于调用方法。Java使用的通用语法是

这等价于函数调用。

在这个示例中,调用了println方法并传递给它一个字符串参数。这个方法将传递给它的字符串参数显示在控制台上。然后,终止这个输出行,使得每次调用println都会在新的一行上显示输出。需要注意一点,Java与C/C++一样,都采用双引号分隔字符串。(本章稍后将会详细地讲解有关字符串的知识)。

与其他程序设计语言中的函数一样,在Java的方法中,可以没有参数,也可以有一个或多个参数(有的程序员把参数叫做实参)。对于一个方法,即使没有参数也需要使用空括号。例如,不带参数的println方法只打印一个空行。使用下面的语句来调用:

注释:System.out还有一个print方法,它在输出之后不换行。例如,System.out.print(“Hello”)打印“Hello”之后不换行,后面的输出紧跟在字母“o”之后。

3.2 注释

与大多数程序设计语言一样,Java中的注释也不会出现在可执行程序中。因此,可以在源程序中根据需要添加任意多的注释,而不必担心可执行代码会膨胀。在Java中,有3种标记注释的方式。最常用的方式是使用//,其注释内容从//开始到本行结尾。

当需要长篇的注释时,既可以在每行的注释前面标记//,也可以使用/*和*/将一段比较长的注释括起来。

最后,第3种注释可以用来自动地生成文档。这种注释以/**开始,以*/结束。请参见程序清单3-1。有关这种注释的详细内容和自动生成文档的具体方法请参见第4章。

程序清单3-1 FirstSample/FirstSample.java

警告:在Java中,/* */注释不能嵌套。也就是说,不能简单地把代码用/*和*/括起来作为注释,因为这段代码本身可能也包含一个*/。

3.3 数据类型

Java是一种强类型语言。这就意味着必须为每一个变量声明一种类型。在Java中,一共有8种基本类型(primitive type),其中有4种整型、2种浮点类型、1种用于表示Unicode编码的字符单元的字符类型char(请参见论述char类型的章节)和1种用于表示真值的boolean类型。

注释:Java有一个能够表示任意精度的算术包,通常称为“大数值”(big number)。虽然被称为大数值,但它并不是一种新的Java类型,而是一个Java对象。本章稍后将会详细地介绍它的用法。

3.3.1 整型

整型用于表示没有小数部分的数值,它允许是负数。Java提供了4种整型,具体内容如表3-1所示。

表3-1 Java整型

类型 存储需求 取值范围

int 4字节 -2 147 483 648~2 147 483 647(正好超过20亿)

short 2字节 -32 768~32 767

long 8字节 -9 223 372 036 854 775 808~9 223 372 036 854 775 807

byte 1字节 -128~127

在通常情况下,int类型最常用。但如果表示星球上的居住人数,就需要使用long类型了。byte和short类型主要用于特定的应用场合,例如,底层的文件处理或者需要控制占用存储空间量的大数组。

在Java中,整型的范围与运行Java代码的机器无关。这就解决了软件从一个平台移植到另一个平台,或者在同一个平台中的不同操作系统之间进行移植给程序员带来的诸多问题。与此相反,C和C++程序需要针对不同的处理器选择最为高效的整型,这样就有可能造成一个在32位处理器上运行很好的C程序在16位系统上运行却发生整数溢出。由于Java程序必须保证在所有机器上都能够得到相同的运行结果,所以各种数据类型的取值范围必须固定。

长整型数值有一个后缀L或l(如4000000000L)。十六进制数值有一个前缀0x或0X(如0xCAFE)。八进制有一个前缀0,例如,010对应八进制中的8。很显然,八进制表示法比较容易混淆,所以建议最好不要使用八进制常数。

从Java 7开始,加上前缀0b或0B就可以写二进制数。例如,0b1001就是9。另外,同样是从Java 7开始,还可以为数字字面量加下划线,如用1_000_000(或0b1111_0100_0010_0100_0000)表示一百万。这些下划线只是为了让人更易读。Java编译器会去除这些下划线。

C++注释:在C和C++中,int和long等类型的大小与目标平台相关。在8086这样的16位处理器上整型数值占2字节;不过,在32位处理器(比如Pentium或SPARC)上,整型数值则为4字节。类似地,在32位处理器上long值为4字节,在64位处理器上则为8字节。由于存在这些差别,这对编写跨平台程序带来了很大难度。在Java中,所有的数值类型所占据的字节数量与平台无关。

注意,Java没有任何无符号(unsigned)形式的int、long、short或byte类型。

3.3.2 浮点类型

浮点类型用于表示有小数部分的数值。在Java中有两种浮点类型,具体内容如表3-2所示。

表3-2 浮点类型

类型 存储需求 取值范围

f?loat 4字节 大约±3.402 823 47E+38F(有效位数为6~7位)

double 8字节 大约±1.797 693 134 862 315 70E+308(有效位数为15位)

double表示这种类型的数值精度是f?loat类型的两倍(有人称之为双精度数值)。绝大部分应用程序都采用double类型。在很多情况下,f?loat类型的精度很难满足需求。实际上,只有很少的情况适合使用f?loat类型,例如,需要单精度数据的库,或者需要存储大量数据。

f?loat类型的数值有一个后缀F或f(例如,3.14F)。没有后缀F的浮点数值(如3.14)默认为double类型。当然,也可以在浮点数值后面添加后缀D或d(例如,3.14D)。

注释:可以使用十六进制表示浮点数值。例如,0.125=2-3可以表示成0x1.0p-3。在十六进制表示法中,使用p表示指数,而不是e。注意,尾数采用十六进制,指数采用十进制。指数的基数是2,而不是10。

所有的浮点数值计算都遵循IEEE 754规范。具体来说,下面是用于表示溢出和出错情况的三个特殊的浮点数值:

正无穷大

负无穷大

NaN(不是一个数字)

例如,一个正整数除以0的结果为正无穷大。计算0/0或者负数的平方根结果为NaN。

注释:常量Double.POSITIVE_INFINITY、Double.NEGATIVE_INFINITY和Double.NaN(以及相应的Float类型的常量)分别表示这三个特殊的值,但在实际应用中很少遇到。特别要说明的是,不能这样检测一个特定值是否等于Double.NaN:

所有“非数值”的值都认为是不相同的。然而,可以使用Double.isNaN方法:

警告:浮点数值不适用于无法接受舍入误差的金融计算中。例如,命令System.out.println (2.0–1.1)将打印出0.8999999999999999,而不是人们想象的0.9。这种舍入误差的主要原因是浮点数值采用二进制系统表示,而在二进制系统中无法精确地表示分数1/10。这就好像十进制无法精确地表示分数1/3一样。如果在数值计算中不允许有任何舍入误差,就应该使用BigDecimal类,本章稍后将介绍这个类。

3.3.3 char类型

char类型原本用于表示单个字符。不过,现在情况已经有所变化。如今,有些Unicode字符可以用一个char值描述,另外一些Unicode字符则需要两个char值。有关的详细信息请阅读下一节。

char类型的字面量值要用单引号括起来。例如:'A'是编码值为65所对应的字符常量。它与"A"不同,"A"是包含一个字符A的字符串。char类型的值可以表示为十六进制值,其范围从\u0000到\Uffff。例如:\u2122表示注册符号(TM),\u03C0表示希腊字母π。

除了转义序列\u之外,还有一些用于表示特殊字符的转义序列,请参看表3-3。所有这些转义序列都可以出现在加引号的字符字面量或字符串中。例如,'\u2122'或"Hello\n"。转义序列\u还可以出现在加引号的字符常量或字符串之外(而其他所有转义序列不可以)。例如:

就完全符合语法规则,\u005B和\u005D是[和]的编码。

表3-3 特殊字符的转义序列

转义序列 名称 Unicode值 转义序列 名称 Unicode值

\b 退格 \u0008 \” 双引号 \u0022

\t 制表 \u0009 \’ 单引号 \u0027

\n 换行 \u000a \\ 反斜杠 \u005c

\r 回车 \u000d

警告:Unicode转义序列会在解析代码之前得到处理。例如,"\u0022+\u0022"并不是一个由引号(U+0022)包围加号构成的字符串。实际上,\u0022会在解析之前转换为",这会得到""+"",也就是一个空串。

更隐秘地,一定要当心注释中的\u。注释

会产生一个语法错误,因为读程序时\u00A0会替换为一个换行符。类似地,下面这个注释

也会产生一个语法错误,因为\u后面并未跟着4个十六进制数。

3.3.4 Unicode和char类型

要想弄清char类型,就必须了解Unicode编码机制。Unicode打破了传统字符编码机制的限制。在Unicode出现之前,已经有许多种不同的标准:美国的ASCII、西欧语言中的ISO 8859-1、俄罗斯的KOI-8、中国的GB 18030和BIG-5等。这样就产生了下面两个问题:一个是对于任意给定的代码值,在不同的编码方案下有可能对应不同的字母;二是采用大字符集的语言其编码长度有可能不同。例如,有些常用的字符采用单字节编码,而另一些字符则需要两个或更多个字节。

设计Unicode编码的目的就是要解决这些问题。在20世纪80年代开始启动设计工作时,人们认为两个字节的代码宽度足以对世界上各种语言的所有字符进行编码,并有足够的空间留给未来的扩展。在1991年发布了Unicode 1.0,当时仅占用65 536个代码值中不到一半的部分。在设计Java时决定采用16位的Unicode字符集,这样会比使用8位字符集的程序设计语言有很大的改进。

十分遗憾,经过一段时间,不可避免的事情发生了。Unicode字符超过了65 536个,其主要原因是增加了大量的汉语、日语和韩语中的表意文字。现在,16位的char类型已经不能满足描述所有Unicode字符的需要了。

下面利用一些专用术语解释一下Java语言解决这个问题的基本方法。从Java SE 5.0开始。码点(code point)是指与一个编码表中的某个字符对应的代码值。在Unicode标准中,码点采用十六进制书写,并加上前缀U+,例如U+0041就是拉丁字母A的码点。Unicode的码点可以分成17个代码级别(code plane)。第一个代码级别称为基本的多语言级别(basic multilingual plane),码点从U+0000到U+FFFF,其中包括经典的Unicode代码;其余的16个级别码点从U+10000到U+10FFFF,其中包括一些辅助字符(supplementary character)。

UTF-16编码采用不同长度的编码表示所有Unicode码点。在基本的多语言级别中,每个字符用16位表示,通常被称为代码单元(code unit);而辅助字符采用一对连续的代码单元进行编码。这样构成的编码值落入基本的多语言级别中空闲的2048字节内,通常被称为替代区域(surrogate area)[U+D800~U+DBFF用于第一个代码单元,U+DC00~U+DFFF用于第二个代码单元]。这样设计十分巧妙,我们可以从中迅速地知道一个代码单元是一个字符的编码,还是一个辅助字符的第一或第二部分。例如,是八元数集(http://math.ucr.edu/home/baez/octonions)的一个数学符号,码点为U+1D546,编码为两个代码单元U+D835和U+DD46。(关于编码算法的具体描述见http://en.wikipedia.org/wiki/UTF-16)。

在Java中,char类型描述了UTF-16编码中的一个代码单元。

我们强烈建议不要在程序中使用char类型,除非确实需要处理UTF-16代码单元。最好将字符串作为抽象数据类型处理(有关这方面的内容将在3.6节讨论)。

3.3.5 boolean类型

boolean(布尔)类型有两个值:false和true,用来判定逻辑条件。整型值和布尔值之间不能进行相互转换。

C++注释:在C++中,数值甚至指针可以代替boolean值。值0相当于布尔值false,非0值相当于布尔值true。在Java中则不是这样。因此,Java程序员不会遇到下述麻烦:

在C++中这个测试可以编译运行,其结果总是false。而在Java中,这个测试将不能通过编译,其原因是整数表达式x = 0不能转换为布尔值。

3.4 变量

在Java中,每个变量都有一个类型(type)。在声明变量时,变量的类型位于变量名之前。这里列举一些声明变量的示例:

可以看到,每个声明以分号结束。由于声明是一条完整的Java语句,所以必须以分号结束。

变量名必须是一个以字母开头并由字母或数字构成的序列。需要注意,与大多数程序设计语言相比,Java中“字母”和“数字”的范围更大。字母包括'A'~'Z'、'a'~'z'、'_'、'$'或在某种语言中表示字母的任何Unicode字符。例如,德国的用户可以在变量名中使用字母‘?’;希腊人可以用π。同样,数字包括'0'~'9'和在某种语言中表示数字的任何Unicode字符。但'+'和'?'这样的符号不能出现在变量名中,空格也不行。变量名中所有的字符都是有意义的,并且大小写敏感。变量名的长度基本上没有限制。

提示:如果想要知道哪些Unicode字符属于Java中的“字母”,可以使用Character类的isJavaIdentif?ierStart和isJavaIdentif?ierPart方法来检查。

提示:尽管$是一个合法的Java字符,但不要在你自己的代码中使用这个字符。它只用在Java编译器或其他工具生成的名字中。

另外,不能使用Java保留字作为变量名(请参看附录A中的保留字列表)。

可以在一行中声明多个变量:

不过,不提倡使用这种风格。逐一声明每一个变量可以提高程序的可读性。

注释:如前所述,变量名对大小写敏感,例如,hireday和hireDay是两个不同的变量名。在对两个不同的变量进行命名时,最好不要只存在大小写上的差异。不过,在有些时候,确实很难给变量取一个好的名字。于是,许多程序员将变量名命名为类型名,例如:

还有一些程序员更加喜欢在变量名前加上前缀“a”:

3.4.1 变量初始化

声明一个变量之后,必须用赋值语句对变量进行显式初始化,千万不要使用未初始化的变量。例如,Java编译器认为下面的语句序列是错误的:

要想对一个已经声明过的变量进行赋值,就需要将变量名放在等号(=)左侧,相应取值的Java表达式放在等号的右侧。

也可以将变量的声明和初始化放在同一行中。例如:

最后,在Java中可以将声明放在代码中的任何地方。例如,下列代码的书写形式在Java中是完全合法的:

在Java中,变量的声明尽可能地靠近变量第一次使用的地方,这是一种良好的程序编写风格。

C++注释:C和C++区分变量的声明与定义。例如:

是一个定义,而

是一个声明。在Java中,不区分变量的声明与定义。

3.4.2 常量

在Java中,利用关键字f?inal指示常量。例如:

关键字f?inal表示这个变量只能被赋值一次。一旦被赋值之后,就不能够再更改了。习惯上,常量名使用全大写。

在Java中,经常希望某个常量可以在一个类中的多个方法中使用,通常将这些常量称为类常量。可以使用关键字static f?inal设置一个类常量。下面是使用类常量的示例:

需要注意,类常量的定义位于main方法的外部。因此,在同一个类的其他方法中也可以使用这个常量。而且,如果一个常量被声明为public,那么其他类的方法也可以使用这个常量。在这个示例中,Constants2.CM_PER-INCH就是这样一个常量。

C++注释:const是Java保留的关键字,但目前并没有使用。在Java中,必须使用f?inal定义常量。

3.5 运算符

在Java中,使用算术运算符+、-、*、/表示加、减、乘、除运算。当参与/运算的两个操作数都是整数时,表示整数除法;否则,表示浮点除法。整数的求余操作(有时称为取模)用%表示。例如,15/2等于7,15%2等于1,15.0/2等于7.5。

需要注意,整数被0除将会产生一个异常,而浮点数被0除将会得到无穷大或NaN结果。

注释:可移植性是Java语言的设计目标之一。无论在哪个虚拟机上运行,同一运算应该得到同样的结果。对于浮点数的算术运算,实现这样的可移植性是相当困难的。double类型使用64位存储一个数值,而有些处理器使用80位浮点寄存器。这些寄存器增加了中间过程的计算精度。例如,以下运算:

很多Intel处理器计算x * y,并且将结果存储在80位的寄存器中,再除以z并将结果截断为64位。这样可以得到一个更加精确的计算结果,并且还能够避免产生指数溢出。但是,这个结果可能与始终在64位机器上计算的结果不一样。因此,Java虚拟机的最初规范规定所有的中间计算都必须进行截断。这种行为遭到了数值计算团体的反对。截断计算不仅可能导致溢出,而且由于截断操作需要消耗时间,所以在计算速度上实际上要比精确计算慢。为此,Java程序设计语言承认了最优性能与理想结果之间存在的冲突,并给予了改进。在默认情况下,虚拟机设计者允许对中间计算结果采用扩展的精度。但是,对于使用strictfp关键字标记的方法必须使用严格的浮点计算来生成可再生的结果。例如,可以把main方法标记为

于是,在main方法中的所有指令都将使用严格的浮点计算。如果将一个类标记为strictfp,这个类中的所有方法都要使用严格的浮点计算。

实际的计算方式将取决于Intel处理器的行为。在默认情况下,中间结果允许使用扩展的指数,但不允许使用扩展的尾数(Intel芯片在截断尾数时并不损失性能)。因此,这两种方式的区别仅仅在于采用默认的方式不会产生溢出,而采用严格的计算有可能产生溢出。

如果没有仔细阅读这个注释,也没有什么关系。对大多数程序来说,浮点溢出不属于大问题。在本书中,将不使用strictfp关键字。

3.5.1 数学函数与常量

在Math类中,包含了各种各样的数学函数。在编写不同类别的程序时,可能需要的函数也不同。

要想计算一个数值的平方根,可以使用sqrt方法:

注释:println方法和sqrt方法存在微小的差异。println方法处理System.out对象。但是,Math类中的sqrt方法处理的不是对象,这样的方法被称为静态方法。有关静态方法的详细内容请参看第4章。

在Java中,没有幂运算,因此需要借助于Math类的pow方法。语句:

将y的值设置为x的a次幂(xa)。pow方法有两个double类型的参数,其返回结果也为double类型。

f?loorMod方法的目的是解决一个长期存在的有关整数余数的问题。考虑表达式n % 2。所有人都知道,如果n是偶数,这个表达式为0;如果n是奇数,表达式则为1。当然,除非n是负数。如果n为负,这个表达式则为-1。为什么呢?设计最早的计算机时,必须有人制定规则,明确整数除法和求余对负数操作数该如何处理。数学家们几百年来都知道这样一个最优(或“欧几里德”)规则:余数总是要≥0。不过,最早制定规则的人并没有翻开数学书好好研究,而是提出了一些看似合理但实际上很不方便的规则。

下面考虑这样一个问题:计算一个时钟时针的位置。这里要做一个时间调整,而且要归一化为一个0~11之间的数。这很简单:(position + adjustment) % 12。不过,如果这个调整为负会怎么样呢?你可能会得到一个负数。所以要引入一个分支,或者使用((position + adjustment) % 12 + 12) % 12。不管怎样,总之都很麻烦。

f?loorMod方法就让这个问题变得容易了:f?loorMod(position + adjustment, 12)总会得到一个0~11之间的数。(遗憾的是,对于负除数,f?loorMod会得到负数结果,不过这种情况在实际中很少出现。)

Math类提供了一些常用的三角函数:

还有指数函数以及它的反函数——自然对数以及以10为底的对数:

最后,Java还提供了两个用于表示π和e常量的近似值:

提示:不必在数学方法名和常量名前添加前缀“Math”,只要在源文件的顶部加上下面这行代码就可以了。

例如:

在第4章中将讨论静态导入。

注释:在Math类中,为了达到最快的性能,所有的方法都使用计算机浮点单元中的例程。如果得到一个完全可预测的结果比运行速度更重要的话,那么就应该使用StrictMath类。它使用“自由发布的Math库”(fdlibm)实现算法,以确保在所有平台上得到相同的结果。有关这些算法的源代码请参看www.netlib.org/fdlibm(当fdlibm为一个函数提供了多个定义时,StrictMath类就会遵循IEEE 754版本,它的名字将以“e”开头)。

3.5.2 数值类型之间的转换

经常需要将一种数值类型转换为另一种数值类型。图3-1给出了数值类型之间的合法转换。

在图3-1中有6个实心箭头,表示无信息丢失的转换;有3个虚箭头,表示可能有精度损失的转换。例如,123 456 789是一个大整数,它所包含的位数比f?loat类型所能够表达的位数多。当将这个整型数值转换为f?loat类型时,将会得到同样大小的结果,但却失去了一定的精度。

当使用上面两个数值进行二元操作时(例如n + f,n是整数,f是浮点数),先要将两个操作数转换为同一种类型,然后再进行计算。

如果两个操作数中有一个是double类型,另一个操作数就会转换为double类型。

否则,如果其中一个操作数是f?loat类型,另一个操作数将会转换为f?loat类型。

否则,如果其中一个操作数是long类型,另一个操作数将会转换为long类型。

否则,两个操作数都将被转换为int类型。

图3-1 数值类型之间的合法转换

3.5.3 强制类型转换

在上一小节中看到,在必要的时候,int类型的值将会自动地转换为double类型。但另一方面,有时也需要将double转换成int。在Java中,允许进行这种数值之间的类型转换。当然,有可能会丢失一些信息。在这种情况下,需要通过强制类型转换(cast)实现这个操作。强制类型转换的语法格式是在圆括号中给出想要转换的目标类型,后面紧跟待转换的变量名。例如:

这样,变量nx的值为9。强制类型转换通过截断小数部分将浮点值转换为整型。

如果想对浮点数进行舍入运算,以便得到最接近的整数(在很多情况下,这种操作更有用),那就需要使用Math.round方法:

现在,变量nx的值为10。当调用round的时候,仍然需要使用强制类型转换(int)。其原因是round方法返回的结果为long类型,由于存在信息丢失的可能性,所以只有使用显式的强制类型转换才能够将long类型转换成int类型。

警告:如果试图将一个数值从一种类型强制转换为另一种类型,而又超出了目标类型的表示范围,结果就会截断成一个完全不同的值。例如,(byte)300的实际值为44。

C++注释:不要在boolean类型与任何数值类型之间进行强制类型转换,这样可以防止发生错误。只有极少数的情况才需要将布尔类型转换为数值类型,这时可以使用条件表达式b?1:0。

3.5.4 结合赋值和运算符

可以在赋值中使用二元运算符,这是一种很方便的简写形式。例如,

等价于:

(一般地,要把运算符放在=号左边,如*=或%=)。

注释:如果运算符得到一个值,其类型与左侧操作数的类型不同,就会发生强制类型转换。例如,如果x是一个int,则以下语句

是合法的,将把x设置为(int)(x + 3.5)。

3.5.5 自增与自减运算符

当然,程序员都知道加1、减1是数值变量最常见的操作。在Java中,借鉴了C和C++的做法,也提供了自增、自减运算符:n++将变量n的当前值加1,n--则将n的值减1。例如,以下代码:

将n的值改为13。由于这些运算符会改变变量的值,所以它们的操作数不能是数值。例如,4++就不是一个合法的语句。

实际上,这些运算符有两种形式;上面介绍的是运算符放在操作数后面的“后缀”形式。还有一种“前缀”形式:++n。后缀和前缀形式都会使变量值加1或减1。但用在表达式中时,二者就有区别了。前缀形式会先完成加1;而后缀形式会使用变量原来的值。

建议不要在表达式中使用++,因为这样的代码很容易让人困惑,而且会带来烦人的bug。

3.5.6 关系和boolean运算符

Java包含丰富的关系运算符。要检测相等性,可以使用两个等号==。例如,

的值为false。

另外可以使用!=检测不相等。例如,

的值为true。

最后,还有经常使用的< (小于)、>(大于)、<= (小于等于)和>= (大于等于)运算符。

Java沿用了C++的做法,使用&&表示逻辑“与”运算符,使用||表示逻辑“或”运算符。从!=运算符可以想到,感叹号!就是逻辑非运算符。&&和||运算符是按照“短路”方式来求值的:如果第一个操作数已经能够确定表达式的值,第二个操作数就不必计算了。如果用&&运算符合并两个表达式,

而且已经计算得到第一个表达式的真值为false,那么结果就不可能为true。因此,第二个表达式就不必计算了。可以利用这一点来避免错误。例如,在下面的表达式中:

如果x等于0,那么第二部分就不会计算。因此,如果x为0,也就不会计算1 / x ,除以0的错误就不会出现。

类似地,如果第一个表达式为true,expression1 || expression2的值就自动为true,而无需计算第二个表达式。

最后一点,Java支持三元操作符?: ,这个操作符有时很有用。如果条件为true,下面的表达式

就为第一个表达式的值,否则计算为第二个表达式的值。例如,

会返回x和y中较小的一个。

3.5.7 位运算符

处理整型类型时,可以直接对组成整型数值的各个位完成操作。这意味着可以使用掩码技术得到整数中的各个位。位运算符包括:

这些运算符按位模式处理。例如,如果n是一个整数变量,而且用二进制表示的n从右边数第4位为1,则

会返回1,否则返回0。利用&并结合使用适当的2的幂,可以把其他位掩掉,而只保留其中的某一位。

注释:应用在布尔值上时,&和|运算符也会得到一个布尔值。这些运算符与&&和||运算符很类似,不过&和|运算符不采用“短路”方式来求值,也就是说,得到计算结果之前两个操作数都需要计算。

另外,还有>>和<<运算符将位模式左移或右移。需要建立位模式来完成位掩码时,这两个运算符会很方便:

最后,>>>运算符会用0填充高位,这与>>不同,它会用符号位填充高位。不存在<<<运算符。

警告:移位运算符的右操作数要完成模32的运算(除非左操作数是long类型,在这种情况下需要对右操作数模64)。例如,1 << 35的值等同于1 << 3或8。

C++注释:在C/C++中,不能保证>>是完成算术移位(扩展符号位)还是逻辑移位(填充0)。实现者可以选择其中更高效的任何一种做法。这意味着C/C++ >>运算符对于负数生成的结果可能会依赖于具体的实现。Java则消除了这种不确定性。

3.5.8 括号与运算符级别

表3-4给出了运算符的优先级。如果不使用圆括号,就按照给出的运算符优先级次序进行计算。同一个级别的运算符按照从左到右的次序进行计算(除了表中给出的右结合运算符外。)例如,由于&&的优先级比||的优先级高,所以表达式

等价于

又因为+=是右结合运算符,所以表达式

等价于

也就是将b += c的结果(加上c之后的b)加到a上。

C++注释:与C或C++不同,Java不使用逗号运算符。不过,可以在for语句的第1和第3部分中使用逗号分隔表达式列表。

表3-4 运算符优先级

运 算 符 结合性

[ ] . ( ) (方法调用) 从左向右

! ~ ++ -- + (一元运算) - (一元运算) ( ) (强制类型转换) new 从右向左

*/ % 从左向右

+ - 从左向右

<<  >>  >>> 从左向右

<  <=  >  >=  instanceof 从左向右

= =  != 从左向右

& 从左向右

^ 从左向右

| 从左向右

&&  从左向右

|| 从左向右

?: 从右向左

=  +=  – =  *=  /=  %=  &=  |=  ^=  <<=  >>=  >>>= 从右向左

3.5.9 枚举类型

有时候,变量的取值只在一个有限的集合内。例如:销售的服装或比萨饼只有小、中、大和超大这四种尺寸。当然,可以将这些尺寸分别编码为1、2、3、4或S、M、L、X。但这样存在着一定的隐患。在变量中很可能保存的是一个错误的值(如0或m)。

针对这种情况,可以自定义枚举类型。枚举类型包括有限个命名的值。例如,

现在,可以声明这种类型的变量:

Size类型的变量只能存储这个类型声明中给定的某个枚举值,或者null值,null表示这个变量没有设置任何值。

有关枚举类型的详细内容将在第5章介绍。

3.6 字符串

从概念上讲,Java字符串就是Unicode字符序列。例如,串“Java\u2122”由5个Unicode字符J、a、v、a和TM。Java没有内置的字符串类型,而是在标准Java类库中提供了一个预定义类,很自然地叫做String。每个用双引号括起来的字符串都是String类的一个实例:

3.6.1 子串

String类的substring方法可以从一个较大的字符串提取出一个子串。例如:

创建了一个由字符“Hel”组成的字符串。

substring方法的第二个参数是不想复制的第一个位置。这里要复制位置为0、1和2(从0到2,包括0和2)的字符。在substring中从0开始计数,直到3为止,但不包含3。

substring的工作方式有一个优点:容易计算子串的长度。字符串s.substring(a, b)的长度为b-a。例如,子串“Hel”的长度为3-0=3。

3.6.2 拼接

与绝大多数的程序设计语言一样,Java语言允许使用+号连接(拼接)两个字符串。

上述代码将“Expletivedeleted”赋给变量message(注意,单词之间没有空格,+号按照给定的次序将两个字符串拼接起来)。

当将一个字符串与一个非字符串的值进行拼接时,后者被转换成字符串(在第5章中可以看到,任何一个Java对象都可以转换成字符串)。例如:

rating设置为“PG13”。

这种特性通常用在输出语句中。例如:

这是一条合法的语句,并且将会打印出所希望的结果(因为单词is后面加了一个空格,输出时也会加上这个空格)。

如果需要把多个字符串放在一起,用一个定界符分隔,可以使用静态join方法:

3.6.3 不可变字符串

String类没有提供用于修改字符串的方法。如果希望将greeting的内容修改为“Help!”,不能直接地将greeting的最后两个位置的字符修改为‘p’和‘!’。这对于C程序员来说,将会感到无从下手。如何修改这个字符串呢?在Java中实现这项操作非常容易。首先提取需要的字符,然后再拼接上替换的字符串:

上面这条语句将greeting当前值修改为“Help!”。

由于不能修改Java字符串中的字符,所以在Java文档中将String类对象称为不可变字符串,如同数字3永远是数字3一样,字符串“Hello”永远包含字符H、e、l、l和o的代码单元序列,而不能修改其中的任何一个字符。当然,可以修改字符串变量greeting,让它引用另外一个字符串,这就如同可以将存放3的数值变量改成存放4一样。

这样做是否会降低运行效率呢?看起来好像修改一个代码单元要比创建一个新字符串更加简洁。答案是:也对,也不对。的确,通过拼接“Hel”和“p!”来创建一个新字符串的效率确实不高。但是,不可变字符串却有一个优点:编译器可以让字符串共享。

为了弄清具体的工作方式,可以想象将各种字符串存放在公共的存储池中。字符串变量指向存储池中相应的位置。如果复制一个字符串变量,原始字符串与复制的字符串共享相同的字符。

总而言之,Java的设计者认为共享带来的高效率远远胜过于提取、拼接字符串所带来的低效率。查看一下程序会发现:很少需要修改字符串,而是往往需要对字符串进行比较(有一种例外情况,将来自于文件或键盘的单个字符或较短的字符串汇集成字符串。为此,Java提供了一个独立的类,在3.6.9节中将详细介绍)。

C++注释:在C程序员第一次接触Java字符串的时候,常常会感到迷惑,因为他们总将字符串认为是字符型数组:

这种认识是错误的,Java字符串大致类似于char*指针,

当采用另一个字符串替换greeting的时候,Java代码大致进行下列操作:

的确,现在greeting指向字符串“Help!”。即使一名最顽固的C程序员也得承认Java语法要比一连串的strncpy调用舒适得多。然而,如果将greeting赋予另外一个值又会怎样呢?

这样做会不会产生内存遗漏呢?毕竟,原始字符串放置在堆中。十分幸运,Java将自动地进行垃圾回收。如果一块内存不再使用了,系统最终会将其回收。

对于一名使用ANSI C++定义的string类的C++程序员,会感觉使用Java的String类型更为舒适。C++ string对象也自动地进行内存的分配与回收。内存管理是通过构造器、赋值操作和析构器显式执行的。然而,C++字符串是可修改的,也就是说,可以修改字符串中的单个字符。

3.6.4 检测字符串是否相等

可以使用equals方法检测两个字符串是否相等。对于表达式:

如果字符串s与字符串t相等,则返回true;否则,返回false。需要注意,s与t可以是字符串变量,也可以是字符串字面量。例如,下列表达式是合法的:

要想检测两个字符串是否相等,而不区分大小写,可以使用equalsIgnoreCase方法。

一定不要使用==运算符检测两个字符串是否相等!这个运算符只能够确定两个字符串是否放置在同一个位置上。当然,如果字符串放置在同一个位置上,它们必然相等。但是,完全有可能将内容相同的多个字符串的拷贝放置在不同的位置上。

如果虚拟机始终将相同的字符串共享,就可以使用==运算符检测是否相等。但实际上只有字符串常量是共享的,而+或substring等操作产生的结果并不是共享的。因此,千万不要使用==运算符测试字符串的相等性,以免在程序中出现糟糕的bug。从表面上看,这种bug很像随机产生的间歇性错误。

C++注释:对于习惯使用C++的string类的人来说,在进行相等性检测的时候一定要特别小心。C++的string类重载了==运算符以便检测字符串内容的相等性。可惜Java没有采用这种方式,它的字符串“看起来、感觉起来”与数值一样,但进行相等性测试时,其操作方式又类似于指针。语言的设计者本应该像对+那样也进行特殊处理,即重定义==运算符。当然,每一种语言都会存在一些不太一致的地方。

C程序员从不使用==对字符串进行比较,而使用strcmp函数。Java的compareTo方法与strcmp完全类似,因此,可以这样使用:

不过,使用equals看起来更为清晰。

3.6.5 空串与Null串

空串""是长度为0的字符串。可以调用以下代码检查一个字符串是否为空:

空串是一个Java对象,有自己的串长度(0)和内容(空)。不过,String变量还可以存放一个特殊的值,名为null,这表示目前没有任何对象与该变量关联(关于null的更多信息请参见第4章)。要检查一个字符串是否为null,要使用以下条件:

有时要检查一个字符串既不是null也不为空串,这种情况下就需要使用以下条件:

首先要检查str不为null。在第4章会看到,如果在一个null值上调用方法,会出现

错误。

3.6.6 码点与代码单元

Java字符串由char值序列组成。从3.3.3节“char类型”已经看到,char数据类型是一个采用UTF-16编码表示Unicode码点的代码单元。大多数的常用Unicode字符使用一个代码单元就可以表示,而辅助字符需要一对代码单元表示。

length方法将返回采用UTF-16编码表示的给定字符串所需要的代码单元数量。例如:

要想得到实际的长度,即码点数量,可以调用:

调用s.charAt(n)将返回位置n的代码单元,n介于0~s.length()-1之间。例如:

要想得到第i个码点,应该使用下列语句

注释:类似于C和C++,Java对字符串中的代码单元和码点从0开始计数。

为什么会对代码单元如此大惊小怪?请考虑下列语句:

使用UTF-16编码表示字符(U+1D546)需要两个代码单元。调用

返回的不是一个空格,而是的第二个代码单元。为了避免这个问题,不要使用char类型。这太底层了。

如果想要遍历一个字符串,并且依次查看每一个码点,可以使用下列语句:

可以使用下列语句实现回退操作:

显然,这很麻烦。更容易的办法是使用codePoints方法,它会生成一个int值的“流”,每个int值对应一个码点。(流将在卷Ⅱ的第2章中讨论)。可以将它转换为一个数组(见3.10节),再完成遍历。

反之,要把一个码点数组转换为一个字符串,可以使用构造函数(我们将在第4章详细讨论构造函数和new操作符)。

3.6.7 String API

Java中的String类包含了50多个方法。令人惊讶的是绝大多数都很有用,可以设想使用的频繁非常高。下面的API注释汇总了一部分最常用的方法。

注释:可以发现,本书中给出的API注释会有助于理解Java应用程序编程接口(API)。每一个API的注释都以形如java.lang.String的类名开始。(java.lang包的重要性将在第4章给出解释。)类名之后是一个或多个方法的名字、解释和参数描述。

在这里,一般不列出某个类的所有方法,而是选择一些最常用的方法,并以简洁的方式给予描述。完整的方法列表请参看联机文档(请参看3.6.8节)。

这里还列出了所给类的版本号。如果某个方法是在这个版本之后添加的,就会给出一个单独的版本号。

java.lang.string 1.0

char charAt (int index)

返回给定位置的代码单元。除非对底层的代码单元感兴趣,否则不需要调用这个方法。

int codePointAt(int index) 5.0

返回从给定位置开始的码点。

int offsetByCodePoints(int startIndex, int cpCount) 5.0

返回从startIndex代码点开始,位移cpCount后的码点索引。

int compareTo(String other)

按照字典顺序,如果字符串位于other之前,返回一个负数;如果字符串位于other之后,返回一个正数;如果两个字符串相等,返回0。

IntStream codePoints() 8

将这个字符串的码点作为一个流返回。调用toArray将它们放在一个数组中。

new String(int[] codePoints, int offset, int count) 5.0

用数组中从offset开始的count个码点构造一个字符串。

boolean equals(Object other)

如果字符串与other相等,返回true。

boolean equalsIgnoreCase(String other)

如果字符串与other相等(忽略大小写),返回true。

boolean startsWith(String pref?ix)

boolean endsWith(String suff?ix)

如果字符串以suff?ix开头或结尾,则返回true。

int index0f(String str)

int index0f(String str, int fromIndex)

int index0f(int cp)

int index0f(int cp, int fromIndex)

返回与字符串str或代码点cp匹配的第一个子串的开始位置。这个位置从索引0或fromIndex开始计算。如果在原始串中不存在str,返回-1。

int lastIndex0f(String str)

int lastIndex0f(String str, int fromIndex)

int lastindex0f(int cp)

int lastindex0f(int cp, int fromIndex)

返回与字符串str或代码点cp匹配的最后一个子串的开始位置。这个位置从原始串尾端或fromIndex开始计算。

int length( )

返回字符串的长度。

int codePointCount(int startIndex, int endIndex) 5.0

返回startIndex和endIndex-1之间的代码点数量。没有配成对的代用字符将计入代码点。

String replace(CharSequence oldString,CharSequence newString)

返回一个新字符串。这个字符串用newString代替原始字符串中所有的oldString。可以用String或StringBuilder对象作为CharSequence参数。

String substring(int beginIndex)

String substring(int beginIndex, int endIndex)

返回一个新字符串。这个字符串包含原始字符串中从beginIndex到串尾或endIndex–1的所有代码单元。

String toLowerCase( )

String toUpperCase( )

返回一个新字符串。这个字符串将原始字符串中的大写字母改为小写,或者将原始字符串中的所有小写字母改成了大写字母。

String trim( )

返回一个新字符串。这个字符串将删除了原始字符串头部和尾部的空格。

String join(CharSequence delimiter, CharSequence... elements) 8

返回一个新字符串,用给定的定界符连接所有元素。

注释:在API注释中,有一些CharSequence类型的参数。这是一种接口类型,所有字符串都属于这个接口。第6章将介绍更多有关接口类型的内容。现在只需要知道只要看到一个CharSequence形参,完全可以传入String类型的实参。

3.6.8 阅读联机API文档

正如前面所看到的,String类包含许多方法。而且,在标准库中有几千个类,方法数量更加惊人。要想记住所有的类和方法是一件不太不可能的事情。因此,学会使用在线API文档十分重要,从中可以查阅到标准类库中的所有类和方法。API文档是JDK的一部分,它是HTML格式的。让浏览器指向安装JDK的docs/api/index.html子目录,就可以看到如图3-2所示的屏幕。

图3-2 API文档的三个窗格

可以看到,屏幕被分成三个窗框。在左上方的小窗框中显示了可使用的所有包。在它下面稍大的窗框中列出了所有的类。点击任何一个类名之后,这个类的API文档就会显示在右侧的大窗框中(请参看图3-3)。例如,要获得有关String类方法的更多信息,可以滚动第二个窗框,直到看见String链接为止,然后点击这个链接。

接下来,滚动右面的窗框,直到看见按字母顺序排列的所有方法为止(请参看图3-4)。点击任何一个方法名便可以查看这个方法的详细描述(参见图3-5)。例如,如果点击compareToIgnoreCase链接,就会看到compareToIgnoreCase方法的描述。

图3-3 String类的描述

提示:马上在浏览器中将docs/api/index.html页面建一个书签。

图3-4 String类方法的小结

图3-5 String方法的详细描述

3.6.9 构建字符串

有些时候,需要由较短的字符串构建字符串,例如,按键或来自文件中的单词。采用字符串连接的方式达到此目的效率比较低。每次连接字符串,都会构建一个新的String对象,既耗时,又浪费空间。使用StringBuilder类就可以避免这个问题的发生。

如果需要用许多小段的字符串构建一个字符串,那么应该按照下列步骤进行。首先,构建一个空的字符串构建器:

当每次需要添加一部分内容时,就调用append方法。

在需要构建字符串时就调用toString方法,将可以得到一个String对象,其中包含了构建器中的字符序列。

注释:在JDK5.0中引入StringBuilder类。这个类的前身是StringBuffer,其效率稍有些低,但允许采用多线程的方式执行添加或删除字符的操作。如果所有字符串在一个单线程中编辑(通常都是这样),则应该用StringBuilder替代它。这两个类的API是相同的。

下面的API注释包含了StringBuilder类中的重要方法。

java.lang.StringBuilder 5.0

StringBuilder()

构造一个空的字符串构建器。

int length()

返回构建器或缓冲器中的代码单元数量。

StringBuilder append(String str)

追加一个字符串并返回this。

StringBuilder append(char c)

追加一个代码单元并返回this。

StringBuilder appendCodePoint(int cp)

追加一个代码点,并将其转换为一个或两个代码单元并返回this。

void setCharAt(int i,char c)

将第i个代码单元设置为c。

StringBuilder insert(int offset,String str)

在offset位置插入一个字符串并返回this。

StringBuilder insert(int offset,Char c)

在offset位置插入一个代码单元并返回this。

StringBuilder delete(int startIndex,int endIndex)

删除偏移量从startIndex到-endIndex-1的代码单元并返回this。

String toString()

返回一个与构建器或缓冲器内容相同的字符串。

3.7 输入输出

为了增加后面示例程序的趣味性,需要程序能够接收输入,并以适当的格式输出。当然,现代的程序都使用GUI收集用户的输入,然而,编写这种界面的程序需要使用较多的工具与技术,目前还不具备这些条件。主要原因是需要熟悉Java程序设计语言,因此只要有简单的用于输入输出的控制台就可以了。第10章~第12章将详细地介绍GUI程序设计。

3.7.1 读取输入

前面已经看到,打印输出到“标准输出流”(即控制台窗口)是一件非常容易的事情,只要调用System.out.println即可。然而,读取“标准输入流”System.in就没有那么简单了。要想通过控制台进行输入,首先需要构造一个Scanner对象,并与“标准输入流”System.in关联。

(构造函数和new操作符将在第4章中详细地介绍。)

现在,就可以使用Scanner类的各种方法实现输入操作了。例如,nextLine方法将输入一行。

在这里,使用nextLine方法是因为在输入行中有可能包含空格。要想读取一个单词(以空白符作为分隔符),就调用

要想读取一个整数,就调用nextInt方法。

与此类似,要想读取下一个浮点数,就调用nextDouble方法。

在程序清单3-2的程序中,询问用户姓名和年龄,然后打印一条如下格式的消息:

最后,在程序的最开始添加上一行:

Scanner类定义在java.util包中。当使用的类不是定义在基本java.lang包中时,一定要使用import指示字将相应的包加载进来。有关包与import指示字的详细描述请参看第4章。

程序清单3-2 InputTest/InputTest.java

注释:因为输入是可见的,所以Scanner类不适用于从控制台读取密码。Java SE 6特别引入了Console类实现这个目的。要想读取一个密码,可以采用下列代码:

为了安全起见,返回的密码存放在一维字符数组中,而不是字符串中。在对密码进行处理之后,应该马上用一个填充值覆盖数组元素(数组处理将在3.10节介绍)。

采用Console对象处理输入不如采用Scanner方便。每次只能读取一行输入,而没有能够读取一个单词或一个数值的方法。

java.util.Scanner 5.0

Scanner (InputStream in)

用给定的输入流创建一个Scanner对象。

String nextLine( )

读取输入的下一行内容。

String next( )

读取输入的下一个单词(以空格作为分隔符)。

int nextInt( )

double nextDouble( )

读取并转换下一个表示整数或浮点数的字符序列。

boolean hasNext( )

检测输入中是否还有其他单词。

boolean hasNextInt( )

boolean hasNextDouble( )

检测是否还有表示整数或浮点数的下一个字符序列。

java.lang.System 1.0

static Console console( ) 6

如果有可能进行交互操作,就通过控制台窗口为交互的用户返回一个Console对象,否则返回null。对于任何一个通过控制台窗口启动的程序,都可使用Console对象。否则,其可用性将与所使用的系统有关。

java.io.Console 6

static char[] readPassword(String prompt, Object...args)

static String readLine(String prompt, Object...args)

显示字符串prompt并且读取用户输入,直到输入行结束。args参数可以用来提供输入格式。有关这部分内容将在下一节中介绍。

3.7.2 格式化输出

可以使用System.out.print(x)将数值x输出到控制台上。这条命令将以x对应的数据类型所允许的最大非0数字位数打印输出x。例如:

打印

如果希望显示美元、美分等符号,则有可能会出现问题。

在早期的Java版本中,格式化数值曾引起过一些争议。庆幸的是,Java SE 5.0沿用了C语言库函数中的printf方法。例如,调用

可以用8个字符的宽度和小数点后两个字符的精度打印x。也就是说,打印输出一个空格和7个字符,如下所示:

在printf中,可以使用多个参数,例如:

每一个以%字符开始的格式说明符都用相应的参数替换。格式说明符尾部的转换符将指示被格式化的数值类型:f表示浮点数,s表示字符串,d表示十进制整数。表3-5列出了所有转换符。

表3-5 用于printf的转换符

转换符 类  型 举  例 转换符 类  型 举  例

d 十进制整数 159 s 字符串 Hello

x 十六进制整数 9f c 字符 H

o 八进制整数 237 b 布尔 True

f 定点浮点数 15.9 h 散列码 42628b2

e 指数浮点数 1.59e+01 tx或Tx 日期时间(T强制大写) 已经过时,应当改为使用java.time类,参见卷Ⅱ第6章

g 通用浮点数 — % 百分号 %

a 十六进制浮点数 0x1.fccdp3 n 与平台有关的行分隔符 —

另外,还可以给出控制格式化输出的各种标志。表3-6列出了所有的标志。例如,逗号标志增加了分组的分隔符。即

打印

可以使用多个标志,例如,“%, ( .2f”使用分组的分隔符并将负数括在括号内。

表3-6 用于printf的标志

标  志 目  的 举  例

+ 打印正数和负数的符号 +3333.33

空格 在正数之前添加空格 |  3333.33|

0 数字前面补0 003333.33

- 左对齐 |3333.33  |

( 将负数括在括号内 (3333.33)

, 添加分组分隔符 3,333.33

#(对于f格式) 包含小数点 3,333.

#(对于x或0格式) 添加前缀0x或0 0xcafe

$ 给定被格式化的参数索引。例如,%1$d,%1$x将以十进制和十六进制格式打印第1个参数 159  9F

< 格式化前面说明的数值。例如,%d%<x以十进制和十六进制打印同一个数值 159  9F

注释:可以使用s转换符格式化任意的对象。对于任意实现了Formattable接口的对象都将调用formatTo方法;否则将调用toString方法,它可以将对象转换为字符串。在第5章中将讨论toString方法,在第6章中将讨论接口。

可以使用静态的String.format方法创建一个格式化的字符串,而不打印输出:

基于完整性的考虑,下面简略地介绍printf方法中日期与时间的格式化选项。在新代码中,应当使用卷Ⅱ第6章中介绍的java.time包的方法。不过你可能会在遗留代码中看到Date类和相关的格式化选项。格式包括两个字母,以t开始,以表3-7中的任意字母结束。

例如,

这条语句将用下面的格式打印当前的日期和时间:

表3-7 日期和时间的转换符

转换符 类  型 举  例

c 完整的日期和时间 Mon Feb 09

18:05:19 PST

2015

F ISO 8601日期 2015-02-09

D 美国格式的日期(月/日/年) 02/09/2015

T 24小时时间 18:05:19

r 12小时时间 06:05:19 pm

R 24小时时间没有秒 18:05

Y 4位数字的年(前面补0) 2015

y 年的后两位数字(前面补0) 15

C 年的前两位数字(前面补0) 20

B 月的完整拼写 February

b或h 月的缩写 Feb

m 两位数字的月(前面补0) 02

d 两位数字的日(前面补0) 09

e 两位数字的日(前面不补0) 9

A 星期几的完整拼写 Monday

a 星期几的缩写 Mon

j 三位数的年中的日子(前面补0),在001到366之间 069

H 两位数字的小时(前面补0),在0到23之间 18

k 两位数字的小时(前面不补0),在0到23之间 18

I 两位数字的小时(前面补0),在0到12之间 06

l 两位数字的小时(前面不补0),在0到12之间 6

M 两位数字的分钟(前面补0) 05

S 两位数字的秒(前面补0) 19

L 三位数字的毫秒(前面补0) 047

N 九位数字的毫微秒(前面补0) 047000000

p 上午或下午的标志 pm

z 从GMT起,RFC822数字位移 -0800

Z 时区 PST

s 从格林威治时间1970-01-01 00:00:00起的秒数 1078884319

Q 从格林威治时间1970-01-01 00:00:00起的毫秒数 1078884319047

从表3-7可以看到,某些格式只给出了指定日期的部分信息。例如,只有日期或月份。如果需要多次对日期操作才能实现对每一部分进行格式化的目的就太笨拙了。为此,可以采用一个格式化的字符串指出要被格式化的参数索引。索引必须紧跟在%后面,并以$终止。例如,

打印

还可以选择使用<标志。它指示前面格式说明中的参数将被再次使用。也就是说,下列语句将产生与前面语句同样的输出结果:

提示:参数索引值从1开始,而不是从0开始,%1$...对第1个参数格式化。这就避免了与0标志混淆。

现在,已经了解了printf方法的所有特性。图3-6给出了格式说明符的语法图。

图3-6 格式说明符语法

注释:许多格式化规则是本地环境特有的。例如,在德国,组分隔符是句号而不是逗号,Monday被格式化为Montag。在卷Ⅱ第5章中将介绍如何控制应用的国际化行为。

3.7.3 文件输入与输出

要想对文件进行读取,就需要一个用File对象构造一个Scanner对象,如下所示:

如果文件名中包含反斜杠符号,就要记住在每个反斜杠之前再加一个额外的反斜杠: “c:\\mydirectory\\myf?ile.txt”。

注释:在这里指定了UTF-8字符编码,这对于互联网上的文件很常见(不过并不是普遍适用)。读取一个文本文件时,要知道它的字符编码——更多信息参见卷Ⅱ第2章。如果省略字符编码,则会使用运行这个Java程序的机器的“默认编码”。这不是一个好主意,如果在不同的机器上运行这个程序,可能会有不同的表现。

现在,就可以利用前面介绍的任何一个Scanner方法对文件进行读取。

要想写入文件,就需要构造一个PrintWriter对象。在构造器中,只需要提供文件名:

如果文件不存在,创建该文件。可以像输出到System.out一样使用print、println以及printf命令。

警告:可以构造一个带有字符串参数的Scanner,但这个Scanner将字符串解释为数据,而不是文件名。例如,如果调用:

这个scanner会将参数作为包含10个字符的数据:‘m’,‘y’,‘f’等。在这个示例中所显示的并不是人们所期望的效果。

注释:当指定一个相对文件名时,例如,“myf?ile.txt”,“mydirectory/myf?ile.txt”或“../myf?ile.txt”,文件位于Java虚拟机启动路径的相对位置。如果在命令行方式下用下列命令启动程序:

启动路径就是命令解释器的当前路径。然而,如果使用集成开发环境,那么启动路径将由IDE控制。可以使用下面的调用方式找到路径的位置:

如果觉得定位文件比较烦恼,则可以考虑使用绝对路径,例如:“c:\\mydirectory\\ myf?ile.txt”或者“/home/me/mydirectory/myf?ile.txt”。

正如读者所看到的,访问文件与使用System.in和System.out一样容易。要记住一点:如果用一个不存在的文件构造一个Scanner,或者用一个不能被创建的文件名构造一个PrintWriter,那么就会发生异常。Java编译器认为这些异常比“被零除”异常更严重。在第7章中,将会学习各种处理异常的方式。现在,应该告知编译器:已经知道有可能出现“输入/输出”异常。这需要在main方法中用throws子句标记,如下所示:

现在读者已经学习了如何读写包含文本数据的文件。对于更加高级的技术,例如,处理不同的字符编码、处理二进制数据、读取目录以及写压缩文件,请参看卷Ⅱ第2章。

注释:当采用命令行方式启动一个程序时,可以利用Shell的重定向语法将任意文件关联到System.in和System.out:

这样,就不必担心处理IOException异常了。

java.util.Scanner 5.0

Scanner(File f)

构造一个从给定文件读取数据的Scanner。

Scanner(String data)

构造一个从给定字符串读取数据的Scanner。

java.io.PrintWriter 1.1

PrintWriter(String f?ileName)

构造一个将数据写入文件的PrintWriter。文件名由参数指定。

java.nio.f?ile.Paths 7

static Path get(String pathname)

根据给定的路径名构造一个Path。

3.8 控制流程

与任何程序设计语言一样,Java使用条件语句和循环结构确定控制流程。本节先讨论条件语句,然后讨论循环语句,最后介绍看似有些笨重的switch语句,当需要对某个表达式的多个值进行检测时,可以使用switch语句。

C++注释:Java的控制流程结构与C和C++的控制流程结构一样,只有很少的例外情况。没有goto语句,但break语句可以带标签,可以利用它实现从内层循环跳出的目的(这种情况C语言采用goto语句实现)。另外,还有一种变形的for循环,在C或C++中没有这类循环。它有点类似于C#中的foreach循环。

3.8.1 块作用域

在深入学习控制结构之前,需要了解块(block)的概念。

块(即复合语句)是指由一对大括号括起来的若干条简单的Java语句。块确定了变量的作用域。一个块可以嵌套在另一个块中。下面就是在main方法块中嵌套另一个语句块的示例。

但是,不能在嵌套的两个块中声明同名的变量。例如,下面的代码就有错误,而无法通过编译:

C++注释:在C++中,可以在嵌套的块中重定义一个变量。在内层定义的变量会覆盖在外层定义的变量。这样,有可能会导致程序设计错误,因此在Java中不允许这样做。

3.8.2 条件语句

在Java中,条件语句的格式为

这里的条件必须用括号括起来。

与绝大多数程序设计语言一样,Java常常希望在某个条件为真时执行多条语句。在这种情况下,应该使用块语句(block statement),形式为

例如:

当yourSales大于或等于target时,将执行括号中的所有语句(请参看图3-7)。

注释:使用块(有时称为复合语句)可以在Java程序结构中原本只能放置一条(简单)语句的地方放置多条语句。

在Java中,更一般的条件语句格式如下所示(请参看图3-8):

  

    图3-7 if语句的流程图           图3-8 if/else语句的流程图

例如:

其中else部分是可选的。else子句与最邻近的if构成一组。因此,在语句

中else与第2个if配对。当然,用一对括号将会使这段代码更加清晰:

重复地交替出现if...else if...是一种很常见的情况(请参看图3-9)。例如:

图3-9 if/else if(多分支)的流程图

3.8.3 循环

当条件为true时,while循环执行一条语句(也可以是一个语句块)。一般格式为

如果开始循环条件的值就为false,则while循环体一次也不执行(请参看图3-10)。

图3-10 while语句的流程图

程序清单3-3中的程序将计算需要多长时间才能够存储一定数量的退休金,假定每年存入相同数量的金额,而且利率是固定的。

程序清单3-3 Retirement/Retirement.java

在这个示例中,增加了一个计数器,并在循环体中更新当前的累积数量,直到总值超过目标值为止。

(千万不要使用这个程序安排退休计划。这里忽略了通货膨胀和所期望的生活水准。)

while循环语句首先检测循环条件。因此,循环体中的代码有可能不被执行。如果希望循环体至少执行一次,则应该将检测条件放在最后。使用do/while循环语句可以实现这种操作方式。它的语法格式为:

这种循环语句先执行语句(通常是一个语句块),再检测循环条件;然后重复语句,再检测循环条件,以此类推。在程序清单3-4中,首先计算退休账户中的余额,然后再询问是否打算退休:

只要用户回答“N”,循环就重复执行(见图3-11)。这是一个需要至少执行一次的循环的很好示例,因为用户必须先看到余额才能知道是否满足退休所用。

程序清单3-4 Retirement2/Retirement2.java

3.8.4 确定循环

for循环语句是支持迭代的一种通用结构,利用每次迭代之后更新的计数器或类似的变量来控制迭代次数。如图3-12所示,下面的程序将数字1~10输出到屏幕上。

   

     图3-11 do/while语句的流程图        ???图3-12 for语句的流程图

for语句的第1部分通常用于对计数器初始化;第2部分给出每次新一轮循环执行前要检测的循环条件;第3部分指示如何更新计数器。

与C++一样,尽管Java允许在for循环的各个部分放置任何表达式,但有一条不成文的规则:for语句的3个部分应该对同一个计数器变量进行初始化、检测和更新。若不遵守这一规则,编写的循环常常晦涩难懂。

即使遵守了这条规则,也还有可能出现很多问题。例如,下面这个倒计数的循环:

警告:在循环中,检测两个浮点数是否相等需要格外小心。下面的for循环

可能永远不会结束。由于舍入的误差,最终可能得不到精确值。例如,在上面的循环中,因为0.1无法精确地用二进制表示,所以,x将从9.999 999 999 999 98跳到10.099 999 999 999 98。

当在for语句的第1部分中声明了一个变量之后,这个变量的作用域就为for循环的整个循环体。

特别指出,如果在for语句内部定义一个变量,这个变量就不能在循环体之外使用。因此,如果希望在for循环体之外使用循环计数器的最终值,就要确保这个变量在循环语句的前面且在外部声明!

另一方面,可以在各自独立的不同for循环中定义同名的变量:

for循环语句只不过是while循环的一种简化形式。例如,

可以重写为:

程序清单3-5给出了一个应用for循环的典型示例。这个程序用来计算抽奖中奖的概率。例如,如果必须从1~50之间的数字中取6个数字来抽奖,那么会有(50×49×48×47×46×45)/(1×2×3×4×5×6)种可能的结果,所以中奖的几率是1/15 890 700。祝你好运!

程序清单3-5 LotteryOdds/LotteryOdds.java

一般情况下,如果从n个数字中抽取k个数字,就可以使用下列公式得到结果。

下面的for循环语句计算了上面这个公式的值:

注释:3.10.1节将会介绍“通用for循环”(又称为for each循环),这是Java SE 5.0新增加的一种循环结构。

3.8.5 多重选择:switch语句

在处理多个选项时,使用if/else结构显得有些笨拙。Java有一个与C/C++完全一样的switch语句。

例如,如果建立一个如图3-13所示的包含4个选项的菜单系统,可以使用下列代码:

switch语句将从与选项值相匹配的case标签处开始执行直到遇到break语句,或者执行到switch语句的结束处为止。如果没有相匹配的case标签,而有default子句,就执行这个子句。

警告:有可能触发多个case分支。如果在case分支语句的末尾没有break语句,那么就会接着执行下一个case分支语句。这种情况相当危险,常常会引发错误。为此,我们在程序中从不使用switch语句。

如果你比我们更喜欢switch语句,编译代码时可以考虑加上-Xlint:fallthrough选项,如下所示:

这样一来,如果某个分支最后缺少一个break语句,编译器就会给出一个警告消息。

如果你确实正是想使用这种“直通式”(fallthrough)行为,可以为其外围方法加一个标注@SuppressWarnings("fallthrough")。这样就不会对这个方法生成警告了。(标注是为编译器或处理Java源文件或类文件的工具提供信息的一种机制。我们将在卷Ⅱ的第8章详细讨论标注。)

图3-13 switch语句的流程图

case标签可以是:

类型为char、byte、short或int的常量表达式。

枚举常量。

从Java SE 7开始,case标签还可以是字符串字面量。

例如:

当在switch语句中使用枚举常量时,不必在每个标签中指明枚举名,可以由switch的表达式值确定。例如:

3.8.6 中断控制流程语句

尽管Java的设计者将goto作为保留字,但实际上并没有打算在语言中使用它。通常,使用goto语句被认为是一种拙劣的程序设计风格。当然,也有一些程序员认为反对goto的呼声似乎有些过分(例如,Donald Knuth就曾编著过一篇名为《Structured Programming with goto statements》的著名文章)。这篇文章说:无限制地使用goto语句确实是导致错误的根源,但在有些情况下,偶尔使用goto跳出循环还是有益处的。Java设计者同意这种看法,甚至在Java语言中增加了一条带标签的break,以此来支持这种程序设计风格。

下面首先看一下不带标签的break语句。与用于退出switch语句的break语句一样,它也可以用于退出循环语句。例如,

在循环开始时,如果years > 100,或者在循环体中balance≥goal,则退出循环语句。当然,也可以在不使用break的情况下计算years的值,如下所示:

但是需要注意,在这个版本中,检测了两次balance < goal。为了避免重复检测,有些程序员更加偏爱使用break语句。

与C++不同,Java还提供了一种带标签的break语句,用于跳出多重嵌套的循环语句。有时候,在嵌套很深的循环语句中会发生一些不可预料的事情。此时可能更加希望跳到嵌套的所有循环语句之外。通过添加一些额外的条件判断实现各层循环的检测很不方便。

这里有一个示例说明了break语句的工作状态。请注意,标签必须放在希望跳出的最外层循环之前,并且必须紧跟一个冒号。

如果输入有误,通过执行带标签的break跳转到带标签的语句块末尾。对于任何使用break语句的代码都需要检测循环是正常结束,还是由break跳出。

注释:事实上,可以将标签应用到任何语句中,甚至可以应用到if语句或者块语句中,如下所示:

因此,如果希望使用一条goto语句,并将一个标签放在想要跳到的语句块之前,就可以使用break语句!当然,并不提倡使用这种方式。另外需要注意,只能跳出语句块,而不能跳入语句块。

最后,还有一个continue语句。与break语句一样,它将中断正常的控制流程。continue语句将控制转移到最内层循环的首部。例如:

如果n<0,则continue语句越过了当前循环体的剩余部分,立刻跳到循环首部。

如果将continue语句用于for循环中,就可以跳到for循环的“更新”部分。例如,下面这个循环:

如果n<0,则continue语句跳到count++语句。

还有一种带标签的continue语句,将跳到与标签匹配的循环首部。

提示:许多程序员容易混淆break和continue语句。这些语句完全是可选的,即不使用它们也可以表达同样的逻辑含义。在本书中,将不使用break和continue。

3.9 大数值

如果基本的整数和浮点数精度不能够满足需求,那么可以使用java.math包中的两个很有用的类:BigInteger和BigDecimal。这两个类可以处理包含任意长度数字序列的数值。BigInteger类实现了任意精度的整数运算,BigDecimal实现了任意精度的浮点数运算。

使用静态的valueOf方法可以将普通的数值转换为大数值:

遗憾的是,不能使用人们熟悉的算术运算符(如:+和*)处理大数值。而需要使用大数值类中的add和multiply方法。

C++注释:与C++不同,Java没有提供运算符重载功能。程序员无法重定义+和*运算符,使其应用于BigInteger类的add和multiply运算。Java语言的设计者确实为字符串的连接重载了+运算符,但没有重载其他的运算符,也没有给Java程序员在自己的类中重载运算符的机会。

程序清单3-6是对程序清单3-5中彩概率程序的改进,使其可以采用大数值进行运算。假设你被邀请参加抽奖活动,并从490个可能的数值中抽取60个,这个程序将会得到中彩概率1/716395843461995557415116222540092933411717612789263493493351013459481104668848。祝你好运!

程序清单3-6 BigIntegerTest/BigIntegerTest.java

在程序清单3-5中,用于计算的语句是

如果使用大数值,则相应的语句为:

API java.math.BigInteger 1.1

BigInteger add(BigInteger other)

BigInteger subtract(BigInteger other)

BigInteger multiply(BigInteger other)

BigInteger divide(BigInteger other)

BigInteger mod(BigInteger other)

返回这个大整数和另一个大整数other的和、差、积、商以及余数。

int compareTo(BigInteger other)

如果这个大整数与另一个大整数other相等,返回0;如果这个大整数小于另一个大整数other,返回负数;否则,返回正数。

static BigInteger valueOf(long x)

返回值等于x的大整数。

java.math.BigInteger 1.1

BigDecimal add(BigDecimal other)

BigDecimal subtract(BigDecimal other)

BigDecimal multiply(BigDecimal other)

BigDecimal divide(BigDecimal other,RoundingMode mode) 5.0

返回这个大实数与另一个大实数other的和、差、积、商。要想计算商,必须给出舍入方式(rounding mode)。RoundingMode.HALF_UP是在学校中学习的四舍五入方式(即,数值0到4舍去,数值5到9进位)。它适用于常规的计算。有关其他的舍入方式请参看API文档。

int compareTo(BigDecimal other)

如果这个大实数与另一个大实数相等,返回0;如果这个大实数小于另一个大实数,返回负数;否则,返回正数。

static BigDecimal valueOf(long x)

static BigDecimal valueOf(long x,int scale)

返回值为x或x / 10scale的一个大实数。

3.10 数组

数组是一种数据结构,用来存储同一类型值的集合。通过一个整型下标可以访问数组中的每一个值。例如,如果a是一个整型数组,a[i]就是数组中下标为i的整数。

在声明数组变量时,需要指出数组类型(数据元素类型紧跟[])和数组变量的名字。下面声明了整型数组a:

不过,这条语句只声明了变量a,并没有将a初始化为一个真正的数组。应该使用new运算符创建数组。

这条语句创建了一个可以存储100个整数的数组。数组长度不要求是常量:new int[n]会创建一个长度为n的数组。

注释:可以使用下面两种形式声明数组

大多数Java应用程序员喜欢使用第一种风格,因为它将类型int[](整型数组)与变量名分开了。

这个数组的下标从0~99(不是1~100)。一旦创建了数组,就可以给数组元素赋值。例如,使用一个循环:

创建一个数字数组时,所有元素都初始化为0。boolean数组的元素会初始化为false。对象数组的元素则初始化为一个特殊值null,这表示这些元素(还)未存放任何对象。初学者对此可能有些不解。例如,

会创建一个包含10个字符串的数组,所有字符串都为null。如果希望这个数组包含空串,可以为元素指定空串:

警告:如果创建了一个100个元素的数组,并且试图访问元素a[100](或任何在0~99之外的下标),程序就会引发“array index out of bounds”异常而终止执行。

要想获得数组中的元素个数,可以使用array.length。例如,

一旦创建了数组,就不能再改变它的大小(尽管可以改变每一个数组元素)。如果经常需要在运行过程中扩展数组的大小,就应该使用另一种数据结构——数组列表(array list)有关数组列表的详细内容请参看第5章。

3.10.1 for each循环

Java有一种功能很强的循环结构,可以用来依次处理数组中的每个元素(其他类型的元素集合亦可)而不必为指定下标值而分心。

这种增强的for循环的语句格式为:

定义一个变量用于暂存集合中的每一个元素,并执行相应的语句(当然,也可以是语句块)。collection这一集合表达式必须是一个数组或者是一个实现了Iterable接口的类对象(例如ArrayList)。有关数组列表的内容将在第5章中讨论,有关Iterable接口的内容将在第9章中讨论。

例如,

打印数组a的每一个元素,一个元素占一行。

这个循环应该读作“循环a中的每一个元素”(for each element in a)。Java语言的设计者认为应该使用诸如foreach、in这样的关键字,但这种循环语句并不是最初就包含在Java语言中的,而是后来添加进去的,并且没有人打算废除已经包含同名(例如System.in)方法或变量的旧代码。

当然,使用传统的for循环也可以获得同样的效果:

但是,for each循环语句显得更加简洁、更不易出错(不必为下标的起始值和终止值而操心)。

注释:for each循环语句的循环变量将会遍历数组中的每个元素,而不需要使用下标值。

如果需要处理一个集合中的所有元素,for each循环语句对传统循环语句所进行的改进更是叫人称赞不已。然而,在很多场合下,还是需要使用传统的for循环。例如,如果不希望遍历集合中的每个元素,或者在循环内部需要使用下标值等。

提示:有个更加简单的方式打印数组中的所有值,即利用Arrays类的toString方法。调用Arrays.toString(a),返回一个包含数组元素的字符串,这些元素被放置在括号内,并用逗号分隔,例如,“[2,3,5,7,11,13]”。要想打印数组,可以调用

3.10.2 数组初始化以及匿名数组

在Java中,提供了一种创建数组对象并同时赋予初始值的简化书写形式。下面是一个例子:

请注意,在使用这种语句时,不需要调用new。

甚至还可以初始化一个匿名的数组:

这种表示法将创建一个新数组并利用括号中提供的值进行初始化,数组的大小就是初始值的个数。使用这种语法形式可以在不创建新变量的情况下重新初始化一个数组。例如:

这是下列语句的简写形式:

注释:在Java中,允许数组长度为0。在编写一个结果为数组的方法时,如果碰巧结果为空,则这种语法形式就显得非常有用。此时可以创建一个长度为0的数组:

注意,数组长度为0与null不同。

3.10.3 数组拷贝

在Java中,允许将一个数组变量拷贝给另一个数组变量。这时,两个变量将引用同一个数组:

图3-14显示了拷贝的结果。如果希望将一个数组的所有值拷贝到一个新的数组中去,就要使用Arrays类的copyOf方法:

第2个参数是新数组的长度。这个方法通常用来增加数组的大小:

如果数组元素是数值型,那么多余的元素将被赋值为0;如果数组元素是布尔型,则将赋值为false。相反,如果长度小于原始数组的长度,则只拷贝最前面的数据元素。

C++注释:Java数组与C++数组在堆栈上有很大不同,但基本上与分配在堆(heap)上的数组指针一样。也就是说,

不同于

而等同于

Java中的[ ]运算符被预定义为检查数组边界,而且没有指针运算,即不能通过a加1得到数组的下一个元素。

3.10.4 命令行参数

前面已经看到多个使用Java数组的示例。每一个Java应用程序都有一个带String arg[]参数的main方法。这个参数表明main方法将接收一个字符串数组,也就是命令行参数。

例如,看一看下面这个程序:

如果使用下面这种形式运行这个程序:

args数组将包含下列内容:

这个程序将显示下列信息:

C++注释:在Java应用程序的main方法中,程序名并没有存储在args数组中。例如,当使用下列命令运行程序时

args[0]是“-h”,而不是“Message”或“java”。

3.10.5 数组排序

要想对数值型数组进行排序,可以使用Arrays类中的sort方法:

这个方法使用了优化的快速排序算法。快速排序算法对于大多数数据集合来说都是效率比较高的。Arrays类还提供了几个使用很便捷的方法,在稍后的API注释中将介绍它们。

程序清单3-7中的程序用到了数组,它产生一个抽彩游戏中的随机数值组合。假如抽彩是从49个数值中抽取6个,那么程序可能的输出结果为:

要想选择这样一个随机的数值集合,就要首先将数值1,2,…,n存入数组numbers中:

而用第二个数组存放抽取出来的数值:

现在,就可以开始抽取k个数值了。Math.random方法将返回一个0到1之间(包含0、不包含1)的随机浮点数。用n乘以这个浮点数,就可以得到从0到n-1之间的一个随机数。

下面将result的第i个元素设置为numbers[r]存放的数值,最初是r+1。但正如所看到的,numbers数组的内容在每一次抽取之后都会发生变化。

现在,必须确保不会再次抽取到那个数值,因为所有抽彩的数值必须不相同。因此,这里用数组中的最后一个数值改写number[r],并将n减1。

关键在于每次抽取的都是下标,而不是实际的值。下标指向包含尚未抽取过的数组元素。

在抽取了k个数值之后,就可以对result数组进行排序了,这样可以让输出效果更加清晰:

程序清单3-7 LotteryDrawing/LotteryDrawing.java

java.util.Arrays 1.2

static String toString(type[] a)  5.0

返回包含a中数据元素的字符串,这些数据元素被放在括号内,并用逗号分隔。

参数:a 类型为int、long、short、char、byte、boolean、f?loat或double的数组。

static type copyOf(type[] a, int length) 6

static type copyOfRange(type[] a, int start, int end) 6

返回与a类型相同的一个数组,其长度为length或者end-start,数组元素为a的值。

参数:a 类型为int、long、short、char、byte、boolean、f?loat或double的数组。

   start 起始下标(包含这个值)。

   end 终止下标(不包含这个值)。这个值可能大于a.length。在这种情况下,结果为0或false。

   length 拷贝的数据元素长度。如果length值大于a.length,结果为0或false;否则,数组中只有前面length个数据元素的拷贝值。

static void sort(type[] a)

采用优化的快速排序算法对数组进行排序。

参数:a 类型为int、long、short、char、byte、boolean、f?loat或double的数组。

static int binarySearch(type[] a, type v)

static int binarySearch(type[] a, int start, int end, type v)  6

采用二分搜索算法查找值v。如果查找成功,则返回相应的下标值;否则,返回一个负数值r。-r-1是为保持a有序v应插入的位置。

参数:a 类型为int、long、short、char、byte、boolean、f?loat或double的有序数组。

   start 起始下标(包含这个值)。

   end 终止下标(不包含这个值)。

   v 同a的数据元素类型相同的值。

static void f?ill(type[] a, type v)

将数组的所有数据元素值设置为v。

参数:a 类型为int、long、short、char、byte、boolean、f?loat或double的数组。

   v 与a数据元素类型相同的一个值。

static boolean equals(type[] a, type[] b)

如果两个数组大小相同,并且下标相同的元素都对应相等,返回true。

参数:a、b 类型为int、long、short、char、byte、boolean、f?loat或double的两个数组。

3.10.6 多维数组

多维数组将使用多个下标访问数组元素,它适用于表示表格或更加复杂的排列形式。这一节的内容可以先跳过,等到需要使用这种存储机制时再返回来学习。

假设需要建立一个数值表,用来显示在不同利率下投资$10,000会增长多少,利息每年兑现,而且又被用于投资(见表3-8)。

表3-8 不同利率下的投资增长情况

10% 11% 12% 13% 14% 15%

10 000.00 10 000.00 10 000.00 10 000.00 10 000.00 10 000.00

11 000.00 11 100.00 11 200.00 11 300.00 11 400.00 11 500.00

12 100.00 12 321.00 12 544.00 12 769.00 12 996.00 13 225.00

13 310.00 13 676.31 14 049.28 14 428.97 14 815.44 15 208.75

14 641 00 15 180.70 15 735.19 16 304.74 16 889.60 17 490.06

16 105.10 16 850.58 17 623.42 18 424 .35 19 254.15 20 113.57

17 715.61 18 704.15 19 738.23 20 819.52 21 949.73 23 130.61

19 487.17 20 761.60 22 106.81 23 526.05 25 022.69 26 600.20

21 435.89 23 045.38 24 759.63 26 584.44 28 525.86 30 590.23

23 579.48 25 580.37 27 730.79 30 040.42 32 519.49 35 178.76

可以使用一个二维数组(也称为矩阵)存储这些信息。这个数组被命名为balances。

在Java中,声明一个二维数组相当简单。例如:

与一维数组一样,在调用new对多维数组进行初始化之前不能使用它。在这里可以这样初始化:

另外,如果知道数组元素,就可以不调用new,而直接使用简化的书写形式对多维数组进行初始化。例如:

一旦数组被初始化,就可以利用两个方括号访问每个元素,例如,balances[i][j]。

在示例程序中用到了一个存储利率的一维数组interest与一个存储余额的二维数组balances。一维用于表示年,另一维用于表示利率,最初使用初始余额来初始化这个数组的第一行:

然后,按照下列方式计算其他行:

程序清单3-8给出了完整的程序。

注释:for each循环语句不能自动处理二维数组的每一个元素。它是按照行,也就是一维数组处理的。要想访问二维数组a的所有元素,需要使用两个嵌套的循环,如下所示:

提示:要想快速地打印一个二维数组的数据元素列表,可以调用:

输出格式为:

程序清单3-8 CompoundInterest/CompoundInterest.java

3.10.7 不规则数组

到目前为止,读者所看到的数组与其他程序设计语言中提供的数组没有多大区别。但实际存在着一些细微的差异,而这正是Java的优势所在:Java实际上没有多维数组,只有一维数组。多维数组被解释为“数组的数组。”

例如,在前面的示例中,balances数组实际上是一个包含10个元素的数组,而每个元素又是一个由6个浮点数组成的数组(请参看图3-15)。

图3-15 一个二维数组

表达式balances[i]引用第i个子数组,也就是二维表的第i行。它本身也是一个数组,balances[i][j]引用这个数组的第j项。

由于可以单独地存取数组的某一行,所以可以让两行交换。

还可以方便地构造一个“不规则”数组,即数组的每一行有不同的长度。下面是一个典型的示例。在这个示例中,创建一个数组,第i行第j列将存放“从i个数值中抽取j个数值”产生的结果。

由于j不可能大于i,所以矩阵是三角形的。第i行有i + 1个元素(允许抽取0个元素,也是一种选择)。要想创建一个不规则的数组,首先需要分配一个具有所含行数的数组。

接下来,分配这些行。

在分配了数组之后,假定没有超出边界,就可以采用通常的方式访问其中的元素了。

程序清单3-9给出了完整的程序。

C++注释:在C++中,Java声明

不同于

也不同于

而是分配了一个包含10个指针的数组:

然后,指针数组的每一个元素被填充了一个包含6个数字的数组:

庆幸的是,当创建new double[10][6]时,这个循环将自动地执行。当需要不规则的数组时,只能单独地创建行数组。

程序清单3-9 LotteryArray/LotteryArray.java

现在,已经看到了Java语言的基本程序结构,下一章将介绍Java中的面向对象的程序设计。

时间: 2024-11-28 23:10:25

Java核心技术 卷Ⅰ 基础知识(原书第10版)的相关文章

Java核心技术 卷Ⅰ基础知识 1.1 Java程序设计平台

第1章 Java程序设计概述 ▲  Java程序设计平台         ▲  Java发展简史 ▲  Java"白皮书"的关键术语   ▲  关于Java的常见误解 ▲  Java applet与Internet   1996年Java第一次发布就引起了人们的极大兴趣.关注Java的人士不仅限于计算机出版界,还有诸如<纽约时报><华盛顿邮报><商业周刊>这样的主流媒体.Java是第一种也是唯一一种在National Public Radio上占用了

《Java核心技术 卷Ⅱ 高级特性(原书第10版)》一导读

前 言 致读者 本书是按照Java SE 8完全更新后的<Java核心技术 卷Ⅱ 高级特性(原书第10版)>.卷Ⅰ主要介绍了Java语言的一些关键特性:而本卷主要介绍编程人员进行专业软件开发时需要了解的高级主题.因此,与本书卷Ⅰ和之前的版本一样,我们仍将本书定位于用Java技术进行实际项目开发的编程人员. 编写任何一本书籍都难免会有一些错误或不准确的地方.我们非常乐意听到读者的意见.当然,我们更希望对本书问题的报告只听到一次.为此,我们创建了一个FAQ.bug修正以及应急方案的网站http:/

《Java语言导学(原书第6版)》一一第2章 面向对象的编程概念 2.0

第2章The Java Tutorial: A Short Course on the Basics, Sixth Edition面向对象的编程概念如果没有用过面向对象的程序语言,编写Java程序之前一定要先学一些面向对象编程的基本概念.本章介绍对象.类.继承.接口和包等基本概念.每节都通过生活中的例子来解释这些基本概念,同时介绍Java程序语言的语法.2.1节关注对象的概念.对象是具有相关状态和行为的软件.软件对象经常用于建模生活中的对象.本节介绍对象的状态和行为的表示方式以及数据封装的概念,

《Java语言导学(原书第6版)》一1.1 关于Java技术

1.1 关于Java技术 大家都在谈Java技术,但Java技术到底是什么?本节解释Java技术如何成为程序语言和平台,并概述Java技术的功能特性. 1.1.1 Java程序语言 Java程序语言是一种高级的编程语言,它具备如下性质: 简单 面向对象 分布式 多线程 动态 架构中立 可移植 高性能 强壮 安全 上述术语的定义可参考James Gosling和Henry McGilton的白皮书<The Java Language Environment>. 图1-1描述了Java应用程序开发

《Java语言导学(原书第6版)》一一1.1 关于Java技术

1.1 关于Java技术 大家都在谈Java技术,但Java技术到底是什么?本节解释Java技术如何成为程序语言和平台,并概述Java技术的功能特性.1.1.1 Java程序语言 Java程序语言是一种高级的编程语言,它具备如下性质: 简单 面向对象 分布式 多线程 动态 架构中立 可移植 高性能 强壮 安全 上述术语的定义可参考James Gosling和Henry McGilton的白皮书<The Java Language Environment>. 图1-1描述了Java应用程序开发的

《Java核心技术 卷Ⅱ 高级特性(原书第10版)》一3.8 XSL转换

3.8 XSL转换 XSL转换(XSLT)机制可以指定将XML文档转换为其他格式的规则,例如,转换为纯文本.XHTML或任何其他的XML格式.XSLT通常用来将某种机器可读的XML格式转译为另一种机器可读的XML格式,或者将XML转译为适于人类阅读的表示格式. 你需要提供XSLT样式表,它描述了XML文档向某种其他格式转换的规则.XSLT处理器将读入XML文档和这个样式表,并产生所要的输出(参见图3-7). XSLT规范很复杂,已经有很多书描述了该主题.我们不可能讨论XSLT的全部特性,所以我们

《Java核心技术 卷Ⅱ 高级特性(原书第10版)》一2.1.2 完整的流家族

2.1.2 完整的流家族 与C语言只有单一类型FILE*包打天下不同,Java拥有一个流家族,包含各种输入/输出流类型,其数量超过60个!请参见图2-1和图2-2. 让我们把输入/输出流家族中的成员按照它们的使用方法来进行划分,这样就形成了处理字节和字符的两个单独的层次结构.正如所见,InputStream和OutputStream类可以读写单个字节或字节数组,这些类构成了图2-1所示的层次结构的基础.要想读写字符串和数字,就需要功能更强大的子类,例如,DataInputStream和DataO

《Java核心技术 卷Ⅱ 高级特性(原书第10版)》一3.6.1 使用SAX解析器

3.6.1 使用SAX解析器 SAX解析器在解析XML输入数据的各个组成部分时会报告事件,但不会以任何方式存储文档,而是由事件处理器建立相应的数据结构.实际上,DOM解析器是在SAX解析器的基础上构建的,它在接收到解析器事件时构建DOM树. 在使用SAX解析器时,需要一个处理器来为各种解析器事件定义事件动作.ContentHandler接口定义了若干个在解析文档时解析器会调用的回调方法.下面是最重要的几个: startElement和endElement在每当遇到起始或终止标签时调用. char

《Java核心技术 卷Ⅱ 高级特性(原书第10版)》一2.3.3 ZIP文档

2.3.3 ZIP文档 ZIP文档(通常)以压缩格式存储了一个或多个文件,每个ZIP文档都有一个头,包含诸如每个文件名字和所使用的压缩方法等信息.在Java中,可以使用ZipInputStream来读入ZIP文档.你可能需要浏览文档中每个单独的项,getNextEntry方法就可以返回一个描述这些项的ZipEntry类型的对象.向ZipInputStream的getInputStream方法传递该项可以获取用于读取该项的输入流.然后调用closeEntry来读入下一项.下面是典型的通读ZIP文件