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
號
責任編輯:
王 軍 於 平
封面設計:
牛豔敏
版式設計:
思創景點
責任校對:
成鳳進
責任印製:
楊 豔
出版發行:
清華大學出版社
網 址
:
,
地 址
:北京清華大學學研大廈
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
獲得了美國伊利諾伊大學的本科和研究生學位。他的個人網站是
。
技術編輯簡介
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<V>
的例子
······
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> {
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<Integer> iOb;
// Create a Gen<Integer> object and assign its
// reference to iOb. Notice the use of autoboxing
// to encapsulate the value 88 within an Integer object.
iOb = new Gen<Integer>(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<String> strOb = new Gen<String> ("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> {
其中,
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<Integer> iOb;
請仔細分析這個宣告。首先,注意型別
Integer
是在
Gen
後面的尖括號中指定的。在此,
Integer
是傳遞給
Gen
的型別引數。這有效地建立了
Gen
的一個版本,在該版本中,對
T
的所有引用都被轉換為對
Integer
的引用。因此
對於這個宣告,
ob
是
Integer
型別,並且
getob()
方法的返回型別是
Integer
。
在繼續之前,必須先說明的是,
Java
編譯器實際上沒有建立不同版本的
Gen
類,或者說沒有建立任何其他泛型
類。儘管那樣認為是有幫助的,但是實際情況並非如此。相反,編譯器移除了所有泛型型別資訊,進行必需的型別
轉換,從而使程式碼的行為好像是建立了特定版本的
Gen
類一樣。因此,在程式中實際上只有一個版本的
Gen
類。
移除泛型型別資訊的過程被稱為擦除
(erasure)
,在本章後面還會繼續介紹該主題。
下一行程式碼將一個引用
(
指向
Integer
版本的
Gen
類的一個例項
)
賦給
iOb
:
iOb = new Gen<Integer>(88);
注意在呼叫
Gen
建構函式時,仍然指定了型別引數
Integer
。這是必要的,因為將為其賦值的物件
(
在此為
iOb)
的型別是
Gen<Integer>
。因此,
new
返回的引用也必須是
Gen<Integer>
型別。如果不是的話,就會生成編譯時錯誤。
例如,下面的賦值操作會導致編譯時錯誤:
iOb = new Gen<Double>(88.0); // Error!
因為
iOb
是
Gen<Integer>
型別, 所以不能引用
Gen<Double>
型別的物件。 這種型別檢查是泛型的主要優點之一,
因為可以確保型別安全。
注意:
在本章後面可以看到,可以縮短建立泛型類的例項的語法。為了清晰起見,現在使用完整語法。
244
第Ⅰ部分 Java 語言
正如程式中的註釋所表明的,下面的賦值語句:
iOb = new Gen<Integer>(88);
使用自動裝箱特性封裝數值
88
,將這個
int
型數值轉換成
Integer
物件。這可以工作,因為
Gen<Integer>
建立了
一個使用
Integer
引數的建構函式。因為期望
Integer
型別的物件,所以
Java
會自動將數值
88
裝箱到
Integer
物件中。
當然,也可以像下面這樣顯式地編寫這條賦值語句:
iOb = new Gen<Integer>(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<String>
型別的一個物件:
Gen<String> strOb = new Gen<String>("Generics Test");
因為型別引數是
String
,所以使用
String
替換
Gen
中的
T
。
(
從概念上講
)
這會建立
Gen
的
String
版本,就像程
序中的剩餘程式碼所演示的那樣。
14.2.1
泛型只使用引用型別
當宣告泛型類的例項時,傳遞的型別引數必須是引用型別。不能使用基本型別,如
int
或
char
。例如,對於
Gen
,
可以將任何類傳遞給
T
,但是不能將基本型別傳遞給型別引數
T
。所以,下面的宣告是非法的:
Gen<int> intOb = new Gen<int>(53); // Error, can't use primitive type
當然,不能使用基本型別並不是一個嚴格的限制,因為可以使用型別封裝器封裝基本型別
(
就像前面的例子那
樣
)
。此外,
Java
的自動裝箱和拆箱機制使得型別封裝器的使用是透明的。
14.2.2
基於不同型別引數的泛型型別是不同的
對特定版本的泛型型別的引用和同一泛型型別的其他版本不是型別相容的,這是關於泛型型別方面更需要理解
的關鍵一點。例如,對於剛才顯示的程式,下面這行程式碼是錯誤的,並且不能透過編譯:
iOb = strOb; // Wrong!
儘管
iOb
和
strOb
都是
Gen<T>
型別,但它們是對不同型別的引用,因為它們的型別引數不同。這是泛型新增
型別安全性以及防止錯誤的方式的一部分。
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, V> {
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<Integer, String> tgObj =
new TwoGen<Integer, String>(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> {
在此指定了兩個型別引數:
T
和
V
,使用逗號將它們隔開。建立物件時必須為
TwoGen
傳遞兩個型別引數,如
下所示:
TwoGen<Integer, String> tgObj =
new TwoGen<Integer, String>(88, "Generics");
在此,
Integer
替換
T
,
String
替換
V
。
在這個例子中,儘管兩個型別引數是不同的,但是可以將兩個型別引數設定為相同的型別。例如,下面這行代
碼是合法的:
TwoGen<String, String> x = new TwoGen<String, String> ("A", "B");
在此,
T
和
V
都是
String
型別。當然,如果型別引數總是相同的,就不必使用兩個型別引數了。
14.4
泛型類的一般形式
在前面例子中展示的泛型語法可以一般化。下面是宣告泛型類的語法:
class
class-name
<
type-param-list
> { // …
下面是宣告指向泛型類的引用的語法:
class-name<type-arg-list > var-name
=
new
class-name<type-arg-list>
(
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> {
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 extends Number> {
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<Integer> iob = new Stats<Integer>(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<Double> dob = new Stats<Double>(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<String> strob = new Stats<String>(strs);
// double x = strob.average();
// System.out.println("strob average is " + v);
}
}
該程式的輸出如下所示:
Average is 3.0
Average is 3.3
注意現在使用下面這行程式碼宣告
Stats
的方式:
class Stats<T extends Number> {
現在使用
Number
對型別
T
進行了限定,
Java
編譯器知道所有
T
型別的物件都可以呼叫
doubleValue()
方法,因
為該方法是由
Number
宣告的。就其本身來說,這是一個主要優勢。除此之外,還有另外一個好處:限制
T
的範圍
也會阻止建立非數值型別的
Stats
物件。例如,如果嘗試移除對程式底部幾行程式碼的註釋,然後重新編譯,就會收
到編譯時錯誤,因為
String
不是
Number
的子類。
除了使用類作為邊界之外,也可以使用介面。實際上,可以指定多個介面作為邊界。此外,邊界可以包含一個
類和一個或多個介面。對於這種情況,必須首先指定類型別。如果邊界包含介面型別,那麼只有實現了那種介面的
型別引數是合法的。當指定具有一個類和一個或多個介面的邊界時,使用
&
運算子連線它們。例如:
class Gen<T extends MyClass & MyInterface> { // ...
在此, 透過類
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<Integer> iob = new Stats<Integer>(inums);
Stats<Double> dob = new Stats<Double>(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<T> ob) {
if(average() == ob.average())
return true;
return false;
}
這種嘗試存在的問題是:只有當其他
Stats
物件的型別和呼叫物件的型別相同時才能工作。例如,如果呼叫對
象是
Stats<Integer>
型別,那麼引數
ob
也必須是
Stats<Integer>
型別。不能用於比較
Stats<Double>
型別物件的平均值
和
Stats<Short>
型別物件的平均值。所以,這種方式的適用範圍很窄,無法得到通用的
(
即泛型化的
)
解決方案。
為了建立泛型化的
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 extends Number> {
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<Integer> iob = new Stats<Integer>(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<Double> dob = new Stats<Double>(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<Float> fob = new Stats<Float>(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 extends TwoD> {
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<TwoD>
物件只有
X
和
Y
座標。所以,如何編寫能夠顯示
Coords<ThreeD>
和
Coords<FourD>
物件的
X
、
Y
和
Z
座標的方法,而又不會阻止該方法使用
Coords<TwoD>
物件呢?答案是使用有界
的萬用字元引數。
有界的萬用字元為型別引數指定上界或下界,從而可以限制方法能夠操作的物件型別。最常用的有界萬用字元是上
界,是使用
extends
子句建立的,具體方式和用於建立有界型別的方式大體相同。
如果物件實際擁有
3
個座標的話,使用有界萬用字元可以很容易建立出顯示
Coords
物件中
X
、
Y
和
Z
座標的方
法。例如下面的
showXYZ()
方法,如果
Coords
物件中儲存的元素的實際型別是
ThreeD(
或派生自
ThreeD)
,那麼
showXYZ()
方法將顯示這些元素的
X
、
Y
和
Z
座標:
static void showXYZ(Coords<? extends ThreeD> 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<ThreeD>
或
Coords<FourD>
型別物件的引用呼叫
showXYZ()
方法,但不能使用
Coords<TwoD>
型別的引用進行呼叫。如果試
圖使用
Coords<TwoD>
引用呼叫
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 extends TwoD> {
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<? extends ThreeD> 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<? extends FourD> 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<TwoD> tdlocs = new Coords<TwoD>(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<FourD> fdlocs = new Coords<FourD>(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()
方法,因為在這兩個方法宣告中的有界通
配符引數對此進行了阻止。為了自己證實這一點,可以嘗試移除註釋符號,然後編譯該程式,你將會收到型別不匹
配的編譯錯誤。
一般來說,要為萬用字元建立上界,可以使用如下所示的萬用字元表示式:
<? extends
superclass
>
其中,
superclass
是作為上界的類的名稱。記住,這是一條包含子句,因為形成上界
(
由
superclass
指定的邊界
)
的類也位於邊界之內。
還可以透過為萬用字元新增一條
super
子句,為萬用字元指定下界。下面是一般形式:
<? 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 <T extends Comparable<T>, 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 <T extends Comparable<T>, V extends T> boolean isIn(T x, V[] y) {
型別引數在方法的返回型別之前宣告。其次,注意
T
擴充套件了
Comparable<T>
。
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.<Integer, Integer>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;
<T extends Number> 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 extends Comparable<T>> {
T min();
T max();
}
// Now, implement MinMax
class MyClass<T extends Comparable<T>> implements MinMax<T> {
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<Integer> iob = new MyClass<Integer>(inums);
MyClass<Character> cob = new MyClass<Character>(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 extends Comparable<T>> {
一般而言,宣告泛型介面的方式與宣告泛型類相同。對於這個例子,型別引數是
T
,它的上界是
Comparable
,
如前所述,這是由
java.lang
定義的介面,指定了比較物件的方式。它的型別引數指定了將進行比較的物件的型別。
接下來,
MyClass
實現了
MinMax
。注意
MyClass
的宣告,如下所示:
class MyClass<T extends Comparable<T>> implements MinMax<T> {
請特別注意
MyClass
宣告型別引數
T
以及將
T
傳遞給
MinMax
的方式。因為
MinMax
需要實現了
Comparable
的型別, 所以實現類
(
在該例中是
MyClass)
必須指定相同的界限。此外, 一旦建立這個界限, 就不需要再在
implements
子句中指定。實際上,那麼做是錯誤的。例如,下面這行程式碼是不正確的,不能透過編譯:
// This is wrong!
class MyClass<T extends Comparable<T>>
implements MinMax<T extends Comparable<T>> {
一旦建立型別引數,就可以不加修改地將之傳遞給介面。
一般而言,如果類實現了泛型介面,那麼類也必須是泛型化的,至少需要帶有將被傳遞給介面的型別引數。例
如,下面對
MyClass
的宣告是錯誤的:
class MyClass implements MinMax<T> { // Wrong!
因為
MyClass
沒有宣告型別引數,所以無法為
MinMax
傳遞型別引數。對於這種情況,識別符號
T
是未知的,編
譯器會報告錯誤。當然,如果類實現了某種具體型別的泛型介面,如下所示:
class MyClass implements MinMax<Integer> { // OK
那麼實現類不需要是泛型化的。
泛型介面具有兩個優勢。首先,不同型別的資料都可以實現它。其次,可以為實現介面的資料型別設定限制條
件
(
即界限
)
。在
MinMax
例子中,只能向
T
傳遞實現了
Comparable
介面的型別。
下面是泛型介面的通用語法:
interface
interface-name<type-param-list>
{ // …
在此,
type-param-list
是由逗號分隔的型別引數列表。當實現泛型介面時,必須指定型別引數,如下所示:
class
class-name
<
type-param-list
>
implements
interface-name<type-arg-list>
{
14.9
原始型別與遺留程式碼
因為
Java
在
JDK 5
之前不支援泛型,所以需要為舊的、在支援泛型之前編寫的程式碼提供一些過渡路徑。在編
寫本書時,仍然有大量在支援泛型之前編寫的遺留程式碼,這些遺留程式碼既要保留功能,又要和泛型相相容。在支援
泛型之前編寫的程式碼必須能夠使用泛型,並且泛型必須能夠使用在支援泛型之前編寫的程式碼。
為了處理泛型過渡,
Java
允許使用泛型類而不提供任何型別引數。這會為類建立原始型別
(raw type)
,這種原始
型別與不使用泛型的遺留程式碼是相容的。使用原始型別的主要缺點是丟失了泛型的型別安全性。
下面是一個使用原始型別的例子:
// Demonstrate a raw type.
class Gen<T> {
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<Integer> iOb = new Gen<Integer>(88);
// Create a Gen object for Strings.
Gen<String> strOb = new Gen<String>("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<String>
型別的引用
)
:
strOb = raw; // OK, but potentially wrong
// String str = strOb.getob(); // run-time error
就這條賦值語句本身而言,語法是正確的,但是存在問題。因為
strOb
是
Gen<String>
型別,所以被假定包含一
個字串。但是,在這條賦值語句之後,
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> {
T ob;
Gen(T o) {
ob = o;
}
// Return ob.
T getob() {
return ob;
}
}
// A subclass of Gen.
class Gen2<T> extends Gen<T> {
Gen2(T o) {
super(o);
}
}
在這個類層次中,
Gen2
擴充套件了泛型類
Gen
。注意下面這行程式碼宣告
Gen2
的方式:
class Gen2<T> extends Gen<T> {
Gen2
指定的型別引數
T
也被傳遞給
extends
子句中的
Gen
,這意味著傳遞給
Gen2
的任何型別也會被傳遞給
Gen
。
例如下面這個宣告:
Gen2<Integer> num = new Gen2<Integer>(100);
會將
Integer
作為型別引數傳遞給
Gen
。因此,對於
Gen2
中
Gen
部分的
ob
來說,其型別將是
Integer
。
還應當注意,除了將
T
傳遞給
Gen
超類外,
Gen2
沒有再使用型別引數
T
。因此,即使泛型超類的子類不必泛
型化,也仍然必須指定泛型超類所需要的型別引數。
當然,如果需要的話,子類可以自由新增自己的型別引數。例如,下面是前面類層次的另一個版本,此處的
Gen2
新增了它自己的型別引數:
// A subclass can add its own type parameters.
class Gen<T> {
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<T, V> extends Gen<T> {
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<String, Integer> x =
new Gen2<String, Integer>("Value is: ", 99);
System.out.print(x.getob());
System.out.println(x.getob2());
}
}
注意該版本中
Gen2
的宣告,如下所示:
class Gen2<T, V> extends Gen<T> {
在此,
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<T> 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<String> w = new Gen<String>("Hello", 47);
System.out.print(w.getob() + " ");
System.out.println(w.getnum());
}
}
該程式的輸出如下所示:
Hello 47
在該程式中,注意在下面的宣告中
Gen
繼承
NonGen
的方式:
class Gen<T> 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> {
T ob;
Gen(T o) {
ob = o;
}
// Return ob.
T getob() {
return ob;
}
}
// A subclass of Gen.
class Gen2<T> extends Gen<T> {
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<Integer> iOb = new Gen<Integer>(88);
// Create a Gen2 object for Integers.
Gen2<Integer> iOb2 = new Gen2<Integer>(99);
// Create a Gen2 object for Strings.
Gen2<String> strOb2 = new Gen2<String>("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<Integer>)
// System.out.println("iOb2 is instance of Gen2<Integer>");
}
}
該程式的輸出如下所示:
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<Integer>
型別的物件。第
2
個物件是
iOb2
,它是
Gen2<Integer>
型別的物件。最後一個物件是
strOb2
,
它是
Gen2<String>
型別的物件。
然後,該程式針對
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<Integer>(
超類
)
型別的物件,透過下面這些程式碼進行測試:
// 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<Integer>)
// System.out.println("iOb2 is instance of Gen2<Integer>");
正如註釋所說明的,這些程式碼行不能被編譯,因為它們試圖將
iOb2
與特定型別的
Gen2
進行比較,在這個例子
中是與
Gen2<Integer>
進行比較。請記住,在執行時不能使用泛型型別資訊。所以,
instanceof
無法知道
iOb2
是否是
Gen2<Integer>
型別的例項。
14.10.4
強制轉換
只有當兩個泛型類例項的型別相互相容並且它們的型別引數也相同時,才能將其中的一個例項轉換為另一個實
例。例如,對於前面的程式,下面這個轉換是合法的:
(Gen<Integer>) iOb2 // legal
因為
iOb2
是
Gen<Integer>
型別的例項。但是,下面這個轉換:
(Gen<Long>) iOb2 // illegal
不是合法的,因為
iOb2
不是
Gen<Long>
型別的例項。
14.10.5
重寫泛型類的方法
可以像重寫其他任何方法那樣重寫泛型類的方法。例如,分析下面這個程式,該程式重寫了
getob()
方法:
// Overriding a generic method in a generic class.
class Gen<T> {
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<T> extends Gen<T> {
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<Integer> iOb = new Gen<Integer>(88);
// Create a Gen2 object for Integers.
Gen2<Integer> iOb2 = new Gen2<Integer>(99);
// Create a Gen2 object for Strings.
Gen2<String> strOb2 = new Gen2<String> ("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, V> {
T ob1;
V ob2;
MyClass(T o1, V o2) {
ob1 = o1;
ob2 = o2;
}
// ...
}
在
JDK 7
之前,為了建立
MyClass
的例項,需要使用類似於下面的語句:
MyClass<Integer, String> mcOb =
new MyClass<Integer, String>(98, "A String");
在此,型別引數
(Integer
和
String)
被指定了兩次:第
1
次是在宣告
mcOb
時指定的,第
2
次是當使用
new
建立
MyClass
例項時指定的。自從
JDK 5
引入泛型以來,這是
JDK 7
以前所有版本所要求的形式。儘管這種形式本身沒
有任何錯誤,但是相對於需要來說有些煩瑣。在
new
子句中,型別引數的型別可以立即根據
mcOb
的型別推斷出;
所以,實際上不需要第
2
次指定。為了應對這類情況,
JDK 7
增加了避免第
2
次指定型別引數的語法元素。
現在,可以重寫前面的宣告,如下所示:
MyClass<Integer, String> 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<T, V> 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> {
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<String> {
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<String> {
此外,在
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<java.lang.String> {
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, V> {
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<String, String> obj = new MyGenClass<String, String>()
對於這種情況,
T
和
V
都將被
String
替換。這使得
set()
方法的兩個版本完全相同,這當然是錯誤。
第二個問題,也是更基礎的問題,對
set()
方法的型別擦除會使兩個版本都變為如下形式:
void set(Object o) { // ...
因此,在
MyGenClass
中試圖過載
set()
方法本身就是含糊不清的。
修復模糊性錯誤很棘手。例如,如果知道
V
總是某種
Number
型別,那麼你可能會嘗試像下面這樣改寫其宣告,
從而修復
MyGenClass
:
class MyGenClass<T, V extends Number> { // almost OK!
上述修改使
MyGenClass
可以透過編譯,並且甚至可以像下面這樣例項化物件:
MyGenClass<String, Number> x = new MyGenClass<String, Number>();
這種修改方式是可行的,因為
Java
能夠準確地確定呼叫哪個方法。但是,當你試圖使用下面這行程式碼時,就
會出現模糊性問題:
MyGenClass<Number, Number> x = new MyGenClass<Number, Number>();
對於這種情況,
T
和
V
都是
Number
,將呼叫哪個版本的
set()
方法呢?現在,對
set()
方法的呼叫是模糊不清的。
坦白而言,在前面的例子中,使用兩個獨立的方法名會更好些,而不是試圖過載
set()
方法。通常,模糊性錯誤
的解決方案涉及調整程式碼結構,因為模糊性通常意味著在設計中存在概念性錯誤。
14.14
使用泛型的一些限制
使用泛型時有幾個限制需要牢記。這些限制涉及型別引數的建立物件、靜態成員、異常以及陣列。下面逐一分
析這些限制。
14.14.1
不能例項化型別引數
不能建立型別引數的例項。例如,分析下面這個類:
// Can't create an instance of T.
class Gen<T> {
T ob;
Gen() {
ob = new T(); // Illegal!!!
}
}
在此,試圖建立
T
的例項,這是非法的。原因很容易理解:編譯器不知道建立哪種型別的物件。
T
只是一個佔
位符。
14.14.2
對靜態成員的一些限制
靜態成員不能使用在類中宣告的型別引數。例如,下面這個類中的兩個靜態成員都是非法的:
class Wrong<T> {
// 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 extends Number> {
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<Integer> iOb = new Gen<Integer>(50, n);
// Can't create an array of type-specific generic references.
// Gen<Integer> gens[] = new Gen<Integer>[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<Integer> gens[] = new Gen<Integer>[10]; // Wrong!
不過,如果使用萬用字元的話,可以建立指向泛型型別的引用的陣列,如下所示:
Gen<?> gens[] = new Gen<?>[10]; // OK
相對於使用原始型別陣列,這種方式更好些,因為至少仍然會強制進行某些型別檢查。
14.14.4
對泛型異常的限制
泛型類不能擴充套件
Throwable
,這意味著不能建立泛型異常類。
購買地址:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/26421423/viewspace-2216573/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- POCO庫中文程式設計參考指南(9)Poco::Net::DNS程式設計DNS
- AIX5.2命令參考大全中文版AI
- JavaScript完全參考手冊第3版pdfJavaScript
- Oracle官方參考資料Oracle
- Java程式設計書籍大全Java程式設計
- 聯發科MT6763處理器最新參考設計資料大全
- 《Java語言程式設計(基礎篇)(原書第10版)》第2~4章部分程式設計練習題程式碼Java程式設計
- 《父與子的程式設計之旅(第3版)》第9章習題答案程式設計
- POCO庫中文程式設計參考指南(10)如何使用TCPServer框架?程式設計TCPServer框架
- Global.asa程式設計完全參考手冊程式設計
- Java9系列第8篇-Module模組化程式設計Java程式設計
- JavaScript 物件與陣列參考大全JavaScript物件陣列
- 降級MySQL(參考MySQL官方文件)MySql
- POCO庫中文程式設計參考指南(8)豐富的Socket程式設計程式設計
- 9i Dataguard (Standby) 引數設定參考
- ASP.NET中Cookie程式設計簡明參考ASP.NETCookie程式設計
- POCO庫中文程式設計參考指南(1)總覽程式設計
- 國外APP介面設計參考APP
- Java Web程式開發參考手冊JavaWeb
- 每週薦書——《程式碼大全第2版》
- POCO庫中文程式設計參考指南(7)Poco::Net::DatagramSocket程式設計
- POCO庫中文程式設計參考指南(6)Poco::Timestamp程式設計
- POCO庫中文程式設計參考指南(3)Poco::Net::Socket程式設計
- 系統設計面試參考-設計Spotify系統面試
- 100道JAVA面試題+JAVA面試題參考答案Java面試題
- 《JAVA程式設計案例教程(第2版)》pdf 附下載連結Java程式設計
- 美食類網頁設計版式參考網頁
- 《Linux命令列與shell指令碼程式設計大全 第3版》Linux命令列---46Linux命令列指令碼程式設計
- 20145320《Java程式設計》第9周學習總結Java程式設計
- 轉職成為TypeScript程式設計師的參考手冊TypeScript程式設計師
- POCO庫中文程式設計參考指南(11)如何使用Reactor框架?程式設計React框架
- 深入HTML5程式設計(第2版)HTML程式設計
- 後臺介面設計之表格設計規範參考
- JAVA 程式設計思想 第13章 字串Java程式設計字串
- Java程式設計思想第四版隨書原始碼官方下載方法Java程式設計原始碼
- 模擬考試參考程式碼
- 《JavaScript高階程式設計》第3版與第2版有何差異?JavaScript程式設計
- 程式程式設計3 - UNIX高階環境程式設計第9章讀書筆記程式設計筆記