Java 9编程参考官方大全(第10版)


Java 9 编程参考
官方大全
(第 10 版)
[ ] 赫伯特 · 希尔德特 (Herbert Schildt)
吕 争 李周芳 译
北 京
Herbert Schildt
Java:The Complete Reference
Tenth Edition
EISBN
978-1-259-58933-1
Copyright © 2018 by McGraw-Hill Education.
All Rights reserved. No part of this publication may be reproduced or transmitted in any form or by any means,
electronic or mechanical, including without limitation photocopying, recording taping, or any database,
information or retrieval system, without the prior written permission of the publisher.
This authorized Chinese translation edition is jointly published by McGraw-Hill Education and Tsinghua University Press
Limited. This edition is authorized for sale in the People’s Republic of China only, excluding Hong Kong, Macao SAR and
Taiwan.
Translation copyright © 2018 by McGraw-Hill Education and Tsinghua University Press Limited.
版权所有。未经出版人事先书面许可,对本出版物的任何部分不得以任何方式或途径复制或传播,包括但不限于复
印、录制、录音,或通过任何数据库、信息或可检索的系统。
本授权中文简体字翻译版由麦格劳
- 希尔 ( 亚洲 ) 教育出版公司和清华大学出版社有限公司合作出版。此版本经授权仅
限在中国大陆区域销售,不能销往中国香港、澳门特别行政区和中国台湾地区。
版权
©2018 由麦格劳 - 希尔 ( 亚洲 ) 教育出版公司与清华大学出版社有限公司所有。
北京市版权局著作权合同登记号 图字:
01-2018-0332
本书封面贴有 McGraw-Hill Education 公司防伪标签,无标签者不得销售。
版权所有,侵权必究。侵权举报电话:
010-62782989 13701121 933
图书在版编目 (CIP) 数据
Java 9 编程参考官方大全 ( 10 ) / ( ) 赫伯特·希尔德特 (Herbert Schildt) 著;吕争,李周芳 译 . —北京:清华大学出
版社,
2018
书名原文: Java : The Complete Reference, Tenth Edition
ISBN 978-7-302-50606-5
. J … Ⅱ . ①赫… ②吕… ③李… Ⅲ . JAVA 语言-程序设计 Ⅳ . TP312.8
中国版本图书馆 CIP 数据核字 (2018) 151300
责任编辑: 王 军 于 平
封面设计: 牛艳敏
版式设计: 思创景点
责任校对: 成凤进
责任印制: 杨 艳
出版发行: 清华大学出版社
网 址 http://www.tup.com.cn http://www.wqbook.com
地 址 :北京清华大学学研大厦 A 邮 编 100 084
社 总 机 010-62770175 邮 购 010-62786 544
投稿与读者服务 010-62776969 c-service@tup.tsinghua.edu.cn
质 量 反 馈: 010-62772015 zhiliang@tup.tsinghua.edu.cn
印 装 者: 三河市吉祥印务有限公司
经 销: 全国新华书店
开 本: 190mm × 260mm 印 张 58.75 字 数: 2119 千字
版 次: 2018 9 月第 1 印 次: 2018 9 月第 1 次印刷
定 价: 158.00
—————————————————————————————————————————————————
产品编号:
078951-01
译 者 序
Java 是一种跨平台的语言,一次编写,到处运行,在世界编程语言排行榜中稳居第二名 ( 第一名是 C 语言 ) Java
用途广泛,既可以用来开发传统的客户端软件和网站后台,又可以开发如火如荼的 Android 应用和云计算平台。
众所周知,
Java 发展已经超过 20 (1995 年最初发布 ) Java 和相关生态在不断丰富的同时,也越来越暴露
出一些问题:①
Java 运行环境的膨胀和臃肿。②当代码库越来越大时,创建复杂,盘根错节的“意大利面条式代码”
的概率呈指数级的增长。③很难真正地对代码进行封装,而系统并没有对不同部分
( 也就是 JAR 文件 ) 之间的依赖关
系有个明确的概念。④每一个公共类都可以被类路径之下任何其他的公共类所访问到,这样就会导致无意中使用了
并不想被公开访问的
API
模块化
( Java 平台模块系统的形式 ) JDK 分成一组模块,可以在编译时、运行时或者构建时进行组合。其
主要目的在于减少内存的开销;只需要必要模块,而非全部
jdk 模块,可简化各种类库和大型应用的开发和维护;
改进
Java SE 平台,使其可以适应不同大小的计算设备;改进其安全性,可维护性,提高性能。
本质上讲,模块
(module) 的概念,其实就是包外再裹一层,也就是说,用模块来管理各个包,通过声明某个包
暴露,不声明默认就是隐藏。因此,模块化使得代码组织上更安全,因为它可以指定哪些部分可以暴露,哪些部分
隐藏。
作为
Java 9 平台最大的一个特性,随着 Java 平台模块化系统的落地,开发人员不需要再为不断膨胀的 Java
台而苦恼,例如,可以使用
jlink 工具,根据需要定制运行时环境。这对于拥有大量镜像的容器应用场景或复杂依
赖关系的大型应用等,都具有非常重要的意义。因为模块的重要性,本书用一整章的篇幅介绍模块。
Java 9 的另一个主要新特性是 JShell JShell 提供了一个控制台交互界面,通过这个交互界面,开发者可以快速
编写代码片段并运行,可以查看类库的文档,可以编写简单的程序。开发者可以轻松地与
JShell 交互,其中包括:
编辑历史,
Tab 键代码补全,自动添加分号,可配置的 imports definitions
在需要快速验证的场景,我们不再需要先定义类、再定义方法、接着定义
main() 方法或单元测试来验证我们的
想法;对于
Java 初学者来说,这也提供了一个简便的方法来学习 Java ,是一个实用的特性。每一门编程语言的第
一个练习就是打印“
Hello,World ”,有了 JShell 之后, Java 开发者终于不用先编写一个类,再编写“奇怪的” main
方法,相信这对于初学者来说是一个福音。初学者和老手都会觉得 JShell 是个十分有用的交互式编程工具,本书的
附录
C 将介绍 JShell
最后,
Java 9 废弃了 applet applet API ,因此本书不再介绍它。但因为旧代码还使用它,所以附录 D 会简要
讨论
applet
II Java 9 编程参考官方大全(第 10 版)
Java 并不是最容易入手的开发语言,根据这个特性,本教程精心编排,优先讲解 Java 语言的基础知识,再讲
Java 的各种库,最后介绍 Java GUI 编程和应用,以求用最易懂的方式,最精简的语句,最充实的内容,向读
者介绍
Java 。这些丰富的内容,包含了 Java 语言基础语法以及高级特性,适合各个层次的 Java 程序员阅读,同时
也是高等院校讲授面向对象程序设计语言以及
Java 语言的绝佳教材和参考书。
在这里要感谢清华大学出版社的编辑,她们为本书的翻译投入了巨大的热情并付出了很多心血。没有她们的帮
助和鼓励,本书不可能顺利付梓。本书全部章节由吕争和李周芳翻译,参与翻译的还有陈妍、何美英、陈宏波、熊
晓磊、管兆昶、潘洪荣、曹汉鸣、高娟妮、王燕、谢李君、李珍珍、王璐、王华健、柳松洋、曹晓松、陈彬、洪妍、
刘芸、邱培强、高维杰、张素英、颜灵佳、方峻、顾永湘、孔祥亮。
对于这本经典之作,译者本着“诚惶诚恐”的态度,在翻译过程中力求“信、达、雅”,但是鉴于译者水平有
限,错误和失误在所难免,如有任何意见和建议,请不吝指正。
译 者

作 者 简 介
Herbert Schildt 是一位畅销书作家,在几乎 30 年的时间里,他撰写了大量关于编程的图书。 Herbert Java
言的权威。他撰写的编程书籍在世界范围内销售了数百万册,并且已经被翻译成所有主要的非英语语言。他撰写了
大量
Java 方面的书籍, 包括 Java A Beginner’s Guide Herb Schildt’s Java Programming Cookbook Introducing JavaFX
8 Programming
Swing A Beginner’s Guide ,还撰写了许多关于 C C++ C# 的图书。尽管对计算机的所有方面都
感兴趣,但是他主要关注计算机语言。
Schildt 获得了美国伊利诺伊大学的本科和研究生学位。他的个人网站是
www.HerbSchildt.com
技术编辑简介
Danny Coward 博士在所有版本的 Java 平台上都工作过。他将 Java Servlet 的定义引入 Java EE 平台的第一个版
本及后续版本,将
Web 服务引入 Java ME 平台,并且主持 Java SE 7 的战略和规划设计。他开发了 JavaFX 技术,
并且最近还设计了
Java WebSocket API , 这是 Java EE 7 标准程度最大的新增内容。他的从业经历丰富, 包括从事 Java
编码,与业界专家一起设计 API ,担任 Java 社区进程执行委员会 (Java Community Process Executive Committee) 的成
员,他对
Java 技术的多个方面有着独特的见解。另外,他还是图书 Java WebSocket Programming Java EE The Big
Picture
的作者。 Coward 博士从英国牛津大学获得了数学学士、硕士和博士学位。
前 言
Java 是当今世界最重要、也是使用最广泛的计算机语言之一。而且,在多年之前它就已经拥有这一荣誉。与其
他一些计算机语言随着时间的流逝影响也逐渐减弱不同,
Java 随着时间的推移反而变得更加强大。从首次发布开始,
Java 就跃到了 Internet 编程的前沿。后续的每一个版本都进一步巩固了这一地位。如今, Java 依然是开发 Web 应用
的最佳选择。
Java 是一门功能强大且通用的编程语言,适合于多种目的的开发。简言之:在现实世界中,很多应用
都是使用
Java 开发的,掌握 Java 语言非常重要。
Java 成功的一个关键原因在于它的敏捷性。自从最初的 Java 1.0 版发布以来, Java 不断地进行完善以适应编程
环境和开发人员编程方式的变化。最重要的是,
Java 不仅仅是在跟随潮流,更是在帮助创造潮流。 Java 能够适应计
算机世界快速变化的能力,是它一直并且仍将如此成功的关键因素。
Java 编程参考官方大全》自从 1996 年首次出版以来,已经经历了数次改版,每次改版都反映了 Java 的不断
演化进程。《
Java 9 编程参考官方大全 ( 10 ) 》已经针对 Java SE 9(JDK 9) 进行了升级。因为 Java SE 9 Java
言添加了几个新特性,所以本书包含了大量新内容。最重要的一个新增特性是模块
(module) ,通过该特性可以指定
应用程序中代码间的关系和依赖性。模块也会影响对元素的访问。此外,模块代表对
Java 语言最具深远意义的更
改之一,其中添加了两个新的语法元素和
10 个新关键字。模块还对 Java API 库产生了巨大影响,因为现在模块由
API 库中的包构成。另外,为了支持模块,新增了一些工具,对现有的工具也进行了更新,还定义了新的文件格式。
由于模块是一个非常重要的新特性,因此本书的第
16 章专门对其进行了讲解。
除模块外,
JDK 9 中还新增了一些其他特性。其中最有趣的是 JShell ,该工具提供了一个交互式环境,开发人
员不需要编写完整的程序就可以方便地在其中体验代码片段。不管是初学者还是有经验的编程人员都将发现该工具
非常有用。本书的附录
C 对该工具进行了介绍。与前几版一样, JDK 9 也对 Java 语言及其 API 库进行了一些细小
的更新和增强。因此,你在整本书中都会看到这些更新内容。最后要提到的一点是:
Java SE 9 中删除了 applet
applet API 。因此,本书不再详细介绍它们,而仅在本书的附录 D 中对 applet 做了简要介绍。
一本适合所有编程人员的书
本书面向所有开发人员,不管是初学者还是有经验的编程人员。初学者将从本书中发现每个主题的详细讨论,
以及许多特别有帮助的例子。而对
Java 更高级特性和库的深入讨论,将会吸引有经验的编程人员。无论是对于初
学者还是有经验的编程人员,本书都提供了持久的资源和方便实用的参考。

VI Java 9 编程参考官方大全(第 10 版)
本书内容
本书是对 Java 语言的全面指导,描述了它的语法、关键字以及基本的编程原则,另外还介绍了 Java API 库的
重要部分。本书分为
5 部分,每部分关注 Java 开发环境的不同方面。
第Ⅰ部分是对
Java 语言的深入阐述。该部分从基础知识开始讲解,包括数据类型、运算符、控制语句以及类
等。然后介绍了继承、包、接口、异常处理以及多线程,还介绍了注解、枚举、自动装箱、泛型、
I/O 以及 lambda
表达式等内容。本部分最后一章阐述了模块。模块是 Java SE 9 中最重要的新增特性。
第Ⅱ部分介绍
Java 的标准 API 库的关键内容。本部分的主题包括字符串、 I/O 、网络、标准实用工具、集合框
架、
AWT 、事件处理、图像、并发编程 ( 包括 Fork/Join 框架 ) 、正则表达式和新增的流库。
第Ⅲ部分用三章内容介绍
Swing
IV 部分用三章内容介绍 JavaFX
V 部分包含两章,这两章展示了 Java 的实际应用。本部分首先介绍 Java Bean ,然后介绍 servlet
本书下载资源包可扫描封底二维码获得。
致 谢
在此我要特别感谢 Patrick Naughton Joe O’Neil Danny Coward
Patrick Naughton Java 语言的创立者之一,他还参与编写了本书的第 1 版。本书第 21 23 27 章的大部分
材料最初都是由
Patrick 提供的。他的洞察力、专业知识和活力都对本书的成功付梓贡献极大。
在准备本书的第
2 版和第 3 版的过程中, Joe O’Neil 提供了原始素材,这些素材呈现在本书的第 30 32 37
38 章中。 Joe 对我的数本书都有帮助,并且他提供的帮助一直都是最高质量的。
Danny Coward 是本书第 10 版的技术编辑。 Danny 对我的数本书都有贡献,他的忠告、洞察力和建议都有巨大
价值,对此表示感谢。
如何进一步学习
Java 编程参考官方教程》为读者开启了 Herb Schildt Java 编程图书系列的大门。下面是其他一些你可能感兴趣
的图书:
Herb Schildt’s Java Programming Cookbook
Java
A Beginner’s Guide
Introducing JavaFX 8 Programming
Swing
A Beginner’s Guide
The Art of Java

目 录
部分 Java 语言
1 Java 的历史和演变 ······························3
1.1 Java 的家世 ············································ 3
1.1.1 现代编程语言的诞生: C 语言 ·················· 3
1.1.2 C++ :下一个阶段 ······································ 4
1.1.3 Java 出现的时机已经成熟 ························· 4
1.2 Java
的诞生 ············································ 5
1.3 Java
改变 Internet 的方式 ······················ 6
1.3.1 Java applet··················································· 6
1.3.2 安全性 ························································· 6
1.3.3 可移植性 ····················································· 6
1.4 Java
的魔力:字节码 ···························· 7
1.5
不再推荐使用 applet ····························· 7
1.6 servlet
:服务器端的 Java······················ 8
1.7 Java
的关键特性 ···································· 8
1.7.1 简单性 ························································· 8
1.7.2 面向对象 ····················································· 8
1.7.3 健壮性 ························································· 8
1.7.4 多线程 ························································· 9
1.7.5 体系结构中立 ············································· 9
1.7.6 解释执行和高性能 ····································· 9
1.7.7 分布式 ························································· 9
1.7.8 动态性 ························································· 9
1.8 Java
的演变历程 ···································· 9
1.9 Java SE 9···············································11
1.10
文化革新 ·············································12
2 Java 综述 ··········································13
2.1 面向对象编程 ······································ 13
2.1.1 两种范式 ··················································· 13
2.1.2 抽象 ··························································· 13
2.1.3 OOP 三原则 ·············································· 14
2.2
第一个简单程序 ·································· 16
2.2.1 输入程序 ··················································· 17
2.2.2 编译程序 ··················································· 17
2.2.3 深入分析第一个示例程序 ······················· 17
2.3
第二个简短程序 ·································· 19
2.4
两种控制语句 ······································ 20
2.4.1 if 语句 ······················································· 20
2.4.2 for 循环 ····················································· 21
2.5
使用代码块 ·········································· 22
2.6
词汇问题 ·············································· 23
2.6.1 空白符 ······················································· 23
2.6.2 标识符 ······················································· 23
2.6.3 字面值 ······················································· 23
2.6.4 注释 ··························································· 23
2.6.5 分隔符 ······················································· 23
2.6.6 Java 关键字 ··············································· 24
2.7 Java
类库 ·············································· 24
3 章 数据类型、变量和数组 ······················25
3.1 Java 是强类型化的语言 ······················ 25
3.2
基本类型 ·············································· 25
3.3
整型 ····················································· 25
VIII Java 9 编程参考官方大全(第 10 版)
3.3.1 byte···························································· 26
3.3.2 short··························································· 26
3.3.3 int ······························································ 26
3.3.4 long ··························································· 26
3.4
浮点型 ···················································27
3.4.1 float ··························································· 27
3.4.2 double························································ 27
3.5
字符型 ···················································27
3.6
布尔型 ···················································29
3.7
深入分析字面值 ···································29
3.7.1 整型字面值 ··············································· 29
3.7.2 浮点型字面值 ··········································· 30
3.7.3 布尔型字面值 ··········································· 30
3.7.4 字符型字面值 ··········································· 31
3.7.5 字符串字面值 ··········································· 31
3.8
变量 ······················································31
3.8.1 变量的声明 ··············································· 31
3.8.2 动态初始化 ··············································· 32
3.8.3 变量的作用域和生存期 ··························· 32
3.9
类型转换和强制类型转换 ····················34
3.9.1 Java 的自动类型转换 ······························· 34
3.9.2 强制转换不兼容的类型 ··························· 34
3.10
表达式中的自动类型提升 ··················35
3.11
数组 ·····················································36
3.11.1 一维数组 ··············································· 36
3.11.2 多维数组 ··············································· 38
3.11.3 另一种数组声明语法 ··························· 41
3.12
关于字符串的一些说明 ·····················41
4 章 运算符 ···············································43
4.1 算术运算符 ···········································43
4.1.1 基本算术运算符 ······································· 43
4.1.2 求模运算符 ··············································· 44
4.1.3 算术与赋值复合运算符 ··························· 44
4.1.4 自增与自减运算符 ··································· 45
4.2
位运算符 ···············································46
4.2.1 位逻辑运算符 ··········································· 47
4.2.2 左移 ··························································· 49
4.2.3 右移 ··························································· 50
4.2.4 无符号右移 ··············································· 51
4.2.5 位运算符与赋值的组合 ··························· 52
4.3
关系运算符 ···········································52
4.4
布尔逻辑运算符 ···································53
4.5
赋值运算符 ···········································54
4.6
? ”运算符 ···········································55
4.7
运算符的优先级 ···································55
4.8
使用圆括号 ·········································· 56
5 章 控制语句 ············································57
5.1 Java 的选择语句 ·································· 57
5.1.1 if 语句 ······················································· 57
5.1.2 switch 语句 ··············································· 59
5.2
迭代语句 ·············································· 62
5.2.1 while 语句 ················································· 63
5.2.2 do-while 语句 ············································ 64
5.2.3 for 语句 ····················································· 66
5.2.4 for 循环的 for-each 版本 ·························· 68
5.2.5 嵌套的循环 ··············································· 72
5.3
跳转语句 ·············································· 72
5.3.1 使用 break 语句 ········································ 72
5.3.2 使用 continue 语句 ··································· 75
5.3.3 return 语句 ················································ 76
6 章 类 ·······················································77
6.1 类的基础知识 ······································ 77
6.1.1 类的一般形式 ··········································· 77
6.1.2 一个简单的类 ··········································· 78
6.2
声明对象 ·············································· 79
6.3
为对象引用变量赋值 ·························· 80
6.4
方法 ····················································· 81
6.4.1 Box 类添加方法 ·································· 81
6.4.2 返回值 ······················································· 83
6.4.3 添加带参数的方法 ··································· 84
6.5
构造函数 ·············································· 85
6.6 this
关键字 ··········································· 87
6.7
垃圾回收 ·············································· 88
6.8
堆栈类 ·················································· 88
7 章 方法和类的深入分析 ··························91
7.1 重载方法 ·············································· 91
7.2
将对象用作参数 ·································· 94
7.3
实参传递的深入分析 ·························· 96
7.4
返回对象 ·············································· 97
7.5
递归 ····················································· 98
7.6
访问控制 ·············································100
7.7
理解 static ···········································102
7.8 final
介绍 ············································104
7.9
重新审视数组 ·····································104
7.10
嵌套类和内部类 ·······························105
7.11 String
类介绍 ····································107
7.12
使用命令行参数 ·······························109
7.13 varargs
:可变长度实参 ····················110
7.13.1 重载 varargs 方法 ······························· 112
目 录 IX
7.13.2 varargs 方法与模糊性 ························ 113
8 章 继承 ·················································115
8.1 继承的基础知识 ·································115
8.1.1 成员访问与继承 ····································· 116
8.1.2 一个更实际的例子 ································· 117
8.1.3 超类变量可以引用子类对象 ················· 118
8.2
使用 super 关键字 ······························119
8.2.1 使用 super 调用超类的构造函数 ··········· 119
8.2.2 super 的另一种用法 ······························· 122
8.3
创建多级继承层次 ·····························123
8.4
构造函数的执行时机 ·························125
8.5
方法重写 ·············································126
8.6
动态方法调度 ·····································128
8.6.1 重写方法的目的 ····································· 129
8.6.2 应用方法重写 ········································· 129
8.7
使用抽象类 ·········································130
8.8
在继承中使用 final 关键字 ················132
8.8.1 使用 final 关键字阻止重写 ···················· 132
8.8.2 使用 final 关键字阻止继承 ···················· 133
8.9 Object
·············································133
9 章 包和接口 ··········································135
9.1 ························································135
9.1.1 定义包 ····················································· 135
9.1.2 包查找与 CLASSPATH ·························· 136
9.1.3 一个简短的包示例 ································· 136
9.2
包和成员访问 ·····································137
9.3
导入包 ·················································140
9.4
接口 ····················································141
9.4.1 定义接口 ················································· 141
9.4.2 实现接口 ················································· 142
9.4.3 嵌套接口 ················································· 144
9.4.4 应用接口 ················································· 144
9.4.5 接口中的变量 ········································· 147
9.4.6 接口可以扩展 ········································· 148
9.5
默认接口方法 ·····································149
9.5.1 默认方法的基础知识 ····························· 149
9.5.2 一个更加实用的例子 ····························· 151
9.5.3 多级继承的问题 ····································· 151
9.6
在接口中使用静态方法 ·····················152
9.7
私有接口方法 ·····································152
9.8
关于包和接口的最后说明 ··················153
10 章 异常处理 ········································155
10.1 异常处理的基础知识 ·······················155
10.2
异常类型 ···········································155
10.3
未捕获的异常 ···································156
10.4
使用 try catch ·······························157
10.5
多条 catch 子句 ·································158
10.6
嵌套的 try 语句 ·································159
10.7 throw ·················································161
10.8 throws················································162
10.9 finally ················································162
10.10 Java
的内置异常 ·····························164
10.11
创建自己的异常子类 ······················165
10.12
链式异常 ·········································166
10.13
其他三个异常特性 ·························167
10.14
使用异常 ·········································168
11 章 多线程编程 ····································169
11.1 Java 线程模型 ···································169
11.1.1 线程优先级 ········································ 170
11.1.2 同步 ···················································· 170
11.1.3 消息传递 ············································ 171
11.1.4 Thread 类和 Runnable 接口 ··············· 171
11.2
主线程 ···············································171
11.3
创建线程 ···········································172
11.3.1 实现 Runnable 接口 ··························· 172
11.3.2 扩展 Thread ··································· 174
11.3.3 选择一种创建方式 ···························· 175
11.4
创建多个线程 ···································175
11.5
使用 isAlive() join() 方法 ···············176
11.6
线程优先级 ·······································178
11.7
同步 ···················································179
11.7.1 使用同步方法 ···································· 179
11.7.2 synchronized 语句 ······························ 180
11.8
线程间通信 ·······································182
11.9
挂起、恢复与停止线程 ····················187
11.10
获取线程的状态 ·····························189
11.11
使用工厂方法创建和启动线程 ······189
11.12
使用多线程 ·····································190
12 章 枚举、自动装箱与注解 ··················191
12.1 枚举 ···················································191
12.1.1 枚举的基础知识 ································ 191
12.1.2 values() valueOf() 方法 ··················· 193
12.1.3 Java 枚举是类类型 ···························· 194
12.1.4 枚举继承自 Enum ························ 195
12.1.5 另一个枚举示例 ································ 196
12.2
类型封装器 ·······································198
12.2.1 Character 封装器 ······························· 198
12.2.2 Boolean 封装器 ·································· 198
12.2.3 数值类型封装器 ································ 198
X Java 9 编程参考官方大全(第 10 版)
12.3 自动装箱 ···········································199
12.3.1 自动装箱与方法 ································ 200
12.3.2 表达式中发生的自动装箱 / 拆箱 ······· 201
12.3.3 布尔型和字符型数值的自动装箱 /
拆箱 ··················································· 202
12.3.4 自动装箱 / 拆箱有助于防止错误 ······· 202
12.3.5 一些警告 ············································ 203
12.4
注解 ··················································203
12.4.1 注解的基础知识 ································ 203
12.4.2 指定保留策略 ···································· 204
12.4.3 在运行时使用反射获取注解 ············ 204
12.4.4 AnnotatedElement 接口 ····················· 208
12.4.5 使用默认值 ········································ 208
12.4.6 标记注解 ············································ 209
12.4.7 单成员注解 ········································ 210
12.4.8 内置注解 ············································ 211
12.5
类型注解 ···········································213
12.6
重复注解 ···········································216
13 I/O 、带资源的 try 语句以及其他
主题
···············································219
13.1 I/O 的基础知识 ·································219
13.1.1 ······················································· 219
13.1.2 字节流和字符流 ································ 219
13.1.3 预定义流 ············································ 221
13.2
读取控制台输入 ·······························221
13.2.1 读取字符 ············································ 222
13.2.2 读取字符串 ········································ 222
13.3
向控制台写输出 ·······························223
13.4 PrintWriter
····································224
13.5
/ 写文件 ··········································225
13.6
自动关闭文件 ···································229
13.7 transient
volatile 修饰符 ···············231
13.8
使用 instanceof 运算符 ·····················232
13.9 strictfp ···············································233
13.10
本地方法 ·········································234
13.11
使用 assert·······································234
13.12
静态导入 ·········································236
13.13
通过 this() 调用重载的构造函数 ·····238
13.14
紧凑 API 配置文件 ·························239
14 章 泛型 ···············································241
14.1 什么是泛型 ·······································241
14.2
一个简单的泛型示例 ·······················241
14.2.1 泛型只使用引用类型 ························ 244
14.2.2 基于不同类型参数的泛型类型
是不同的
············································ 244
14.2.3 泛型提升类型安全性的原理 ············ 244
14.3
带两个类型参数的泛型类 ················246
14.4
泛型类的一般形式 ···························247
14.5
有界类型 ···········································247
14.6
使用通配符参数 ·······························249
14.7
创建泛型方法 ···································255
14.8
泛型接口 ···········································257
14.9
原始类型与遗留代码 ·······················259
14.10
泛型类层次 ·····································260
14.10.1 使用泛型超类 ·································· 260
14.10.2 泛型子类 ·········································· 262
14.10.3 泛型层次中的运行时类型比较 ······ 263
14.10.4 强制转换 ·········································· 265
14.10.5 重写泛型类的方法 ·························· 265
14.11
泛型的类型推断 ·····························266
14.12
擦除 ·················································267
14.13
模糊性错误 ·····································268
14.14
使用泛型的一些限制 ·····················269
14.14.1 不能实例化类型参数 ······················ 269
14.14.2 对静态成员的一些限制 ·················· 269
14.14.3 对泛型数组的一些限制 ·················· 270
14.14.4 对泛型异常的限制 ·························· 271
15 lambda 表达式 ·······························273
15.1 lambda 表达式简介 ···························273
15.1.1 lambda 表达式的基础知识 ··············· 273
15.1.2 函数式接口 ········································ 274
15.1.3 几个 lambda 表达式示例 ·················· 275
15.2
lambda 表达式 ·····························277
15.3
泛型函数式接口 ·······························278
15.4
作为参数传递 lambda 表达式 ··········280
15.5 lambda
表达式与异常 ·······················282
15.6 lambda
表达式和变量捕获 ···············282
15.7
方法引用 ···········································283
15.7.1 静态方法的方法引用 ························ 283
15.7.2 实例方法的方法引用 ························ 284
15.7.3 泛型中的方法引用 ···························· 287
15.8
构造函数引用 ···································289
15.9
预定义的函数式接口 ·······················293
16 章 模块 ···············································295
16.1 模块基础知识 ···································295
16.1.1 简单的模块示例 ································ 295
16.1.2 编译、运行第一个模块示例 ············ 298
16.1.3 requires exports ····························· 299
16.2 java.base
和平台模块 ························299
16.3
旧代码和未命名的模块 ····················300
目 录 XI
16.4 导出到特定的模块 ···························300
16.5
使用 requires transitive······················301
16.6
使用服务 ···········································304
16.6.1 服务和服务提供程序的基础知识 ···· 304
16.6.2 基于服务的关键字 ···························· 305
16.6.3 基于模块的服务示例 ························ 305
16.7
模块图 ···············································310
16.8
三个特殊的模块特性 ·······················310
16.8.1 open 模块 ··········································· 310
16.8.2 opens 语句 ········································· 310
16.8.3 requires static ····································· 311
16.9 jlink
工具和模块 JAR 文件介绍 ······311
16.9.1 链接 exploded directory 中的文件 ···· 311
16.9.2 链接模块化的 JAR 文件 ··················· 311
16.9.3 JMOD 文件 ········································ 312
16.10
层与自动模块简述 ·························312
16.11
小结 ·················································312
第Ⅱ部分 Java
17 章 字符串处理 ····································315
17.1 String 类的构造函数 ·························315
17.2
字符串的长度 ···································317
17.3
特殊的字符串操作 ···························317
17.3.1 字符串字面值 ···································· 317
17.3.2 字符串连接 ········································ 317
17.3.3 字符串和其他数据类型的连接 ········ 318
17.3.4 字符串转换和 toString() 方法 ············ 318
17.4
提取字符 ···········································319
17.4.1 charAt() ·············································· 319
17.4.2 getChars()··········································· 319
17.4.3 getBytes() ··········································· 320
17.4.4 toCharArray()····································· 320
17.5
比较字符串 ·······································320
17.5.1 equals() equalsIgnoreCase() ··········· 320
17.5.2 regionMatches() ································· 321
17.5.3 startsWith() endsWith() ·················· 321
17.5.4 equals() == ······································ 321
17.5.5 compareTo() ······································· 322
17.6
查找字符串 ·······································323
17.7
修改字符串 ·······································324
17.7.1 substring() ·········································· 324
17.7.2 concat()··············································· 325
17.7.3 replace() ············································· 325
17.7.4 trim() ·················································· 325
17.8
使用 valueOf() 转换数据 ···················326
17.9
改变字符串中字符的大小写 ············326
17.10
连接字符串 ·····································327
17.11
其他 String 方法 ······························327
17.12 StringBuffer
································328
17.12.1 StringBuffer 类的构造函数 ············· 328
17.12.2 length() capacity() ························ 329
17.12.3 ensureCapacity()······························· 329
17.12.4 setLength() ······································· 329
17.12.5 charAt() setCharAt()····················· 329
17.12.6 getChars()········································· 330
17.12.7 append() ··········································· 330
17.12.8 insert() ·············································· 330
17.12.9 reverse() ··········································· 331
17.12.10 delete() deleteCharAt() ··············· 331
17.12.11 replace()·········································· 332
17.12.12 substring() ······································ 332
17.12.13 其他 StringBuffer 方法 ·················· 332
17.13 StringBuilder
······························333
18 章 探究 java.lang································335
18.1 基本类型封装器 ·······························335
18.1.1 Number ·············································· 336
18.1.2 Double Float·································· 336
18.1.3 理解 isInfinite() isNaN() ················ 338
18.1.4 Byte Short Integer Long ·········· 339
18.1.5 Character ············································ 346
18.1.6 Unicode 代码点的附加支持 ········· 348
18.1.7 Boolean ·············································· 349
18.2 Void
··············································349
18.3 Process
··········································349
18.4 Runtime
········································350
18.4.1 内存管理 ············································ 351
18.4.2 执行其他程序 ···································· 352
18.5 Runtime.Version ································353
18.6 ProcessBuilder
······························353
18.7 System
··········································355
18.7.1 使用 currentTimeMillis() 计时程序的
执行
···················································· 356
18.7.2 使用 arraycopy() 方法 ························ 357
18.7.3 环境属性 ············································ 357
18.8 System.Logger
System.Logger
Finder················································358
18.9 Object
···········································358
18.10
使用 clone() 方法和 Cloneable
接口 ················································358
18.11 Class
···········································360
XII Java 9 编程参考官方大全(第 10 版)
18.12 ClassLoader ································362
18.13 Math
···········································362
18.13.1 三角函数 ········································ 362
18.13.2 指数函数 ········································ 363
18.13.3 舍入函数 ········································ 363
18.13.4 其他数学方法 ································· 364
18.14 StrictMath
···································365
18.15 Compiler
·····································365
18.16 Thread
类、 ThreadGroup 类和
Runnable 接口 ································366
18.16.1 Runnable 接口 ································ 366
18.16.2 Thread ········································ 366
18.16.3 ThreadGroup ······························ 368
18.17 ThreadLocal
InheritableThread
Local
··········································371
18.18 Package
·······································371
18.19 Module
·······································372
18.20 ModuleLayer
······························372
18.21 RuntimePermission
·····················372
18.22 Throwable
···································373
18.23 SecurityManager
·························373
18.24 StackTraceElement
·····················373
18.25 StackWalker
类和 StackWalker.
StackFrame
接口 ·····························373
18.26 Enum
··········································373
18.27 ClassValue
··································374
18.28 CharSequence
接口 ·························374
18.29 Comparable
接口 ·····························374
18.30 Appendable
接口 ·····························375
18.31 Iterable
接口 ····································375
18.32 Readable
接口 ·································375
18.33 AutoCloseable
接口 ·························375
18.34 Thread.UncaughtExceptionHandler
接口 ················································376
18.35 java.lang
子包 ·································376
18.35.1 java.lang.annotation ························ 376
18.35.2 java.lang.instrument ························ 376
18.35.3 java.lang.invoke ······························ 376
18.35.4 java.lang.module ····························· 376
18.35.5 java.lang.management····················· 376
18.35.6 java.lang.ref ···································· 376
18.35.7 java.lang.reflect······························· 377
19 java.util 1 部分:集合框架 ········379
19.1 集合概述 ···········································380
19.2
集合接口 ···········································380
19.2.1 Collection 接口 ·································· 381
19.2.2 List 接口 ············································ 382
19.2.3 Set 接口 ·············································· 384
19.2.4 SortedSet 接口 ··································· 384
19.2.5 NavigableSet 接口 ····························· 385
19.2.6 Queue 接口 ········································ 385
19.2.7 Deque 接口 ········································ 386
19.3
集合类 ···············································387
19.3.1 ArrayList ······································· 388
19.3.2 LinkedList ····································· 390
19.3.3 HashSet ········································· 391
19.3.4 LinkedHashSet ······························ 392
19.3.5 TreeSet ·········································· 392
19.3.6 PriorityQueue ································ 393
19.3.7 ArrayDeque ··································· 394
19.3.8 EnumSet ········································ 395
19.4
通过迭代器访问集合 ·······················395
19.4.1 使用迭代器 ········································ 396
19.4.2 使用 for-each 循环替代迭代器 ········· 398
19.5 Spliterator··········································398
19.6
在集合中存储用户定义的类 ············400
19.7 RandomAccess
接口 ·························401
19.8
使用映射 ···········································401
19.8.1 映射接口 ············································ 402
19.8.2 映射类 ················································ 406
19.9
比较器 ···············································409
19.10
集合算法 ·········································415
19.11 Arrays
·········································420
19.12
遗留的类和接口 ·····························423
19.12.1 Enumeration 接口 ··························· 424
19.12.2 Vector ········································· 424
19.12.3 Stack ·········································· 427
19.12.4 Dictionary ·································· 428
19.12.5 Hashtable ··································· 429
19.12.6 Properties ··································· 431
19.12.7 使用 store() load() 方法 ··············· 434
19.13
集合小结 ·········································435
20 java.util 2 部分:更多实用
工具类
············································437
20.1 StringTokenizer ·····························437
20.2 BitSet
············································438
20.3 Optional
OptionalDouble
OptionalInt OptionalLong·············440
20.4 Date
··············································442
20.5 Calendar
·······································443
目 录 XIII
20.6 GregorianCalendar ························445
20.7 TimeZone
·····································446
20.8 SimpleTimeZone
··························447
20.9 Locale
···········································448
20.10 Random
······································449
20.11 Timer
TimerTask ····················450
20.12 Currency
·····································452
20.13 Formatter
····································453
20.13.1 Formatter 类的构造函数 ················ 453
20.13.2 Formatter 类的方法 ························ 453
20.13.3 格式化的基础知识 ························· 454
20.13.4 格式化字符串和字符 ····················· 455
20.13.5 格式化数字 ···································· 455
20.13.6 格式化时间和日期 ························· 456
20.13.7 %n %% 说明符 ···························· 458
20.13.8 指定最小字段宽度 ························· 458
20.13.9 指定精度 ········································ 459
20.13.10 使用格式标志 ······························· 460
20.13.11 对齐输出 ······································· 460
20.13.12 空格、“ + ”、“ ”以及“ (
标志
·············································· 461
20.13.13 逗号标志 ······································ 462
20.13.14 # ”标志 ···································· 462
20.13.15 大写选项 ······································ 462
20.13.16 使用参数索引 ······························· 462
20.13.17 关闭 Formatter 对象 ····················· 463
20.13.18 printf() 方法 ··································· 464
20.14 Scanner
·······································464
20.14.1 Scanner 类的构造函数 ··················· 464
20.14.2 扫描的基础知识 ····························· 465
20.14.3 一些 Scanner 示例 ·························· 467
20.14.4 设置定界符 ···································· 470
20.14.5 其他 Scanner 特性 ·························· 471
20.15 ResourceBundle
ListResourceBundle
PropertyResourceBundle ·········472
20.16
其他实用工具类和接口 ··················475
20.17 java.util
子包 ···································475
20.17.1 java.util.concurrent java.util.concurrent.
atomic
java.util.concurrent.locks··· 476
20.17.2 java.util.function ····························· 476
20.17.3 java.util.jar ······································ 478
20.17.4 java.util.logging ······························ 478
20.17.5 java.util.prefs ·································· 478
20.17.6 java.util.regex·································· 478
20.17.7 java.util.spi······································ 478
20.17.8 java.util.stream································ 478
20.17.9 java.util.zip······································ 478
21 章 输入 / 输出:探究 java.io·················479
21.1 I/O 类和接口 ·····································479
21.2 File
···············································480
21.2.1 目录 ···················································· 482
21.2.2 使用 FilenameFilter 接口 ··················· 483
21.2.3 listFiles() 方法 ····································· 483
21.2.4 创建目录 ············································ 484
21.3 AutoCloseable
Closeable
Flushable 接口 ··································484
21.4 I/O
异常 ············································484
21.5
关闭流的两种方式 ···························484
21.6
流类 ···················································485
21.7
字节流 ···············································486
21.7.1 InputStream ···································· 486
21.7.2 OutputStream ·································· 486
21.7.3 FileInputStream ······························ 487
21.7.4 FileOutputStream ··························· 488
21.7.5 ByteArrayInputStream ···················· 490
21.7.6 ByteArrayOutputStream ················· 491
21.7.7 过滤的字节流 ····································· 492
21.7.8 缓冲的字节流 ····································· 492
21.7.9 SequenceInputStream ····················· 495
21.7.10 PrintStream ··································· 496
21.7.11 DataOutputStream
DataInputStream ··························· 498
21.7.12 RandomAccessFile ······················· 499
21.8
字符流 ···············································500
21.8.1 Reader ············································ 500
21.8.2 Writer ············································· 501
21.8.3 FileReader ······································ 501
21.8.4 FileWriter ······································· 502
21.8.5 CharArrayReader ···························· 503
21.8.6 CharArrayWriter ····························· 504
21.8.7 BufferedReader ······························· 505
21.8.8 BufferedWriter ································ 506
21.8.9 PushbackReader ····························· 506
21.8.10 PrintWriter ···································· 507
21.9 Console
·········································508
21.10
串行化 ·············································509
21.10.1 Serializable 接口 ······························· 509
21.10.2 Externalizable 接口 ··························· 509
21.10.3 ObjectOutput 接口 ···························· 509
21.10.4 ObjectOutputStream ····················· 510
21.10.5 ObjectInput 接口 ······························· 510
21.10.6 ObjectInputStream ························ 511
XIV Java 9 编程参考官方大全(第 10 版)
21.10.7 串行化示例 ······································· 512
21.11
流的优点 ·········································513
22 章 探究 NIO········································515
22.1 NIO ··············································515
22.2 NIO
的基础知识 ·······························515
22.2.1 缓冲区 ················································· 515
22.2.2 通道 ····················································· 517
22.2.3 字符集和选择器 ································· 518
22.3 NIO.2
NIO 的增强 ·······················518
22.3.1 Path 接口 ············································· 518
22.3.2 Files ················································ 519
22.3.3 Path 接口 ············································· 521
22.3.4 文件属性接口 ····································· 521
22.3.5 FileSystem FileSystems
FileStore ········································· 522
22.4
使用 NIO 系统 ··································522
22.4.1 为基于通道的 I/O 使用 NIO·············· 523
22.4.2 为基于流的 I/O 使用 NIO·················· 529
22.4.3 为路径和文件系统操作使用 NIO ····· 531
23 章 联网 ···············································539
23.1 联网的基础知识 ·······························539
23.2
联网类和接口 ···································540
23.3 InetAddress
···································540
23.3.1 工厂方法 ············································· 540
23.3.2 实例方法 ············································· 541
23.4 Inet4Address
类和 Inet6Address ·····541
23.5 TCP/IP
客户端套接字 ······················542
23.6 URL
··············································544
23.7 URLConnection
····························545
23.8 HttpURLConnection
·····················547
23.9 URI
···············································549
23.10 cookie··············································549
23.11 TCP/IP
服务器套接字 ·····················549
23.12
数据报 ·············································549
23.12.1 DatagramSocket ························· 550
23.12.2 DatagramPacket ························· 550
23.12.3 数据报示例 ···································· 551
24 章 事件处理 ········································553
24.1 两种事件处理机制 ···························553
24.2
委托事件模型 ···································553
24.2.1 事件 ····················································· 554
24.2.2 事件源 ················································· 554
24.2.3 事件监听器 ········································· 554
24.3
事件类 ···············································554
24.3.1 ActionEvent ···································· 555
24.3.2 AdjustmentEvent ···························· 556
24.3.3 ComponentEvent ···························· 556
24.3.4 ContainerEvent ······························· 557
24.3.5 FocusEvent ····································· 557
24.3.6 InputEvent ······································ 558
24.3.7 ItemEvent ······································· 558
24.4 KeyEvent
······································559
24.4.1 MouseEvent ···································· 559
24.4.2 MouseWheelEvent ························· 560
24.4.3 TextEvent ······································· 561
24.4.4 WindowEvent ································· 561
24.5
事件源 ···············································562
24.6
事件监听器接口 ·······························562
24.6.1 ActionListener 接口 ···························· 563
24.6.2 AdjustmentListener 接口 ···················· 563
24.6.3 ComponentListener 接口 ···················· 563
24.6.4 ContainerListener 接口 ······················· 563
24.6.5 FocusListener 接口 ····························· 563
24.6.6 ItemListener 接口 ································ 563
24.6.7 KeyListener 接口 ································ 564
24.6.8 MouseListener 接口 ···························· 564
24.6.9 MouseMotionListener 接口 ················ 564
24.6.10 MouseWheelListener 接口 ················ 564
24.6.11 TextListener 接口 ······························ 564
24.6.12 WindowFocusListener 接口 ·············· 564
24.6.13 WindowListener 接口 ······················· 564
24.7
使用委托事件模型 ···························565
24.7.1 一些重要的 GUI 概念 ························ 565
24.7.2 处理鼠标事件 ····································· 565
24.7.3 处理键盘事件 ····································· 568
24.8
适配器类 ···········································571
24.9
内部类 ···············································572
25 AWT 介绍:使用窗口、图形和
文本
···············································577
25.1 AWT ·············································577
25.2
窗口基本元素 ···································579
25.2.1 Component ····································· 579
25.2.2 Container ········································ 579
25.2.3 Panel ··············································· 580
25.2.4 Window ·········································· 580
25.2.5 Frame ············································· 580
25.2.6 Canvas ············································ 580
25.3
使用框架窗口 ···································580
25.3.1 设置窗口的尺寸 ································· 580
目 录 XV
25.3.2 隐藏和显示窗口 ································· 580
25.3.3 设置窗口的标题 ································· 581
25.3.4 关闭框架窗口 ····································· 581
25.3.5 paint() 方法 ·········································· 581
25.3.6 显示字符串 ········································· 581
25.3.7 设置前景色和背景色 ························· 581
25.3.8 请求重画 ············································· 582
25.3.9 创建基于框架的应用程序 ················· 583
25.4
使用图形 ···········································583
25.4.1 绘制直线 ············································· 583
25.4.2 绘制矩形 ············································· 583
25.4.3 绘制椭圆和圆 ····································· 584
25.4.4 绘制弧形 ············································· 584
25.4.5 绘制多边形 ········································· 584
25.4.6 演示绘制方法 ····································· 584
25.4.7 改变图形的大小 ································· 586
25.5
使用颜色 ···········································587
25.5.1 Color 类的方法 ··································· 587
25.5.2 设置当前图形的颜色 ························· 588
25.5.3 一个演示颜色的 applet ······················ 588
25.6
设置绘图模式 ···································589
25.7
使用字体 ···········································590
25.7.1 确定可用字体 ····································· 591
25.7.2 创建和选择字体 ································· 592
25.7.3 获取字体信息 ····································· 594
25.8
使用 FontMetrics 管理文本输出 ······595
26 章 使用 AWT 控件、布局管理器和
菜单
···············································599
26.1 AWT 控件的基础知识 ······················599
26.1.1 添加和移除控件 ································· 599
26.1.2 响应控件 ············································· 600
26.1.3 HeadlessException 异常 ····················· 600
26.2
使用标签 ······················································ 600
26.3
使用命令按钮 ···································601
26.4
使用复选框 ·······································604
26.5
使用复选框组 ···································606
26.6
使用下拉列表 ···································607
26.7
使用列表框 ·······································609
26.8
管理滚动条 ·······································611
26.9
使用 TextField···································613
26.10
使用 TextArea ·································615
26.11
理解布局管理器 ·····························617
26.11.1 FlowLayout 布局管理器 ················ 617
26.11.2 BorderLayout 布局管理器 ············· 618
26.11.3 使用 Insets······································ 619
26.11.4 GridLayout 布局管理器 ················· 620
26.11.5 CardLayout 布局管理器 ················ 621
26.11.6 GridBagLayout 布局管理器 ·········· 623
26.12
菜单栏和菜单 ·································627
26.13
对话框 ·············································630
26.14
关于重写 paint() 方法 ······················634
27 章 图像 ···············································635
27.1 文件格式 ···········································635
27.2
图像基础:创建、加载与显示 ········635
27.2.1 创建 Image 对象 ································· 635
27.2.2 加载图像 ············································· 636
27.2.3 显示图像 ············································· 636
27.3
双缓冲 ···············································637
27.4 ImageProducer
接口 ··························639
27.5 ImageConsumer
接口 ························641
27.6 ImageFilter
···································643
27.6.1 CropImageFilter ····························· 643
27.6.2 RGBImageFilter ····························· 645
27.7
其他图像类 ·······································653
28 章 并发实用工具 ·································655
28.1 并发 API ······································655
28.1.1 java.util.concurrent ························· 655
28.1.2 java.util.concurrent.atomic ············· 656
28.1.3 java.util.concurrent.locks ················ 656
28.2
使用同步对象 ···································657
28.2.1 Semaphore ······································ 657
28.2.2 CountDownLatch ··························· 661
28.2.3 CyclicBarrier ·································· 662
28.2.4 Exchanger ·······························664
28.2.5 Phaser ············································· 666
28.3
使用执行器 ·······································671
28.3.1 一个简单的执行器示例 ····················· 672
28.3.2 使用 Callable Future 接口 ·············· 673
28.4 TimeUnit
枚举 ···································675
28.5
并发集合 ···········································676
28.6
······················································676
28.7
原子操作 ···········································678
28.8
通过 Fork/Join 框架进行并行编程 ····679
28.8.1 主要的 Fork/Join ···························· 680
28.8.2 分而治之的策略 ································· 682
28.8.3 一个简单的 Fork/Join 示例 ················ 682
28.8.4 理解并行级别带来的影响 ················· 684
28.8.5 一个使用 RecursiveTask 的例子 ······ 686
28.8.6 异步执行任务 ····································· 688
28.8.7 取消任务 ············································· 688
XVI Java 9 编程参考官方大全(第 10 版)
28.8.8 确定任务的完成状态 ························· 689
28.8.9 重新启动任务 ····································· 689
28.8.10 深入研究 ··········································· 689
28.8.11 关于 Fork/Join 框架的一些提示 ·····690
28.9
并发实用工具与 Java 传统方式的
比较
··················································690
29 章 流 API············································691
29.1 流的基础知识 ···································691
29.1.1 流接口 ················································· 691
29.1.2 如何获得流 ········································· 693
29.1.3 一个简单的流示例 ····························· 693
29.2
缩减操作 ···········································696
29.3
使用并行流 ·······································697
29.4
映射 ··················································699
29.5
收集 ··················································702
29.6
迭代器和流 ·······································705
29.6.1 对流使用迭代器 ································· 705
29.6.2 使用 Spliterator ··································· 706
29.7
API 中更多值得探究的地方 ·······708
30 章 正则表达式和其他包 ······················709
30.1 正则表达式处理 ·······························709
30.1.1 Pattern ············································ 709
30.1.2 Matcher ·········································· 709
30.1.3 正则表达式的语法 ····························· 710
30.1.4 演示模式匹配 ····································· 710
30.1.5 模式匹配的两个选项 ························· 714
30.1.6 探究正则表达式 ································· 715
30.2
反射 ··················································715
30.3
远程方法调用 ···································718
30.4
使用 java.text 格式化日期和时间 ····720
30.4.1 DateFormat ····································· 720
30.4.2 SimpleDateFormat ·························· 722
30.5 java.time
的时间和日期 API ············723
30.5.1 时间和日期的基础知识 ····················· 723
30.5.2 格式化日期和时间 ····························· 724
30.5.3 解析日期和时间字符串 ····················· 726
30.6
探究 java.time 包的其他方面 ···········727
第Ⅲ部分 使用 Swing 进行 GUI 编程
31 Swing 简介 ····································731
31.1 Swing 的起源 ····································731
31.2 Swing
AWT 为基础 ·····················731
31.3
两个关键的 Swing 特性 ···················731
31.3.1 Swing 组件是轻量级的 ······················ 732
31.3.2 Swing 支持可插入外观 ······················ 732
31.4 MVC
连接 ·········································732
31.5
组件与容器 ·······································733
31.5.1 组件 ····················································· 733
31.5.2 容器 ····················································· 733
31.5.3 顶级容器窗格 ····································· 733
31.6 Swing
············································734
31.7
一个简单的 Swing 应用程序 ············734
31.8
事件处理 ···········································737
31.9
Swing 中绘图 ·······························739
31.9.1 绘图的基础知识 ································· 739
31.9.2 计算可绘制区域 ································· 740
31.9.3 一个绘图示例 ····································· 740
32 章 探索 Swing ····································743
32.1 JLabel ImageIcon··························743
32.2 JTextField··········································744
32.3 Swing
按钮 ········································745
32.3.1 JButton ················································ 746
32.3.2 JToggleButton ····································· 748
32.3.3 复选框 ················································· 749
32.3.4 单选按钮 ············································· 750
32.4 JTabbedPane······································752
32.5 JScrollPane········································754
32.6 JList···················································756
32.7 JComboBox·······································758
32.8
······················································760
32.9 JTable ················································762
33 Swing 菜单简介 ·····························765
33.1 菜单的基础知识 ·······························765
33.2 JMenuBar
JMenu JMenuItem
概述 ···················································766
33.2.1 JMenuBar ············································ 766
33.2.2 JMenu·················································· 767
33.2.3 JMenuItem··········································· 767
33.3
创建主菜单 ·······································768
33.4
向菜单项添加助记符和加速键 ········771
33.5
向菜单项添加图片和工具提示 ········773
33.6
使用 JRadioButtonMenuItem
JCheckBoxMenuItem ························773
33.7
创建弹出菜单 ···································775
33.8
创建工具栏 ·······································777
33.9
使用动作 ···········································778
33.10
完整演示 MenuDemo 程序 ·············782
33.11
继续探究 Swing ······························787
目 录 XVII
第Ⅳ部分 使用 JavaFX 进行 GUI 编程
34 JavaFX GUI 编程简介 ···················791
34.1 JavaFX 的基础概念 ··························791
34.1.1 JavaFX ············································ 791
34.1.2 Stage Scene ································ 792
34.1.3 节点和场景图 ····································· 792
34.1.4 布局 ····················································· 792
34.1.5 Application 类和生命周期方法 ········· 792
34.1.6 启动 JavaFX 应用程序 ······················· 792
34.2 JavaFX
应用程序的骨架 ··················793
34.3
编译和运行 JavaFX 程序 ·················795
34.4
应用程序线程 ···································795
34.5
一个简单的 JavaFX 控件: Label·····796
34.6
使用按钮和事件 ·······························797
34.6.1 事件的基础知识 ································· 797
34.6.2 按钮控件简介 ····································· 798
34.6.3 演示事件处理和按钮 ························· 798
34.7
直接在画布上绘制 ···························800
35 章 探究 JavaFX 控件 ··························805
35.1 使用 Image ImageView ················805
35.1.1 向标签添加图片 ································· 807
35.1.2 在按钮中使用图片 ····························· 808
35.2 ToggleButton·····································810
35.3 RadioButton ······································812
35.3.1 处理开关组中的变化事件 ················· 814
35.3.2 处理单选按钮的另一种方式 ············· 815
35.4 CheckBox··········································817
35.5 ListView ············································820
35.5.1 ListView 的滚动条 ····························· 822
35.5.2 启用多项选择 ····································· 823
35.6 ComboBox ········································823
35.7 TextField ···········································826
35.8 ScrollPane ·········································828
35.9 TreeView ···········································830
35.10
效果和变换简介 ·····························833
35.10.1 效果 ··············································· 834
35.10.2 变换 ··············································· 834
35.10.3 演示效果和变换 ···························· 835
35.11
添加工具提示 ·································837
35.12
禁用控件 ·········································838
36 JavaFX 菜单简介 ···························839
36.1 菜单的基础知识 ·······························839
36.2 MenuBar
Menu MenuItem
概述 ··················································840
36.2.1 MenuBar·············································· 840
36.2.2 Menu ··················································· 841
36.2.3 MenuItem ············································ 841
36.3
创建主菜单 ·······································841
36.4
向菜单项添加助记符和加速键 ········845
36.5
向菜单项添加图片 ···························846
36.6
使用 RadioMenuItem
CheckMenuItem ································847
36.7
创建上下文菜单 ·······························848
36.8
创建工具栏 ·······································850
36.9
完整的 MenuDemo 程序 ··················852
36.10
继续探究 JavaFX ····························857
第Ⅴ部分 应用 Java
37 Java Bean ·····································861
37.1 Java Bean 是什么 ······························861
37.2 Java Bean
的优势 ······························861
37.3
内省 ···················································861
37.3.1 属性的设计模式 ································· 862
37.3.2 事件的设计模式 ································· 863
37.3.3 方法与设计模式 ································· 863
37.3.4 使用 BeanInfo 接口 ···························· 863
37.4
绑定属性与约束属性 ·······················863
37.5
持久性 ···············································864
37.6
定制器 ···············································864
37.7 Java Bean API ···································864
37.7.1 Introspector ····································· 865
37.7.2 PropertyDescriptor ························· 865
37.7.3 EventSetDescriptor ························· 865
37.7.4 MethodDescriptor ··························· 866
37.8
一个 Bean 示例 ·································866
38 servlet············································869
38.1 背景 ···················································869
38.2 servlet
的生命周期 ····························869
38.3 servlet
开发选项 ·······························870
38.4
使用 Tomcat ······································870
38.5
一个简单的 servlet····························871
38.5.1 创建和编译 servlet 源代码 ················· 871
38.5.2 启动 Tomcat········································ 872
38.5.3 启动 Web 浏览器并请求 servlet········· 872
38.6 Servlet API ········································872
38.7 javax.servlet
··································872
38.7.1 Servlet 接口 ········································ 873
XVIII Java 9 编程参考官方大全(第 10 版)
38.7.2 ServletConfig 接口 ······························ 873
38.7.3 ServletContext 接口 ···························· 873
38.7.4 ServletRequest 接口 ···························· 874
38.7.5 ServletResponse 接口 ························· 874
38.7.6 GenericServlet ································ 875
38.7.7 ServletInputStream ························· 875
38.7.8 ServletOutputStream ······················ 875
38.7.9 servlet 异常类 ····································· 875
38.8
读取 servlet 参数 ······························875
38.9 javax.servlet.http
···························876
38.9.1 HttpServletRequest 接口 ····················· 877
38.9.2 HttpServletResponse 接口 ·················· 878
38.9.3 HttpSession 接口 ································· 878
38.9.4 Cookie ············································ 879
38.9.5 HttpServlet ····································· 879
38.10
处理 HTTP 请求和响应 ··················880
38.10.1 处理 HTTP GET 请求 ···················· 880
38.10.2 处理 HTTP POST 请求 ·················· 881
38.11
使用 cookie ·····································882
38.12
会话跟踪 ·········································884
第Ⅵ部分 附录
附录 A 使用 Java 的文档注释 ······················889
附录 B Java Web Start 概述 ························895
附录 C JShell 简介 ·······································901
附录 D applet 基础 ·······································909
附录 E JDK 10 的两个重要特性 ···················915
第 11 章 多线程编程
Java 对多线程编程 (multithreaded programming) 提供了内置支持。多线程程序包含可以同时运行的两个或更多个
部分。这种程序的每一部分被称为一个线程
(thread) ,并且每个线程定义了单独的执行路径。因此,多线程是特殊形
式的多任务处理。
几乎可以肯定,你对多任务处理有所了解,因为实际上所有现代操作系统都支持多任务处理。但是,多任务处
理有两种不同的类型:基于进程的多任务处理和基于线程的多任务处理。理解这两者之间的区别很重要。对于许多
读者,往往更熟悉基于进程的多任务处理形式。进程
(process) 本质上是正在执行的程序。因此,基于进程的多任务
处理就是允许计算机同时运行两个或更多个程序的特性。例如,基于进程的
(process-based) 多任务处理可以在运行
Java 编译器的同时使用文本编辑器或浏览网站。在基于进程的多任务处理中,程序是调度程序能够调度的最小代码
单元。
在基于线程的
(thread-based) 多任务环境中,最小的可调度代码单元是线程,这意味着单个程序可以同时执行两
个或更多个任务。例如,文本编辑器可以在打印的同时格式化文本,只要这两个动作是通过两个独立的线程执行即
可。因此,基于进程的多任务处理“大局”,而基于线程的多任务处理“细节”。
多任务线程需要的开销比多任务进程小。进程是重量级的任务,它们需要自己的地址空间。进程间通信开销很
大并且有许多限制。从一个进程上下文切换到另一个进程上下文的开销也很大。另一方面,线程是轻量级的任务。
它们共享相同的地址空间,并且协作共享同一个重量级的进程。线程间通信的开销不大,并且从一个线程上下文切
换到另一个线程上下文的开销更小。虽然
Java 程序使用的是基于多进程的多任务环境,但是基于多进程的多任务
处理不是由
Java 控制的。不过,基于多线程的多任务处理是由 Java 控制的。
使用多线程可以编写出更加高效的程序,以最大限度地利用系统提供的处理功能。多线程实现最大限度利用系
统功能的一种重要方式是使空闲时间保持最少。对于交互式网络环境中的
Java 操作这很重要,因为对于这种情况
空闲时间很普遍。例如,网络上数据的传输速率比计算机能够处理的速率低很多。即使是读写本地文件系统资源,
速度也比
CPU 的处理速度慢很多。并且,用户输入速度当然也比计算机的处理速度慢很多。在单线程环境中,程
序在处理这些任务中的下一任务之前必须等待当前任务完成——尽管在等待输入时,程序在大部分时间是空闲的。
多线程有助于减少空闲时间,因为当等待输入时可以运行另一个线程。
如果曾经编写过基于
Windows 这类操作系统的程序,那么你肯定已经熟悉多线程编程了。但是, Java 管理线
程这一事实使得多线程编程特别方便,因为
Java 为你处理了许多细节。
11.1 Java 线程模型
Java 运行时系统在许多方面依赖于线程,并且所有类库在设计时都考虑了多线程。事实上, Java 通过利用线程
使得整个环境能够异步执行。这有助于通过防止浪费
CPU 时钟周期来提高效率。
通过与单线程环境进行比较, 可以更好地理解多线程环境的价值。 单线程系统使用一种称为轮询事件循环
(event
loop with polling)
的方法。在这种模型中,单线程在一个无限循环中控制运行,轮询一个事件队列以决定下一步做什
么。一旦轮询返回一个信号,比如准备读取网络文件的信号,事件循环就将控制调度至适当的事件处理程序。在这
个事件处理程序返回之前,程序不能执行任何其他工作。这浪费了
CPU 时间,并且会导致程序的一部分支配着系
统而阻止对所有其他部分进行处理。通常,在单线程环境中,当线程因为等待某些资源而阻塞
( 即挂起执行 ) 时,整
个程序会停止运行。
Java 多线程的优点消除了主循环 / 轮询机制。可以暂停一个线程而不会停止程序的其他部分。例如,由于线程
170 第Ⅰ部分 Java 语言
从网络读取数据或等待用户输入而造成的空闲时间,可以在其他地方得以利用。多线程允许当前激活的循环在两帧
之间休眠,而不会造成整个系统暂停。当
Java 程序中的线程阻塞时,只有被阻塞的线程会暂停,所有其他线程仍
将继续运行。
大部分读者都知道,在过去几年,多核系统已经变得很普遍了。当然,单核系统仍然在广泛使用。
Java 的多线
程系统在这两种类型的系统中都可以工作,理解这一点很重要。在单核系统中,并发执行的线程共享
CPU ,每个线
程得到一片
CPU 时钟周期。所以,在单核系统中,两个或更多个线程不是真正同时运行的,但是空闲时间被利用
了。然而,在多核系统中,两个或更多个线程可能是真正同步执行的。在许多情况下,这会进一步提高程序的效率
并提高特定操作的速度。
注意:
除了本章中描述的多线程处理特性外,你还希望探讨 Fork/Join 框架,该框架为创建能够自动伸缩以充分利用
多核环境的多线程应用程序提供了强大的方法。
Fork/Join 框架是 Java 对并行编程 (parallel programming) 支持的一部
分,并行编程通常是指优化某些类型的算法,以便能够在多
CPU 系统中并行执行的一种技术。对 Fork/Join 框架及
其他并发实用工具的讨论,请查看第
28 章。在此介绍 Java 的传统多线程功能。
线程有多种状态,下面是一般描述。线程可以处于运行 (running) 状态,只要获得 CPU 时间就准备运行。运行的
线程可以被挂起
(suspended) ,这会临时停止线程的活动。挂起的线程可以被恢复 (resumed) ,从而允许线程从停止处
恢复执行。当等待资源时,线程会被阻塞
(blocked) 。在任何时候,都可以终止线程,这会立即停止线程的执行。线
程一旦终止,就不能再恢复。
11.1.1 线程优先级
Java 为每个线程都指定了优先级,优先级决定了相对于其他线程应当如何处理某个线程。线程优先级是一些整
数,它们指定了一个线程相对于另一个线程的优先程度。优先级的绝对数值没有意义;如果只有一个线程在运行,
优先级高的线程不会比优先级低的线程运行快。反而,线程的优先级用于决定何时从一个运行的线程切换到下一个
线程,这称为上下文切换
(context switch) 。决定上下文切换发生时机的规则比较简单:
线程自愿地放弃控制。 线程显式地放弃控制权、休眠或在 I/O 之前阻塞,都会出现这种情况。在这种情况
下,检查所有其他线程,并且准备运行的线程中优先级最高的那个线程会获得
CPU 资源。
线程被优先级更高的线程取代。 对于这种情况,没有放弃控制权的低优先级线程不管正在做什么,都会被
高优先级线程简单地取代。基本上,只要高优先级线程希望运行,它就会取代低优先级线程,这称为抢占
式多任务处理
(preemptive multitasking)
如果具有相同优先级的两个线程竞争
CPU 资源,这种情况就有些复杂。对于 Windows 这类操作系统,优先级
相同的线程以循环方式自动获得
CPU 资源。对于其他类型的操作系统,优先级相同的线程必须自愿地向其他线程
放弃控制权,否则其他线程就不能运行。
警告:
操作系统以不同的方式对具有相同优先级的线程进行上下文切换,这可能会引起可移植性问题。
11.1.2 同步
因为多线程为程序引入了异步行为,所以必须提供一种在需要时强制同步的方法。例如,如果希望两个线程进
行通信并共享某个复杂的数据结构,如链表,就需要以某种方式确保它们相互之间不会发生冲突。也就是说,当一
个线程正在读取该数据结构时,必须阻止另外一个线程向该数据结构写入数据。为此,
Java 以监视器 (monitor) 这一
年代久远的进程间同步模型为基础,实现了一种巧妙的方案。监视器最初是由
C.A.R. Hoare 定义的一种控制机制,
可以将监视器看作非常小的只能包含一个线程的盒子。一旦某个线程进入监视器,其他所有线程就必须等待,直到
该线程退出监视器。通过这种方式,可以将监视器用于保护共享的资源,以防止多个线程同时对资源进行操作。
Java 没有提供“ Monitor ”类;相反,每个对象都有自己的隐式监视器。如果调用对象的同步方法,就会自动
进入对象的隐式监视器。一旦某个线程位于一个同步方法中,其他线程就不能调用同一对象的任何其他同步方法。
因为语言本身内置了同步支持,所以可以编写出非常清晰并且简明的多线程代码。

11.1.3 消息传递
将程序划分成独立的线程之后,需要定义它们之间相互通信的方式。当使用某些其他语言编写程序时,必须依
赖操作系统建立线程之间的通信。当然,这会增加系统开销。相反,通过调用所有对象都具有的预先定义的方法,
Java 为两个或更多个线程之间的相互通信提供了一种简洁的低成本方式。 Java 的消息传递系统允许某个线程进入对
象的同步方法,然后进行等待,直到其他线程显式地通知这个线程退出为止。
11.1.4 Thread 类和 Runnable 接口
Java 的多线程系统是基于 Thread 类、 Thread 类的方法及其伴随接口 Runnable 而构建的。 Thread 类封装了线程
的执行。 因为不能直接引用正在运行的线程的细微状态, 所以需要通过代理进行处理,
Thread 实例就是线程的代理。
为了创建新线程,程序可以扩展
Thread 类或实现 Runnable 接口。
Thread 类定义了一些用于帮助管理线程的方法,表 11-1 中显示的是本章将要用到的几个方法。
11-1 Thread 类定义的一些方法

方 法 含 义
getName() 获取线程的名称
getPriority() 获取线程的优先级
isAlive() 确定线程是否仍然在运行
join() 等待线程终止
run() 线程的入口点
sleep() 挂起线程一段时间
start() 通过调用线程的 run() 方法启动线程


到目前为止,本书的所有例子都使用单线程来执行。本章的剩余部分将解释如何使用 Thread 类和 Runnable
口创建和管理线程,首先介绍所有
Java 程序都有的线程——主线程。
11.2 主线程
Java 程序启动时,会立即开始运行一个线程,因为它是程序开始时执行的线程,所以这个线程通常称为程
序的主线程。主线程很重要,有以下两个原因:
其他子线程都是从主线程生成的。
通常,主线程必须是最后才结束执行的线程,因为它要执行各种关闭动作。
尽管主线程是在程序启动时自动创建的,但是可以通过
Thread 对象对其进行控制。为此,必须调用 currentThread()
方法获取对主线程的一个引用。该方法是 Thread 类的公有静态成员,它的一般形式如下所示:
static Thread currentThread()
这个方法返回对调用它的线程的引用。一旦得到对主线程的引用,就可以像控制其他线程那样控制主线程。
首先分析下面的例子:
// Controlling the main Thread.
class CurrentThreadDemo {
public static void main(String args[]) {
Thread t = Thread.currentThread();
System.out.println("Current thread: " + t);
// change the name of the thread
t.setName("My Thread");
System.out.println("After name change: " + t);
try {

172 第Ⅰ部分 Java 语言
for(int n = 5; n > 0; n--) {
System.out.println(n);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("Main thread interrupted");
}
}
}
在这个程序中,通过调用 currentThread() 方法来获取对当前线程 ( 在本例中是主线程 ) 的引用,并将这个引用存储
在局部变量
t 中。接下来,程序显示有关线程的信息。然后程序调用 setName() 方法更改线程的内部名称。之后再次
显示有关线程的信息。接下来是一个从
5 开始递减的循环,在两次循环之间暂停 1 秒。暂停是通过 sleep() 方法实现
的。传递给
sleep() 方法的参数以毫秒为单位指定延迟的间隔时间。请注意封装循环的 try/catch 代码块。 Thread 类的
sleep() 方法可能会抛出 InterruptedException 异常。如果其他线程试图中断这个正在睡眠的线程,就会发生这种情况。
在这个例子中,如果线程被中断,只会输出一条消息。在真实的程序中,可能需要以不同的方式处理这种情况。下
面是该程序生成的输出:
Current thread: Thread[main,5,main]
After name change: Thread[My Thread,5,main]
5
4
3
2
1
注意,当将 t 用作 println() 方法的参数时生成的输出,这将依次显示线程的名称、优先级以及线程所属线程组
的名称。默认情况下,主线程的名称是
main ,优先级是 5 ,这是默认值,并且 main 也是主线程所属线程组的名称。
线程组
(thread group) 是将一类线程作为整体来控制状态的数据结构。在更改了线程的名称后,再次输出 t ,这一次
将显示线程新的名称。
下面进一步分析在程序中使用的
Thread 类定义的方法。 sleep() 方法使线程从调用时挂起,暂缓执行指定的时间
间隔
( 毫秒数 ) ,它的一般形式如下所示:
static void sleep(long milliseconds ) throws InterruptedException
挂起的毫秒数由 milliseconds 指定,这个方法可能会抛出 InterruptedException 异常。
sleep() 方法还有第二种形式,如下所示,这种形式允许按照毫秒加纳秒的形式指定挂起的时间间隔:
static void sleep(long milliseconds , int nanoseconds ) throws InterruptedException
只有在计时周期精确到纳秒级的环境中, sleep() 方法的第二种形式才有用。
正如前面的程序所示,使用
setName() 方法可以设置线程的名称。通过 getName() 方法可以获得线程的名称 (
过,上述程序没有演示该方法
) 。这些方法都是 Thread 类的成员,它们的声明如下所示:
final void setName(String threadName )
final String getName()
其中, threadName 指定了线程的名称。
11.3 创建线程
在最通常的情况下,通过实例化 Thread 类型的对象创建线程。 Java 定义了创建线程的两种方法:
实现 Runnable 接口
扩展 Thread 类本身
接下来的两小节依次分析这两种方法。
11.3.1 实现 Runnable 接口
创建线程的最简单方式是创建实现了 Runnable 接口的类。 Runnable 接口抽象了一个可执行代码单元。可以依
托任何实现了 Runnable 接口的对象来创建线程。为了实现 Runnable 接口,类只需要实现 run() 方法,该方法的声明
如下所示:
public void run( )
run() 方法内部,定义组成新线程的代码。 run() 方法可以调用其他方法,使用其他类,也可以声明变量,就像
main 线程那样,理解这一点很重要。唯一的区别是: run() 方法为程序中另外一个并发线程的执行建立了入口点。当
run() 方法返回时,这个线程将结束。
在创建实现了
Runnable 接口的类之后,可以在类中实例化 Thread 类型的对象。 Thread 类定义了几个构造函数。
我们将使用的那个构造函数如下所示:
Thread(Runnable threadOb , String threadName )
在这个构造函数中, threadOb 是实现了 Runnable 接口的类的实例,这定义了从何处开始执行线程。新线程的
名称由
threadName 指定。
在创建了新线程之后,只有调用线程的
start() 方法,线程才会运行,该方法是在 Thread 类中声明的。本质上,
start() 方法初始化对 run() 方法的调用。 start() 方法的声明如下所示:
void start()
下面的例子创建了一个新的线程并开始运行:
// Create a second thread.
class NewThread implements Runnable {
Thread t;
NewThread() {
// Create a new, second thread
t = new Thread(this, "Demo Thread");
System.out.println("Child thread: " + t);
}
// This is the entry point for the second thread.
public void run() {
try {
for(int i = 5; i > 0; i--) {
System.out.println("Child Thread: " + i);
Thread.sleep(500);
}
} catch (InterruptedException e) {
System.out.println("Child interrupted.");
}
System.out.println("Exiting child thread.");
}
}
class ThreadDemo {
public static void main(String args[]) {
NewThread nt = new NewThread(); // create a new thread
nt.t.start(); // Start the thread
try {
for(int i = 5; i > 0; i--) {
System.out.println("Main Thread: " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("Main thread interrupted.");
}
System.out.println("Main thread exiting.");
}
}

NewThread 类的构造函数中,通过下面这条语句创建了一个新的 Thread 对象:
t = new Thread(this, "Demo Thread");
传递 this 作为第一个参数,以表明希望新线程调用 this 对象的 run() 方法。在 main() 方法中调用 start() 方法,从
run() 方法开始启动线程的执行。这会导致开始执行子线程的 for 循环。接下来主线程进入 for 循环。两个线程继续
运行,在单核系统中它们会共享
CPU ,直到它们的循环结束。这个程序生成的输出如下所示 ( 基于特定的执行环境,
输出可能有所变化
)
Child thread: Thread[Demo Thread,5,main]
Main Thread: 5
Child Thread: 5
Child Thread: 4
Main Thread: 4
Child Thread: 3
Child Thread: 2
Main Thread: 3
Child Thread: 1
Exiting child thread.
Main Thread: 2
Main Thread: 1
Main thread exiting.
如前所述,在多线程程序中,主线程必须在最后结束运行,这通常很有用。上面的程序确保主线程在最后结束,
因为主线程在每次迭代之间休眠
1 000 毫秒,而子线程只休眠 500 毫秒。这使得子线程比主线程终止得更早。稍后,
你将会看到等待线程结束的更好方法。
11.3.2 扩展 Thread
创建线程的第二种方式是创建一个扩展了 Thread 的新类,然后创建该类的实例。扩展类必须重写 run() 方法,
run() 方法是新线程的入口点。同以前一样,调用 start() 方法以开始新线程的执行。下面的程序对前面的程序进行了
改写以扩展
Thread 类:
// Create a second thread by extending Thread
class NewThread extends Thread {
NewThread() {
// Create a new, second thread
super("Demo Thread");
System.out.println("Child thread: " + this);
}
// This is the entry point for the second thread.
public void run() {
try {
for(int i = 5; i > 0; i--) {
System.out.println("Child Thread: " + i);
Thread.sleep(500);
}
} catch (InterruptedException e) {
System.out.println("Child interrupted.");
}
System.out.println("Exiting child thread.");
}
}
class ExtendThread {
public static void main(String args[]) {
NewThread nt = new NewThread(); // create a new thread
nt.start(); // start the thread
try {

for(int i = 5; i > 0; i--) {
System.out.println("Main Thread: " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("Main thread interrupted.");
}
System.out.println("Main thread exiting.");
}
}
这个程序生成的输出和前面版本的相同。可以看出,子线程是通过实例化 NewThread 类的对象创建的,
NewThread 类派生自 Thread
注意在
NewThread 类中对 super() 方法的调用。这会调用以下形式的 Thread 构造函数:
public Thread(String threadName )
其中, threadName 指定了线程的名称。
11.3.3 选择一种创建方式
至此,你可能会好奇 Java 为什么提供两种创建子线程的方式,哪种方式更好一些呢?这两个问题的答案涉及
同一原因。
Thread 类定义了派生类可以重写的几个方法。在这些方法中,只有一个方法必须重写,即 run() 方法。
当然,这也是实现
Runnable 接口时需要实现的方法。许多 Java 程序员认为:只有当类正在以某种方式增强或修改
时,才应当对类进行扩展。因此,如果不重写
Thread 类的其他方法,创建子线程的最好方式可能是简单地实现
Runnable 接口。此外,通过实现 Runnable 接口,你的线程类不需要继承 Thread 类,从而可以自由地继承其他类。
最终,使用哪种方式取决于你自己。但是,在本章的剩余部分,将使用实现了
Runnable 接口的类来创建线程。
11.4 创建多个线程
到目前为止,只使用了两个线程:主线程和一个子线程。但是,程序可以生成所需要的任意多个线程。例如,
下面的程序创建了三个子线程:
// Create multiple threads.
class NewThread implements Runnable {
String name; // name of thread
Thread t;
NewThread(String threadname) {
name = threadname;
t = new Thread(this, name);
System.out.println("New thread: " + t);
}
// This is the entry point for thread.
public void run() {
try {
for(int i = 5; i > 0; i--) {
System.out.println(name + ": " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println(name + "Interrupted");
}
System.out.println(name + " exiting.");
}
}
class MultiThreadDemo {
public static void main(String args[]) {
NewThread nt1 = new NewThread("One");
NewThread nt2 = new NewThread("Two");

NewThread nt3 = new NewThread("Three");
// Start the threads.
nt1.t.start();
nt2.t.start();
nt3.t.start();
try {
// wait for other threads to end
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("Main thread Interrupted");
}
System.out.println("Main thread exiting.");
}
}
这个程序的一次样本输出如下所示 ( 根据特定的执行环境,输出可能会有所变化 )
New thread: Thread[One,5,main]
New thread: Thread[Two,5,main]
New thread: Thread[Three,5,main]
One: 5
Two: 5
Three: 5
One: 4
Two: 4
Three: 4
One: 3
Three: 3
Two: 3
One: 2
Three: 2
Two: 2
One: 1
Three: 1
Two: 1
One exiting.
Two exiting.
Three exiting.
Main thread exiting.
可以看出,启动之后,所有三个子线程共享 CPU 。注意在 main() 方法中对 sleep(10000) 的调用,这会导致主线
程休眠
10 秒钟,从而确保主线程在最后结束。
11.5 使用 isAlive() join() 方法
如前所述,通常希望主线程在最后结束。在前面的例子中,通过在 main() 方法中调用 sleep() 方法,并指定足够
长的延迟时间来确保所有子线程在主线程之前终止。但是,这完全不是一个令人满意的方案,并且还会造成一个更
大的问题:一个线程如何知道另一个线程何时结束?幸运的是,
Thread 类提供了能够解决这个问题的方法。
有两种方法可以确定线程是否已经结束。 首次, 可以为线程调用
isAlive() 方法。 这个方法是由 Thread 类定义的,
它的一般形式如下所示:
final boolean isAlive()
如果线程仍然在运行, isAlive() 方法就返回 true ,否则返回 false
虽然
isAlive() 方法有时很有用,但是通常使用 join() 方法来等待线程结束,如下所示:
final void join() throws InterruptedException
该方法会一直等待,直到调用线程终止。如此命名该方法的原因是:调用线程一直等待,直到指定的线程加入
(join) 其中为止。 join() 方法的另外一种形式允许指定希望等待指定线程终止的最长时间。
下面是前面例子的改进版本,该版本使用 join() 方法确保主线程在最后结束,另外还演示了 isAlive() 方法的使用:
// Using join() to wait for threads to finish.
class NewThread implements Runnable {
String name; // name of thread
Thread t;
NewThread(String threadname) {
name = threadname;
t = new Thread(this, name);
System.out.println("New thread: " + t);
}
// This is the entry point for thread.
public void run() {
try {
for(int i = 5; i > 0; i--) {
System.out.println(name + ": " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println(name + " interrupted.");
}
System.out.println(name + " exiting.");
}
}
class DemoJoin {
public static void main(String args[]) {
NewThread nt1 = new NewThread("One");
NewThread nt2 = new NewThread("Two");
NewThread nt3 = new NewThread("Three");
// Start the threads.
nt1.t.start();
nt2.t.start();
nt3.t.start();
System.out.println("Thread One is alive: "
+ nt1.t.isAlive());
System.out.println("Thread Two is alive: "
+ nt2.t.isAlive());
System.out.println("Thread Three is alive: "
+ nt3.t.isAlive());
// wait for threads to finish
try {
System.out.println("Waiting for threads to finish.");
nt1.t.join();
nt2.t.join();
nt3.t.join();
} catch (InterruptedException e) {
System.out.println("Main thread Interrupted");
}
System.out.println("Thread One is alive: "
+ nt1.t.isAlive());
System.out.println("Thread Two is alive: "
+ nt2.t.isAlive());
System.out.println("Thread Three is alive: "
+ nt3.t.isAlive());
System.out.println("Main thread exiting.");
}
}

下面是该程序的一次样本输出 ( 基于特定的执行环境,输出可能会有所不同 )
New thread: Thread[One,5,main]
New thread: Thread[Two,5,main]
New thread: Thread[Three,5,main]
Thread One is alive: true
Thread Two is alive: true
Thread Three is alive: true
Waiting for threads to finish.
One: 5
Two: 5
Three: 5
One: 4
Two: 4
Three: 4
One: 3
Two: 3
Three: 3
One: 2
Two: 2
Three: 2
One: 1
Two: 1
Three: 1
Two exiting.
Three exiting.
One exiting.
Thread One is alive: false
Thread Two is alive: false
Thread Three is alive: false
Main thread exiting.
可以看出,在对 join() 方法的调用返回之后,线程停止执行。
11.6 线程优先级
线程调度程序根据线程优先级决定每个线程应当何时运行。理论上,优先级更高的线程比优先级更低的线程会
获得更多的
CPU 时间。实际上,线程得到的 CPU 时间除了依赖于优先级外,通常还依赖于其他几个因素 ( 例如,操
作系统实现多任务的方式可能会影响
CPU 时间的相对可用性 ) 。具有更高优先级的线程还可能取代更低优先级的线
程。例如,当一个低优先级的线程正在运行时,需要恢复一个更高优先级的线程
( 例如,从休眠或等待 I/O 中恢复 )
时,高优先级的线程将取代低优先级的线程。
理论上,具有相同优先级的线程应当得到相等的
CPU 时间。但是,这需要谨慎对待。请记住, Java 被设计为
在范围广泛的环境中运行。有些环境实现多任务的方式与其他环境不同。为了安全起见,具有相同优先级的线程应
当时不时释放控制权。这样可以确保所有线程在非抢占式操作系统中有机会运行。实际上,即使是在非抢占式环境
中,大部分线程仍然有机会运行,因为大部分线程不可避免地会遇到一些阻塞情况,例如
I/O 等待。当发生这种情
况时,阻塞的线程被挂起,其他线程就可以运行。但是,如果希望使多个线程的执行平滑,最好不要依赖于这种情
况。此外,某些类型的任务是
CPU 密集型的,这种线程会支配 CPU 。对于这类线程,你会希望经常地释放控制权,
以使其他线程能够运行。
为了设置线程的优先级,需要使用
setPriority() 方法,它是 Thread 类的成员。下面是该方法的一般形式:
final void setPriority(int level )
其中, level 指定了为调用线程设置的新优先级。 level 的值必须在 MIN_PRIORITY MAX_PRIORITY 之间选
择。目前,这些值分别是
1 10 。如果希望将线程设置为默认优先级,可以使用 NORM_PRIORITY ,目前的值是
5 。这些优先级是在 Thread 类中作为 static final 变量定义的。
可以通过调用
Thread 类的 getPriority() 方法获取当前设置的优先级,该方法如下所示:
final int getPriority()
不同的 Java 实现对于任务调度可能有很大的区别。如果线程依赖于抢占式行为,而不是协作性地放弃 CPU
那么经常会引起不一致性。使用 Java 实现可预测、跨平台行为的最安全方法是使用自愿放弃 CPU 控制权的线程。
11.7 同步
当两个或多个线程需要访问共享的资源时,它们需要以某种方式确保每次只有一个线程使用资源。实现这一目
的的过程称为同步。正如即将看到的,
Java 为同步提供了独特的、语言级的支持。
同步的关键是监视器的概念,监视器是用作互斥锁的对象。在给定时刻,只有一个线程可以拥有监视器。当线
程取得锁时,也就是进入了监视器。其他所有企图进入加锁监视器的线程都会被挂起,直到第一个线程退出监视器。
也就是说,这些等待的其他线程在等待监视器。如果需要的话,拥有监视器的线程可以再次进入监视器。
可以使用两种方法同步代码。这两种方法都要用到
synchronized 关键字,下面分别介绍这两种方法。
11.7.1 使用同步方法
Java 中进行同步很容易,因为所有对象都有与它们自身关联的隐式监视器。为了进入对象的监视器,只需
要调用使用
synchronized 关键字修饰过的方法。当某个线程进入同步方法中时,调用同一实例的该同步方法 ( 或任何
其他同步方法
) 的所有其他线程都必须等待。为了退出监视器并将对象的控制权交给下一个等待线程,监视器的拥
有者只需要简单地从同步方法返回。
为了理解对同步的需求,下面介绍一个应当使用但是还没有使用同步的例子。下面的程序有
3 个简单的类。第
1 个类是 Callme ,其中只有一个方法 call() call() 方法带有一个 String 类型的参数 msg ,这个方法尝试在方括号中输
msg 字符串。需要注意的一件有趣的事情是: call() 方法在输出开括号和 msg 字符串之后调用 Thread.sleep(1000)
这会导致当前线程暂停
1 秒。
下一个类是
Caller ,其构造函数带有两个参数:对 Callme 实例的引用和 String 类型的字符串。这两个参数分别
存储在成员变量
target msg 中。 该构造函数还创建了一个新的调用对象 run() 方法的线程。 线程会立即启动。 Caller
类的 run() 方法调用 Callme 类实例 target call() 方法,并传入 msg 字符串。最后, Synch 类通过创建 1 Callme
实例和
3 Caller 类实例来启动程序,每个 Caller 类实例都带有唯一的消息字符串。同一个 Callme 实例被传递给
每个
Caller 类实例。
// This program is not synchronized.
class Callme {
void call(String msg) {
System.out.print("[" + msg);
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println("]");
}
}
class Caller implements Runnable {
String msg;
Callme target;
Thread t;
public Caller(Callme targ, String s) {
target = targ;
msg = s;
t = new Thread(this);
}
public void run() {
target.call(msg);
}
}

class Synch {
public static void main(String args[]) {
Callme target = new Callme();
Caller ob1 = new Caller(target, "Hello");
Caller ob2 = new Caller(target, "Synchronized");
Caller ob3 = new Caller(target, "World");
// Start the threads.
ob1.t.start();
ob2.t.start();
ob3.t.start();
// wait for threads to end
try {
ob1.t.join();
ob2.t.join();
ob3.t.join();
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
}
}
下面是该程序生成的输出:
Hello[Synchronized[World]
]
]
可以看出,通过调用 sleep() 方法, call() 方法允许执行切换到另一个线程,这会导致混合输出 3 个消息字符串。
在这个程序中,没有采取什么方法以阻止
3 个线程在相同的时间调用同一对象的同一个方法,这就是所谓的竞态条
(race condition) ,因为 3 个线程相互竞争以完成方法。这个例子使用了 sleep() 方法,使得效果可以重复并且十分
明显。在大多数情况下,竞态条件会更加微妙并且更不可预测,因为不能确定何时会发生线程上下文切换。这会造
成程序在某一次运行正确,而在下一次可能运行错误。
为了修复前面的程序,必须按顺序调用
call() 方法。也就是说,必须限制每次只能由一个线程调用 call() 方法。
为此,只需要简单地在
call() 方法定义的前面添加关键字 synchronized 即可,如下所示:
class Callme {
synchronized void call(String msg) {
...
当一个线程使用 call() 方法时,这会阻止其他线程进入该方法。将 synchronized 关键字添加到 call() 方法之后,
程序的输出如下所示:
[Hello]
[Synchronized]
[World]
在多线程情况下, 如果有一个或一组方法用来操作对象的内部状态, 那么每次都应当使用 synchronized 关键字,
以保证状态不会进入竞态条件。请记住,一旦线程进入一个实例的同步方法,所有其他线程就都不能再进入相同实
例的任何同步方法。但是,仍然可以继续调用同一实例的非同步方法。
11.7.2 synchronized 语句
虽然在类中创建同步方法是一种比较容易并且行之有效的实现同步的方式,但并不是在所有情况下都可以使用
这种方式。为了理解其中的原因,我们分析下面的内容。假设某个类没有针对多线程访问而进行设计,即类没有使
用同步方法,而又希望同步对类的访问。进一步讲,类不是由你创建的,而是由第三方创建的,并且你不能访问类
的源代码。因此,不能为类中的合适方法添加
synchronized 关键字。如何同步访问这种类的对象呢?幸运的是,这个问
题的解决方案很容易:可以简单地将对这种类定义的方法的调用放到
synchronized 代码块中。
下面是
synchronized 语句的一般形式:
synchronized( objRef ){
// statements to be synchronized
}
其中, objRef 是对被同步对象的引用。 synchronized 代码块确保对 objRef 对象的成员方法的调用,只会在当前
线程成功进入
objRef 的监视器之后发生。
下面是前面例子的另一版本,该版本在
run() 方法中使用 synchronized 代码块:
// This program uses a synchronized block.
class Callme {
void call(String msg) {
System.out.print("[" + msg);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println("]");
}
}
class Caller implements Runnable {
String msg;
Callme target;
Thread t;
public Caller(Callme targ, String s) {
target = targ;
msg = s;
t = new Thread(this);
}
// synchronize calls to call()
public void run() {
synchronized(target) { // synchronized block
target.call(msg);
}
}
}
class Synch1 {
public static void main(String args[]) {
Callme target = new Callme();
Caller ob1 = new Caller(target, "Hello");
Caller ob2 = new Caller(target, "Synchronized");
Caller ob3 = new Caller(target, "World");
// Start the threads.
ob1.t.start();
ob2.t.start();
ob3.t.start();
// wait for threads to end
try {
ob1.t.join();
ob2.t.join();
ob3.t.join();
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
}
}
在此,没有使用 synchronized 修饰 call() 方法。反而,在 Caller 类的 run() 方法中使用了 synchronized 语句。这会
使该版本的输出和前面版本的相同,因为每个线程在开始之前都要等待前面的线程先结束。

11.8 线程间通信
前面的例子无条件地锁住其他线程对特定方法的异步访问。 Java 对象的隐式监视器的这种用途很强大,但是通
过进程间通信可以实现更细微级别的控制。正如即将看到的,在
Java 中这特别容易实现。
在前面讨论过,多线程任务处理通过将任务分隔到独立的逻辑单元来替换事件循环编程。线程还提供了第二个
优点:消除了轮询检测。轮询检测通常是通过重复检查某些条件的循环实现的。一旦条件为
true ,就会发生恰当的
动作,这会浪费
CPU 时间。例如,分析经典的队列问题,对于这种问题,一个线程生成一些数据,另外一个线程
使用这些数据。为了使问题更有趣,假定生产者在生成更多数据之前,必须等待消费者结束。在轮询检测系统中,
消费者在等待生产者生产时需要消耗许多的
CPU 时间。一旦生产者结束生产数据,就会开始轮询,在等待消费者
结束的过程中,会浪费更多
CPU 时间。显然,这种情况不是你所期望的。
为了避免轮询检测,
Java 通过 wait() notify() 以及 notifyAll() 方法,提供了一种巧妙的进程间通信机制,这些
方法在
Object 中是作为 final 方法实现的,因此所有类都具有这些方法。所有这 3 个方法都只能在同步上下文中调
用。尽管从计算机科学角度看,在概念上这些方法很高级,但是使用这些方法的规则实际上很简单:
● wait() 方法通知调用线程放弃监视器并进入休眠,直到其他一些线程进入同一个监视器并调用 notify() 方法
notifyAll() 方法。
● notify() 方法唤醒调用相同对象的 wait() 方法的线程。
● notifyAll() 方法唤醒调用相同对象的 wait() 方法的所有线程,其中的一个线程将得到
访问授权。
这些方法都是在
Object 类中声明的,如下所示:
final void wait() throws InterruptedException
final void notify()
final void notifyAll()
wait() 方法还有另外一种形式,允许指定等待的时间间隔。
在通过例子演示线程间通信之前,还有重要的一点需要指出。尽管在正常情况下,
wait() 方法会等待直到调用
notify() notifyAll() 方法,但是还有一种概率很小却可能会发生的情况,等待线程由于假唤醒 (spurious wakeup) 而被
唤醒。对于这种情况,等待线程也会被唤醒,然而却没有调用
notify() notifyAll() 方法 ( 本质上,线程在没有什么明
显理由的情况下就被恢复了
) 。因为存在这种极小的可能,所以 Oracle 推荐应当在一个检查线程等待条件的循环中调
wait() 方法。下面的例子演示了这种技术。
现在通过一个使用
wait() notify() 方法的例子演示线程间通信。首先分析下面的示例程序,该示例以不正确的
方式实现了一个简单形式的生产者
/ 消费者问题。该例包含 4 个类:类 Q 是试图同步的队列;类 Producer 是生成队
列条目的线程对象;类
Consumer 是使用队列条目的线程对象;类 PC 是一个小型类,用于创建类 Q Producer
Consumer 的实例。
// An incorrect implementation of a producer and consumer.
class Q {
int n;
synchronized int get() {
System.out.println("Got: " + n);
return n;
}
synchronized void put(int n) {
this.n = n;
System.out.println("Put: " + n);
}
}
class Producer implements Runnable {
Q q;
Thread t;

Producer(Q q) {
this.q = q;
t = new Thread(this, "Producer");
}
public void run() {
int i = 0;
while(true) {
q.put(i++);
}
}
}
class Consumer implements Runnable {
Q q;
Thread t;
Consumer(Q q) {
this.q = q;
t = new Thread(this, "Consumer");
}
public void run() {
while(true) {
q.get();
}
}
}
class PC {
public static void main(String args[]) {
Q q = new Q();
Producer p = new Producer(q);
Consumer c = new Consumer(q);
// Start the threads.
p.t.start();
c.t.start();
System.out.println("Press Control-C to stop.");
}
}
尽管类 Q 中的 put() get() 方法是同步的,但是没有什么措施能够停止生产者过度运行消费者,也没有什么措
施能够停止消费者两次消费相同的队列值。因此,得到的输出是错误的,如下所示
( 根据处理器的速度和加载的任
务,实际输出可能会不同
)
Put: 1
Got: 1
Got: 1
Got: 1
Got: 1
Got: 1
Put: 2
Put: 3
Put: 4
Put: 5
Put: 6
Put: 7
Got: 7
可以看出,生产者在将 1 放入队列之后,消费者开始运行,并且连续 5 次获得相同的数值 1 。然后,生产者恢
复执行,并生成数值
2 7 ,而不让消费者有机会使用它们。
184 第Ⅰ部分 Java 语言
使用 Java 编写这个程序的正确方式是使用 wait() notify() 方法在两个方向上发信号,如下所示:
// A correct implementation of a producer and consumer.
class Q {
int n;
boolean valueSet = false;
synchronized int get() {
while(!valueSet)
try {
wait();
} catch(InterruptedException e) {
System.out.println("InterruptedException caught");
}
System.out.println("Got: " + n);
valueSet = false;
notify();
return n;
}
synchronized void put(int n) {
while(valueSet)
try {
wait();
} catch(InterruptedException e) {
System.out.println("InterruptedException caught");
}
this.n = n;
valueSet = true;
System.out.println("Put: " + n);
notify();
}
}
class Producer implements Runnable {
Q q;
Thread t;
Producer(Q q) {
this.q = q;
t = new Thread(this, "Producer");
}
public void run() {
int i = 0;
while(true) {
q.put(i++);
}
}
}
class Consumer implements Runnable {
Q q;
Thread t;
Consumer(Q q) {
this.q = q;
t = new Thread(this, "Consumer");
}
public void run() {

while(true) {
q.get();
}
}
}
class PCFixed {
public static void main(String args[]) {
Q q = new Q();
Producer p = new Producer(q);
Consumer c = new Consumer(q);
// Start the threads.
p.t.start();
c.t.start();
System.out.println("Press Control-C to stop.");
}
}
get() 方法中调用 wait() 方法,这会导致 get() 方法的执行被挂起,直到生产者通知你已经准备好一些数据。当
发出通知时,恢复
get() 方法中的执行。在获得数据之后, get() 方法调用 notify() 方法。该调用通知生产者可以在队
列中放入更多数据。在
put() 方法中, wait() 方法暂停执行直到消费者从队列中删除条目。当执行恢复时,下一个数
据条目被放入队列中,并调用
notify() 方法。这会通知消费者,现在应当删除该数据条目。
下面是这个程序的一些输出,这些输出显示了清晰的同步行为:
Put: 1
Got: 1
Put: 2
Got: 2
Put: 3
Got: 3
Put: 4
Got: 4
Put: 5
Got: 5
死锁
需要避免的与多任务处理明确相关的特殊类型的错误是死锁 (deadlock) ,当两个线程循环依赖一对同步对象时,
会发生这种情况。例如,假设一个线程进入对象
X 的监视器,另一个线程进入对象 Y 的监视器。如果 X 中的线程
试图调用对象
Y 的任何同步方法,那么会如你所期望的那样被阻塞。但是,如果对象 Y 中的线程也试图调用对象
A 的任何同步方法,那么会永远等待下去,因为为了进入 X ,必须释放对 Y 加的锁,这样第一个线程才能完成。死
锁是一种很难调试的错误,原因有两点:
死锁通常很少发生,只有当两个线程恰好以这种方式获取 CPU 时钟周期时才会发生死锁。
死锁可能涉及更多的线程以及更多的同步对象 ( 也就是说,死锁可能是通过更复杂的事件序列发生的,而
不是通过刚才描述的情况发生的
)
为了完全理解死锁, 实际进行演示是有用的。下一个例子创建了两个类——
A B , 这两个类分别具有方法 foo()
bar() ,在调用对方类中的方法之前会暂停一会儿。主类 Deadlock 创建 A 的一个实例和 B 的一个实例,然后开始
第二个线程以设置死锁条件。方法
foo() bar() 使用 sleep() 作为强制死锁条件发生的手段。
// An example of deadlock.
class A {
synchronized void foo(B b) {
String name = Thread.currentThread().getName();
System.out.println(name + " entered A.foo");
try {

Thread.sleep(1000);
} catch(Exception e) {
System.out.println("A Interrupted");
}
System.out.println(name + " trying to call B.last()");
b.last();
}
synchronized void last() {
System.out.println("Inside A.last");
}
}
class B {
synchronized void bar(A a) {
String name = Thread.currentThread().getName();
System.out.println(name + " entered B.bar");
try {
Thread.sleep(1000);
} catch(Exception e) {
System.out.println("B Interrupted");
}
System.out.println(name + " trying to call A.last()");
a.last();
}
synchronized void last() {
System.out.println("Inside B.last");
}
}
class Deadlock implements Runnable {
A a = new A();
B b = new B();
Thread t;
Deadlock() {
Thread.currentThread().setName("MainThread");
t = new Thread(this, "RacingThread");
}
void deadlockStart() {
t.start();
a.foo(b); // get lock on a in this thread.
System.out.println("Back in main thread");
}
public void run() {
b.bar(a); // get lock on b in other thread.
System.out.println("Back in other thread");
}
public static void main(String args[]) {
Deadlock dl = new Deadlock();
dl.deadlockStart();
}
}
当运行这个程序时,会看到如下所示的输出:
MainThread entered A.foo
RacingThread entered B.bar
MainThread trying to call B.last()
RacingThread trying to call A.last()
因为程序被死锁,所以你需要按下 Ctrl+C 组合键来结束程序。通过在 PC 上按下 Ctrl+Break 组合键,可以看到
完整的线程和监视器缓存转储。可以看出,当等待
a 的监视器时, RacingThread 拥有 b 的监视器。同时, MainThread
拥有 a ,并且在等待获取 b 。这个程序永远不会结束。正如该程序所演示的,如果多线程程序偶尔被锁住,那么首
先应当检查是否是由于死锁造成的。
11.9 挂起、恢复与停止线程
有时,挂起线程的执行是有用的。例如,可以使用单独的线程显示一天的时间。如果用户不想要时钟,那么可
以挂起时钟线程。无论是什么情况,挂起线程都是一件简单的事情。线程一旦挂起,重新启动线程也很简单。
Java 早期版本 ( 例如 Java 1.0) 和现代版本 ( Java 2 开始 ) 提供的用来挂起、停止以及恢复线程的机制不同。在 Java
2
以前,程序使用 Thread 类定义的 suspend() resume() stop() 方法来暂停、重启和停止线程的执行。虽然这些方
法对于管理线程执行看起来是一种合理并且方便的方式,但是在新的
Java 程序中不能使用它们。下面是其中的原
因。在几年前,
Java 2 不推荐使用 Thread 类的 suspend() 方法,因为 suspend() 方法有时会导致严重的系统故障。假
定线程为关键数据结构加锁,如果这时线程被挂起,那么这些锁将无法释放。其他可能等待这些资源的线程会被死
锁。
方法
resume() 也不推荐使用。虽然不会造成问题,但是如果不使用 suspend() 方法,就不能使用 resume() 方法,
它们是配对使用的。
对于
Thread 类的 stop() 方法, Java 2 也反对使用,因为有时这个方法也会造成严重的系统故障。假定线程正在
向关键的重要数据结构中写入数据,并且只完成了部分发生变化的数据。如果这时停止线程,那么数据结构可能会
处于损坏状态。问题是:
stop() 会导致释放调用线程的所有锁。因此,另一个正在等待相同锁的线程可能会使用这
些已损坏的数据。
因为现在不能使用
suspend() resume() 以及 stop() 方法控制线程,所以你可能会认为没有办法来暂停、重启以及终
止线程。但幸运的是,这不是真的。反而,线程必须被设计为
run() 方法周期性地进行检查,以确定是否应当挂起、恢
复或停止线程自身的执行。 通常, 这是通过建立用来标志线程执行状态的变量完成的。 只要这个标志变量被设置为“运
行”,
run() 方法就必须让线程继续执行。如果标志变量被设置为“挂起”,线程就必须暂停。如果标志变量被设置为“停
止”,线程就必须终止。当然,编写这种代码的方式有很多,但是对于所有程序,中心主题是相同的。
下面的例子演示了如何使用继承自
Object wait() notify() 方法控制线程的执行。下面分析这个程序中的操作。
NewThread 类包含布尔型实例变量 suspendFlag ,该变量用于控制线程的执行,构造函数将该变量初始化为 false
方法
run() 包含检查 suspendFlag 变量的 synchronized 代码块。如果该变量为 true ,就调用 wait() 方法,挂起线程的执
行。
mysuspend() 方法将 suspendFlag 变量设置为 true myresume() 方法将 suspendFlag 设置为 false ,并调用 notify()
方法以唤醒线程。最后,对 main() 方法进行修改以调用 mysuspend() myresume() 方法。
// Suspending and resuming a thread the modern way.
class NewThread implements Runnable {
String name; // name of thread
Thread t;
boolean suspendFlag;
NewThread(String threadname) {
name = threadname;
t = new Thread(this, name);
System.out.println("New thread: " + t);
suspendFlag = false;
}
// This is the entry point for thread.
public void run() {
try {
for(int i = 15; i > 0; i--) {
System.out.println(name + ": " + i);

Thread.sleep(200);
synchronized(this) {
while(suspendFlag) {
wait();
}
}
}
} catch (InterruptedException e) {
System.out.println(name + " interrupted.");
}
System.out.println(name + " exiting.");
}
synchronized void mysuspend() {
suspendFlag = true;
}
synchronized void myresume() {
suspendFlag = false;
notify();
}
}
class SuspendResume {
public static void main(String args[]) {
NewThread ob1 = new NewThread("One");
NewThread ob2 = new NewThread("Two");
ob1.t.start(); // Start the thread
ob2.t.start(); // Start the thread
try {
Thread.sleep(1000);
ob1.mysuspend();
System.out.println("Suspending thread One");
Thread.sleep(1000);
ob1.myresume();
System.out.println("Resuming thread One");
ob2.mysuspend();
System.out.println("Suspending thread Two");
Thread.sleep(1000);
ob2.myresume();
System.out.println("Resuming thread Two");
} catch (InterruptedException e) {
System.out.println("Main thread Interrupted");
}
// wait for threads to finish
try {
System.out.println("Waiting for threads to finish.");
ob1.t.join();
ob2.t.join();
} catch (InterruptedException e) {
System.out.println("Main thread Interrupted");
}
System.out.println("Main thread exiting.");
}
}
运行这个程序时,会看到线程被挂起和恢复。在本书的后面,会看到更多使用现代线程控制机制的例子。尽管
这种机制没有旧机制那么“清晰”,但是不管怎样,却可以确保不会发生运行错误。对于所有新代码,必须使用这
种方式。

11.10 获取线程的状态
在本章前面提到过,线程可以处于许多不同的状态。可以调用 Thread 类定义的 getState() 方法来获取线程的当
前状态,该方法如下所示:
Thread.State getState()
该方法返回 Thread.State 类型的值,指示在调用该方法时线程所处的状态。 State 是由 Thread 类定义的一个枚举
类型
( 枚举是一系列具有名称的常量,将在第 12 章详细讨论 ) 。表 11-2 中列出了 getState() 可以返回的值。
11-2 getState() 方法的返回值

状 态
BLOCKED 线程因为正在等待需要的锁而挂起执行
NEW 线程还没有开始运行
RUNNABLE 线程要么当前正在执行,要么在获得 CPU 的访问权之后执行
TERMINATED 线程已经完成执行
TIMED_WAITING 线程挂起执行一段指定的时间,例如当调用 sleep() 方法时就会处于这种状态。当调用 wait() join()
方法的暂停版 (timeout version) 时,也会进入这种状态
WAITING 线程因为等待某些动作而挂起执行。例如,因为调用非暂停版的 wait() join() 方法而等待时,会
处于这种状态


11-1 显示了各种线程状态之间的联系。

线程结束 线程启动
等待锁 等待
已经获得锁 等待结束
11-1 线程状态

对于给定的 Thread 实例,可以使用 getState() 方法获取线程的状态。例如,下面的代码判断调用线程 thrd 在调
getState() 方法时是否处于 RUNNABLE 状态:
Thread.State ts = thrd.getState();
if(ts == Thread.State.RUNNABLE) // ...
在调用 getState() 方法之后,线程的状态可能会发生变化,理解这一点很重要。因此,基于具体的环境,通过调
getState() 方法获取的状态,可能无法反映之后一段较短的时间内线程的实际状态。由于该原因 ( 以及其他原因 )
getState() 方法的目标不是提供一种同步线程的方法,而主要用于调试或显示线程的运行时特征。
11.11 使用工厂方法创建和启动线程
在有些情况下,不必将线程的创建和启动单独分开。换句话说,有时可以非常方便地同时创建和启动线程。为
此可以使用一种静态的工厂方法。工厂方法 (factory method) 的返回值为类的对象。通常,工厂方法是指类的静态方
法,它们用于各种目的,例如,在对象使用前为其设置初始状态,配置特定类型的对象,在有些情况下还可以使对
象重用。因为工厂方法与线程的创建和启动相关联,所以它们会创建线程并对该线程调用
start() 方法,并返回对该
线程的引用。通过这种方法,使用单个方法调用就可以实现线程的创建和启动,这样可以让代码更简洁流畅。
例如,假设为本章开头的
ThreadDemo 程序中的 NewThread 线程添加了如下所示的工厂方法,在单一步骤中创
建和启动线程:
// A factory method that creates and starts a thread.
public static NewThread createAndStart() {
NewThread myThrd = new NewThread();
myThrd.t.start();
return myThrd;
}
使用 createAndStart() 方法,现在可以将如下代码:
NewThread nt = new NewThread(); // create a new thread
nt.t.start(); // Start the thread
替换为
NewThread nt = NewThread.createAndStart();
现在在一个步骤中就完成了线程的创建和启动。
在有些情况下,可以不必保持对正在执行的线程的引用,有时通过一行代码就能实现线程的创建和启动,而不
需要使用工厂方法。例如,再次对
ThreadDemo 程序进行假设,使用下面的代码行创建和启动 NewThread 线程:
new NewThread().t.start();
但在实际的应用程序中,通常需要保持对线程的引用,因而工厂方法通常还是一个不错的选择。
11.12 使用多线程
有效利用 Java 多线程特性的关键是并发地而不是顺序地思考问题。例如,当程序中有两个可以并发执行的子
系统时,可以在单独的线程中执行它们。通过细心地使用多线程,可以创建非常高效的程序。但是需要注意:如果
创建的线程太多,实际上可能会降低程序的性能,而不是增强性能。请记住,线程上下文切换需要一定的开销。如
果创建的线程过多,花费在上下文切换上的
CPU 时间会比执行程序的实际时间更长。最后一点:为了创建能够自
动伸缩以尽可能利用多核系统中可用处理器的计算密集型应用程序,可以考虑使用
Fork/Join 框架,该框架将在第
28 章介绍。
第 14 章 泛 型
自从 1995 年发布最初的 1.0 版以来, Java 增加了许多新特性。其中最具影响力的新特性之一是泛型 (generics)
泛型是由
JDK 5 引入的,在两个重要方面改变了 Java 。首先,泛型为语言增加了一个新的语法元素。其次,泛型改
变了核心
API 中的许多类和方法。今天,泛型已成为 Java 编程的组成部分, Java 程序员需要深入理解这一重要特
性。接下来就详细介绍泛型。
通过使用泛型,可以创建以类型安全的方式使用各种类型数据的类、接口以及方法。许多算法虽然操作的数据
类型不同,但算法逻辑是相同的。例如,不管堆栈存储的数据类型是
Integer String Object 还是 Thread ,支持堆
栈的机制是相同的。使用泛型,可以只定义算法一次,使其独立于特定的数据类型,然后将算法应用于各种数据类
型而不需要做任何额外的工作。泛型为语言添加的强大功能从根本上改变了编写
Java 代码的方式。
在泛型所影响的
Java 特性中, 受影响程度最大的一个可能是集合框架 (Collections Framework) 。 集合框架是 Java
API
的组成部分,将在第 19 章进行详细分析,但是在此先简要提及一下是有用的。集合是一组对象。集合框架定
义了一些类,例如列表和映射,这些类用来管理集合。集合类总是可以使用任意类型的对象。因为增加了泛型特性,
所以现在可以采用类型绝对安全的方式使用集合类。因此,除了本身是一个强大的语言元素外,泛型还能够从根本
上改进已有的特性。这就是为什么泛型被认为是如此重要的
Java 新增特性的另一个原因。
本章介绍泛型的语法、理论以及用法,还将展示泛型为一些以前的困难情况提供类型安全的原理。一旦学习完
本章,就可以阅读第
19 章,在那一章将介绍集合框架,你将发现许多使用泛型的例子。
14.1 什么是泛型
就本质而言,术语“泛型”的意思是参数化类型 (parameterized type) 。参数化类型很重要,因为使用该特性创
建的类、接口以及方法,可以作为参数指定所操作数据的类型。例如,使用泛型可以创建自动操作不同类型数据的
类。操作参数化类型的类、接口或方法被称为泛型,例如泛型类
(generic class) 或泛型方法 (generic method)
通过操作
Object 类型的引用, Java 总是可以创建一般化的类、接口以及方法,理解这一点很重要。因为 Object
是所有其他类的超类,所以 Object 引用变量可以引用所有类型的对象。因此,在 Java 提供泛型特性之前编写的代
码,一般化的类、接口以及方法使用
Object 引用来操作各种类型的对象。问题是它们不能以类型安全的方式工作。
泛型提供了以前缺失的类型安全性,并且还可以简化处理过程,因为不再需要显式地使用强制类型转换,即不
再需要在
Object 和实际操作的数据类型之间进行转换。使用泛型,所有类型转换都是自动和隐式进行的。因此,泛
型扩展了重用代码的能力,并且可以安全、容易地重用代码。
注意:
C++ 程序员的警告:尽管泛型和 C++ 中的模板很类似,但它们不是一回事。这两种处理泛型类型的方式之
间有一些本质区别。如果具有
C++ 背景,不要草率地认为 Java 中泛型的工作机理与 C++ 中的模板相同,这一点
很重要。
14.2 一个简单的泛型示例
下面首先看一个泛型类的简单示例。下面的程序定义了两个类。第一个是泛型类 Gen ;第二个是泛型类
GenDemo ,该类使用 Gen 类。
242 第Ⅰ部分 Java 语言
// A simple generic class.
// Here, T is a type parameter that
// will be replaced by a real type
// when an object of type Gen is created.
class Gen {
T ob; // declare an object of type T
// Pass the constructor a reference to
// an object of type T.
Gen(T o) {
ob = o;
}
// Return ob.
T getob() {
return ob;
}
// Show type of T.
void showType() {
System.out.println("Type of T is " +
ob.getClass().getName());
}
}
// Demonstrate the generic class.
class GenDemo {
public static void main(String args[]) {
// Create a Gen reference for Integers.
Gen iOb;
// Create a Gen object and assign its
// reference to iOb. Notice the use of autoboxing
// to encapsulate the value 88 within an Integer object.
iOb = new Gen(88);
// Show the type of data used by iOb.
iOb.showType();
// Get the value in iOb. Notice that
// no cast is needed.
int v = iOb.getob();
System.out.println("value: " + v);
System.out.println();
// Create a Gen object for Strings.
Gen strOb = new Gen ("Generics Test");
// Show the type of data used by strOb.
strOb.showType();
// Get the value of strOb. Again, notice
// that no cast is needed.
String str = strOb.getob();
System.out.println("value: " + str);
}
}
该程序生成的输出如下所示:
Type of T is java.lang.Integer
value: 88
Type of T is java.lang.String

第 14 章 泛 型 243
value: Generics Test
下面详细分析该程序。
首先,注意下面这行代码声明泛型类
Gen 的方式:
class Gen {
其中, T 是类型参数的名称。这个名称是实际类型的占位符,当创建对象时,将实际类型传递给 Gen 。因此在
Gen 中,只要需要类型参数,就使用 T 。注意 T 被包含在 <> 中。可以推广该语法。只要是声明类型参数,就需要在
尖括号中指定。因为
Gen 使用类型参数,所以 Gen 是泛型类,也称为参数化类型。
接下来使用
T 声明对象 ob ,如下所示:
T ob; // declare an object of type T
前面解释过, T 是将在创建 Gen 对象时指定的实际类型的占位符。因此, ob 是传递给 T 的那种实际类型的对
象。例如,如果将
String 类型传递给 T ob 将是 String 类型。
现在分析
Gen 的构造函数:
Gen(T o) {
ob = o;
}
注意参数 o 的类型是 T ,这意味着 o 的实际类型取决于创建 Gen 对象时传递给 T 的类型。此外,因为参数 o
和成员变量 ob 的类型都是 T ,所以在创建 Gen 对象时,它们将具有相同的类型。
还可以使用类型参数
T 指定方法的返回类型,就像 getOb() 方法那样,如下所示:
T getob() {
return ob;
}
因为 ob 也是 T 类型,所以 ob 的类型和 getOb() 方法指定的返回类型是兼容的。
showType() 方法通过对 Class 对象调用 getName() 方法来显示 T 的类型,而这个 Class 对象是通过对 ob 调用
getClass() 方法返回的。 getClass() 方法是由 Object 类定义的,因此该方法是所有类的成员。该方法返回一个 Class
象,这个
Class 对象与调用对象所属的类对应。 Class 定义了 getName() 方法,该方法返回类名的字符串表示形式。
GenDemo 类演示了泛型化的 Gen 类。它首先创建整型版本的 Gen 类,如下所示:
Gen iOb;
请仔细分析这个声明。首先,注意类型 Integer 是在 Gen 后面的尖括号中指定的。在此, Integer 是传递给 Gen
的类型参数。这有效地创建了 Gen 的一个版本,在该版本中,对 T 的所有引用都被转换为对 Integer 的引用。因此
对于这个声明,
ob Integer 类型,并且 getob() 方法的返回类型是 Integer
在继续之前,必须先说明的是,
Java 编译器实际上没有创建不同版本的 Gen 类,或者说没有创建任何其他泛型
类。尽管那样认为是有帮助的,但是实际情况并非如此。相反,编译器移除了所有泛型类型信息,进行必需的类型
转换,从而使代码的行为好像是创建了特定版本的
Gen 类一样。因此,在程序中实际上只有一个版本的 Gen 类。
移除泛型类型信息的过程被称为擦除
(erasure) ,在本章后面还会继续介绍该主题。
下一行代码将一个引用
( 指向 Integer 版本的 Gen 类的一个实例 ) 赋给 iOb
iOb = new Gen(88);
注意在调用 Gen 构造函数时,仍然指定了类型参数 Integer 。这是必要的,因为将为其赋值的对象 ( 在此为 iOb)
的类型是 Gen 。因此, new 返回的引用也必须是 Gen 类型。如果不是的话,就会生成编译时错误。
例如,下面的赋值操作会导致编译时错误:
iOb = new Gen(88.0); // Error!
因为 iOb Gen 类型, 所以不能引用 Gen 类型的对象。 这种类型检查是泛型的主要优点之一,
因为可以确保类型安全。
注意:
在本章后面可以看到,可以缩短创建泛型类的实例的语法。为了清晰起见,现在使用完整语法。
244 第Ⅰ部分 Java 语言
正如程序中的注释所表明的,下面的赋值语句:
iOb = new Gen(88);
使用自动装箱特性封装数值 88 ,将这个 int 型数值转换成 Integer 对象。这可以工作,因为 Gen 创建了
一个使用
Integer 参数的构造函数。因为期望 Integer 类型的对象,所以 Java 会自动将数值 88 装箱到 Integer 对象中。
当然,也可以像下面这样显式地编写这条赋值语句:
iOb = new Gen(new Integer(88));
但是,使用这个版本的代码没有任何好处。
然后程序显示
iOb ob 的类型,也就是 Integer 类型。接下来,程序使用下面这行代码获取 ob 的值:
int v = iOb.getob();
因为 getob() 方法的返回类型是 T ,当声明 iOb T 已被替换为 Integer 类型,所以 getob() 方法的返回类型也是
Integer ,当将返回值赋给 v( int 类型 ) 时会自动拆箱为 int 类型。因此,不需要将 getob() 方法的返回类型强制转换
Integer 。当然,并不是必须使用自动拆箱特性。前面这行代码也可以像下面这样编写:
int v = iOb.getob().intValue();
但是,自动拆箱特性能使代码更紧凑。
接下来,
GenDemo 声明了 Gen 类型的一个对象:
Gen strOb = new Gen("Generics Test");
因为类型参数是 String ,所以使用 String 替换 Gen 中的 T ( 从概念上讲 ) 这会创建 Gen String 版本,就像程
序中的剩余代码所演示的那样。
14.2.1 泛型只使用引用类型
当声明泛型类的实例时,传递的类型参数必须是引用类型。不能使用基本类型,如 int char 。例如,对于 Gen
可以将任何类传递给
T ,但是不能将基本类型传递给类型参数 T 。所以,下面的声明是非法的:
Gen intOb = new Gen(53); // Error, can't use primitive type
当然,不能使用基本类型并不是一个严格的限制,因为可以使用类型封装器封装基本类型 ( 就像前面的例子那
) 。此外, Java 的自动装箱和拆箱机制使得类型封装器的使用是透明的。
14.2.2 基于不同类型参数的泛型类型是不同的
对特定版本的泛型类型的引用和同一泛型类型的其他版本不是类型兼容的,这是关于泛型类型方面更需要理解
的关键一点。例如,对于刚才显示的程序,下面这行代码是错误的,并且不能通过编译:
iOb = strOb; // Wrong!
尽管 iOb strOb 都是 Gen 类型,但它们是对不同类型的引用,因为它们的类型参数不同。这是泛型添加
类型安全性以及防止错误的方式的一部分。
14.2.3 泛型提升类型安全性的原理
至此,你可能会思考以下问题:既然在泛型类 Gen 中,通过简单地将 Object 作为数据类型并使用正确的类型
转换,即使不使用泛型也可以得到相同的功能,那么将
Gen 泛型化有什么好处呢?答案是对于所有涉及 Gen 的操
作,泛型都可以自动确保类型安全。在这个过程中,消除了手动输入类型转换以及类型检查的需要。
为了理解泛型带来的好处,首先考虑下面的程序,这个程序创建了一个非泛型化的
Gen 的等价类:
// NonGen is functionally equivalent to Gen
// but does not use generics.
class NonGen {
Object ob; // ob is now of type Object
// Pass the constructor a reference to

// an object of type Object
NonGen(Object o) {
ob = o;
}
// Return type Object.
Object getob() {
return ob;
}
// Show type of ob.
void showType() {
System.out.println("Type of ob is " +
ob.getClass().getName());
}
}
// Demonstrate the non-generic class.
class NonGenDemo {
public static void main(String args[]) {
NonGen iOb;
// Create NonGen Object and store
// an Integer in it. Autoboxing still occurs.
iOb = new NonGen(88);
// Show the type of data used by iOb.
iOb.showType();
// Get the value of iOb.
// This time, a cast is necessary.
int v = (Integer) iOb.getob();
System.out.println("value: " + v);
System.out.println();
// Create another NonGen object and
// store a String in it.
NonGen strOb = new NonGen("Non-Generics Test");
// Show the type of data used by strOb.
strOb.showType();
// Get the value of strOb.
// Again, notice that a cast is necessary.
String str = (String) strOb.getob();
System.out.println("value: " + str);
// This compiles, but is conceptually wrong!
iOb = strOb;
v = (Integer) iOb.getob(); // run-time error!
}
}
在这个版本中有几个有趣的地方。首先,注意 NonGen 类使用 Object 替换了所有的 T 。这使得 NonGen 能够存
储任意类型的对象,就像泛型版本那样。但是,这样做也使得
Java 编译器不知道在 NonGen 中实际存储的数据类型
的任何相关信息,这是一件坏事情,原因有二。首先,对于存储的数据,必须显式地进行类型转换才能提取。其次,
许多类型不匹配错误直到运行时才能发现。下面深入分析每个问题。
请注意下面这行代码:
int v = (Integer) iOb.getob();
因为 getob() 方法的返回类型是 Object ,所以为了能够对返回值进行自动拆箱并保存在 v 中,必须将返回值强制
转换为
Integer 类型。如果移除强制转换,程序就不能通过编译。若使用泛型版本,这个类型转换是隐式进行的。
246 第Ⅰ部分 Java 语言
在非泛型版本中,必须显式地进行类型转换。这不但不方便,而且还是潜在的错误隐患。
现在,分析下面的代码,这些代码位于程序的末尾:
// This compiles, but is conceptually wrong!
iOb = strOb;
v = (Integer) iOb.getob(); // run-time error!
在此,将 strOb 赋给 iOb 。但是 strOb 引用的是包含字符串而非包含整数的对象。这条赋值语句在语法上是合法
的,因为所有
NonGen 引用都是相同的,所有 NonGen 引用变量都可以引用任意类型的 NonGen 对象。但是,这条
语句在语义上是错误的,正如下一行所显示的那样。这一行将
getob() 方法的返回值强制转换成 Integer 类型,然后
试图将这个值赋给
v 。现在的麻烦是: iOb 引用的是包含字符串而非包含整数的对象。遗憾的是,由于没有使用泛
型,
Java 编译器无法知道这一点。相反,当试图强制转换为 Integer 时会发生运行时异常。在代码中发生运行时异
常是非常糟糕的。
如果使用泛型,就不会发生上面的问题。在程序的泛型版本中,如果试图使用这条语句,编译器会捕获该语句
并报告错误,从而防止会导致运行时异常的严重
bug 。创建类型安全的代码,从而在编译时能够捕获类型不匹配错
误,这是泛型的一个关键优势。尽管使用
Object 引用创建“泛型”代码总是可能的,但这类代码不是类型安全的,
并且对它们的误用会导致运行时异常。泛型可以防止这种问题的发生。本质上,通过泛型可以将运行时错误转换成
编译时错误,这是泛型的主要优势。
14.3 带两个类型参数的泛型类
在泛型中可以声明多个类型参数。为了指定两个或更多个类型参数,只需要使用逗号分隔参数列表即可。例如,下
面的
TwoGen 类是 Gen 泛型类的另一个版本,它具有两个类型参数:
// A simple generic class with two type
// parameters: T and V.
class TwoGen {
T ob1;
V ob2;
// Pass the constructor a reference to
// an object of type T and an object of type V.
TwoGen(T o1, V o2) {
ob1 = o1;
ob2 = o2;
}
// Show types of T and V.
void showTypes() {
System.out.println("Type of T is " +
ob1.getClass().getName());
System.out.println("Type of V is " +
ob2.getClass().getName());
}
T getob1() {
return ob1;
}
V getob2() {
return ob2;
}
}
// Demonstrate TwoGen.
class SimpGen {
public static void main(String args[]) {
TwoGen tgObj =

new TwoGen(88, "Generics");
// Show the types.
tgObj.showTypes();
// Obtain and show values.
int v = tgObj.getob1();
System.out.println("value: " + v);
String str = tgObj.getob2();
System.out.println("value: " + str);
}
}
这个程序的输出如下所示:
Type of T is java.lang.Integer
Type of V is java.lang.String
value: 88
value: Generics
注意 TwoGen 的声明方式:
class TwoGen {
在此指定了两个类型参数: T V ,使用逗号将它们隔开。创建对象时必须为 TwoGen 传递两个类型参数,如
下所示:
TwoGen tgObj =
new TwoGen(88, "Generics");
在此, Integer 替换 T String 替换 V
在这个例子中,尽管两个类型参数是不同的,但是可以将两个类型参数设置为相同的类型。例如,下面这行代
码是合法的:
TwoGen x = new TwoGen ("A", "B");
在此, T V 都是 String 类型。当然,如果类型参数总是相同的,就不必使用两个类型参数了。
14.4 泛型类的一般形式
在前面例子中展示的泛型语法可以一般化。下面是声明泛型类的语法:
class class-name < type-param-list > { // …
下面是声明指向泛型类的引用的语法:
class-name var-name =
new
class-name ( cons-arg-list );
14.5 有界类型
在前面的例子中,可以使用任意类替换类型参数。对于大多数情况这很好,但是限制能够传递给类型参数的类
型有时是有用的。例如,假设希望创建一个泛型类,类中包含一个返回数组中数字平均值的方法。此外,希望能使
用这个类计算一组任意类型数字的平均值,包括整数、单精度浮点数以及双精度浮点数。因此,希望使用类型参数
以泛型化的方式指定数字类型。为了创建这样的一个类,你可能会尝试编写类似下面的代码:
// Stats attempts (unsuccessfully) to
// create a generic class that can compute
// the average of an array of numbers of
// any given type.
//
// The class contains an error!
class Stats {
T[] nums; // nums is an array of type T

// Pass the constructor a reference to
// an array of type T.
Stats(T[] o) {
nums = o;
}
// Return type double in all cases.
double average() {
double sum = 0.0;
for(int i=0; i < nums.length; i++)
sum += nums[i].doubleValue(); // Error!!!
return sum / nums.length;
}
}
Stats 类中, average() 方法通过调用 doubleValue() ,试图获得 nums 数组中每个数字的 double 版本。因为所有
数值类,比如
Integer Double ,都是 Number 的子类,而 Number 定义了 doubleValue() 方法,所以所有数值类型的
封装器都可以使用该方法。 问题是编译器不知道你正试图创建只使用数值类型的
Stats 对象。 因此, 当试图编译 Stats
时,会报告错误,指出 doubleValue() 方法是未知的。为了解决这个问题,需要以某种方式告诉编译器,你打算只向
T 传递数值类型。此外,需要以某种方式确保实际上只传递了数值类型。
为了处理这种情况,
Java 提供了有界类型 (bounded type) 。在指定类型参数时,可以创建声明超类的上界,所有
类型参数都必须派生自超类。这是当指定类型参数时通过使用
extends 子句完成的,如下所示:
< T extends superclass >
这样就指定 T 只能被 superclass 或其子类替代。因此, superclass 定义了包括 superclass 在内的上限。
可以通过将
Number 指定为上界,修复前面显示的 Stats 类,如下所示:
// In this version of Stats, the type argument for
// T must be either Number, or a class derived
// from Number.
class Stats {
T[] nums; // array of Number or subclass
// Pass the constructor a reference to
// an array of type Number or subclass.
Stats(T[] o) {
nums = o;
}
// Return type double in all cases.
double average() {
double sum = 0.0;
for(int i=0; i < nums.length; i++)
sum += nums[i].doubleValue();
return sum / nums.length;
}
}
// Demonstrate Stats.
class BoundsDemo {
public static void main(String args[]) {
Integer inums[] = { 1, 2, 3, 4, 5 };
Stats iob = new Stats(inums);
double v = iob.average();
System.out.println("iob average is " + v);
Double dnums[] = { 1.1, 2.2, 3.3, 4.4, 5.5 };

Stats dob = new Stats(dnums);
double w = dob.average();
System.out.println("dob average is " + w);
// This won't compile because String is not a
// subclass of Number.
// String strs[] = { "1", "2", "3", "4", "5" };
// Stats strob = new Stats(strs);
// double x = strob.average();
// System.out.println("strob average is " + v);
}
}
该程序的输出如下所示:
Average is 3.0
Average is 3.3
注意现在使用下面这行代码声明 Stats 的方式:
class Stats {
现在使用 Number 对类型 T 进行了限定, Java 编译器知道所有 T 类型的对象都可以调用 doubleValue() 方法,因
为该方法是由
Number 声明的。就其本身来说,这是一个主要优势。除此之外,还有另外一个好处:限制 T 的范围
也会阻止创建非数值类型的
Stats 对象。例如,如果尝试移除对程序底部几行代码的注释,然后重新编译,就会收
到编译时错误,因为
String 不是 Number 的子类。
除了使用类作为边界之外,也可以使用接口。实际上,可以指定多个接口作为边界。此外,边界可以包含一个
类和一个或多个接口。对于这种情况,必须首先指定类类型。如果边界包含接口类型,那么只有实现了那种接口的
类型参数是合法的。当指定具有一个类和一个或多个接口的边界时,使用
& 运算符连接它们。例如:
class Gen { // ...
在此, 通过类 MyClass 和接口 MyInterface T 进行了限制。 因此, 所有传递给 T 的类型参数都必须是 MyClass
的子类,并且必须实现 MyInterface 接口。
14.6 使用通配符参数
类型安全虽然有用,但是有时可能会影响完全可以接受的结构。例如,对于上一节末尾显示的 Stats 类,假设
希望添加方法
sameAvg() ,该方法用于判定两个 Stats 对象包含的数组的平均值是否相同,而不考虑每个对象包含的
数值数据的具体类型。例如,如果一个对象包含
double 1.0 2.0 3.0 ,另一个对象包含整数值 2 1 3 ,那么
平均值是相同的。实现
sameAvg() 方法的一种方式是传递 Stats 参数,然后根据调用对象比较参数的平均值,只有当
平均值相同时才返回
true 。例如,你希望能够像下面这样调用 sameAvg() 方法:
Integer inums[] = { 1, 2, 3, 4, 5 };
Double dnums[] = { 1.1, 2.2, 3.3, 4.4, 5.5 };
Stats iob = new Stats(inums);
Stats dob = new Stats(dnums);
if(iob.sameAvg(dob))
System.out.println("Averages are the same.");
else
System.out.println("Averages differ.");
起初,创建 sameAvg() 方法看起来是一个简单的问题。因为 Stats 是泛型化的,并且它的 average() 方法可以使用
任意类型的
Stats 对象,看起来创建 sameAvg() 方法将会很直观。遗憾的是,一旦试图声明 Stats 类型的参数,麻烦
就开始了。
Stats 是参数化类型,当声明这种类型的参数时,将 Stats 的类型参数指定为什么好呢?
乍一看,你可能会认为解决方案与下面类似,其中的
T 用作类型参数:
// This won't work!
// Determine if two averages are the same.
boolean sameAvg(Stats ob) {
if(average() == ob.average())
return true;
return false;
}
这种尝试存在的问题是:只有当其他 Stats 对象的类型和调用对象的类型相同时才能工作。例如,如果调用对
象是
Stats 类型,那么参数 ob 也必须是 Stats 类型。不能用于比较 Stats 类型对象的平均值
Stats 类型对象的平均值。所以,这种方式的适用范围很窄,无法得到通用的 ( 即泛型化的 ) 解决方案。
为了创建泛型化的
sameAvg() 方法,必须使用 Java 泛型的另一个特性:通配符 (wildcard) 参数。通配符参数是由
“?”指定的,表示未知类型。下面是使用通配符编写
sameAvg() 方法的一种方式:
// Determine if two averages are the same.
// Notice the use of the wildcard.
boolean sameAvg(Stats ob) {
if(average() == ob.average())
return true;
return false;
}
在此, Stats 和所有 Stats 对象匹配,允许任意两个 Stats 对象比较它们的平均值。下面的程序演示了这一点:
// Use a wildcard.
class Stats {
T[] nums; // array of Number or subclass
// Pass the constructor a reference to
// an array of type Number or subclass.
Stats(T[] o) {
nums = o;
}
// Return type double in all cases.
double average() {
double sum = 0.0;
for(int i=0; i < nums.length; i++)
sum += nums[i].doubleValue();
return sum / nums.length;
}
// Determine if two averages are the same.
// Notice the use of the wildcard.
boolean sameAvg(Stats ob) {
if(average() == ob.average())
return true;
return false;
}
}
// Demonstrate wildcard.
class WildcardDemo {
public static void main(String args[]) {
Integer inums[] = { 1, 2, 3, 4, 5 };
Stats iob = new Stats(inums);
double v = iob.average();
System.out.println("iob average is " + v);
Double dnums[] = { 1.1, 2.2, 3.3, 4.4, 5.5 };
Stats dob = new Stats(dnums);

double w = dob.average();
System.out.println("dob average is " + w);
Float fnums[] = { 1.0F, 2.0F, 3.0F, 4.0F, 5.0F };
Stats fob = new Stats(fnums);
double x = fob.average();
System.out.println("fob average is " + x);
// See which arrays have same average.
System.out.print("Averages of iob and dob ");
if(iob.sameAvg(dob))
System.out.println("are the same.");
else
System.out.println("differ.");
System.out.print("Averages of iob and fob ");
if(iob.sameAvg(fob))
System.out.println("are the same.");
else
System.out.println("differ.");
}
}
输出如下所示:
iob average is 3.0
dob average is 3.3
fob average is 3.0
Averages of iob and dob differ.
Averages of iob and fob are the same.
最后一点:通配符不会影响能够创建什么类型的 Stats 对象,理解这一点很重要。这是由 Stats 声明中的 extends
子句控制的。通配符只是简单地匹配所有有效的 Stats 对象。
有界通配符
可以使用与界定类型参数大体相同的方式来界定通配符参数。对于创建用于操作类层次的泛型来说,有界通配
符很重要。为了理解其中的原因,下面看一个例子。分析下面的类层次,其中的类封装了坐标:
// Two-dimensional coordinates.
class TwoD {
int x, y;
TwoD(int a, int b) {
x = a;
y = b;
}
}
// Three-dimensional coordinates.
class ThreeD extends TwoD {
int z;
ThreeD(int a, int b, int c) {
super(a, b);
z = c;
}
}
// Four-dimensional coordinates.
class FourD extends ThreeD {
int t;
FourD(int a, int b, int c, int d) {
super(a, b, c);

t = d;
}
}
在这个类层次的顶部是 TwoD ,该类封装了二维坐标 (XY 坐标 ) ThreeD 派生自 TwoD ,该类添加了第三维,创
XYZ 坐标。 FourD 派生自 ThreeD ,该类添加了第四维 ( 时间 ) ,生成四维坐标。
下面显示的是泛型类
Coords ,该类存储了一个坐标数组:
// This class holds an array of coordinate objects.
class Coords {
T[] coords;
Coords(T[] o) { coords = o; }
}
注意 Coords 指定了一个由 TwoD 界定的类型参数。这意味着在 Coords 对象中存储的所有数组将包含 TwoD
或其子类的对象。
现在,假设希望编写一个方法,显示
Coords 对象的 coords 数组中每个元素的 X Y 坐标。因为所有 Coords
对象的类型都至少有两个坐标 (X Y) ,所以使用通配符很容易实现,如下所示:
static void showXY(Coords c) {
System.out.println("X Y Coordinates:");
for(int i=0; i < c.coords.length; i++)
System.out.println(c.coords[i].x + " " +
c.coords[i].y);
System.out.println();
}
因为 Coords 是有界的泛型类,并且将 TwoD 指定为上界,所以能够用于创建 Coords 对象的所有对象都将是 TwoD
类及其子类的数组。因此, showXY() 方法可以显示所有 Coords 对象的内容。
但是,如果希望创建显示
ThreeD FourD 对象的 X Y Z 坐标的方法,该怎么办呢?麻烦是,并非所有 Coords
对象都有 3 个坐标,因为 Coords 对象只有 X Y 坐标。所以,如何编写能够显示 Coords
Coords 对象的 X Y Z 坐标的方法,而又不会阻止该方法使用 Coords 对象呢?答案是使用有界
的通配符参数。
有界的通配符为类型参数指定上界或下界,从而可以限制方法能够操作的对象类型。最常用的有界通配符是上
界,是使用
extends 子句创建的,具体方式和用于创建有界类型的方式大体相同。
如果对象实际拥有
3 个坐标的话,使用有界通配符可以很容易创建出显示 Coords 对象中 X Y Z 坐标的方
法。例如下面的
showXYZ() 方法,如果 Coords 对象中存储的元素的实际类型是 ThreeD( 或派生自 ThreeD) ,那么
showXYZ() 方法将显示这些元素的 X Y Z 坐标:
static void showXYZ(Coords c) {
System.out.println("X Y Z Coordinates:");
for(int i=0; i < c.coords.length; i++)
System.out.println(c.coords[i].x + " " +
c.coords[i].y + " " +
c.coords[i].z);
System.out.println();
}
注意, 在参数 c 的声明中为通配符添加了 extends 子句。这表明“?”可以匹配任意类型, 只要这些类型为 ThreeD
或其派生类即可。因此, extends 子句建立了“?”能够匹配的上界。因为这个界定,可以使用对 Coords
Coords 类型对象的引用调用 showXYZ() 方法,但不能使用 Coords 类型的引用进行调用。如果试
图使用
Coords 引用调用 showXYZ() 方法,就会导致编译时错误,从而确保类型安全。
下面是演示使用有界通配符参数的整个程序:
// Bounded Wildcard arguments.
// Two-dimensional coordinates.
class TwoD {
int x, y;

TwoD(int a, int b) {
x = a;
y = b;
}
}
// Three-dimensional coordinates.
class ThreeD extends TwoD {
int z;
ThreeD(int a, int b, int c) {
super(a, b);
z = c;
}
}
// Four-dimensional coordinates.
class FourD extends ThreeD {
int t;
FourD(int a, int b, int c, int d) {
super(a, b, c);
t = d;
}
}
// This class holds an array of coordinate objects.
class Coords {
T[] coords;
Coords(T[] o) { coords = o; }
}
// Demonstrate a bounded wildcard.
class BoundedWildcard {
static void showXY(Coords c) {
System.out.println("X Y Coordinates:");
for(int i=0; i < c.coords.length; i++)
System.out.println(c.coords[i].x + " " +
c.coords[i].y);
System.out.println();
}
static void showXYZ(Coords c) {
System.out.println("X Y Z Coordinates:");
for(int i=0; i < c.coords.length; i++)
System.out.println(c.coords[i].x + " " +
c.coords[i].y + " " +
c.coords[i].z);
System.out.println();
}
static void showAll(Coords c) {
System.out.println("X Y Z T Coordinates:");
for(int i=0; i < c.coords.length; i++)
System.out.println(c.coords[i].x + " " +
c.coords[i].y + " " +
c.coords[i].z + " " +
c.coords[i].t);
System.out.println();
}
public static void main(String args[]) {
TwoD td[] = {

new TwoD(0, 0),
new TwoD(7, 9),
new TwoD(18, 4),
new TwoD(-1, -23)
};
Coords tdlocs = new Coords(td);
System.out.println("Contents of tdlocs.");
showXY(tdlocs); // OK, is a TwoD
// showXYZ(tdlocs); // Error, not a ThreeD
// showAll(tdlocs); // Error, not a FourD
// Now, create some FourD objects.
FourD fd[] = {
new FourD(1, 2, 3, 4),
new FourD(6, 8, 14, 8),
new FourD(22, 9, 4, 9),
new FourD(3, -2, -23, 17)
};
Coords fdlocs = new Coords(fd);
System.out.println("Contents of fdlocs.");
// These are all OK.
showXY(fdlocs);
showXYZ(fdlocs);
showAll(fdlocs);
}
}
来自该程序的输出如下所示:
Contents of tdlocs.
X Y Coordinates:
0 0
7 9
18 4
-1 -23
Contents of fdlocs.
X Y Coordinates:
1 2
6 8
22 9
3 -2
X Y Z Coordinates:
1 2 3
6 8 14
22 9 4
3 -2 -23
X Y Z T Coordinates:
1 2 3 4
6 8 14 8
22 9 4 9
3 -2 -23 17
注意那些被注释掉的代码行:
// showXYZ(tdlocs); // Error, not a ThreeD
// showAll(tdlocs); // Error, not a FourD
tdlocs Coords(TwoD) 对象,不能用来调用 showXYZ() showAll() 方法,因为在这两个方法声明中的有界通
配符参数对此进行了阻止。为了自己证实这一点,可以尝试移除注释符号,然后编译该程序,你将会收到类型不匹

配的编译错误。
一般来说,要为通配符建立上界,可以使用如下所示的通配符表达式:
superclass >
其中, superclass 是作为上界的类的名称。记住,这是一条包含子句,因为形成上界 ( superclass 指定的边界 )
的类也位于边界之内。
还可以通过为通配符添加一条
super 子句,为通配符指定下界。下面是一般形式:
subclass >
对于这种情况,只有 subclass 的超类是可接受的参数。这是一条排除子句,因此与 subclass 指定的类不相匹配。
14.7 创建泛型方法
正如前面的例子所显示的, 泛型类中的方法可以使用类的类型参数, 所以它们是自动相对于类型参数泛型化的。
不过,可以声明本身使用一个或多个类型参数的泛型方法。此外,可以在非泛型类中创建泛型方法。
让我们通过一个例子开始。 下面的程序声明了非泛型类
GenMethDemo , 并在该类中声明了静态泛型方法 isIn()
isIn() 方法用于判定某个对象是否是数组的成员,可以用于任意类型的对象和数据,只要数组包含的对象和将要检查
对象的类型兼容即可。
// Demonstrate a simple generic method.
class GenMethDemo {
// Determine if an object is in an array.
static , V extends T> boolean isIn(T x, V[] y) {
for(int i=0; i < y.length; i++)
if(x.equals(y[i])) return true;
return false;
}
public static void main(String args[]) {
// Use isIn() on Integers.
Integer nums[] = { 1, 2, 3, 4, 5 };
if(isIn(2, nums))
System.out.println("2 is in nums");
if(!isIn(7, nums))
System.out.println("7 is not in nums");
System.out.println();
// Use isIn() on Strings.
String strs[] = { "one", "two", "three",
"four", "five" };
if(isIn("two", strs))
System.out.println("two is in strs");
if(!isIn("seven", strs))
System.out.println("seven is not in strs");
// Oops! Won't compile! Types must be compatible.
// if(isIn("two", nums))
// System.out.println("two is in strs");
}
}
该程序的输出如下所示:
2 is in nums
7 is not in nums
two is in strs
seven is not in strs
下面详细分析 isIn() 方法。首先,注意下面这行代码声明 isIn() 方法的方式:
static , V extends T> boolean isIn(T x, V[] y) {
类型参数在方法的返回类型之前声明。其次,注意 T 扩展了 Comparable Comparable 是在 java.lang 中声明
的一个接口。实现
Comparable 接口的类定义了可被排序的对象。因此,限制上界为 Comparable 确保了在 isIn() 中只
能使用可被比较的对象。
Comparable 是泛型接口,其类型参数指定了要比较的对象的类型 ( 稍后将看到如何创建泛
型接口
) 。接下来,注意 T 为类型 V 设置了上界。因此, V 必须是类 T 或其子类。这种关系强制只能使用相互兼容
的参数来调用
isIn() 方法。还应当注意 isIn() 方法是静态的,因而可以独立于任何对象进行调用。泛型方法既可以是
静态的也可以是非静态的,对此没有限制。
现在,注意在
main() 中调用 isIn() 方法的方式,使用常规的调用语法,不需要指定类型参数。这是因为参数的
类型是自动辨别的,并且会相应地调整
T V 的类型。例如,在第 1 次调用中:
if(isIn(2, nums))
1 个参数的类型是 Integer( 由于自动装箱 ) ,这会导致使用 Integer 替换 T 。第 2 个参数的基类型 (base type)
Integer ,因而也用 Integer 替换 V 。在第 2 次调用中,使用的是 String 类型,因而使用 String 替换 T V 代表的
类型。
尽管对于大多数泛型方法调用,类型推断就足够了,但是需要时,也可以显式指定类型参数。例如,下面显示
了当指定类型参数时对
isIn() 方法的第 1 次调用:
GenMethDemo.isIn(2, nums)
当然,在本例中,指定类型参数不会带来什么好处。而且, JDK 8 改进了有关方法的类型推断。所以,需要显
式指定类型参数的场合不是太多。
现在,注意注释掉的代码,如下所示:
// if(isIn("two", nums))
// System.out.println("two is in strs");
如果移除注释符号,然后尝试编译程序,将会收到错误。原因在于 V 声明中 extends 子句中的 T 对类型参数 V
进行了界定。这意味着 V 必须是 T 类型或其子类类型。而在此处, 第 1 个参数是 String 类型, 因而将 T 转换为 String
但第
2 个参数是 Integer 类型,不是 String 的子类,这会导致类型不匹配的编译时错误。这种强制类型安全的能力是
泛型方法最重要的优势之一。
用于创建
isIn() 方法的语法可以通用化。下面是泛型方法的语法:
< type-param-list > ret-type meth-name ( param-list ) { // …
对于所有情况, type-param-list 是由逗号分隔的类型参数列表。注意对于泛型方法,类型参数列表位于返回类
型之前。
泛型构造函数
可以将构造函数泛型化,即使它们的类不是泛型类。例如,分析下面的简短程序:
// Use a generic constructor.
class GenCons {
private double val;
GenCons(T arg) {
val = arg.doubleValue();
}
void showval() {
System.out.println("val: " + val);

}
}
class GenConsDemo {
public static void main(String args[]) {
GenCons test = new GenCons(100);
GenCons test2 = new GenCons(123.5F);
test.showval();
test2.showval();
}
}
该程序的输出如下所示:
val: 100.0
val: 123.5
因为 GenCons() 指定了一个泛型类型的参数,并且这个参数必须是 Number 的子类,所以可以使用任意数值类
型调用
GenCons() ,包括 Integer Float 以及 Double 。因此,虽然 GenCons 不是泛型类,但是它的构造函数可以泛
型化。
14.8 泛型接口
除了可以定义泛型类和泛型方法外,还可以定义泛型接口。泛型接口的定义和泛型类相似。下面是一个例子。
该例创建了接口
MinMax ,该接口声明了 min() max() 方法,它们返回某些对象的最小值和最大值。
// A generic interface example.
// A Min/Max interface.
interface MinMax> {
T min();
T max();
}
// Now, implement MinMax
class MyClass> implements MinMax {
T[] vals;
MyClass(T[] o) { vals = o; }
// Return the minimum value in vals.
public T min() {
T v = vals[0];
for(int i=1; i < vals.length; i++)
if(vals[i].compareTo(v) < 0) v = vals[i];
return v;
}
// Return the maximum value in vals.
public T max() {
T v = vals[0];
for(int i=1; i < vals.length; i++)
if(vals[i].compareTo(v) > 0) v = vals[i];
return v;
}
}
class GenIFDemo {

public static void main(String args[]) {
Integer inums[] = {3, 6, 2, 8, 6 };
Character chs[] = {'b', 'r', 'p', 'w' };
MyClass iob = new MyClass(inums);
MyClass cob = new MyClass(chs);
System.out.println("Max value in inums: " + iob.max());
System.out.println("Min value in inums: " + iob.min());
System.out.println("Max value in chs: " + cob.max());
System.out.println("Min value in chs: " + cob.min());
}
}
输出如下所示:
Max value in inums: 8
Min value in inums: 2
Max value in chs: w
Min value in chs: b
尽管这个程序的大多数方面应当很容易理解,但是有几个关键点需要指出。首先,注意 MinMax 的声明方式,
如下所示:
interface MinMax> {
一般而言,声明泛型接口的方式与声明泛型类相同。对于这个例子,类型参数是 T ,它的上界是 Comparable
如前所述,这是由
java.lang 定义的接口,指定了比较对象的方式。它的类型参数指定了将进行比较的对象的类型。
接下来,
MyClass 实现了 MinMax 。注意 MyClass 的声明,如下所示:
class MyClass> implements MinMax {
请特别注意 MyClass 声明类型参数 T 以及将 T 传递给 MinMax 的方式。因为 MinMax 需要实现了 Comparable
的类型, 所以实现类 ( 在该例中是 MyClass) 必须指定相同的界限。此外, 一旦建立这个界限, 就不需要再在 implements
子句中指定。实际上,那么做是错误的。例如,下面这行代码是不正确的,不能通过编译:
// This is wrong!
class MyClass>
implements MinMax> {
一旦建立类型参数,就可以不加修改地将之传递给接口。
一般而言,如果类实现了泛型接口,那么类也必须是泛型化的,至少需要带有将被传递给接口的类型参数。例
如,下面对
MyClass 的声明是错误的:
class MyClass implements MinMax { // Wrong!
因为 MyClass 没有声明类型参数,所以无法为 MinMax 传递类型参数。对于这种情况,标识符 T 是未知的,编
译器会报告错误。当然,如果类实现了某种具体类型的泛型接口,如下所示:
class MyClass implements MinMax { // OK
那么实现类不需要是泛型化的。
泛型接口具有两个优势。首先,不同类型的数据都可以实现它。其次,可以为实现接口的数据类型设置限制条
( 即界限 ) 。在 MinMax 例子中,只能向 T 传递实现了 Comparable 接口的类型。
下面是泛型接口的通用语法:
interface interface-name { // …
在此, type-param-list 是由逗号分隔的类型参数列表。当实现泛型接口时,必须指定类型参数,如下所示:
class class-name < type-param-list >
implements
interface-name {
14.9 原始类型与遗留代码
因为 Java JDK 5 之前不支持泛型,所以需要为旧的、在支持泛型之前编写的代码提供一些过渡路径。在编
写本书时,仍然有大量在支持泛型之前编写的遗留代码,这些遗留代码既要保留功能,又要和泛型相兼容。在支持
泛型之前编写的代码必须能够使用泛型,并且泛型必须能够使用在支持泛型之前编写的代码。
为了处理泛型过渡,
Java 允许使用泛型类而不提供任何类型参数。这会为类创建原始类型 (raw type) ,这种原始
类型与不使用泛型的遗留代码是兼容的。使用原始类型的主要缺点是丢失了泛型的类型安全性。
下面是一个使用原始类型的例子:
// Demonstrate a raw type.
class Gen {
T ob; // declare an object of type T
// Pass the constructor a reference to
// an object of type T.
Gen(T o) {
ob = o;
}
// Return ob.
T getob() {
return ob;
}
}
// Demonstrate raw type.
class RawDemo {
public static void main(String args[]) {
// Create a Gen object for Integers.
Gen iOb = new Gen(88);
// Create a Gen object for Strings.
Gen strOb = new Gen("Generics Test");
// Create a raw-type Gen object and give it
// a Double value.
Gen raw = new Gen(Double.valueOf(98.6));
// Cast here is necessary because type is unknown.
double d = (Double) raw.getob();
System.out.println("value: " + d);
// The use of a raw type can lead to run-time
// exceptions. Here are some examples.
// The following cast causes a run-time error!
// int i = (Integer) raw.getob(); // run-time error
// This assignment overrides type safety.
strOb = raw; // OK, but potentially wrong
// String str = strOb.getob(); // run-time error
// This assignment also overrides type safety.
raw = iOb; // OK, but potentially wrong
// d = (Double) raw.getob(); // run-time error
}
}

260 第Ⅰ部分 Java 语言
这个程序包含了几件有趣的事情。首先,通过下面的声明创建了泛型类 Gen 的原始类型:
Gen raw = new Gen(new Double(98.6));
注意没有指定类型参数。本质上,这会创建使用 Object 替换其类型 T Gen 对象。
原始类型不是类型安全的。因此,可以将指向任意
Gen 对象类型的引用赋给原始类型的变量。反过来也可以;
可以将指向原始
Gen 对象的引用赋给特定 Gen 类型的变量。但是,这两种操作潜在都是不安全的,因为它们绕过
了泛型的类型检查机制。
在程序末尾部分注释掉的代码演示了类型安全性的丢失。现在让我们分析每一种情况。首先,分析下面的情形:
// int i = (Integer) raw.getob(); // run-time error
在这条语句中,获取 raw ob 的值,并将这个值转换为 Integer 类型。问题是: raw 包含 Double 值而不是整数
值。但是,在编译时不会检测出这一点,因为
raw 的类型是未知的。因此,在运行时这条语句会失败。
下一条语句将一个指向原始
Gen 对象的引用赋给 strOb( 一个 Gen 类型的引用 )
strOb = raw; // OK, but potentially wrong
// String str = strOb.getob(); // run-time error
就这条赋值语句本身而言,语法是正确的,但是存在问题。因为 strOb Gen 类型,所以被假定包含一
个字符串。但是,在这条赋值语句之后,
strOb 引用的对象包含一个 Double 值。因此在运行时,当试图将 strOb
内容赋给
str 时,会导致运行时错误,因为 strOb 现在包含的是 Double 值。因此,将原始引用赋给泛型引用绕过了
类型安全机制。
下面的代码与前面的情形相反:
raw = iOb; // OK, but potentially wrong
// d = (Double) raw.getob(); // run-time error
在此,将泛型引用赋给原始引用变量。尽管在语法上是正确的,但是可能导致问题,正如上面第 2 行所演示的。
对于这种情况,
raw 现在指向包含 Integer 对象的对象,但是类型转换假定 raw 包含 Double 对象。在编译时无法防
止这个错误,甚至会导致运行时错误。
因为原始类型固有的潜在危险,当以可能危及类型安全的方式使用原始类型时,
javac 会显示未检查警告。在
前面的程序中,下面这些代码会引起未检查警告:
Gen raw = new Gen(new Double(98.6));
strOb = raw; // OK, but potentially wrong
在第 1 行中,由于调用 Gen 构造函数时没有提供类型参数,因此导致警告生成。在第 2 行中,将原始引用赋给
泛型变量导致警告生成。
乍一看,你可能会认为下面这行代码应当引起未检查警告,但是并非如此:
raw = iOb; // OK, but potentially wrong
这行代码不会生成编译警告,因为与创建 raw 时发生的类型安全丢失相比,这条赋值语句不会导致任何进一步
的类型安全丢失。
最后一点:应当限制使用原始类型,只有在必须混合遗留代码和新的泛型代码时才使用。原始类型只是一个过
渡性的特性,对于新代码不应当使用。
14.10 泛型类层次
泛型类可以是类层次的一部分,就像非泛型类那样。因此,泛型类可以作为超类或子类。泛型和非泛型层次之
间的关键区别是:在泛型层次中,类层次中的所有子类都必须向上传递超类所需要的所有类型参数。这与必须沿着
类层次向上传递构造函数的参数类似。
14.10.1 使用泛型超类
下面是一个简单的类层次示例,该类层次使用了泛型超类:
// A simple generic class hierarchy.
class Gen {
T ob;
Gen(T o) {
ob = o;
}
// Return ob.
T getob() {
return ob;
}
}
// A subclass of Gen.
class Gen2 extends Gen {
Gen2(T o) {
super(o);
}
}
在这个类层次中, Gen2 扩展了泛型类 Gen 。注意下面这行代码声明 Gen2 的方式:
class Gen2 extends Gen {
Gen2 指定的类型参数 T 也被传递给 extends 子句中的 Gen ,这意味着传递给 Gen2 的任何类型也会被传递给 Gen
例如下面这个声明:
Gen2 num = new Gen2(100);
会将 Integer 作为类型参数传递给 Gen 。因此,对于 Gen2 Gen 部分的 ob 来说,其类型将是 Integer
还应当注意,除了将
T 传递给 Gen 超类外, Gen2 没有再使用类型参数 T 。因此,即使泛型超类的子类不必泛
型化,也仍然必须指定泛型超类所需要的类型参数。
当然,如果需要的话,子类可以自由添加自己的类型参数。例如,下面是前面类层次的另一个版本,此处的
Gen2 添加了它自己的类型参数:
// A subclass can add its own type parameters.
class Gen {
T ob; // declare an object of type T
// Pass the constructor a reference to
// an object of type T.
Gen(T o) {
ob = o;
}
// Return ob.
T getob() {
return ob;
}
}
// A subclass of Gen that defines a second
// type parameter, called V.
class Gen2 extends Gen {
V ob2;
Gen2(T o, V o2) {
super(o);
ob2 = o2;
}
V getob2() {
return ob2;
}
}

// Create an object of type Gen2.
class HierDemo {
public static void main(String args[]) {
// Create a Gen2 object for String and Integer.
Gen2 x =
new Gen2("Value is: ", 99);
System.out.print(x.getob());
System.out.println(x.getob2());
}
}
注意该版本中 Gen2 的声明,如下所示:
class Gen2 extends Gen {
在此, T 是传递给 Gen 的类型, V 是特定于 Gen2 的类型。 V 用于声明对象 ob2 ,并且作为 getob2() 方法的返回
类型。在
main() 中创建了一个 Gen2 对象,它的类型参数 T String 、类型参数 V Integer 。该程序如你所愿地显
示如下结果:
Value is: 99
14.10.2 泛型子类
非泛型类作为泛型子类的超类是完全可以的。例如,分析下面这个程序:
// A non-generic class can be the superclass
// of a generic subclass.
// A non-generic class.
class NonGen {
int num;
NonGen(int i) {
num = i;
}
int getnum() {
return num;
}
}
// A generic subclass.
class Gen extends NonGen {
T ob; // declare an object of type T
// Pass the constructor a reference to
// an object of type T.
Gen(T o, int i) {
super(i);
ob = o;
}
// Return ob.
T getob() {
return ob;
}
}
// Create a Gen object.
class HierDemo2 {
public static void main(String args[]) {
// Create a Gen object for String.

Gen w = new Gen("Hello", 47);
System.out.print(w.getob() + " ");
System.out.println(w.getnum());
}
}
该程序的输出如下所示:
Hello 47
在该程序中,注意在下面的声明中 Gen 继承 NonGen 的方式:
class Gen extends NonGen {
因为 NonGen 是非泛型类,所以没有指定类型参数。因此,尽管 Gen 声明了类型参数 T ,但 NonGen 却不需要 (
不能使用
) 。因此, Gen 以常规方式继承 NonGen ,没有应用特殊的条件。
14.10.3 泛型层次中的运行时类型比较
回顾一下在第 13 章介绍的运行时类型信息运算符 instanceof 。如前所述, instanceof 运算符用于判定对象是否是
某个类的实例。如果对象是指定类型的实例或者可以转换为指定的类型,就返回
true 。可以将 instanceof 运算符应
用于泛型类对象。下面的类演示了泛型层次的类型兼容性的一些内涵:
// Use the instanceof operator with a generic class hierarchy.
class Gen {
T ob;
Gen(T o) {
ob = o;
}
// Return ob.
T getob() {
return ob;
}
}
// A subclass of Gen.
class Gen2 extends Gen {
Gen2(T o) {
super(o);
}
}
// Demonstrate run-time type ID implications of generic
// class hierarchy.
class HierDemo3 {
public static void main(String args[]) {
// Create a Gen object for Integers.
Gen iOb = new Gen(88);
// Create a Gen2 object for Integers.
Gen2 iOb2 = new Gen2(99);
// Create a Gen2 object for Strings.
Gen2 strOb2 = new Gen2("Generics Test");
// See if iOb2 is some form of Gen2.
if(iOb2 instanceof Gen2)
System.out.println("iOb2 is instance of Gen2");
// See if iOb2 is some form of Gen.

if(iOb2 instanceof Gen)
System.out.println("iOb2 is instance of Gen");
System.out.println();
// See if strOb2 is a Gen2.
if(strOb2 instanceof Gen2)
System.out.println("strOb2 is instance of Gen2");
// See if strOb2 is a Gen.
if(strOb2 instanceof Gen)
System.out.println("strOb2 is instance of Gen");
System.out.println();
// See if iOb is an instance of Gen2, which it is not.
if(iOb instanceof Gen2)
System.out.println("iOb is instance of Gen2");
// See if iOb is an instance of Gen, which it is.
if(iOb instanceof Gen)
System.out.println("iOb is instance of Gen");
// The following can't be compiled because
// generic type info does not exist at run time.
// if(iOb2 instanceof Gen2)
// System.out.println("iOb2 is instance of Gen2");
}
}
该程序的输出如下所示:
iOb2 is instance of Gen2
iOb2 is instance of Gen
strOb2 is instance of Gen2
strOb2 is instance of Gen
iOb is instance of Gen
在该程序中, Gen2 Gen 的子类, Gen 是泛型类,类型参数为 T 。在 main() 中创建了 3 个对象。第 1 个对象
iOb ,它是 Gen 类型的对象。第 2 个对象是 iOb2 ,它是 Gen2 类型的对象。最后一个对象是 strOb2
它是
Gen2 类型的对象。
然后,该程序针对
iOb2 的类型执行以下这些 instanceof 测试:
// See if iOb2 is some form of Gen2.
if(iOb2 instanceof Gen2)
System.out.println("iOb2 is instance of Gen2");
// See if iOb2 is some form of Gen.
if(iOb2 instanceof Gen)
System.out.println("iOb2 is instance of Gen");
正如输出所显示的,这些测试都是成功的。在第 1 个测试中,根据 Gen2 iOb2 进行测试。这个测试成功
了,因为很容易就可以确定
iOb2 是某种类型的 Gen2 对象。通过使用通配符, instanceof 能够检查 iOb2 是否是 Gen2
任意特定类型的对象。接下来根据超类类型 Gen 测试 iOb2 。这个测试也为 true ,因为 iOb2 是某种形式的 Gen
类型, Gen 是超类。在 main() 方法中,接下来的几行代码显示了对 strOb2 进行的相同测试 ( 并且测试结果也相同 )
接下来对
iOb 进行测试, iOb Gen( 超类 ) 类型的对象,通过下面这些代码进行测试:
// See if iOb is an instance of Gen2, which it is not.
if(iOb instanceof Gen2)
System.out.println("iOb is instance of Gen2");
// See if iOb is an instance of Gen, which it is.

if(iOb instanceof Gen)
System.out.println("iOb is instance of Gen");
1 个测试失败了,因为 iOb 不是某种类型的 Gen2 对象。第 2 个测试成功了,因为 iOb 是某种类型的 Gen
对象。
现在,仔细分析下面这些被注释掉的代码行:
// The following can't be compiled because
// generic type info does not exist at run time.
// if(iOb2 instanceof Gen2)
// System.out.println("iOb2 is instance of Gen2");
正如注释所说明的,这些代码行不能被编译,因为它们试图将 iOb2 与特定类型的 Gen2 进行比较,在这个例子
中是与
Gen2 进行比较。请记住,在运行时不能使用泛型类型信息。所以, instanceof 无法知道 iOb2 是否是
Gen2 类型的实例。
14.10.4 强制转换
只有当两个泛型类实例的类型相互兼容并且它们的类型参数也相同时,才能将其中的一个实例转换为另一个实
例。例如,对于前面的程序,下面这个转换是合法的:
(Gen) iOb2 // legal
因为 iOb2 Gen 类型的实例。但是,下面这个转换:
(Gen) iOb2 // illegal
不是合法的,因为 iOb2 不是 Gen 类型的实例。
14.10.5 重写泛型类的方法
可以像重写其他任何方法那样重写泛型类的方法。例如,分析下面这个程序,该程序重写了 getob() 方法:
// Overriding a generic method in a generic class.
class Gen {
T ob; // declare an object of type T
// Pass the constructor a reference to
// an object of type T.
Gen(T o) {
ob = o;
}
// Return ob.
T getob() {
System.out.print("Gen's getob(): " );
return ob;
}
}
// A subclass of Gen that overrides getob().
class Gen2 extends Gen {
Gen2(T o) {
super(o);
}
// Override getob().
T getob() {
System.out.print("Gen2's getob(): ");
return ob;
}
}

// Demonstrate generic method override.
class OverrideDemo {
public static void main(String args[]) {
// Create a Gen object for Integers.
Gen iOb = new Gen(88);
// Create a Gen2 object for Integers.
Gen2 iOb2 = new Gen2(99);
// Create a Gen2 object for Strings.
Gen2 strOb2 = new Gen2 ("Generics Test");
System.out.println(iOb.getob());
System.out.println(iOb2.getob());
System.out.println(strOb2.getob());
}
}
输出如下所示:
Gen's getob(): 88
Gen2's getob(): 99
Gen2's getob(): Generics Test
正如输出所证实的,为 Gen2 类型的对象调用了重写版本的 getob() 方法,但是为 Gen 类型的对象调用了超类中
的版本。
14.11 泛型的类型推断
JDK 7 开始,可以缩短用于创建泛型类实例的语法。首先,分析下面的泛型类:
class MyClass {
T ob1;
V ob2;
MyClass(T o1, V o2) {
ob1 = o1;
ob2 = o2;
}
// ...
}
JDK 7 之前,为了创建 MyClass 的实例,需要使用类似于下面的语句:
MyClass mcOb =
new MyClass(98, "A String");
在此,类型参数 (Integer String) 被指定了两次:第 1 次是在声明 mcOb 时指定的,第 2 次是当使用 new 创建
MyClass 实例时指定的。自从 JDK 5 引入泛型以来,这是 JDK 7 以前所有版本所要求的形式。尽管这种形式本身没
有任何错误,但是相对于需要来说有些烦琐。在
new 子句中,类型参数的类型可以立即根据 mcOb 的类型推断出;
所以,实际上不需要第
2 次指定。为了应对这类情况, JDK 7 增加了避免第 2 次指定类型参数的语法元素。
现在,可以重写前面的声明,如下所示:
MyClass mcOb = new MyClass<>(98, "A String");
注意,实例创建部分简单地使用 <> ,这是一个空的类型参数列表,这被称为菱形运算符。它告诉编译器,需要
推断
new 表达式中构造函数所需要的类型参数。这种类型推断语法的主要优势是缩短了有时相当长的声明语句。
前面的声明可以一般化。当使用类型推断时,用于泛型引用和实例创建的声明语法具有如下所示的一般形式:
class-name < type-arg-list > var-name = new class-name <>( cons-arg-list );
在此, new 子句中构造函数的类型参数列表是空的。
也可以为参数传递应用类型推断。例如,如果为
MyClass 添加下面的方法:
boolean isSame(MyClass o) {
if(ob1 == o.ob1 && ob2 == o.ob2) return true;
else return false;
}
那么下面的调用是合法的:
if(mcOb.isSame(new MyClass<>(1, "test"))) System.out.println("Same");
在这个例子中,可以推断传递给 isSame() 方法的类型参数。
对于本书中的大部分例子来说,当声明泛型类实例时将继续使用完整的语法。这样的话,这些例子就可以用于
任何支持泛型的
Java 编译器。使用完整的长语法也可以更清晰地表明正在创建什么内容,对于在本书中显示的示
例代码这很重要。但是在你自己的代码中,使用类型推断语法可以简化声明。
14.12 擦除
通常,不必知道 Java 编译器将源代码转换为对象代码的细节。但是对于泛型而言,大致了解这个过程是很重
要的, 因为这揭示了泛型特性的工作原理——以及为什么它们的行为有时有点令人惊奇。为此, 接下来简要讨论
Java
实现泛型的原理。
影响泛型以何种方式添加到
Java 中的一个重要约束是:需要与以前的 Java 版本兼容。简单地说,泛型代码必
须能够与以前的非泛型代码相兼容。因此,对
Java 语言的语法或 JVM 所做的任何修改必须避免破坏以前的代码。
为了满足这条约束,
Java 使用擦除实现泛型。
一般而言,擦除的工作原理如下:编译
Java 代码时,所有泛型信息被移除 ( 擦除 ) 。这意味着使用它们的界定类
型替换类型参数,如果没有显式地指定界定类型,就使用
Object ,然后应用适当的类型转换 ( 根据类型参数而定 )
以保持与类型参数所指定类型的兼容性。编译器也会强制实现这种类型兼容性。使用这种方式实现泛型,意味着在
运行时没有类型参数。它们只是一种源代码机制。
桥接方法
编译器偶尔需要为类添加桥接方法 (bridge method) ,用于处理如下情形:子类中重写方法的类型擦除不能生成
与超类中方法相同的擦除。对于这种情况,会生成使用超类类型擦除的方法,并且这个方法调用具有由子类指定的
类型擦除的方法。当然,桥接方法只会在字节码级别发生,你不会看到,也不能使用。
尽管通常不需要关心桥接方法,但是查看生成桥接方法的情形还是有意义的。分析下面的程序:
// A situation that creates a bridge method.
class Gen {
T ob; // declare an object of type T
// Pass the constructor a reference to
// an object of type T.
Gen(T o) {
ob = o;
}
// Return ob.
T getob() {
return ob;
}
}
// A subclass of Gen.
class Gen2 extends Gen {
Gen2(String o) {
super(o);
}
// A String-specific override of getob().

String getob() {
System.out.print("You called String getob(): ");
return ob;
}
}
// Demonstrate a situation that requires a bridge method.
class BridgeDemo {
public static void main(String args[]) {
// Create a Gen2 object for Strings.
Gen2 strOb2 = new Gen2("Generics Test");
System.out.println(strOb2.getob());
}
}
在这个程序中,子类 Gen2 扩展了 Gen ,但是使用了特定于 String Gen 版本,就像声明显示的那样:
class Gen2 extends Gen {
此外,在 Gen2 中,对 getob() 方法进行了重写,指定 String 作为返回类型:
// A String-specific override of getob().
String getob() {
System.out.print("You called String getob(): ");
return ob;
}
所有这些都是可以接受的。唯一的麻烦是由类型擦除引起的,本来是期望以下形式的 getob() 方法:
Object getob() { // ...
为了处理这个问题,编译器生成一个桥接方法,这个桥接方法使用调用 String 版本的那个签名。因此,如果检
查由
javap Gen2 生成的类文件,就会看到以下方法:
class Gen2 extends Gen {
Gen2(java.lang.String);
java.lang.String getob();
java.lang.Object getob(); // bridge method
}
可以看出,已经包含了桥接方法 ( 注释是笔者添加的,而不是 javap 添加的,并且根据所用的 Java 版本,看到
的精确输出可能有所不同
)
对于这个示例,还有最后一点需要说明。注意两个
getob() 方法之间唯一的区别是它们的返回类型。在正常情况
下,这会导致错误,但是因为这种情况不是在你编写的源代码中发生的,所以不会引起问题,并且
JVM 会正确地
进行处理。
14.13 模糊性错误
泛型的引入,增加了引起一种新类型错误——模糊性错误的可能,必须注意防范。当擦除导致两个看起来不同
的泛型声明,在擦除之后变成相同的类型而导致冲突时,就会发生模糊性错误。下面是一个涉及方法重载的例子:
// Ambiguity caused by erasure on
// overloaded methods.
class MyGenClass {
T ob1;
V ob2;
// ...
// These two overloaded methods are ambiguous
// and will not compile.
void set(T o) {
ob1 = o;

}
void set(V o) {
ob2 = o;
}
}
注意 MyGenClass 声明了两个泛型类型参数: T V 。在 MyGenClass 中,试图根据类型参数 T V 重载 set()
方法。这看起来是合理的,因为 T V 表面上是不同的类型。但是,在此存在两个模糊性问题。
首先,当编写
MyGenClass 时, T V 实际上不必是不同的类型。例如,像下面这样构造 MyGenClass 对象 (
原则上
) 是完全正确的:
MyGenClass obj = new MyGenClass()
对于这种情况, T V 都将被 String 替换。这使得 set() 方法的两个版本完全相同,这当然是错误。
第二个问题,也是更基础的问题,对
set() 方法的类型擦除会使两个版本都变为如下形式:
void set(Object o) { // ...
因此,在 MyGenClass 中试图重载 set() 方法本身就是含糊不清的。
修复模糊性错误很棘手。例如,如果知道
V 总是某种 Number 类型,那么你可能会尝试像下面这样改写其声明,
从而修复
MyGenClass
class MyGenClass { // almost OK!
上述修改使 MyGenClass 可以通过编译,并且甚至可以像下面这样实例化对象:
MyGenClass x = new MyGenClass();
这种修改方式是可行的,因为 Java 能够准确地确定调用哪个方法。但是,当你试图使用下面这行代码时,就
会出现模糊性问题:
MyGenClass x = new MyGenClass();
对于这种情况, T V 都是 Number ,将调用哪个版本的 set() 方法呢?现在,对 set() 方法的调用是模糊不清的。
坦白而言,在前面的例子中,使用两个独立的方法名会更好些,而不是试图重载
set() 方法。通常,模糊性错误
的解决方案涉及调整代码结构,因为模糊性通常意味着在设计中存在概念性错误。
14.14 使用泛型的一些限制
使用泛型时有几个限制需要牢记。这些限制涉及类型参数的创建对象、静态成员、异常以及数组。下面逐一分
析这些限制。
14.14.1 不能实例化类型参数
不能创建类型参数的实例。例如,分析下面这个类:
// Can't create an instance of T.
class Gen {
T ob;
Gen() {
ob = new T(); // Illegal!!!
}
}
在此,试图创建 T 的实例,这是非法的。原因很容易理解:编译器不知道创建哪种类型的对象。 T 只是一个占
位符。
14.14.2 对静态成员的一些限制
静态成员不能使用在类中声明的类型参数。例如,下面这个类中的两个静态成员都是非法的:
class Wrong {
// Wrong, no static variables of type T.
static T ob;
// Wrong, no static method can use T.
static T getob() {
return ob;
}
}
尽管不能声明某些静态成员,它们使用由类声明的类型参数,但是可以声明静态的泛型方法,这种方法可以定
义它们自己的类型参数,就像在本章前面所做的那样。
14.14.3 对泛型数组的一些限制
对数组有两条重要的泛型限制。首先,不能实例化元素类型为类型参数的数组。其次,不能创建特定类型的泛
型引用数组。下面的简短程序演示了这两种情况:
// Generics and arrays.
class Gen {
T ob;
T vals[]; // OK
Gen(T o, T[] nums) {
ob = o;
// This statement is illegal.
// vals = new T[10]; // can't create an array of T
// But, this statement is OK.
vals = nums; // OK to assign reference to existent array
}
}
class GenArrays {
public static void main(String args[]) {
Integer n[] = { 1, 2, 3, 4, 5 };
Gen iOb = new Gen(50, n);
// Can't create an array of type-specific generic references.
// Gen gens[] = new Gen[10]; // Wrong!
// This is OK.
Gen gens[] = new Gen[10]; // OK
}
}
正如该程序所显示的,声明指向类型 T 的数组的引用是合法的,就像下面这行代码这样:
T vals[]; // OK
但是,不能实例化 T 的数组,就像注释掉的这行代码试图所做的那样:
// vals = new T[10]; // can't create an array of T
不能创建 T 的数组,原因是编译器无法知道实际创建什么类型的数组。
然而,当创建泛型类的对象时,可以向
Gen() 方法传递对类型兼容的数组的引用,并将引用赋给 vals ,就像程
序在下面这行代码中所做的那样:
vals = nums; // OK to assign reference to existent array
这行代码可以工作,因为传递给 Gen 的数组的类型是已知的,和创建对象时 T 的类型相同。
main() 方法中,注意不能声明指向特定泛型类型的引用的数组。也就是说,下面这行代码不能编译:
// Gen gens[] = new Gen[10]; // Wrong!
不过,如果使用通配符的话,可以创建指向泛型类型的引用的数组,如下所示:
Gen gens[] = new Gen[10]; // OK
相对于使用原始类型数组,这种方式更好些,因为至少仍然会强制进行某些类型检查。
14.14.4 对泛型异常的限制
泛型类不能扩展 Throwable ,这意味着不能创建泛型异常类。

购买地址:

http://product.dangdang.com/25344282.html


请使用浏览器的分享功能分享到微信等