天天看點

Java核心技術 卷Ⅰ 基礎知識(原書第10版)

Java核心技術 卷Ⅰ 基礎知識(原書第10版)

java核心技術系列

java核心技術

卷Ⅰ 基礎知識

(原書第10版)

core java volume i—fundamentals (10th edition)

[美] 凱s.霍斯特曼(cay s. horstmann) 著

周立新 陳 波 葉乃文 邝勁筠 杜永萍 譯

圖書在版編目(cip)資料

java核心技術 卷Ⅰ 基礎知識(原書第10版) / (美)凱s. 霍斯特曼(cay s. horstmann)著;周立新等譯. —北京:機械工業出版社,2016.8

(java核心技術系列)

書名原文:core java volume i—fundamentals (tenth edition)

isbn 978-7-111-54742-6

i. j… ii. ①凱… ②周… iii. java語言-程式設計 iv. tp312.8

中國版本圖書館cip資料核字(2016)第211440号

本書版權登記号:圖字:01-2016-5145

authorized translation from the english language edition, entitled core java volume i—fundamentals (tenth edition),9780134177304 by cay s. horstmann, published by pearson education, inc., copyright ? 2016 oracle and /or its aff?iliates.

all rights reserved. no part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from pearson education, inc.

chinese simplif?ied language edition published by pearson education asia ltd., and china machine press copyright ? 2016.

本書中文簡體字版由pearson education(培生教育出版集團)授權機械工業出版社在中華人民共和國境内(不包括香港、澳門特别行政區及台灣地區)獨家出版發行。未經出版者書面許可,不得以任何方式抄襲、複制或節錄本書中的任何部分。

本書封底貼有pearson education(培生教育出版集團)雷射防僞标簽,無标簽者不得銷售。

java核心技術 卷Ⅰ 基礎知識(原書第10版)

出版發行:機械工業出版社(北京市西城區百萬莊大街22号 郵政編碼:100037)

責任編輯:關 敏 責任校對:董紀麗

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

開  本:186mm×240mm 1/16 印  張:45.5

書  号:isbn 978-7-111-54742-6 定  價:119.00元

凡購本書,如有缺頁、倒頁、脫頁,由本社發行部調換

客服熱線:(010)88379426 88361066 投稿熱線:(010)88379604

購書熱線:(010)68326294 88379649 68995259 讀者信箱:[email protected]

版權所有 ? 侵權必究

封底無防僞标均為盜版

本書法律顧問:北京大成律師事務所 韓光/鄒曉東

譯 者 序

書寫java傳奇的sun microsystems曾經堪稱“日不落”帝國,但伺服器市場的萎縮卻讓這個聲名赫赫的龐大帝國從蓬勃走向落寞。在2009年被oracle公司收購之後,sun公司逐漸淡出了人們的視線,而與此同時,我們也在很長一段時間内沒能看到java當初活躍的身影。

java就這樣退出曆史舞台了嗎?當然不是!從sun公司2006年12月釋出java 6後,經過5年多的不懈努力,終于在2011年7月底釋出了java 7正式版。3年後,被冠名為“跳票王”的oracle終于釋出了java 8的正式版,但對于很多開發者來說,java 8卻比java 7來得更漫長一些。主要是因為oracle原本計劃在2013年釋出正式版java 8,卻因受困于安全性的問題經過了兩次“跳票”。無論如何,如今java 8來了,全新“革命”而不隻是“進化”的功能将會讓無數開發者動容。

值得一提的是,伴随着java的成長,《java核心技術》也從第1版到第9版一路走來,得到了廣大java程式設計人員的青睐,成為一本暢銷不衰的java經典圖書。經過幾年的蟄伏,針對java 8打造的《java核心技術》第10版終于問世,這一版有了大幅的修訂和更新,以反映java 8增補、删改的内容。它将續寫從前的輝煌,使人們能及時跟上java前進的腳步。

本書由周立新、陳波等主譯,程芳、劉曉兵、張練達、陳峰、江健、謝連寶、張雷生、楊健康、張瑩參與了全書的修改整理,并完善了關鍵部分的翻譯。全體人員共同完成了本書的翻譯工作。特别需要說明的是,按照出版社的要求,這一版的翻譯在老版本基礎上完成,是以尤其感謝之前版本的譯者葉乃文、邝勁筠和杜永萍,他們的辛勤工作為新版本的翻譯奠定了很好的基礎。

書中文字與内容力求忠實原著,不過由于譯者水準有限,譯文肯定有不當之處,敬請批評指正。

譯者

2016年6月于北京

前  言

緻讀者

1995年年底,java語言在internet舞台一亮相便名聲大噪。其原因在于它将有望成為連接配接使用者與資訊的萬能膠,而不論這些資訊來自web伺服器、資料庫、資訊提供商,還是任何其他管道。事實上,就發展前景而言,java的地位是獨一無二的。它是一種完全可信賴的程式設計語言,得到了除微軟之外的所有廠家的認可。其固有的可靠性與安全性不僅令java程式員放心,也令使用java程式的使用者放心。java内建了對網絡程式設計、資料庫連接配接、多線程等進階程式設計任務的支援。

1995年以來,已經釋出了java開發工具包(java development kit)的9個主要版本。在過去的20年中,應用程式程式設計接口(api)已經從200個類擴充到超過4000個類。現在這些api覆寫了使用者界面建構、資料庫管理、國際化、安全性以及xml處理等各個不同的領域。

本書是《java核心技術》第10版的卷Ⅰ。自《java核心技術》出版以來,每個新版本都盡可能快地跟上java開發工具箱發展的步伐,而且每一版都重新改寫了部分内容,以便适應java的最新特性。在這一版中,已經反映了java 标準版(java se 8)的特性。

與前幾版一樣,本版仍然将讀者群定位在那些打算将java應用到實際工程項目中的程式設計人員。本書假設讀者是一名具有程式設計語言(除java之外)堅實背景知識的程式設計人員,并且不希望書中充斥着玩具式的示例(諸如,烤面包機、動物園的動物或神經質的跳動文本)。這些内容絕對不會在本書中出現。本書的目标是讓讀者充分了解書中介紹的java語言及java類庫的相關特性,而不會産生任何誤解。

在本書中,我們選用大量的示例代碼示範所讨論的每一個語言特性和類庫特性。我們有意使用簡單的示例程式以突出重點,然而,其中的大部分既不是赝品也沒有偷工減料。它們将成為讀者自己編寫代碼的良好開端。

我們假定讀者願意(甚至渴望)學習java提供的所有進階特性。例如,本書将詳細介紹下列内容:

面向對象程式設計

反射與代理

接口與内部類

異常處理

泛型程式設計

集合架構

事件監聽器模型

使用swing ui工具箱進行圖形使用者界面設計

并行操作

随着java類庫的爆炸式增長,一本書無法涵蓋程式員需要了解的所有java特性。是以,我們決定将本書分為兩卷。卷i(本書)集中介紹java語言的基本概念以及圖形使用者界面程式設計的基礎知識。卷Ⅱ(進階特性)涉及企業特性以及進階的使用者界面程式設計,其中詳細讨論下列内容:

流api

檔案處理與正規表達式

資料庫

xml處理

注釋

國際化

網絡程式設計

進階gui元件

進階圖形

原生方法

本書中難免出現錯誤和不準确之處。我們很想知道這些錯誤,當然,也希望同一個問題隻被告知一次。我們在網頁http://horstmann.com/corejava中以清單的形式給出了常見的問題、bug修正和解決方法。在勘誤頁(建議先閱讀一遍)最後附有用來報告bug并提出修改意見的表單。如果我們不能回答每一個問題或沒有及時回複,請不要失望。我們會認真地閱讀所有的來信,感謝您的建議使本書後續的版本更清晰、更有指導價值。

關于本書

第1章概述java與其他程式設計語言不同的性能。解釋這種語言的設計初衷,以及在哪些方面達到了預期的效果。然後,簡要叙述java誕生和發展的曆史。

第2章詳細論述如何下載下傳和安裝jdk以及本書的程式示例。然後,通過編譯和運作3個典型的java程式(一個控制台應用、一個圖形應用、一個applet),指導讀者使用簡易的jdk、可啟用java的文本編輯器以及一個java ide。

第3章開始讨論java 語言。這一章涉及的基礎知識有變量、循環以及簡單的函數。對于c或c++程式員來說,學習這一章的内容将會感覺一帆風順,因為這些語言特性的文法本質上與c語言相同。對于沒有c語言程式設計背景,但使用過其他程式設計語言(如visual basic)的程式員來說,仔細地閱讀這一章是非常必要的。

面向對象程式設計(object-oriented programming, oop)是當今程式設計的主流,而java是一種完全面向對象的語言。第4章将介紹面向對象兩個基本成分中最重要的——封裝,以及java語言實作封裝的機制,即類與方法。除了java語言規則之外,還對如何完成合理的oop設計給出了忠告。最後,介紹奇妙的javadoc工具,它将代碼注釋轉換為一組包含超連結的網頁。熟悉c++的程式員可以快速地浏覽這一章,而沒有面向對象程式設計背景的程式員應在進一步學習java之前花一些時間了解oop的有關概念。

類與封裝僅僅是oop中的一部分,第5章将介紹另一部分——繼承。繼承使程式員可以使用現有的類,并根據需要進行修改。這是java程式設計中的一個基礎技術。java中的繼承機制與c++的繼承機制十分相似。c++程式員隻需關注兩種語言的不同之處即可。

第6章展示如何使用java的接口。接口可以讓你的了解超越第5章的簡單繼承模型。掌握接口可以充分獲得java的完全的面向對象程式設計能力。介紹接口之後,我們将轉而介紹lambda表達式(lambda expression),這是一種簡潔的方法,用來表述可以在以後某個時間點執行的代碼塊。本章還将介紹java的一個有用的技術特性——内部類。

第7章讨論異常處理(exception handling),即java的一種健壯機制,用于處理可正常運作程式可能出現意外的情況。異常提供了一種将正常處理代碼與錯誤處理代碼分開的有效手段。當然,即使程式能夠處理所有異常條件,仍然有可能無法按照預計的方式工作。這一章的後半部分将給出大量實用的調試技巧。

第8章概要介紹泛型程式設計。泛型程式設計可以讓程式更可讀、更安全。我們會展示如何使用強類型機制,而舍棄不安全的強制類型轉換,以及如何處理與舊版本java相容所帶來的複雜問題。

第9章讨論的是java平台的集合架構。如果希望收集多個對象并在以後擷取這些對象,就應當使用集合,而不要簡單地把這些元素放在一個數組中,這是這種情況下最适用的做法。這一章會介紹如何充分利用内建的标準集合。

第10章開始介紹gui程式設計。我們會讨論如何建立視窗、如何在視窗中繪圖、如何利用幾何圖形繪圖、如何采用多種字型格式化文本,以及如何顯示圖像。

第11章将詳細讨論抽象視窗工具包(abstract window toolkit,awt)的事件模型。你會看到如何編寫代碼來響應事件,如滑鼠點選事件或按鍵事件。同時,你還會看到如何處理基本的gui元素,如按鈕和面闆。

第12章詳細讨論swing gui工具包。swing工具包允許建立跨平台的圖像使用者界面。在這裡你會了解各種按鈕、文本元件、邊框、滑塊、清單框、菜單以及對話框的有關内容。不過,一些更進階的元件會在卷ii中讨論。

第13章介紹如何将程式部署為應用或applet。在這裡我們會描述如何将程式打包在jar檔案中,以及如何使用java web start和applet機制在internet上釋出應用。另外還會解釋java程式部署之後如何存儲和擷取配置資訊。

第14章是本書的最後一章,這一章将讨論并發,并發能夠讓程式任務并行執行。在當今這個時代,大多數處理器都有多個核心,你往往希望這些核心都在工作,并發是java技術的一個重要而且令人振奮的應用。

附錄列出了java語言的保留字。

約定

本書使用以下圖示表示特殊内容。

注釋:“注釋”資訊會用這樣的“注釋”圖示标志。

提示:“提示”資訊會用這樣的“提示”圖示标志。

警告:對于可能出現的危險,我們用一個“警告”圖示做出警示。

c++注釋:在本書中有許多用來解釋java與c++之間差别的c++注釋。對于沒有c++程式設計背景,或者不擅長c++程式設計、把它當做一場噩夢不願再想起的程式員來說,可以跳過這些注釋。

java提供了一個很大的程式設計庫,即應用程式程式設計接口。第一次使用api調用時,我們會在該節的結尾給出一個概要描述。這些描述十分通俗易懂,希望能夠比聯機api文檔提供更多的資訊。類、接口或方法名後面的編号是介紹該特性的jdk版本号,如下例所示:

應用程式程式設計接口1.2

程式(源代碼見本書網站)以程式清單形式給出,例如:

程式清單1-1 inputtest/inputtest.java

示例代碼

本書網站http://horstmann.com/corejava以壓縮的形式提供了書中的所有示例代碼。可以用熟悉的解壓縮程式或者用java開發包中的jar實用程式解壓這個檔案。有關安裝java開發包和示例代碼的詳細資訊請參看第2章。

緻  謝

寫一本書需要投入大量的精力,改寫一本書也并不像想象的那樣輕松,尤其是java技術一直在持續不斷地更新。編著一本書讓很多人耗費了很多心血,在此衷心地感謝《java核心技術》編寫小組的每一位成員。

prentice hall公司的許多人提供了非常有價值的幫助,卻甘願做幕後英雄。在此,我希望每一位都能夠知道我對他們努力的感恩。與以往一樣,我要真誠地感謝我的編輯,prentice hall公司的greg doench,從本書的寫作到出版他一直在給予我們指導,同時感謝那些不知其姓名的為本書做出貢獻的幕後人士。非常感謝julie nahil在圖書制作方面給予的支援,還要感謝dmitry kirsanov和alina kirsanova完成手稿的編輯和排版工作。我還要感謝早期版本中我的合作者,gary cornell,他已經轉向其他的事業。 

感謝早期版本的許多讀者,他們指出了許多令人尴尬的錯誤并給出了許多具有建設性的修改意見。我還要特别感謝本書優秀的審閱小組,他們仔細地審閱我的手稿,使本書減少了許多錯誤。

本書及早期版本的審閱專家包括:chuck allison (utah valley大學)、lance andersen (oracle)、paul anderson (anderson software group)、alec beaton (ibm)、cliff berg、andrew binstock (oracle)、joshua bloch、david brown、corky cartwright、frank cohen (pushtotest)、chris crane (devxsolution)、dr. nicholas j. de lillo (manhattan學院)、rakesh dhoopar (oracle)、david geary (clarity training)、jim gish (oracle)、brian goetz (oracle)、angela gordon、dan gordon (electric cloud)、rob gordon、john gray (hartford大學)、cameron gregory (olabs.com)、marty hall (coreservlets.com公司)、vincent hardy (adobe systems)、dan harkey (san jose州立大學)、william higgins (ibm)、vladimir ivanovic (pointbase)、jerry jackson (ca technologies)、tim kimmet (walmart)、chris laffra、charlie lai (apple)、angelika langer、doug langston、hang lau (mcgill大學)、mark lawrence、doug lea (suny oswego)、gregory longshore、bob lynch (lynch associates)、philip milne (consultant)、mark morrissey (oregon研究院)、mahesh neelakanta (florida atlantic大學)、hao pham、paul philion、blake ragsdell、stuart reges (arizona大學)、rich rosen (interactive data corporation)、peter sanders (法國尼斯essi大學)、dr. paul sanghera (san jose州立大學brooks學院)、paul sevinc (teamup ag)、devang shah (sun microsystems)、yoshiki shibata、bradley a. smith、steven stelting (oracle)、christopher taylor、luke taylor (valtech)、george thiruvathukal、kim topley (streamingedge)、janet traub、paul tyma (consultant)、peter van der linden、christian ullenboom、burt walsh、dan xu (oracle)和john zavgren (oracle)。

cay horstmann

2015年11月于瑞士比爾

目  錄

譯者序

前言

緻謝

第1章 java程式設計概述  1

1.1 java程式設計平台  1

1.2 java“白皮書”的關鍵術語  2

1.2.1 簡單性  2

1.2.2 面向對象  2

1.2.3 分布式  3

1.2.4 健壯性  3

1.2.5 安全性  3

1.2.6 體系結構中立  4

1.2.7 可移植性  4

1.2.8 解釋型  5

1.2.9 高性能  5

1.2.10 多線程  5

1.2.11 動态性  5

1.3 java applet與internet  6

1.4 java發展簡史  7

1.5 關于java的常見誤解  9

第2章 java程式設計環境  12

2.1 安裝java開發工具包  12

2.1.1 下載下傳jdk  12

2.1.2 設定jdk  13

2.1.3 安裝庫源檔案和文檔  15

2.2 使用指令行工具  16

2.3 使用內建開發環境  18

2.4 運作圖形化應用程式  21

2.5 建構并運作applet  23

第3章 java的基本程式設計結構  28

3.1 一個簡單的java應用程式  28

3.2 注釋  31

3.3 資料類型  32

3.3.1 整型  32

3.3.2 浮點類型  33

3.3.3 char類型  34

3.3.4 unicode和char類型  35

3.3.5 boolean類型  35

3.4 變量  36

3.4.1 變量初始化  37

3.4.2 常量  37

3.5 運算符  38

3.5.1 數學函數與常量  39

3.5.2 數值類型之間的轉換  40

3.5.3 強制類型轉換  41

3.5.4 結合指派和運算符  42

3.5.5 自增與自減運算符  42

3.5.6 關系和boolean運算符  42

3.5.7 位運算符  43

3.5.8 括号與運算符級别  44

3.5.9 枚舉類型  45

3.6 字元串  45

3.6.1 子串  45

3.6.2 拼接  46

3.6.3 不可變字元串  46

3.6.4 檢測字元串是否相等  47

3.6.5 空串與null串  48

3.6.6 碼點與代碼單元  49

3.6.7 string api  50

3.6.8 閱讀聯機api文檔  52

3.6.9 建構字元串  54

3.7 輸入輸出  55

3.7.1 讀取輸入  55

3.7.2 格式化輸出  58

3.7.3 檔案輸入與輸出  61

3.8 控制流程  63

3.8.1 塊作用域  63

3.8.2 條件語句  63

3.8.3 循環  66

3.8.4 确定循環  69

3.8.5 多重選擇:switch語句  72

3.8.6 中斷控制流程語句  74

3.9 大數值  76

3.10 數組  78

3.10.1 for each循環  79

3.10.2 數組初始化以及匿名數組  80

3.10.3 數組拷貝  81

3.10.4 指令行參數  81

3.10.5 數組排序  82

3.10.6 多元數組  85

3.10.7 不規則數組  88

第4章 對象與類  91

4.1 面向對象程式設計概述  91

4.1.1 類  92

4.1.2 對象  93

4.1.3 識别類  93

4.1.4 類之間的關系  94

4.2 使用預定義類  95

4.2.1 對象與對象變量  95

4.2.2 java類庫中的localdate類  98

4.2.3 更改器方法與通路器方法  100

4.3 使用者自定義類  103

4.3.1 employee類  103

4.3.2 多個源檔案的使用  105

4.3.3 剖析employee類  106

4.3.4 從構造器開始  106

4.3.5 隐式參數與顯式參數  108

4.3.6 封裝的優點  109

4.3.7 基于類的通路權限  111

4.3.8 私有方法  111

4.3.9 f?inal執行個體域  112

4.4 靜态域與靜态方法  112

4.4.1 靜态域  112

4.4.2 靜态常量  113

4.4.3 靜态方法  114

4.4.4 工廠方法  115

4.4.5 main方法  115

4.5 方法參數  118

4.6 對象構造  123

4.6.1 重載  123

4.6.2 預設域初始化  123

4.6.3 無參數的構造器  124

4.6.4 顯式域初始化  125

4.6.5 參數名  125

4.6.6 調用另一個構造器  126

4.6.7 初始化塊  127

4.6.8 對象析構與f?inalize方法  130

4.7 包  131

4.7.1 類的導入  131

4.7.2 靜态導入  133

4.7.3 将類放入包中  133

4.7.4 包作用域  136

4.8 類路徑  137

4.8.1 設定類路徑  139

4.9 文檔注釋  140

4.9.1 注釋的插入  140

4.9.2 類注釋  140

4.9.3 方法注釋  141

4.9.4 域注釋  142

4.9.5 通用注釋  142

4.9.6 包與概述注釋  143

4.9.7 注釋的抽取  143

4.10 類設計技巧  144

第5章 繼承  147

5.1 類、超類和子類  147

5.1.1 定義子類  147

5.1.2 覆寫方法  149

5.1.3 子類構造器  150

5.1.4 繼承層次  153

5.1.5 多态  154

5.1.6 了解方法調用  155

5.1.7 阻止繼承:f?inal類和方法  157

5.1.8 強制類型轉換  158

5.1.9 抽象類  160

5.1.10 受保護通路  165

5.2 object:所有類的超類  166

5.2.1 equals方法  166

5.2.2 相等測試與繼承  167

5.2.3 hashcode方法  170

5.2.4 tostring方法  172

5.3 泛型數組清單  178

5.3.1 通路數組清單元素  180

5.3.2 類型化與原始數組清單的相容性  183

5.4 對象包裝器與自動裝箱  184

5.5 參數數量可變的方法  187

5.6 枚舉類  188

5.7 反射  190

5.7.1 class類  190

5.7.2 捕獲異常  192

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

5.7.4 在運作時使用反射分析對象  198

5.7.5 使用反射編寫泛型數組代碼  202

5.7.6 調用任意方法  205

5.8 繼承的設計技巧  208

第6章 接口、lambda表達式與内部類  211

6.1 接口  211

6.1.1 接口概念  211

6.1.2 接口的特性  217

6.1.3 接口與抽象類  218

6.1.4 靜态方法  218

6.1.5 預設方法  219

6.1.6 解決預設方法沖突  220

6.2 接口示例  222

6.2.1 接口與回調  222

6.2.2 comparator接口  224

6.2.3 對象克隆  225

6.3 lambda表達式  231

6.3.1 為什麼引入lambda表達式  231

6.3.2 lambda表達式的文法  232

6.3.3 函數式接口  234

6.3.4 方法引用  235

6.3.5 構造器引用  237

6.3.6 變量作用域  237

6.3.7 處理lambda表達式  239

6.3.8 再談comparator  242

6.4 内部類  242

6.4.1 使用内部類通路對象狀态  244

6.4.2 内部類的特殊文法規則  247

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

6.4.4 局部内部類  250

6.4.5 由外部方法通路變量  250

6.4.6 匿名内部類  252

6.4.7 靜态内部類  255

6.5 代理  258

6.5.1 何時使用代理  259

6.5.2 建立代理對象  259

6.5.3 代理類的特性  262

第7章 異常、斷言和日志  264

7.1 處理錯誤  264

7.1.1 異常分類  265

7.1.2 聲明受查異常  267

7.1.3 如何抛出異常  269

7.1.4 建立異常類  270

7.2 捕獲異常  271

7.2.1 捕獲異常  271

7.2.2 捕獲多個異常  273

7.2.3 再次抛出異常與異常鍊  274

7.2.4 f?inally子句  275

7.2.5 帶資源的try語句  278

7.2.6 分析堆棧軌迹元素  280

7.3 使用異常機制的技巧  282

7.4 使用斷言  285

7.4.1 斷言的概念  285

7.4.2 啟用和禁用斷言  286

7.4.3 使用斷言完成參數檢查  287

7.4.4 為文檔假設使用斷言  288

7.5 記錄日志  289

7.5.1 基本日志  289

7.5.2 進階日志  289

7.5.3 修改日志管理器配置  291

7.5.4 本地化  292

7.5.5 處理器  293

7.5.6 過濾器  296

7.5.7 格式化器  296

7.5.8 日志記錄說明  296

7.6 調試技巧  304

第8章 泛型程式設計  309

8.1 為什麼要使用泛型程式設計  309

8.1.1 類型參數的好處  309

8.1.2 誰想成為泛型程式員  310

8.2 定義簡單泛型類  311

8.3 泛型方法  313

8.4 類型變量的限定  314

8.5 泛型代碼和虛拟機  316

8.5.1 類型擦除  316

8.5.2 翻譯泛型表達式  317

8.5.3 翻譯泛型方法  318

8.5.4 調用遺留代碼  319

8.6 限制與局限性  320

8.6.1 不能用基本類型執行個體化類型參數  320

8.6.2 運作時類型查詢隻适用于原始類型  321

8.6.3 不能建立參數化類型的數組  321

8.6.4 varargs警告  322

8.6.5 不能執行個體化類型變量  323

8.6.6 不能構造泛型數組  323

8.6.7 泛型類的靜态上下文中類型變量無效  325

8.6.8 不能抛出或捕獲泛型類的執行個體  325

8.6.9 可以消除對受查異常的檢查  326

8.6.10 注意擦除後的沖突  327

8.7 泛型類型的繼承規則  328

8.8 通配符類型  330

8.8.1 通配符概念  330

8.8.2 通配符的超類型限定  331

8.8.3 無限定通配符  334

8.8.4 通配符捕獲  334

8.9 反射和泛型  337

8.9.1 泛型class類  337

8.9.2 使用class<t>參數進行類型比對  338

8.9.3 虛拟機中的泛型類型資訊  338

第9章 集合  344

9.1 java集合架構  344

9.1.1 将集合的接口與實作分離  344

9.1.2 collection接口  346

9.1.3 疊代器  347

9.1.4 泛型實用方法  349

9.1.5 集合架構中的接口  352

9.2 具體的集合  353

9.2.1 連結清單  355

9.2.2 數組清單  362

9.2.3 散列集  363

9.2.4 樹集  366

9.2.5 隊列與雙端隊列  369

9.2.6 優先級隊列  371

9.3 映射  372

9.3.1 基本映射操作  372

9.3.2 更新映射項  375

9.3.3 映射視圖  376

9.3.4 弱散列映射  377

9.3.5 連結散列集與映射  378

9.3.6 枚舉集與映射  379

9.3.7 辨別散列映射  380

9.4 視圖與包裝器  381

9.4.1 輕量級集合包裝器  382

9.4.2 子範圍  382

9.4.3 不可修改的視圖  383

9.4.4 同步視圖  384

9.4.5 受查視圖  384

9.4.6 關于可選操作的說明  385

9.5 算法  388

9.5.1 排序與混排  389

9.5.2 二分查找  391

9.5.3 簡單算法  392

9.5.4 批操作  394

9.5.5 集合與數組的轉換  394

9.5.6 編寫自己的算法  395

9.6 遺留的集合  396

9.6.1 hashtable類  397

9.6.2 枚舉  397

9.6.3 屬性映射  398

9.6.4 棧  399

9.6.5 位集  399

第10章 圖形程式設計  403

10.1 swing概述  403

10.2 建立架構  407

10.3 架構定位  409

10.3.1 架構屬性  411

10.3.2 确定合适的架構大小  411

10.4 在元件中顯示資訊  415

10.5 處理2d圖形  419

10.6 使用顔色  426

10.7 文本使用特殊字型  429

10.8 顯示圖像  435

第11章 事件處理  439

11.1 事件處理基礎  439

11.1.1 執行個體:處理按鈕點選事件  441

11.1.2 簡潔地指定監聽器  445

11.1.3 執行個體:改變觀感  447

11.1.4 擴充卡類  450

11.2 動作  453

11.3 滑鼠事件  459

11.4 awt事件繼承層次  465

11.4.1 語義事件和底層事件  466

第12章 swing使用者界面元件  469

12.1 swing和模型–視圖–控制器設計模式  469

12.1.1 設計模式  469

12.1.2 模型–視圖–控制器模式  470

12.1.3 swing按鈕的模型–視圖–控制器分析  473

12.2 布局管理概述  474

12.2.1 邊框布局  477

12.2.2 網格布局  478

12.3 文本輸入  481

12.3.1 文本域  482

12.3.2 标簽和标簽元件  483

12.3.3 密碼域  484

12.3.4 文本區  485

12.3.5 滾動窗格  485

12.4 選擇元件  488

12.4.1 複選框  488

12.4.2 單選鈕  490

12.4.3 邊框  493

12.4.4 組合框  496

12.4.5 滑動條  499

12.5 菜單  504

12.5.1 菜單建立  504

12.5.2 菜單項中的圖示  507

12.5.3 複選框和單選鈕菜單項  508

12.5.4 彈出菜單  508

12.5.5 快捷鍵和加速器  510

12.5.6 啟用和禁用菜單項  511

12.5.7 工具欄  515

12.5.8 工具提示  516

12.6 複雜的布局管理  518

12.6.1 網格組布局  520

12.6.2 組布局  528

12.6.3 不使用布局管理器  537

12.6.4 定制布局管理器  537

12.6.5 周遊順序  541

12.7 對話框  541

12.7.1 選項對話框  542

12.7.2 建立對話框  551

12.7.3 資料交換  554

12.7.4 檔案對話框  559

12.7.5 顔色選擇器  569

12.8 gui程式排錯  573

12.8.1 調試技巧  573

12.8.2 讓awt機器人完成工作  576

第13章 部署java應用程式  580

13.1 jar檔案  580

13.1.1 建立jar檔案  580

13.1.2 清單檔案  581

13.1.3 可執行jar檔案  582

13.1.4 資源  583

13.1.5 密封  585

13.2 應用首選項的存儲  586

13.2.1 屬性映射  586

13.2.2 首選項api  591

13.3 服務加載器  596

13.4 applet  598

13.4.1 一個簡單的applet  599

13.4.2 applet html标記和屬性  602

13.4.3 使用參數向applet傳遞資訊  603

13.4.4 通路圖像和音頻檔案  608

13.4.5 applet上下文  609

13.4.6 applet間通信  609

13.4.7 在浏覽器中顯示資訊項  610

13.4.8 沙箱  611

13.4.9 簽名代碼  612

13.5 java web start  614

13.5.1 釋出java web start應用  614

13.5.2 jnlp api  617

第14章 并發  624

14.1 什麼是線程  624

14.1.1 使用線程給其他任務提供機會  629

14.2 中斷線程  632

14.3 線程狀态  635

14.3.1 新建立線程  635

14.3.2 可運作線程  635

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

14.3.4 被終止的線程  636

14.4 線程屬性  638

14.4.1 線程優先級  638

14.4.2 守護線程  639

14.4.3 未捕獲異常處理器  639

14.5 同步  640

14.5.1 競争條件的一個例子  641

14.5.2 競争條件詳解  644

14.5.3 鎖對象  646

14.5.4 條件對象  648

14.5.5 synchronized關鍵字  653

14.5.6 同步阻塞  656

14.5.7 螢幕概念  657

14.5.8 volatile域  658

14.5.9 f?inal變量  659

14.5.10 原子性  659

14.5.11 死鎖  661

14.5.12 線程局部變量  663

14.5.13 鎖測試與逾時  665

14.5.14 讀/寫鎖  666

14.5.15 為什麼棄用stop和suspend方法  667

14.6 阻塞隊列  668

14.7 線程安全的集合  673

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

14.7.2 映射條目的原子更新  675

14.7.3 對并發散列映射的批操作  676

14.7.4 并發集視圖  678

14.7.5 寫數組的拷貝  679

14.7.6 并行數組算法  679

14.7.7 較早的線程安全集合  680

14.8 callable與future  681

14.9 執行器  685

14.9.1 線程池  685

14.9.2 預定執行  689

14.9.3 控制任務組  690

14.9.4 fork-join架構  691

14.9.5 可完成future  694

14.10 同步器  696

14.10.1 信号量  696

14.10.2 倒計時門栓  697

14.10.3 障栅  697

14.10.4 交換器  698

14.10.5 同步隊列  698

14.11 線程與swing  698

14.11.1 運作耗時的任務  699

14.11.2 使用swing工作線程  703

14.11.3 單一線程規則  708

附錄a java關鍵字  710

第1章 java程式設計概述

▲  java程式設計平台 ▲  java發展簡史

▲  java“白皮書”的關鍵術語 ▲  關于java的常見誤解

▲  java applet與internet

1996年java第一次釋出就引起了人們的極大興趣。關注java的人士不僅限于計算機出版界,還有諸如《紐約時報》《華盛頓郵報》《商業周刊》這樣的主流媒體。java是第一種也是唯一一種在national public radio上占用了10分鐘時間來進行介紹的程式設計語言,并且還得到了$100 000 000的風險投資基金。這些基金全部用來支援用這種特别的計算機語言開發的産品。重溫那些令人興奮的日子是很有意思的。本章将簡要地介紹一下java語言的發展曆史。

1.1 java程式設計平台

本書的第1版是這樣描寫java的:“作為一種計算機語言,java的廣告詞确實有點誇大其辭。然而,java的确是一種優秀的程式設計語言。作為一個名副其實的程式設計人員,使用java無疑是一個好的選擇。有人認為:java将有望成為一種最優秀的程式設計語言,但還需要一個相當長的發展時期。一旦一種語言應用于某個領域,與現存代碼的相容性問題就擺在了人們的面前。”

我們的編輯手中有許多這樣的廣告詞。這是sun公司高層的某位不願透露姓名的人士提供的(sun是原先開發java的公司)。java有許多非常優秀的語言特性,本章稍後将會詳細地讨論這些特性。由于相容性這個嚴峻的問題确實存在于現實中,是以,或多或少地還是有一些“累贅”被加到語言中,這就導緻java并不如想象中的那麼完美無瑕。

但是,正像我們在第1版中已經指出的那樣,java并不隻是一種語言。在此之前出現的那麼多種語言也沒有能夠引起那麼大的轟動。java是一個完整的平台,有一個龐大的庫,其中包含了很多可重用的代碼和一個提供諸如安全性、跨作業系統的可移植性以及自動垃圾收集等服務的執行環境。

作為一名程式設計人員,常常希望能夠有一種語言,它具有令人賞心悅目的文法和易于了解的語義(c++不是這樣的)。與許多其他的優秀語言一樣,java完全滿足了這些要求。有些語言提供了可移植性、垃圾收集等,但是,沒有提供一個大型的庫。如果想要有奇特的繪圖功能、網絡連接配接功能和資料庫存取功能就必須自己動手編寫代碼。java具備所有這些特性,它是一種功能齊全的出色語言,是一個高品質的執行環境,還提供了一個龐大的庫。正是因為它集多種優勢于一身,是以對廣大的程式設計人員有着不可抗拒的吸引力。

1.2 java“白皮書”的關鍵術語

java的設計者已經編寫了頗有影響力的“白皮書”,用來解釋設計的初衷以及完成的情況,并且釋出了一個簡短的摘要。這個摘要用下面11個關鍵術語進行組織:

1)簡單性 7)可移植性

2)面向對象 8)解釋型

3)分布式 9)高性能

4)健壯性 10)多線程

5)安全性 11)動态性

6)體系結構中立

本節将提供一個小結,給出白皮書中相關的說明,這是java設計者對各個關鍵術語的論述,另外還會根據我們對java目前版本的使用經驗,給出對這些術語的了解。

注釋:寫這本書時,白皮書可以在www.oracle.com/technetwork/java/langenv-140151.html上找到。對于11個關鍵術語的論述請參看http://horstmann.com/corejava/java-an-overview/7gosling.pdf。

1.2.1 簡單性

人們希望建構一個無須深奧的專業訓練就可以進行程式設計的系統,并且要符合當今的标準慣例。是以,盡管人們發現c++不太适用,但在設計java的時候還是盡可能地接近c++,以便系統更易于了解。java剔除了c++中許多很少使用、難以了解、易混淆的特性。在目前看來,這些特性帶來的麻煩遠遠多于其帶來的好處。

的确,java文法是c++文法的一個“純淨”版本。這裡沒有頭檔案、指針運算(甚至指針文法)、結構、聯合、操作符重載、虛基類等(請參閱本書各個章節給出的c++注釋,其中比較詳細地解釋了java與c++之間的差別)。然而,設計者并沒有試圖清除c++中所有不适當的特性。例如,switch語句的文法在java中就沒有改變。如果你了解c++就會發現可以輕而易舉地轉換到java文法。

java釋出時,實際上c++并不是最常用的程式設計語言。很多開發人員都在使用visual basic和它的拖放式程式設計環境。這些開發人員并不覺得java簡單。很多年之後java開發環境才迎頭趕上。如今,java開發環境已經遠遠超出大多數其他程式設計語言的開發環境。

簡單的另一個方面是小。java的目标之一是支援開發能夠在小型機器上獨立運作的軟體。基本的解釋器以及類支援大約僅為40kb;再加上基礎的标準類庫和對線程的支援(基本上是一個自包含的微核心)大約需要增加175kb。

在當時,這是一個了不起的成就。當然,由于不斷的擴充,類庫已經相當龐大了。現在有一個獨立的具有較小類庫的java微型版(java micro edition),這個版本适用于嵌入式裝置。

1.2.2 面向對象

簡單地講,面向對象設計是一種程式設計技術。它将重點放在資料(即對象)和對象的接口上。用木匠打一個比方,一個“面向對象的”木匠始終關注的是所制作的椅子,第二位才是所使用的工具;一個“非面向對象的”木匠首先考慮的是所用的工具。在本質上,java的面向對象能力與c++是一樣的。

開發java時面向對象技術已經相當成熟。java的面向對象特性與c++旗鼓相當。java與c++的主要不同點在于多重繼承,在java中,取而代之的是更簡單的接口概念。與c++相比,java提供了更豐富的運作時自省功能(有關這部分内容将在第5章中讨論)。

1.2.3 分布式

java有一個豐富的例程庫,用于處理像http和ftp之類的tcp/ip協定。java應用程式能夠通過url打開和通路網絡上的對象,其便捷程度就好像通路本地檔案一樣。

如今,這一點已經得到認可,不過在1995年,主要還是從c++或visual basic程式連接配接web伺服器。

1.2.4 健壯性

java的設計目标之一在于使得java編寫的程式具有多方面的可靠性。java投入了大量的精力進行早期的問題檢測、後期動态的(運作時)檢測,并消除了容易出錯的情況……java和c++最大的不同在于java采用的指針模型可以消除重寫記憶體和損壞資料的可能性。

java編譯器能夠檢測許多在其他語言中僅在運作時才能夠檢測出來的問題。至于第二點,對于曾經花費幾個小時來檢查由于指針bug而引起記憶體沖突的人來說,一定很喜歡java的這一特性。

1.2.5 安全性

java适用于網絡/分布式環境。為了達到這個目标,在安全方面投入了很大精力。使用java可以建構防病毒、防篡改的系統。

從一開始,java就設計成能夠防範各種攻擊,其中包括:

運作時堆棧溢出。如蠕蟲和病毒常用的攻擊手段。

破壞自己的程序空間之外的記憶體。

未經授權讀寫檔案。

原先,java對下載下傳代碼的态度是“盡管來吧!”。不可信代碼在一個沙箱環境中執行,在這裡它不會影響主系統。使用者可以确信不會發生不好的事情,因為java代碼不論來自哪裡,都不能脫離沙箱。

不過,java的安全模型很複雜。java開發包(java development kit,jdk)的第一版釋出之後不久,普林斯頓大學的一些安全專家就發現一些小bug會允許不可信的代碼攻擊主系統。

最初,安全bug可以快速修複。遺憾的是,經過一段時間之後,黑客已經很擅長找出安全體系結構實作中的小漏洞。sun以及之後的oracle為修複bug度過了一段很是艱難的日子。

遭遇多次高調攻擊之後,浏覽器開發商和oracle都越來越謹慎。java浏覽器插件不再信任遠端代碼,除非代碼有數字簽名而且使用者同意執行這個代碼。

注釋:現在看來,盡管java安全模型沒有原先預想的那麼成功,但java在那個時代确實相當超前。微軟提供了一種與之競争的代碼傳輸機制,其安全性完全依賴于數字簽名。顯然這是不夠的,因為微軟自身産品的任何使用者都可以證明,知名開發商的程式确實會崩潰并對系統産生危害。

1.2.6 體系結構中立

編譯器生成一個體系結構中立的目标檔案格式,這是一種編譯過的代碼,隻要有java運作時系統,這些編譯後的代碼可以在許多處理器上運作。java編譯器通過生成與特定的計算機體系結構無關的位元組碼指令來實作這一特性。精心設計的位元組碼不僅可以很容易地在任何機器上解釋執行,而且還可以動态地翻譯成本地機器代碼。

當時,為“虛拟機”生成代碼并不是一個新思路。諸如lisp、smalltalk和pascal等程式設計語言多年前就已經采用了這種技術。

當然,解釋虛拟機指令肯定會比全速運作機器指令慢很多。然而,虛拟機有一個選項,可以将執行最頻繁的位元組碼序列翻譯成機器碼,這一過程被稱為即時編譯。

java虛拟機還有一些其他的優點。它可以檢測指令序列的行為,進而增強其安全性。

1.2.7 可移植性

與c和c++不同,java規範中沒有“依賴具體實作”的地方。基本資料類型的大小以及有關運算都做了明确的說明。

例如,java中的int永遠為32位的整數,而在c/c++中,int可能是16位整數、32位整數,也可能是編譯器提供商指定的其他大小。唯一的限制隻是int類型的大小不能低于short int,并且不能高于long int。在java中,資料類型具有固定的大小,這消除了代碼移植時令人頭痛的主要問題。二進制資料以固定的格式進行存儲和傳輸,消除了位元組順序的困擾。字元串是用标準的unicode格式存儲的。

作為系統組成部分的類庫,定義了可移植的接口。例如,有一個抽象的window類,并給出了在unix、windows和macintosh環境下的不同實作。

選擇window類作為例子可能并不太合适。凡是嘗試過的人都知道,要編寫一個在windows、macintosh和10種不同風格的unix上看起來都不錯的程式有多麼困難。java 1.0就嘗試着做了這麼一個壯舉,釋出了一個将常用的使用者界面元素映射到不同平台上的簡單工具包。遺憾的是,花費了大量的心血,卻建構了一個在各個平台上都難以讓人接受的庫。原先的使用者界面工具包已經重寫,而且後來又再次重寫,不過跨平台的可移植性仍然是個問題。

不過,除了與使用者界面有關的部分外,所有其他java庫都能很好地支援平台獨立性。你可以處理檔案、正規表達式、xml、日期和時間、資料庫、網絡連接配接、線程等,而不用操心底層作業系統。不僅程式是可移植的,java api往往也比原生api品質更高。

1.2.8 解釋型

java解釋器可以在任何移植了解釋器的機器上執行java位元組碼。由于連結是一個增量式且輕量級的過程,是以,開發過程也變得更加快捷,更加具有探索性。

這看上去很不錯。用過lisp、smalltalk、visual basic、python、r或scala的人都知道“快捷而且具有探索性”的開發過程是怎樣的。你可以做些嘗試,然後就能立即看到結果。java開發環境并沒有将重點放在這種體驗上。

1.2.9 高性能

盡管對解釋後的位元組碼性能已經比較滿意,但在有些場合下還需要更加高效的性能。位元組碼可以(在運作時刻)動态地翻譯成對應運作這個應用的特定cpu的機器碼。

使用java的頭幾年,許多使用者不同意這樣的看法:性能就是“适用性更強”。然而,現在的即時編譯器已經非常出色,以至于成了傳統編譯器的競争對手。在某些情況下,甚至超越了傳統編譯器,原因是它們含有更多的可用資訊。例如,即時編譯器可以監控經常執行哪些代碼并優化這些代碼以提高速度。更為複雜的優化是消除函數調用(即“内聯”)。即時編譯器知道哪些類已經加載。基于目前加載的類集,如果特定的函數不會被覆寫,就可以使用内聯。必要時,還可以撤銷優化。

1.2.10 多線程

多線程可以帶來更好的互動響應和實時行為。

如今,我們非常關注并發性,因為摩爾定律行将完結。我們不再追求更快的處理器,而是着眼于獲得更多的處理器,而且要讓它們一直保持工作。不過,可以看到,大多數程式設計語言對于這個問題并沒有顯示出足夠的重視。

java在當時很超前。它是第一個支援并發程式設計的主流語言。從白皮書中可以看到,它的出發點稍有些不同。當時,多核處理器還很神秘,而web程式設計才剛剛起步,處理器要花很長時間等待伺服器響應,需要并發程式設計來確定使用者界面不會“凍住”。

并發程式設計絕非易事,不過java在這方面表現很出色,可以很好地管理這個工作。

1.2.11 動态性

從各種角度看,java與c或c++相比更加具有動态性。它能夠适應不斷發展的環境。庫中可以自由地添加新方法和執行個體變量,而對用戶端卻沒有任何影響。在java中找出運作時類型資訊十分簡單。

當需要将某些代碼添加到正在運作的程式中時,動态性将是一個非常重要的特性。一個很好的例子是:從internet下載下傳代碼,然後在浏覽器上運作。如果使用c或c++,這确實難度很大,不過java設計者很清楚動态語言可以很容易地實作運作程式的演進。最終,他們将這一特性引入這個主流程式設計語言中。

注釋:java成功地推出後不久,微軟就釋出了一個叫做j++的産品,它與java有幾乎相同的程式設計語言以及虛拟機。現在,微軟不再支援j++,取而代之的是另一種名為c#的語言。c#與java有很多相似之處,然而使用的卻是完全不同的虛拟機。本書不準備介紹j++或c#語言。

1.3 java applet與internet

這裡的想法很簡單:使用者從internet下載下傳java位元組碼,并在自己的機器上運作。在網頁中運作的java程式稱為applet。要使用applet,需要啟用java的web浏覽器執行位元組碼。不需要安裝任何軟體。任何時候隻要通路包含applet的網頁都會得到程式的最新版本。最重要的是,要感謝虛拟機的安全性,它讓我們不必再擔心來自惡意代碼的攻擊。

在網頁中插入一個applet就如同在網頁中嵌入一幅圖檔。applet會成為頁面的一部分。文本環繞着applet所占據的空間周圍。關鍵的一點是這個圖檔是活動的。它可以對使用者指令做出響應,改變外觀,在運作它的計算機與提供它的計算機之間傳遞資料。

圖1-1展示了一個很好的動态網頁的例子。jmol applet顯示了分子結構,這将需要相當複雜的計算。在這個網頁中,可以利用滑鼠進行旋轉,調整焦距等操作,以便更好地了解分子結構。用靜态網頁就無法實作這種直接的操作,而applet卻可以達到此目的(可以在http://jmol.sourceforge.net上找到這個applet)。

圖1-1 jmol applet

當applet首次出現時,人們欣喜若狂。許多人相信applet的魅力将會導緻java迅速地流行起來。然而,初期的興奮很快就淡化了。不同版本的netscape與internet explorer運作不同版本的java,其中有些早已過時。這種糟糕的情況導緻更加難于利用java的最新版本開發applet。實際上,為了在浏覽器中得到動态效果,adobe的flash技術變得相當流行。後來,java遭遇了嚴重的安全問題,浏覽器和java浏覽器插件變得限制越來越多。如今,要在浏覽器中使用applet,這不僅需要一定的水準,而且要付出努力。例如,如果通路jmol網站,可能會看到一個消息,警告你要适當地配置浏覽器允許運作applet。

1.4 java發展簡史

本節将介紹java的發展簡史。這些内容來自很多出版資料(最重要的是sunworld的線上雜志1995年7月刊上對java建立者的專訪)。

java的曆史要追溯到1991年,由patrick naughton和james gosling(一個全能的計算機奇才)帶領的sun公司的工程師小組想要設計一種小型的計算機語言,主要用于像有線電視轉換盒這類的消費裝置。由于這些消費裝置的處理能力和記憶體都很有限,是以語言必須非常小且能夠生成非常緊湊的代碼。另外,由于不同的廠商會選擇不同的中央處理器(cpu),是以這種語言的關鍵是不與任何特定的體系結構捆綁在一起。這個項目被命名為“green”。

代碼短小、緊湊且與平台無關,這些要求促使開發團隊設計一個可移植的語言,可以為虛拟機生成中間代碼。

不過,sun公司的人都有unix的應用背景。是以,所開發的語言以c++為基礎,而不是lisp、smalltalk或pascal。不過,就像gosling在專訪中談到的:“畢竟,語言隻是實作目标的工具,而不是目标本身”。gosling把這種語言稱為“oak”(這麼起名的原因大概是因為他非常喜歡自己辦公室外的橡樹)。sun公司的人後來發現oak是一種已有的計算機語言的名字,于是,将其改名為java。事實證明這是一個很有靈感的選擇。

1992年,green項目釋出了它的第一個産品,稱之為“*7”。這個産品具有非常智能的遠端控制。遺憾的是,sun公司對生産這個産品并不感興趣,green項目組的人員必須找出其他的方法來将他們的技術推向市場。然而,沒有一個标準消費品電子公司對此感興趣。于是,green項目組競标了一個提供視訊點播等新型服務的有線電視盒的項目,但沒有成功(有趣的是,得到這個項目的公司的上司恰恰是開創netscape公司的jim clark。netscape公司後來對java的成功給予了很大的幫助)。

green項目(這時換了一個新名字——“first person公司”)花費了1993年一整年以及1994年的上半年,一直在苦苦尋求其技術的買家。然而,一個也沒有找到(patrick naughton,項目組的創立人之一,也是完成此項目大多數市場工作的人,聲稱為了銷售這項技術,累計飛行了300 000英裡)。1994年first person公司解散了。

當這一切在sun公司發生的時候,internet的網際網路也在日漸發展壯大。網際網路的關鍵是把超文本頁面轉換到螢幕上的浏覽器。1994年大多數人都在使用mosaic,這是一個1993年出自伊利諾斯大學超級計算中心的非商業化的web浏覽器(mosaic的一部分是由marc andreessen編寫的。當時,他作為一名參加半工半讀項目的大學生,編寫了這個軟體,每小時的薪水隻有6.85美元。他後來成了netscape公司的創始人之一和技術總監,可謂名利雙收)。

在接受sunworld采訪的時候,gosling說在1994年中期,java語言的開發者意識到:“我們能夠建立一個相當酷的浏覽器。我們已經擁有在客戶機/伺服器主流模型中所需要的體系結構中立、實時、可靠、安全——這些在工作站環境并不太重要,是以,我們決定開發浏覽器。”

實際的浏覽器是由patrick naughton和jonathan payne開發的,并演變為hotjava浏覽器。為了炫耀java語言超強的能力,hotjava浏覽器采用java編寫。設計者讓hotjava浏覽器具有在網頁中執行内嵌代碼的能力。這一“技術印證”在1995年5月23日的sunworld上得到展示,同時引發了人們延續至今的對java的狂熱追逐。

1996年年初,sun釋出了java的第1個版本。人們很快地意識到java1.0不能用來進行真正的應用開發。的确,可以使用java 1.0來實作在畫布上随機跳動的神經質的文本applet,但它卻沒有提供列印功能。坦率地說,java 1.0的确沒有為其黃金時期的到來做好準備。後來的java 1.1彌補了其中的大多明顯的缺陷,大大改進了反射能力,并為gui程式設計增加了新的事件處理模型。不過它仍然具有很大的局限性。

1998年javaone會議的頭号新聞是即将釋出java 1.2版。這個版本取代了早期玩具式的gui,并且它的圖形工具箱更加精細而具有可伸縮性,更加接近“一次編寫,随處運作”的承諾。在1998年12月java 1.2釋出三天之後,sun公司市場部将其名稱改為更加吸引人的“java 2标準版軟體開發工具箱1.2版”。

除了“标準版”之外,sun還推出了兩個其他的版本:一個是用于手機等嵌入式裝置的“微型版”;另一個是用于伺服器端處理的“企業版”。本書主要講述标準版。

标準版的1.3和1.4版本對最初的java 2版本做出了某些改進,擴充了标準類庫,提高系統性能。當然,還修正了一些bug。在此期間,java applet采用低調姿态,并淡化了用戶端的應用,但java卻成為伺服器端應用的首選平台。

5.0版是自1.1版以來第一個對java語言做出重大改進的版本(這一版本原來被命名為1.5版,在2004年的javaone會議之後,版本數字升至5.0)。經曆了多年的研究,這個版本添加了泛型類型(generic type)(類似于c++的模闆),其挑戰性在于添加這一特性并沒有對虛拟機做出任何修改。另外,還有幾個受c#啟發的很有用的語言特性:“for each”循環、自動裝箱和注解。

版本6(沒有字尾.0)于2006年年末釋出。同樣,這個版本沒有對語言方面再進行改進。但是,改進了其他性能,并增強了類庫。

随着資料中心越來越依賴于商業硬體而不是專用伺服器,sun microsystems終于淪陷,于2009年被oracle收購。java的開發停滞了很長一段時間。直到2011年oracle釋出了java的一個新版本,java 7,其中隻做了一些簡單的改進。

2014年,java 8終于釋出,在近20年中這個版本有了最大的改變。java 8提供了一種“函數式”程式設計方式,可以很容易地表述并發執行的計算。所有程式設計語言都必須與時俱進,java在這方面顯示出非凡的能力。

表1-1展示了java語言以及類庫的發展狀況。可以看到,應用程式程式設計接口(api)的規模發生了驚人的變化。

表1-1 java語言的發展狀況

版  本 年  份 語言新特性 類與接口的數量

1.0 1996 語言本身 211

1.1 1997 内部類 477

1.2 1998 strictfp修飾符 1524

1.3 2000 無 1840

1.4 2002 斷言 2723

5.0 2004 泛型類、“for each”循環、可變元參數、自動裝箱、中繼資料、枚舉、靜态導入 3279

6 2006 無 3793

7 2011 基于字元串的switch、鑽石操作符、二進制字面量、異常處理改進 4024

8 2014 lambda表達式,包含預設方法的接口,流和日期/時間庫 4240

1.5 關于java的常見誤解

在結束本章之前,我們列出了一些關于java的常見誤解,同時給出了解釋。

1.?java是html的擴充

java是一種程式設計語言;html是一種描述網頁結構的方式。除了用于在網頁上放置java applet的html擴充之外,兩者沒有任何共同之處。

2.?使用xml,是以不需要java

java是一種程式設計語言;xml是一種描述資料的方式。可以使用任何一種程式設計語言處理xml資料,而java api對xml處理提供了很好的支援。此外,許多重要的第三方xml工具采用java編寫。有關這方面更加詳細的資訊請參看卷Ⅱ。

3.?java是一種非常容易學習的程式設計語言

像java這種功能強大的語言大都不太容易學習。首先,必須将編寫玩具式程式的輕松和開發實際項目的艱難區分開來。需要注意的是:本書隻用了7章讨論java語言。在兩卷中,其他的章節介紹如何使用java類庫将java語言應用到實際中去。java類庫包含了數千種類和接口以及數萬個函數。幸運的是,并不需要知道它們中的每一個,然而,要想java解決實際問題,還是需要了解不少内容的。

4.?java将成為适用于所有平台的通用性程式設計語言

從理論上講,這是完全有可能的。但在實際中,某些領域其他語言有更出色的表現,比如,objective c和後來的swift在ios裝置上就有着無可取代的地位。浏覽器中的處理幾乎完全由javascript掌控。windows程式通常都用c++或c#編寫。java在伺服器端程式設計和跨平台用戶端應用領域則很有優勢。

5.?java隻不過是另外一種程式設計語言

java是一種很好的程式設計語言,很多程式設計人員喜歡java勝過c、c++或c#。有上百種好的程式設計語言沒有廣泛地流行,而帶有明顯缺陷的語言,如:c++和visual basic卻大行其道。

這是為什麼呢?程式設計語言的成功更多地取決于其支撐系統的能力,而不是優美的文法。人們主要關注:是否提供了易于實作某些功能的易用、便捷和标準的庫?是否有開發工具提供商能建立強大的程式設計和調試環境?語言和工具集是否能夠與其他計算基礎架構整合在一起?java的成功源于其類庫能夠讓人們輕松地完成原本有一定難度的事情。例如:聯網web應用和并發。java減少了指針錯誤,這是一個額外的好處,是以使用java程式設計的效率更高。但這些并不是java成功的全部原因。

6.?java是專用的,應該避免使用

最初建立java時,sun為銷售者和最終使用者提供了免費許可。盡管sun對java擁有最終的控制權,不過在語言版本的不斷發展和新庫的設計過程中還涉及很多其他公司。虛拟機和類庫的源代碼可以免費獲得,不過僅限于檢視,而不能修改和再釋出。java是“閉源的,不過可以很好地使用”。

這種狀況在2007年發生了戲劇性的變化,sun聲稱java未來的版本将在general public license(gpl)下提供。linux使用的是同一個開放源代碼許可。oracle一直緻力于保持java開源。隻有一點美中不足——專利。根據gpl,任何人都可以得到專利許可,允許其使用和修改java,不過僅限于桌面和伺服器平台。如果你想在嵌入式系統中使用java,就需要另外一個不同的許可,這很可能需要付費。不過,這些專利在未來十年就會到期,那時java就完全免費了。

7.?java是解釋型的,是以對于關鍵的應用程式速度太慢了

早期的java是解釋型的。現在java虛拟機使用了即時編譯器,是以采用java編寫的“熱點”代碼其運作速度與c++相差無幾,有些情況下甚至更快。

對于java桌面應用速度慢,人們已經抱怨很多年了。但是,今天的計算機速度遠比人們發出抱怨的時候快了很多。一個較慢的java程式與幾年前相當快的c++程式相比還要快一些。

8.?所有的java程式都是在網頁中運作的

所有的java applet都是在網頁浏覽器中運作的。這也恰恰是applet的定義,即一種在浏覽器中運作的java程式。然而,大多數java程式是運作在web浏覽器之外的獨立應用程式。實際上,很多java程式都在web伺服器上運作并生成用于網頁的代碼。

9.?java程式是主要的安全風險

對于早期的java,有過關于安全系統失效的報道,曾經一度引起公衆嘩然。研究人員将這視為一種挑戰,即努力找出java的漏洞,對applet安全模型的強度和複雜度發起挑戰。随後,人們很快就解決了引發問題的所有技術因素。後來又發現了更嚴重的漏洞,而sun以及後來的oracle反應卻過于遲緩。浏覽器制造商則有些反應過度,他們甚至預設禁用了java。客觀地來講,可以想想針對windows可執行檔案和word宏有數百萬種病毒攻擊,并造成了巨大的損害,不過奇怪的是卻很少有人批評被攻擊平台的脆弱。

有些系統管理者甚至在公司浏覽器中禁用了java,而同時卻允許使用者下載下傳可執行檔案和word文檔,實際上,這些帶來的風險遠甚于使用java。盡管距離java誕生已經20年之久,與其他常用的執行平台相比,java還是安全得多。

10.?javascript是java的簡易版

javascript是一種在網頁中使用的腳本語言,它是由netscape發明的,原來的名字叫做livescript。javascript的文法類似java,除此之外,兩者無任何關系。當然,名字有些相像。javascript的一個子集已經标準化為ecma-262。與java applet相比,javascript更緊密地與浏覽器內建在一起。特别是javascript程式可以修改正在顯示的文檔,而applet隻能在有限的區域内控制外觀。

11.?使用java可以用廉價的internet裝置取代桌面計算機

當java剛剛釋出的時候,一些人打賭:肯定會有這樣的好事情發生。一些公司已經生産出java網絡計算機的原型,不過使用者還不打算放棄功能強大而便利的桌面計算機,而去使用沒有本地存儲而且功能有限的網絡裝置。當然,如今世界已經發生改變,對于大多數最終使用者,常用的平台往往是手機或平闆電腦。這些裝置大多使用安卓平台,這是java的衍生産物。學習java程式設計肯定也對android程式設計很有幫助。

第2章 java程式設計環境

▲ 安裝java開發工具包 ▲ 運作圖形化應用程式

▲ 使用指令行工具 ▲ 建構并運作applet

▲ 使用內建開發環境

本章主要介紹如何安裝java開發工具包(jdk)以及如何編譯和運作不同類型的程式:控制台程式、圖形化應用程式以及applet。運作jdk工具的方法是在終端視窗中鍵入指令。然而,很多程式員更喜歡使用內建開發環境。為此,将在稍後介紹如何使用免費的開發環境編譯和運作java程式。盡管學起來很容易,但內建開發環境需要吞噬大量資源,編寫小型程式時也比較煩瑣。一旦掌握了本章的技術,并標明了自己的開發工具,就可以學習第3章,開始研究java程式設計語言。

2.1 安裝java開發工具包

oracle公司為linux、mac os x、solaris和windows提供了java開發工具包(jdk)的最新、最完整的版本。用于很多其他平台的版本仍處于多種不同的開發狀态中,不過,這些版本都由相應平台的開發商授權并分發。

2.1.1 下載下傳jdk

要想下載下傳java開發工具包,可以通路oracle網站:www.oracle.com/technetwork/java/javase/downloads,在得到所需的軟體之前必須弄清楚大量專業術語。請看表2-1的總結。

表2-1 java術語

術 語 名 縮寫 解  釋

java development kit jdk 編寫java程式的程式員使用的軟體

java runtime environment jre 運作java程式的使用者使用的軟體

server jre — 在伺服器上運作java程式的軟體

standard edition se 用于桌面或簡單伺服器應用的java平台

enterprise edition  ee 用于複雜伺服器應用的java平台

micro edition me 用于手機和其他小型裝置的java平台

java fx — 用于圖形化使用者界面的一個替代工具包,在oracle的java se釋出版本中提供

openjdk — java se的一個免費開源實作,不包含浏覽器內建或javafx

java 2 j2 一個過時的術語,用于描述1998年~2006年之間的java版本

software development kit sdk 一個過時的術語,用于描述1998年~2006年之間的jdk 

update u oracle的術語,表示bug修正版本

netbeans — oracle的內建開發環境

你已經看到,jdk是java development kit的縮寫。有點混亂的是:這個工具包的版本1.2~版本1.4被稱為java sdk(軟體開發包,software development kit)。在某些場合下,還可以看到這個過時的術語。另外,還有一個術語是java運作時環境(jre),它包含虛拟機但不包含編譯器。這并不是開發者想要的環境,而是專門為不需要編譯器的使用者而提供。

接下來,java se會大量出現,相對于java ee(enterprise edition)和java me(micro edition),它是java的标準版。

java 2這種提法始于1998年。當時sun公司的銷售人員感覺增加小數點後面的數值改變版本号并沒有反映出jdk 1.2的重大改進。但是,由于在釋出之後才意識到這個問題,是以決定開發工具包的版本号仍然沿用1.2,接下來的版本是1.3、1.4和5.0。但是,java平台被重新命名為java 2。是以,就有了java 2 standard edition software development kit(java 2标準版軟體開發包)的5.0版,即j2se sdk 5.0。

幸運的是,2006年版本号得到簡化。java标準版的下一個版本取名為java se 6,後來又有了java se 7和java se 8。不過,“内部”版本号分别是1.6.0、1.7.0和1.8.0。

當oracle為解決一些緊急問題做出某些微小的版本改變時,将其稱為更新。例如:java se 8u31是java se 8的第31次更新,它的内部版本号是1.8.0_31。更新不需要安裝在前一個版本上,它會包含整個jdk的最新版本。另外,并不是所有更新都公開釋出,是以如果“更新31”之後沒有“更新32”,你也不用驚慌。

對于windows或linux,需要在x86(32位)和x64(64位)版本之間做出選擇。應當選擇與你的作業系統體系結構比對的版本。

對于linux,還可以在rpm檔案和.tar.gz檔案之間做出選擇。我們建議使用後者,可以在你希望的任何位置直接解壓縮這個壓縮包。

現在你已經了解了如何選擇适當的jdk。下面做一個小結:

你需要的是jdk(java se開發包),而不是jre。

windows或linux:32位選擇x86,64位以x64。

linux:選擇.tar.gz版本。

接受許可協定,然後下載下傳檔案。

注釋:oracle提供了一個捆綁包,其中包含java開發包(jdk)和netbeans內建開發環境。建議現在不要安裝任何捆綁包,而隻需安裝java開發包。如果以後你打算使用netbeans,可以再從http://netbeans.org下載下傳。

2.1.2 設定jdk

下載下傳jdk之後,需要安裝這個開發包并明确要在哪裡安裝,後面還會需要這個資訊。

在windows上,啟動安裝程式。會詢問你要在哪裡安裝jdk。最好不要接受路徑名中包含空格的預設位置,如c:\program files\java\jdk1.8.0_version。取出路徑名中的program files部分就可以了。

在mac上,運作安裝程式。這會把軟體安裝到/library/java/javavirtualmachines/jdk1.8.0_version.jdk/contents/home。用finder找到這個目錄。

在linux上,隻需要把.tar.gz檔案解壓縮到你選擇的某個位置,如你的主目錄,或者/opt。如果從rpm檔案安裝,則要反複檢查是否安裝在/usr/java/jdk1.8.0_version。

在這本書中,安裝目錄用jdk表示。例如,談到jdk/bin目錄時,是指/opt/jdk1.8.0_31/bin或c:\java\jdk1.8.0_31\bin目錄。

在windows或linux上安裝jdk時,還需要另外完成一個步驟:将jdk/bin目錄增加到執行路徑中——執行路徑是作業系統查找可執行檔案時所周遊的目錄清單。

在linux上,需要在?/.bashrc或?/.bash_prof?ile檔案的最後增加這樣一行:

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

在windows上,啟動控制台,選擇“系統與安全”(system and security),再選擇“系統”(system),選擇進階系統設定(advanced system settings)(參見圖2-1)。在系統屬性(system properties)對話框中,點選“進階”(advanced)标簽頁,然後點選“環境”(environment)按鈕。

圖2-1 windows 7中設定系統屬性

滾動“系統變量”(system variables)清單,直到找到名為path的變量。點選“編輯”(edit)按鈕(參見圖2-2)。将jdk\bin目錄增加到路徑最前面,并用一個分号分隔新增的這一項,如下所示:

圖2-2 windows 7中設定path環境變量

注意要把jdk替換為具體的java安裝路徑,如c:\java\jdk1.8.0_31。如果忽視前面的建議,想要保留program files部分,則要把整個路徑用雙引号引起來:"c:\program files\java\jdk1.8.0_31\bin";其他目錄。

儲存所做的設定。之後新打開的所有控制台視窗都會有正确的路徑。

可以如下測試設定是否正确:打開一個終端視窗,鍵入:

然後按Enter鍵。應該能看到顯示以下資訊:

如果得到諸如“javac: command not found”(javac::指令未找到)或“the name specif?ied is not recognized as an internal or external command, operable program or batch f?ile”(指定名不是一個内部或外部指令、可執行的程式或批檔案),就需要退回去反複檢查你的安裝。

2.1.3 安裝庫源檔案和文檔

庫源檔案在jdk中以一個壓縮檔案src.zip的形式釋出,必須将其解壓縮後才能夠通路源代碼。建議按照下面所述的步驟進行操作。很簡單:

1)確定jdk已經安裝,并且jdk/bin目錄在執行路徑中。

2)在主目錄中建立一個目錄javasrc。如果願意,可以在一個終端視窗完成這個步驟。

3)在jdk目錄下找到檔案src.zip。

4)将src.zip檔案解壓縮到javasrc目錄。在一個終端視窗中,可以執行以下指令:

提示:src.zip檔案中包含了所有公共類庫的源代碼。要想獲得更多的源代碼(例如:編譯器、虛拟機、本地方法以及私有輔助類),請通路網站:http://jdk8.java.net。

文檔包含在一個壓縮檔案中,它是一個獨立于jdk的壓縮檔案。可以直接從網站http://www.oracle.com/technetwork/java/javase/downloads下載下傳這個文檔。操作步驟如下:

1)下載下傳文檔壓縮檔案。這個檔案名為jdk-version-docs-all.zip,其中的version表示版本号,例如8u31。

2)解壓縮這個檔案,将doc目錄重命名為一個更有描述性的名字,如javadoc。如果願意,可以從指令行完成這個工作:

這裡version是相應的版本号。

3)在浏覽器中導航到javadoc/api/index.html,将這個頁面增加到書簽。

還要安裝本書的程式示例。可以從http://horstmann.com/corejava下載下傳示例。這些程式打包在一個zip檔案corejava.zip中。可以将程式解壓縮到你的主目錄。它們會放在目錄corejava中。如果願意,可以從指令行完成這個工作:

2.2 使用指令行工具

如果在此之前有過使用microsoft visual studio等開發環境程式設計的經驗,你可能會習慣于有一個内置文本編輯器、用于編譯和啟動程式的菜單以及調試工具的系統。jdk完全沒有這些功能。所有工作都要在終端視窗中鍵入指令來完成。這聽起來很麻煩,不過确實是一個基本技能。第一次安裝java時,你可能希望在安裝開發環境之前先檢查java的安裝是否正确。另外,通過自己執行基本步驟,你可以更好地了解開發環境的背景工作。

不過,掌握了編譯和運作java程式的基本步驟之後,你可能就會希望使用專業的開發環境。下一節會介紹如何使用開發環境。

首先介紹較難的方法:從指令行編譯并運作java程式。

1)打開一個終端視窗。

2)進入corejava/v1ch02/welcome目錄(corejava是安裝本書示例源代碼的目錄,請參看2.1.3節)。

3)鍵入下面的指令:

然後,将會在終端視窗中看到圖2-3所示的輸出。

圖2-3 編譯并運作welcome.java

祝賀你!你已經編譯并運作了第一個java程式。

那麼,剛才都進行了哪些操作呢?javac程式是一個java編譯器。它将檔案welcome.java編譯成welcome.class。java程式啟動java虛拟機。虛拟機執行編譯器放在class檔案中的位元組碼。

welcome程式非常簡單。它隻是向控制台輸出了一條消息。你可能想檢視程式清單2-1的程式代碼。(在下一章中,将解釋它是如何工作的。)

程式清單2-1 welcome/welcome.java

在使用可視化開發環境的年代,許多程式員對于在終端視窗中運作程式已經很生疏了。常常會出現很多錯誤,最終導緻令人沮喪的結果。

一定要注意以下幾點:

如果手工輸入源程式,一定要注意大小寫。尤其是類名為welcome,而不是welcome或welcome。

編譯器需要一個檔案名(welcome.java),而運作程式時,隻需要指定類名(welcome),不要帶擴充名.java或.class。

如果看到諸如bad command or f?ile name或javac:command not found這類消息,就要傳回去反複檢查安裝是否有問題,特别是執行路徑的設定。

如果javac報告了一個錯誤,指出無法找到welcome.java,就應該檢查目錄中是否存在這個檔案。

在linux環境下,檢查welcome.java是否以正确的大寫字母開頭。

在windows環境下,使用指令dir,而不要使用圖形浏覽器工具。有些文本編輯器(特别是notepad)在每個檔案名後面要添加擴充名.txt。如果使用notepad編輯welcome.java就會存為welcome.java.txt。對于預設的windows設定,浏覽器與notepad都隐含.txt擴充名,這是因為這個擴充名屬于“已知的檔案類型”。此時,需要重新命名這個檔案,使用指令ren,或是另存一次,為檔案名加一對雙引号,如:“welcome.java”。

如果運作程式之後,收到關于java.lang.noclassdeffounderror的錯誤消息,就應該仔細地檢查出問題的類的名字。

如果收到關于welcome(w為小寫)的錯誤消息,就應該重新執行指令:java welcome(w為大寫)。記住,java區分大小寫。

如果收到有關welcome/java的錯誤資訊,這說明你錯誤地鍵入了java welcome.java,應該重新執行指令java welcome。

如果鍵入java welcome,而虛拟機沒有找到welcome類,就應該檢查一下是否有人設定了系統的classpath環境變量(将這個變量設定為全局并不是一個提倡的做法,然而,windows中有些比較差的軟體安裝程式就是這樣做的)。可以像設定path環境變量一樣設定classpath,不過這裡将删除這個設定。

提示:在http://docs.oracle.com/javase/tutorial/getstarted/cupojava/上有一個很好的教程。其中提到了初學者經常容易犯的一些錯誤。

2.3 使用內建開發環境

上一節中,你已經了解了如何從指令行編譯和運作一個java程式。這是一個很有用的技能,不過對于大多數日常工作來說,都應當使用內建開發環境。這些環境非常強大,也很友善,不使用這些環境有些不合情理。我們可以免費得到一些很棒的開發環境,如eclipse、netbeans和intellij idea程式。這一章中,我們将學習如何從eclipse起步。當然,如果你喜歡其他開發環境,學習本書時也完全可以使用你喜歡的環境。

本節将介紹如何使用eclipse編譯一個程式。eclipse是一個可以從網站http://eclipse.org/downloads上免費下載下傳得到的內建開發環境。eclipse已經有面向linux、mac os x、solaris和windows的版本。通路下載下傳網站時,選擇“eclipse ide for java developers”。再根據你的作業系統選擇32位或64位版本。

将eclipse解壓縮到你選擇的位置,執行這個zip檔案中的eclipse程式。

下面是用eclipse編寫程式的一般步驟。

1)啟動eclipse之後,從菜單選擇file→ new→project。

2)從向導對話框中選擇java project(如圖2-4所示)。

3)點選next按鈕,不選中“use default location”複選框。點選browse導航到corejava/v1ch02/welcome目錄(見圖2-5)。

圖2-5 配置eclipse工程

4)點選finish按鈕。這個工程已經建立完成了。

5)點選工程視窗左邊窗格中的三角,直到找到welcome.java并輕按兩下。現在應該看到帶有程式代碼的視窗了(如圖2-6所示)。

圖2-6 使用eclipse編輯源檔案

6)用滑鼠右鍵點選最左側窗格中的工程名(welcome),選擇run→run as→java application。程式輸出會顯示在控制台窗格中。

可以假定,這個程式沒有輸入錯誤或bug(畢竟,這段代碼隻有幾行)。為了說明問題,假定在代碼中不小心出現了錄入錯誤(或者甚至文法錯誤)。試着将原來的程式修改一下,讓它包含一些錄入錯誤,例如,将string的大小寫弄錯:

注意string下面的波折線。點選源代碼下标簽頁中的problems,展開小三角,會看到一個錯誤消息,指出有一個未知的string類型(見圖2-7)。點選這個錯誤消息。光标會移到編輯視窗中相應的代碼行,可以在這裡糾正錯誤。利用這個特性可以快速地修正錯誤。

提示:通常,eclipse錯誤報告會伴有一個燈泡圖示。點選這個圖示可以得到一個建議解決這個錯誤的方案清單。

圖2-7 eclipse中的錯誤消息

2.4 運作圖形化應用程式

welcome程式并不會引起人們的興奮。接下來,給出一個圖形化應用程式。這個程式是一個簡單的圖像檔案檢視器(viewer),它可以加載并顯示一個圖像。首先,由指令行編譯并運作這個程式。

2)進入corejava/v1ch02/imageviewer。

3)輸入:

将彈出一個标題欄為imageviewer的新程式視窗(如圖2-8所示)。

現在,選擇file→open,然後找到一個圖像檔案并打開它(我們在同一個目錄下提供了兩個示例檔案)。要關閉這一程式,隻需要點選标題欄中的關閉按鈕或者從菜單中選擇file→exit。

下面快速地浏覽一下源代碼(程式清單2-2)。這個程式比第一個程式要長很多,但是隻要想一想用c或c++編寫同樣功能的應用程式所需要的代碼量,就不會覺得它太複雜了。本書将在第10章~第12章介紹如何編寫像這樣的圖形化應用程式。

程式清單2-2 imageviewer/imageviewer.java

2.5 建構并運作applet

本書給出的前兩個程式是java應用程式。它們與所有本地程式一樣,是獨立的程式。然而,正如第1章提到的,有關java的大量宣傳都在炫耀java在浏覽器中運作applet的能力。如果你對“過去的記憶”感興趣,可以繼續閱讀下面的内容來了解如何建構和運作一個applet,以及如何在web浏覽器中顯示;如果你不感興趣,完全可以跳過這個例子,直接轉到第3章。

首先,打開終端視窗并轉到corejava/v1ch02/roadapplet,然後,輸入下面的指令:

圖2-9顯示了在applet檢視器視窗中顯示的内容。這個applet圖示顯示了司機随意減速可能導緻交通擁堵的情況。1996年,applet是建立這種可視化顯示的絕佳工具。

第一條指令是大家已經非常熟悉的調用java編譯器的指令。它将roadapplet.java源檔案編譯成位元組碼檔案roadapplet.class。

不過這一次不要運作java程式。首先,使用jar工具将類檔案打包到一個“jar檔案”。然後調用appletviewer程式,這是jdk自帶的一個工具,可以用來快速測試applet。需要為這個程式指定一個html檔案名,而不是一個java類檔案名。roadapplet.html檔案的内容如本節最後的程式清單2-3所示。

程式清單2-3 roadapplet/roadapplet.html

如果熟悉html,你會注意這裡的标準html标記和applet标簽,這會告訴applet檢視器加載applet,其代碼存儲在roadapplet.jar中。applet會忽略除applet标簽外的所有html标簽。

當然,applet要在浏覽器中檢視。遺憾的是,現在很多浏覽器并不提供java支援,或者啟用java很困難。對此,最好使用firefox。

如果使用windows或mac os x,firefox會自動啟用計算機上安裝的java。在linux上,則需要用下面的指令啟用這個插件:

作為檢查,可以在位址欄鍵入about:plugins,查找java插件。確定使用這個插件的java se 8版本,為此要查找mime類型application/x-java-applet;version=1.8。

接下來,将浏覽器導航到http://horstmann.com/applets/roadapplet/roadapplet.html,對所有安全提示都選擇接受,保證最後會顯示applet。

遺憾的是,隻是測試剛剛編譯的applet還不夠。horstmann.com伺服器上的applet有數字簽名。還必須再花一些工夫,讓java虛拟機信任的一個證書發行者信任我,為我提供一個證書,我再用這個證書為jar檔案簽名。浏覽器插件不再運作不信任的applet。與過去相比,這是一個很大的變化,原先在螢幕上繪制像素的簡單applet會限制在“沙箱”中,即使沒有簽名也可以工作。可惜,即使是oracle也不再相信沙箱的安全性了。

為了解決這個問題,可以臨時将java配置為信任本地檔案系統的applet。首先,打開java控制台。

在windows中,檢視控制台中的programs(程式)部分。

在mac上,打開system preferences(系統首選項)。

在linux上,運作jcontrol。

然後點選security(安全)标簽頁和edit site list(編輯網站清單)按鈕。再點選add(增加),并鍵入f?ile:///。點選ok,接受下一個安全提示,然後再次點選ok(見圖2-10)。

現在應該可以在浏覽器中加載檔案corejava/v1ch02/roadapplet/roadapplet.html,applet将随周圍的文本一同顯示。結果如圖2-11所示。

最後,在程式清單2-4中給出了這個applet類的代碼。現在,隻需要簡單看一下。在第13章中,還會再來介紹applet的編寫。

圖2-10 配置java信任本地applet

圖2-11 在浏覽器中運作roadapplet

程式清單2-4 roadapplet/roadapplet.java

在本章中,我們學習了有關編譯和運作java程式的機制。現在可以轉到第3章開始學習java語言了。

第3章 java的基本程式設計結構

▲  一個簡單的java應用程式 ▲  字元串

▲  注釋 ▲  輸入輸出

▲  資料類型 ▲  控制流

▲  變量 ▲  大數值

▲  運算符 ▲  數組

現在,假定已經成功地安裝了jdk,并且能夠運作第2章中給出的示例程式。我們從現在開始将介紹java應用程式設計。本章主要介紹程式設計的基本概念(如資料類型、分支以及循環)在java中的實作方式。

非常遺憾,需要告誡大家,使用java編寫gui應用程式并不是一件很容易的事情,程式設計者需要掌握很多相關的知識才能夠建立視窗、添加文本框以及能響應的按鈕等。介紹基于gui的java應用程式設計技術與本章将要介紹的程式設計基本概念相差甚遠,是以本章給出的所有示例都是為了說明一些相關概念而設計的“玩具式”程式,它們僅僅使用終端視窗提供輸入輸出。

最後需要說明,對于一個有c++程式設計經驗的程式員來說,本章的内容隻需要浏覽一下,應該重點閱讀散布在正文中的c/c++注釋。對于具有使用visual basic等其他程式設計背景的程式員來說,可能會發現其中的絕大多數概念都很熟悉,但是在文法上有比較大的差異,是以,需要非常仔細地閱讀本章的内容。

3.1 一個簡單的java應用程式

下面看一個最簡單的java應用程式,它隻發送一條消息到控制台視窗中:

這個程式雖然很簡單,但所有的java應用程式都具有這種結構,還是值得花一些時間來研究。首先,java區分大小寫。如果出現了大小寫拼寫錯誤(例如,将main拼寫成main),程式将無法運作。

下面逐行地檢視一下這段源代碼。關鍵字public稱為通路修飾符(access modif?ier),這些修飾符用于控制程式的其他部分對這段代碼的通路級别。在第5章中将會更加詳細地介紹通路修飾符的具體内容。關鍵字class表明java程式中的全部内容都包含在類中。這裡,隻需要将類作為一個加載程式邏輯的容器,程式邏輯定義了應用程式的行為。在第4章中将會用大量的篇幅介紹java類。正如第1章所述,類是建構所有java應用程式和applet的建構塊。java應用程式中的全部内容都必須放置在類中。

關鍵字class後面緊跟類名。java中定義類名的規則很寬松。名字必須以字母開頭,後面可以跟字母和數字的任意組合。長度基本上沒有限制。但是不能使用java保留字(例如,public或class)作為類名(保留字清單請參看附錄a)。

标準的命名規範為(類名firstsample就遵循了這個規範):類名是以大寫字母開頭的名詞。如果名字由多個單詞組成,每個單詞的第一個字母都應該大寫(這種在一個單詞中間使用大寫字母的方式稱為駱駝命名法。以其自身為例,應該寫成camelcase)。

源代碼的檔案名必須與公共類的名字相同,并用.java作為擴充名。是以,存儲這段源代碼的檔案名必須為firstsample.java(再次提醒大家注意,大小寫是非常重要的,千萬不能寫成f?irstsample.java)。

如果已經正确地命名了這個檔案,并且源代碼中沒有任何錄入錯誤,在編譯這段源代碼之後就會得到一個包含這個類位元組碼的檔案。java編譯器将位元組碼檔案自動地命名為firstsample. class,并與源檔案存儲在同一個目錄下。最後,使用下面這行指令運作這個程式:

(請記住,不要添加.class擴充名。)程式執行之後,控制台上将會顯示“we will not use ‘hello,world’!”。

當使用

運作已編譯的程式時,java虛拟機将從指定類中的main方法開始執行(這裡的“方法”就是java中所說的“函數”),是以為了代碼能夠執行,在類的源檔案中必須包含一個main方法。當然,也可以将使用者自定義的方法添加到類中,并且在main方法中調用它們(第4章将講述如何自定義方法)。

注釋:根據java語言規範,main方法必須聲明為public(java語言規範是描述java語言的官方文檔。可以從網站http://docs.oracle.com/javase/specs上閱讀或下載下傳)。

不過,當main方法不是public時,有些版本的java解釋器也可以執行java應用程式。有個程式員報告了這個bug。如果感興趣的話,可以在網站http://bugs.java.com/ bugdatabase/ index.jsp上輸入bug号碼4252539檢視。這個bug被标明“關閉,不予修複。”sun公司的工程師解釋說:java虛拟機規範(在http://docs.oracle.com/javase/specs/jvms/se8/html)并沒有要求main方法一定是public,并且“修複這個bug有可能帶來其他的隐患”。好在,這個問題最終得到了解決。在java se 1.4及以後的版本中強制main方法是public的。

從上面這段話可以發現一個問題的兩個方面。一方面讓品質保證工程師判斷在bug報告中是否存在問題是一件很頭痛的事情,這是因為其工作量很大,并且工程師對java的所有細節也未必了解得很清楚。另一方面,sun公司在java開源很久以前就把bug報告及其解決方案放到網站上讓所有人監督檢查,這是一種非常了不起的舉動。某些情況下,sun甚至允許程式員為他們最厭惡的bug投票,并用投票結果來決定釋出的下一個jdk版本将修複哪些bug。

需要注意源代碼中的括号{ }。在java中,像在c/c++中一樣,用大括号劃分程式的各個部分(通常稱為塊)。java中任何方法的代碼都用“{”開始,用“}”結束。

大括号的使用風格曾經引發過許多無意義的争論。我們的習慣是把比對的大括号上下對齊。不過,由于空白符會被java編譯器忽略,是以可以選用自己喜歡的大括号風格。在下面講述各種循環語句時,我們還會詳細地介紹大括号的使用。

我們暫且不去理睬關鍵字static void,而僅把它們當作編譯java應用程式必要的部分就行了。在學習完第4章後,這些内容的作用就會揭曉。現在需要記住:每個java應用程式都必須有一個main方法,其聲明格式如下所示:

c++注釋:作為一名c++程式員,一定知道類的概念。java的類與c++的類很相似,但還是有些差異會使人感到困惑。例如,java中的所有函數都屬于某個類的方法(标準術語将其稱為方法,而不是成員函數)。是以,java中的main方法必須有一個外殼類。讀者有可能對c++中的靜态成員函數(static member functions)十分熟悉。這些成員函數定義在類的内部,并且不對對象進行操作。java中的main方法必須是靜态的。最後,與c/c++一樣,關鍵字void表示這個方法沒有傳回值,所不同的是main方法沒有為作業系統傳回“退出代碼”。如果main方法正常退出,那麼java應用程式的退出代碼為0,表示成功地運作了程式。如果希望在終止程式時傳回其他的代碼,那就需要調用system.exit方法。

接下來,研究一下這段代碼:

一對大括号表示方法體的開始與結束,在這個方法中隻包含一條語句。與大多數程式設計語言一樣,可以将java語句看成是這種語言的句子。在java中,每個句子必須用分号結束。特别需要說明,回車不是語句的結束标志,是以,如果需要可以将一條語句寫在多行上。

在上面這個main方法體中隻包含了一條語句,其功能是:将一個文本行輸出到控制台上。

在這裡,使用了system.out對象并調用了它的println方法。注意,點号(·)用于調用方法。java使用的通用文法是

這等價于函數調用。

在這個示例中,調用了println方法并傳遞給它一個字元串參數。這個方法将傳遞給它的字元串參數顯示在控制台上。然後,終止這個輸出行,使得每次調用println都會在新的一行上顯示輸出。需要注意一點,java與c/c++一樣,都采用雙引号分隔字元串。(本章稍後将會詳細地講解有關字元串的知識)。

與其他程式設計語言中的函數一樣,在java的方法中,可以沒有參數,也可以有一個或多個參數(有的程式員把參數叫做實參)。對于一個方法,即使沒有參數也需要使用空括号。例如,不帶參數的println方法隻列印一個空行。使用下面的語句來調用:

注釋:system.out還有一個print方法,它在輸出之後不換行。例如,system.out.print(“hello”)列印“hello”之後不換行,後面的輸出緊跟在字母“o”之後。

3.2 注釋

與大多數程式設計語言一樣,java中的注釋也不會出現在可執行程式中。是以,可以在源程式中根據需要添加任意多的注釋,而不必擔心可執行代碼會膨脹。在java中,有3種标記注釋的方式。最常用的方式是使用//,其注釋内容從//開始到本行結尾。

當需要長篇的注釋時,既可以在每行的注釋前面标記//,也可以使用/*和*/将一段比較長的注釋括起來。

最後,第3種注釋可以用來自動地生成文檔。這種注釋以/**開始,以*/結束。請參見程式清單3-1。有關這種注釋的詳細内容和自動生成文檔的具體方法請參見第4章。

程式清單3-1 firstsample/firstsample.java

警告:在java中,/* */注釋不能嵌套。也就是說,不能簡單地把代碼用/*和*/括起來作為注釋,因為這段代碼本身可能也包含一個*/。

3.3 資料類型

java是一種強類型語言。這就意味着必須為每一個變量聲明一種類型。在java中,一共有8種基本類型(primitive type),其中有4種整型、2種浮點類型、1種用于表示unicode編碼的字元單元的字元類型char(請參見論述char類型的章節)和1種用于表示真值的boolean類型。

注釋:java有一個能夠表示任意精度的算術包,通常稱為“大數值”(big number)。雖然被稱為大數值,但它并不是一種新的java類型,而是一個java對象。本章稍後将會詳細地介紹它的用法。

3.3.1 整型

整型用于表示沒有小數部分的數值,它允許是負數。java提供了4種整型,具體内容如表3-1所示。

表3-1 java整型

類型 存儲需求 取值範圍

int 4位元組 -2 147 483 648~2 147 483 647(正好超過20億)

short 2位元組 -32 768~32 767

long 8位元組 -9 223 372 036 854 775 808~9 223 372 036 854 775 807

byte 1位元組 -128~127

在通常情況下,int類型最常用。但如果表示星球上的居住人數,就需要使用long類型了。byte和short類型主要用于特定的應用場合,例如,底層的檔案處理或者需要控制占用存儲空間量的大數組。

在java中,整型的範圍與運作java代碼的機器無關。這就解決了軟體從一個平台移植到另一個平台,或者在同一個平台中的不同作業系統之間進行移植給程式員帶來的諸多問題。與此相反,c和c++程式需要針對不同的處理器選擇最為高效的整型,這樣就有可能造成一個在32位處理器上運作很好的c程式在16位系統上運作卻發生整數溢出。由于java程式必須保證在所有機器上都能夠得到相同的運作結果,是以各種資料類型的取值範圍必須固定。

長整型數值有一個字尾l或l(如4000000000l)。十六進制數值有一個字首0x或0x(如0xcafe)。八進制有一個字首0,例如,010對應八進制中的8。很顯然,八進制表示法比較容易混淆,是以建議最好不要使用八進制常數。

從java 7開始,加上字首0b或0b就可以寫二進制數。例如,0b1001就是9。另外,同樣是從java 7開始,還可以為數字字面量加下劃線,如用1_000_000(或0b1111_0100_0010_0100_0000)表示一百萬。這些下劃線隻是為了讓人更易讀。java編譯器會去除這些下劃線。

c++注釋:在c和c++中,int和long等類型的大小與目标平台相關。在8086這樣的16位處理器上整型數值占2位元組;不過,在32位處理器(比如pentium或sparc)上,整型數值則為4位元組。類似地,在32位處理器上long值為4位元組,在64位處理器上則為8位元組。由于存在這些差别,這對編寫跨平台程式帶來了很大難度。在java中,所有的數值類型所占據的位元組數量與平台無關。

注意,java沒有任何無符号(unsigned)形式的int、long、short或byte類型。

3.3.2 浮點類型

浮點類型用于表示有小數部分的數值。在java中有兩種浮點類型,具體内容如表3-2所示。

表3-2 浮點類型

f?loat 4位元組 大約±3.402 823 47e+38f(有效位數為6~7位)

double 8位元組 大約±1.797 693 134 862 315 70e+308(有效位數為15位)

double表示這種類型的數值精度是f?loat類型的兩倍(有人稱之為雙精度數值)。絕大部分應用程式都采用double類型。在很多情況下,f?loat類型的精度很難滿足需求。實際上,隻有很少的情況适合使用f?loat類型,例如,需要單精度資料的庫,或者需要存儲大量資料。

f?loat類型的數值有一個字尾f或f(例如,3.14f)。沒有字尾f的浮點數值(如3.14)預設為double類型。當然,也可以在浮點數值後面添加字尾d或d(例如,3.14d)。

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

所有的浮點數值計算都遵循ieee 754規範。具體來說,下面是用于表示溢出和出錯情況的三個特殊的浮點數值:

正無窮大

負無窮大

nan(不是一個數字)

例如,一個正整數除以0的結果為正無窮大。計算0/0或者負數的平方根結果為nan。

注釋:常量double.positive_infinity、double.negative_infinity和double.nan(以及相應的float類型的常量)分别表示這三個特殊的值,但在實際應用中很少遇到。特别要說明的是,不能這樣檢測一個特定值是否等于double.nan:

所有“非數值”的值都認為是不相同的。然而,可以使用double.isnan方法:

警告:浮點數值不适用于無法接受舍入誤差的金融計算中。例如,指令system.out.println (2.0–1.1)将列印出0.8999999999999999,而不是人們想象的0.9。這種舍入誤差的主要原因是浮點數值采用二進制系統表示,而在二進制系統中無法精确地表示分數1/10。這就好像十進制無法精确地表示分數1/3一樣。如果在數值計算中不允許有任何舍入誤差,就應該使用bigdecimal類,本章稍後将介紹這個類。

3.3.3 char類型

char類型原本用于表示單個字元。不過,現在情況已經有所變化。如今,有些unicode字元可以用一個char值描述,另外一些unicode字元則需要兩個char值。有關的詳細資訊請閱讀下一節。

char類型的字面量值要用單引号括起來。例如:'a'是編碼值為65所對應的字元常量。它與"a"不同,"a"是包含一個字元a的字元串。char類型的值可以表示為十六進制值,其範圍從\u0000到\uffff。例如:\u2122表示注冊符号(tm),\u03c0表示希臘字母π。

除了轉義序列\u之外,還有一些用于表示特殊字元的轉義序列,請參看表3-3。所有這些轉義序列都可以出現在加引号的字元字面量或字元串中。例如,'\u2122'或"hello\n"。轉義序列\u還可以出現在加引号的字元常量或字元串之外(而其他所有轉義序列不可以)。例如:

就完全符合文法規則,\u005b和\u005d是[和]的編碼。

表3-3 特殊字元的轉義序列

轉義序列 名稱 unicode值 轉義序列 名稱 unicode值

\b 倒退 \u0008 \” 雙引号 \u0022

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

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

\r 回車 \u000d

警告:unicode轉義序列會在解析代碼之前得到處理。例如,"\u0022+\u0022"并不是一個由引号(u+0022)包圍加号構成的字元串。實際上,\u0022會在解析之前轉換為",這會得到""+"",也就是一個空串。

更隐秘地,一定要當心注釋中的\u。注釋

會産生一個文法錯誤,因為讀程式時\u00a0會替換為一個換行符。類似地,下面這個注釋

也會産生一個文法錯誤,因為\u後面并未跟着4個十六進制數。

3.3.4 unicode和char類型

要想弄清char類型,就必須了解unicode編碼機制。unicode打破了傳統字元編碼機制的限制。在unicode出現之前,已經有許多種不同的标準:美國的ascii、西歐語言中的iso 8859-1、俄羅斯的koi-8、中國的gb 18030和big-5等。這樣就産生了下面兩個問題:一個是對于任意給定的代碼值,在不同的編碼方案下有可能對應不同的字母;二是采用大字元集的語言其編碼長度有可能不同。例如,有些常用的字元采用單位元組編碼,而另一些字元則需要兩個或更多個位元組。

設計unicode編碼的目的就是要解決這些問題。在20世紀80年代開始啟動設計工作時,人們認為兩個位元組的代碼寬度足以對世界上各種語言的所有字元進行編碼,并有足夠的空間留給未來的擴充。在1991年釋出了unicode 1.0,當時僅占用65 536個代碼值中不到一半的部分。在設計java時決定采用16位的unicode字元集,這樣會比使用8位字元集的程式設計語言有很大的改進。

十分遺憾,經過一段時間,不可避免的事情發生了。unicode字元超過了65 536個,其主要原因是增加了大量的漢語、日語和韓語中的表意文字。現在,16位的char類型已經不能滿足描述所有unicode字元的需要了。

下面利用一些專用術語解釋一下java語言解決這個問題的基本方法。從java se 5.0開始。碼點(code point)是指與一個編碼表中的某個字元對應的代碼值。在unicode标準中,碼點采用十六進制書寫,并加上字首u+,例如u+0041就是拉丁字母a的碼點。unicode的碼點可以分成17個代碼級别(code plane)。第一個代碼級别稱為基本的多語言級别(basic multilingual plane),碼點從u+0000到u+ffff,其中包括經典的unicode代碼;其餘的16個級别碼點從u+10000到u+10ffff,其中包括一些輔助字元(supplementary character)。

utf-16編碼采用不同長度的編碼表示所有unicode碼點。在基本的多語言級别中,每個字元用16位表示,通常被稱為代碼單元(code unit);而輔助字元采用一對連續的代碼單元進行編碼。這樣構成的編碼值落入基本的多語言級别中空閑的2048位元組内,通常被稱為替代區域(surrogate area)[u+d800~u+dbff用于第一個代碼單元,u+dc00~u+dfff用于第二個代碼單元]。這樣設計十分巧妙,我們可以從中迅速地知道一個代碼單元是一個字元的編碼,還是一個輔助字元的第一或第二部分。例如,是八元數集(http://math.ucr.edu/home/baez/octonions)的一個數學符号,碼點為u+1d546,編碼為兩個代碼單元u+d835和u+dd46。(關于編碼算法的具體描述見http://en.wikipedia.org/wiki/utf-16)。

在java中,char類型描述了utf-16編碼中的一個代碼單元。

我們強烈建議不要在程式中使用char類型,除非确實需要處理utf-16代碼單元。最好将字元串作為抽象資料類型處理(有關這方面的内容将在3.6節讨論)。

3.3.5 boolean類型

boolean(布爾)類型有兩個值:false和true,用來判定邏輯條件。整型值和布爾值之間不能進行互相轉換。

c++注釋:在c++中,數值甚至指針可以代替boolean值。值0相當于布爾值false,非0值相當于布爾值true。在java中則不是這樣。是以,java程式員不會遇到下述麻煩:

在c++中這個測試可以編譯運作,其結果總是false。而在java中,這個測試将不能通過編譯,其原因是整數表達式x = 0不能轉換為布爾值。

3.4 變量

在java中,每個變量都有一個類型(type)。在聲明變量時,變量的類型位于變量名之前。這裡列舉一些聲明變量的示例:

可以看到,每個聲明以分号結束。由于聲明是一條完整的java語句,是以必須以分号結束。

變量名必須是一個以字母開頭并由字母或數字構成的序列。需要注意,與大多數程式設計語言相比,java中“字母”和“數字”的範圍更大。字母包括'a'~'z'、'a'~'z'、'_'、'$'或在某種語言中表示字母的任何unicode字元。例如,德國的使用者可以在變量名中使用字母‘?’;希臘人可以用π。同樣,數字包括'0'~'9'和在某種語言中表示數字的任何unicode字元。但'+'和'?'這樣的符号不能出現在變量名中,空格也不行。變量名中所有的字元都是有意義的,并且大小寫敏感。變量名的長度基本上沒有限制。

提示:如果想要知道哪些unicode字元屬于java中的“字母”,可以使用character類的isjavaidentif?ierstart和isjavaidentif?ierpart方法來檢查。

提示:盡管$是一個合法的java字元,但不要在你自己的代碼中使用這個字元。它隻用在java編譯器或其他工具生成的名字中。

另外,不能使用java保留字作為變量名(請參看附錄a中的保留字清單)。

可以在一行中聲明多個變量:

不過,不提倡使用這種風格。逐一聲明每一個變量可以提高程式的可讀性。

注釋:如前所述,變量名對大小寫敏感,例如,hireday和hireday是兩個不同的變量名。在對兩個不同的變量進行命名時,最好不要隻存在大小寫上的差異。不過,在有些時候,确實很難給變量取一個好的名字。于是,許多程式員将變量名命名為類型名,例如:

還有一些程式員更加喜歡在變量名前加上字首“a”:

3.4.1 變量初始化

聲明一個變量之後,必須用指派語句對變量進行顯式初始化,千萬不要使用未初始化的變量。例如,java編譯器認為下面的語句序列是錯誤的:

要想對一個已經聲明過的變量進行指派,就需要将變量名放在等号(=)左側,相應取值的java表達式放在等号的右側。

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

最後,在java中可以将聲明放在代碼中的任何地方。例如,下列代碼的書寫形式在java中是完全合法的:

在java中,變量的聲明盡可能地靠近變量第一次使用的地方,這是一種良好的程式編寫風格。

c++注釋:c和c++區分變量的聲明與定義。例如:

是一個定義,而

是一個聲明。在java中,不區分變量的聲明與定義。

3.4.2 常量

在java中,利用關鍵字f?inal訓示常量。例如:

關鍵字f?inal表示這個變量隻能被指派一次。一旦被指派之後,就不能夠再更改了。習慣上,常量名使用全大寫。

在java中,經常希望某個常量可以在一個類中的多個方法中使用,通常将這些常量稱為類常量。可以使用關鍵字static f?inal設定一個類常量。下面是使用類常量的示例:

需要注意,類常量的定義位于main方法的外部。是以,在同一個類的其他方法中也可以使用這個常量。而且,如果一個常量被聲明為public,那麼其他類的方法也可以使用這個常量。在這個示例中,constants2.cm_per-inch就是這樣一個常量。

c++注釋:const是java保留的關鍵字,但目前并沒有使用。在java中,必須使用f?inal定義常量。

3.5 運算符

在java中,使用算術運算符+、-、*、/表示加、減、乘、除運算。當參與/運算的兩個操作數都是整數時,表示整數除法;否則,表示浮點除法。整數的求餘操作(有時稱為取模)用%表示。例如,15/2等于7,15%2等于1,15.0/2等于7.5。

需要注意,整數被0除将會産生一個異常,而浮點數被0除将會得到無窮大或nan結果。

注釋:可移植性是java語言的設計目标之一。無論在哪個虛拟機上運作,同一運算應該得到同樣的結果。對于浮點數的算術運算,實作這樣的可移植性是相當困難的。double類型使用64位存儲一個數值,而有些處理器使用80位浮點寄存器。這些寄存器增加了中間過程的計算精度。例如,以下運算:

很多intel處理器計算x * y,并且将結果存儲在80位的寄存器中,再除以z并将結果截斷為64位。這樣可以得到一個更加精确的計算結果,并且還能夠避免産生指數溢出。但是,這個結果可能與始終在64位機器上計算的結果不一樣。是以,java虛拟機的最初規範規定所有的中間計算都必須進行截斷。這種行為遭到了數值計算團體的反對。截斷計算不僅可能導緻溢出,而且由于截斷操作需要消耗時間,是以在計算速度上實際上要比精确計算慢。為此,java程式設計語言承認了最優性能與理想結果之間存在的沖突,并給予了改進。在預設情況下,虛拟機設計者允許對中間計算結果采用擴充的精度。但是,對于使用strictfp關鍵字标記的方法必須使用嚴格的浮點計算來生成可再生的結果。例如,可以把main方法标記為

于是,在main方法中的所有指令都将使用嚴格的浮點計算。如果将一個類标記為strictfp,這個類中的所有方法都要使用嚴格的浮點計算。

實際的計算方式将取決于intel處理器的行為。在預設情況下,中間結果允許使用擴充的指數,但不允許使用擴充的尾數(intel晶片在截斷尾數時并不損失性能)。是以,這兩種方式的差別僅僅在于采用預設的方式不會産生溢出,而采用嚴格的計算有可能産生溢出。

如果沒有仔細閱讀這個注釋,也沒有什麼關系。對大多數程式來說,浮點溢出不屬于大問題。在本書中,将不使用strictfp關鍵字。

3.5.1 數學函數與常量

在math類中,包含了各種各樣的數學函數。在編寫不同類别的程式時,可能需要的函數也不同。

要想計算一個數值的平方根,可以使用sqrt方法:

注釋:println方法和sqrt方法存在微小的差異。println方法處理system.out對象。但是,math類中的sqrt方法處理的不是對象,這樣的方法被稱為靜态方法。有關靜态方法的詳細内容請參看第4章。

在java中,沒有幂運算,是以需要借助于math類的pow方法。語句:

将y的值設定為x的a次幂(xa)。pow方法有兩個double類型的參數,其傳回結果也為double類型。

f?loormod方法的目的是解決一個長期存在的有關整數餘數的問題。考慮表達式n % 2。所有人都知道,如果n是偶數,這個表達式為0;如果n是奇數,表達式則為1。當然,除非n是負數。如果n為負,這個表達式則為-1。為什麼呢?設計最早的計算機時,必須有人制定規則,明确整數除法和求餘對負數操作數該如何處理。數學家們幾百年來都知道這樣一個最優(或“歐幾裡德”)規則:餘數總是要≥0。不過,最早制定規則的人并沒有翻開數學書好好研究,而是提出了一些看似合理但實際上很不友善的規則。

下面考慮這樣一個問題:計算一個時鐘時針的位置。這裡要做一個時間調整,而且要歸一化為一個0~11之間的數。這很簡單:(position + adjustment) % 12。不過,如果這個調整為負會怎麼樣呢?你可能會得到一個負數。是以要引入一個分支,或者使用((position + adjustment) % 12 + 12) % 12。不管怎樣,總之都很麻煩。

f?loormod方法就讓這個問題變得容易了:f?loormod(position + adjustment, 12)總會得到一個0~11之間的數。(遺憾的是,對于負除數,f?loormod會得到負數結果,不過這種情況在實際中很少出現。)

math類提供了一些常用的三角函數:

還有指數函數以及它的反函數——自然對數以及以10為底的對數:

最後,java還提供了兩個用于表示π和e常量的近似值:

提示:不必在數學方法名和常量名前添加字首“math”,隻要在源檔案的頂部加上下面這行代碼就可以了。

例如:

在第4章中将讨論靜态導入。

注釋:在math類中,為了達到最快的性能,所有的方法都使用計算機浮點單元中的例程。如果得到一個完全可預測的結果比運作速度更重要的話,那麼就應該使用strictmath類。它使用“自由釋出的math庫”(fdlibm)實作算法,以確定在所有平台上得到相同的結果。有關這些算法的源代碼請參看www.netlib.org/fdlibm(當fdlibm為一個函數提供了多個定義時,strictmath類就會遵循ieee 754版本,它的名字将以“e”開頭)。

3.5.2 數值類型之間的轉換

經常需要将一種數值類型轉換為另一種數值類型。圖3-1給出了數值類型之間的合法轉換。

在圖3-1中有6個實心箭頭,表示無資訊丢失的轉換;有3個虛箭頭,表示可能有精度損失的轉換。例如,123 456 789是一個大整數,它所包含的位數比f?loat類型所能夠表達的位數多。當将這個整型數值轉換為f?loat類型時,将會得到同樣大小的結果,但卻失去了一定的精度。

當使用上面兩個數值進行二進制操作時(例如n + f,n是整數,f是浮點數),先要将兩個操作數轉換為同一種類型,然後再進行計算。

如果兩個操作數中有一個是double類型,另一個操作數就會轉換為double類型。

否則,如果其中一個操作數是f?loat類型,另一個操作數将會轉換為f?loat類型。

否則,如果其中一個操作數是long類型,另一個操作數将會轉換為long類型。

否則,兩個操作數都将被轉換為int類型。

圖3-1 數值類型之間的合法轉換

3.5.3 強制類型轉換

在上一小節中看到,在必要的時候,int類型的值将會自動地轉換為double類型。但另一方面,有時也需要将double轉換成int。在java中,允許進行這種數值之間的類型轉換。當然,有可能會丢失一些資訊。在這種情況下,需要通過強制類型轉換(cast)實作這個操作。強制類型轉換的文法格式是在圓括号中給出想要轉換的目标類型,後面緊跟待轉換的變量名。例如:

這樣,變量nx的值為9。強制類型轉換通過截斷小數部分将浮點值轉換為整型。

如果想對浮點數進行舍入運算,以便得到最接近的整數(在很多情況下,這種操作更有用),那就需要使用math.round方法:

現在,變量nx的值為10。當調用round的時候,仍然需要使用強制類型轉換(int)。其原因是round方法傳回的結果為long類型,由于存在資訊丢失的可能性,是以隻有使用顯式的強制類型轉換才能夠将long類型轉換成int類型。

警告:如果試圖将一個數值從一種類型強制轉換為另一種類型,而又超出了目标類型的表示範圍,結果就會截斷成一個完全不同的值。例如,(byte)300的實際值為44。

c++注釋:不要在boolean類型與任何數值類型之間進行強制類型轉換,這樣可以防止發生錯誤。隻有極少數的情況才需要将布爾類型轉換為數值類型,這時可以使用條件表達式b?1:0。

3.5.4 結合指派和運算符

可以在指派中使用二進制運算符,這是一種很友善的簡寫形式。例如,

等價于:

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

注釋:如果運算符得到一個值,其類型與左側操作數的類型不同,就會發生強制類型轉換。例如,如果x是一個int,則以下語句

是合法的,将把x設定為(int)(x + 3.5)。

3.5.5 自增與自減運算符

當然,程式員都知道加1、減1是數值變量最常見的操作。在java中,借鑒了c和c++的做法,也提供了自增、自減運算符:n++将變量n的目前值加1,n--則将n的值減1。例如,以下代碼:

将n的值改為13。由于這些運算符會改變變量的值,是以它們的操作數不能是數值。例如,4++就不是一個合法的語句。

實際上,這些運算符有兩種形式;上面介紹的是運算符放在操作數後面的“字尾”形式。還有一種“字首”形式:++n。字尾和字首形式都會使變量值加1或減1。但用在表達式中時,二者就有差別了。字首形式會先完成加1;而字尾形式會使用變量原來的值。

建議不要在表達式中使用++,因為這樣的代碼很容易讓人困惑,而且會帶來煩人的bug。

3.5.6 關系和boolean運算符

java包含豐富的關系運算符。要檢測相等性,可以使用兩個等号==。例如,

的值為false。

另外可以使用!=檢測不相等。例如,

的值為true。

最後,還有經常使用的< (小于)、>(大于)、<= (小于等于)和>= (大于等于)運算符。

java沿用了c++的做法,使用&&表示邏輯“與”運算符,使用||表示邏輯“或”運算符。從!=運算符可以想到,感歎号!就是邏輯非運算符。&&和||運算符是按照“短路”方式來求值的:如果第一個操作數已經能夠确定表達式的值,第二個操作數就不必計算了。如果用&&運算符合并兩個表達式,

而且已經計算得到第一個表達式的真值為false,那麼結果就不可能為true。是以,第二個表達式就不必計算了。可以利用這一點來避免錯誤。例如,在下面的表達式中:

如果x等于0,那麼第二部分就不會計算。是以,如果x為0,也就不會計算1 / x ,除以0的錯誤就不會出現。

類似地,如果第一個表達式為true,expression1 || expression2的值就自動為true,而無需計算第二個表達式。

最後一點,java支援三元操作符?: ,這個操作符有時很有用。如果條件為true,下面的表達式

就為第一個表達式的值,否則計算為第二個表達式的值。例如,

會傳回x和y中較小的一個。

3.5.7 位運算符

處理整型類型時,可以直接對組成整型數值的各個位完成操作。這意味着可以使用掩碼技術得到整數中的各個位。位運算符包括:

這些運算符按位模式處理。例如,如果n是一個整數變量,而且用二進制表示的n從右邊數第4位為1,則

會傳回1,否則傳回0。利用&并結合使用适當的2的幂,可以把其他位掩掉,而隻保留其中的某一位。

注釋:應用在布爾值上時,&和|運算符也會得到一個布爾值。這些運算符與&&和||運算符很類似,不過&和|運算符不采用“短路”方式來求值,也就是說,得到計算結果之前兩個操作數都需要計算。

另外,還有>>和<<運算符将位模式左移或右移。需要建立位模式來完成位掩碼時,這兩個運算符會很友善:

最後,>>>運算符會用0填充高位,這與>>不同,它會用符号位填充高位。不存在<<<運算符。

警告:移位運算符的右操作數要完成模32的運算(除非左操作數是long類型,在這種情況下需要對右操作數模64)。例如,1 << 35的值等同于1 << 3或8。

c++注釋:在c/c++中,不能保證>>是完成算術移位(擴充符号位)還是邏輯移位(填充0)。實作者可以選擇其中更高效的任何一種做法。這意味着c/c++ >>運算符對于負數生成的結果可能會依賴于具體的實作。java則消除了這種不确定性。

3.5.8 括号與運算符級别

表3-4給出了運算符的優先級。如果不使用圓括号,就按照給出的運算符優先級次序進行計算。同一個級别的運算符按照從左到右的次序進行計算(除了表中給出的右結合運算符外。)例如,由于&&的優先級比||的優先級高,是以表達式

等價于

又因為+=是右結合運算符,是以表達式

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

c++注釋:與c或c++不同,java不使用逗号運算符。不過,可以在for語句的第1和第3部分中使用逗号分隔表達式清單。

表3-4 運算符優先級

運 算 符 結合性

[ ] . ( ) (方法調用) 從左向右

! ~ ++ -- + (一進制運算) - (一進制運算) ( ) (強制類型轉換) new 從右向左

*/ % 從左向右

+ - 從左向右

<<  >>  >>> 從左向右

<  <=  >  >=  instanceof 從左向右

= =  != 從左向右

& 從左向右

^ 從左向右

| 從左向右

&&  從左向右

|| 從左向右

?: 從右向左

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

3.5.9 枚舉類型

有時候,變量的取值隻在一個有限的集合内。例如:銷售的服裝或比薩餅隻有小、中、大和超大這四種尺寸。當然,可以将這些尺寸分别編碼為1、2、3、4或s、m、l、x。但這樣存在着一定的隐患。在變量中很可能儲存的是一個錯誤的值(如0或m)。

針對這種情況,可以自定義枚舉類型。枚舉類型包括有限個命名的值。例如,

現在,可以聲明這種類型的變量:

size類型的變量隻能存儲這個類型聲明中給定的某個枚舉值,或者null值,null表示這個變量沒有設定任何值。

有關枚舉類型的詳細内容将在第5章介紹。

3.6 字元串

從概念上講,java字元串就是unicode字元序列。例如,串“java\u2122”由5個unicode字元j、a、v、a和tm。java沒有内置的字元串類型,而是在标準java類庫中提供了一個預定義類,很自然地叫做string。每個用雙引号括起來的字元串都是string類的一個執行個體:

3.6.1 子串

string類的substring方法可以從一個較大的字元串提取出一個子串。例如:

建立了一個由字元“hel”組成的字元串。

substring方法的第二個參數是不想複制的第一個位置。這裡要複制位置為0、1和2(從0到2,包括0和2)的字元。在substring中從0開始計數,直到3為止,但不包含3。

substring的工作方式有一個優點:容易計算子串的長度。字元串s.substring(a, b)的長度為b-a。例如,子串“hel”的長度為3-0=3。

3.6.2 拼接

與絕大多數的程式設計語言一樣,java語言允許使用+号連接配接(拼接)兩個字元串。

上述代碼将“expletivedeleted”賦給變量message(注意,單詞之間沒有空格,+号按照給定的次序将兩個字元串拼接起來)。

當将一個字元串與一個非字元串的值進行拼接時,後者被轉換成字元串(在第5章中可以看到,任何一個java對象都可以轉換成字元串)。例如:

rating設定為“pg13”。

這種特性通常用在輸出語句中。例如:

這是一條合法的語句,并且将會列印出所希望的結果(因為單詞is後面加了一個空格,輸出時也會加上這個空格)。

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

3.6.3 不可變字元串

string類沒有提供用于修改字元串的方法。如果希望将greeting的内容修改為“help!”,不能直接地将greeting的最後兩個位置的字元修改為‘p’和‘!’。這對于c程式員來說,将會感到無從下手。如何修改這個字元串呢?在java中實作這項操作非常容易。首先提取需要的字元,然後再拼接上替換的字元串:

上面這條語句将greeting目前值修改為“help!”。

由于不能修改java字元串中的字元,是以在java文檔中将string類對象稱為不可變字元串,如同數字3永遠是數字3一樣,字元串“hello”永遠包含字元h、e、l、l和o的代碼單元序列,而不能修改其中的任何一個字元。當然,可以修改字元串變量greeting,讓它引用另外一個字元串,這就如同可以将存放3的數值變量改成存放4一樣。

這樣做是否會降低運作效率呢?看起來好像修改一個代碼單元要比建立一個新字元串更加簡潔。答案是:也對,也不對。的确,通過拼接“hel”和“p!”來建立一個新字元串的效率确實不高。但是,不可變字元串卻有一個優點:編譯器可以讓字元串共享。

為了弄清具體的工作方式,可以想象将各種字元串存放在公共的存儲池中。字元串變量指向存儲池中相應的位置。如果複制一個字元串變量,原始字元串與複制的字元串共享相同的字元。

總而言之,java的設計者認為共享帶來的高效率遠遠勝過于提取、拼接字元串所帶來的低效率。檢視一下程式會發現:很少需要修改字元串,而是往往需要對字元串進行比較(有一種例外情況,将來自于檔案或鍵盤的單個字元或較短的字元串彙內建字元串。為此,java提供了一個獨立的類,在3.6.9節中将詳細介紹)。

c++注釋:在c程式員第一次接觸java字元串的時候,常常會感到迷惑,因為他們總将字元串認為是字元型數組:

這種認識是錯誤的,java字元串大緻類似于char*指針,

當采用另一個字元串替換greeting的時候,java代碼大緻進行下列操作:

的确,現在greeting指向字元串“help!”。即使一名最頑固的c程式員也得承認java文法要比一連串的strncpy調用舒适得多。然而,如果将greeting賦予另外一個值又會怎樣呢?

這樣做會不會産生記憶體遺漏呢?畢竟,原始字元串放置在堆中。十分幸運,java将自動地進行垃圾回收。如果一塊記憶體不再使用了,系統最終會将其回收。

對于一名使用ansi c++定義的string類的c++程式員,會感覺使用java的string類型更為舒适。c++ string對象也自動地進行記憶體的配置設定與回收。記憶體管理是通過構造器、指派操作和析構器顯式執行的。然而,c++字元串是可修改的,也就是說,可以修改字元串中的單個字元。

3.6.4 檢測字元串是否相等

可以使用equals方法檢測兩個字元串是否相等。對于表達式:

如果字元串s與字元串t相等,則傳回true;否則,傳回false。需要注意,s與t可以是字元串變量,也可以是字元串字面量。例如,下清單達式是合法的:

要想檢測兩個字元串是否相等,而不區分大小寫,可以使用equalsignorecase方法。

一定不要使用==運算符檢測兩個字元串是否相等!這個運算符隻能夠确定兩個字元串是否放置在同一個位置上。當然,如果字元串放置在同一個位置上,它們必然相等。但是,完全有可能将内容相同的多個字元串的拷貝放置在不同的位置上。

如果虛拟機始終将相同的字元串共享,就可以使用==運算符檢測是否相等。但實際上隻有字元串常量是共享的,而+或substring等操作産生的結果并不是共享的。是以,千萬不要使用==運算符測試字元串的相等性,以免在程式中出現糟糕的bug。從表面上看,這種bug很像随機産生的間歇性錯誤。

c++注釋:對于習慣使用c++的string類的人來說,在進行相等性檢測的時候一定要特别小心。c++的string類重載了==運算符以便檢測字元串内容的相等性。可惜java沒有采用這種方式,它的字元串“看起來、感覺起來”與數值一樣,但進行相等性測試時,其操作方式又類似于指針。語言的設計者本應該像對+那樣也進行特殊處理,即重定義==運算符。當然,每一種語言都會存在一些不太一緻的地方。

c程式員從不使用==對字元串進行比較,而使用strcmp函數。java的compareto方法與strcmp完全類似,是以,可以這樣使用:

不過,使用equals看起來更為清晰。

3.6.5 空串與null串

空串""是長度為0的字元串。可以調用以下代碼檢查一個字元串是否為空:

空串是一個java對象,有自己的串長度(0)和内容(空)。不過,string變量還可以存放一個特殊的值,名為null,這表示目前沒有任何對象與該變量關聯(關于null的更多資訊請參見第4章)。要檢查一個字元串是否為null,要使用以下條件:

有時要檢查一個字元串既不是null也不為空串,這種情況下就需要使用以下條件:

首先要檢查str不為null。在第4章會看到,如果在一個null值上調用方法,會出現

錯誤。

3.6.6 碼點與代碼單元

java字元串由char值序列組成。從3.3.3節“char類型”已經看到,char資料類型是一個采用utf-16編碼表示unicode碼點的代碼單元。大多數的常用unicode字元使用一個代碼單元就可以表示,而輔助字元需要一對代碼單元表示。

length方法将傳回采用utf-16編碼表示的給定字元串所需要的代碼單元數量。例如:

要想得到實際的長度,即碼點數量,可以調用:

調用s.charat(n)将傳回位置n的代碼單元,n介于0~s.length()-1之間。例如:

要想得到第i個碼點,應該使用下列語句

注釋:類似于c和c++,java對字元串中的代碼單元和碼點從0開始計數。

為什麼會對代碼單元如此大驚小怪?請考慮下列語句:

使用utf-16編碼表示字元(u+1d546)需要兩個代碼單元。調用

傳回的不是一個空格,而是的第二個代碼單元。為了避免這個問題,不要使用char類型。這太底層了。

如果想要周遊一個字元串,并且依次檢視每一個碼點,可以使用下列語句:

可以使用下列語句實作回退操作:

顯然,這很麻煩。更容易的辦法是使用codepoints方法,它會生成一個int值的“流”,每個int值對應一個碼點。(流将在卷Ⅱ的第2章中讨論)。可以将它轉換為一個數組(見3.10節),再完成周遊。

反之,要把一個碼點數組轉換為一個字元串,可以使用構造函數(我們将在第4章詳細讨論構造函數和new操作符)。

3.6.7 string api

java中的string類包含了50多個方法。令人驚訝的是絕大多數都很有用,可以設想使用的頻繁非常高。下面的api注釋彙總了一部分最常用的方法。

注釋:可以發現,本書中給出的api注釋會有助于了解java應用程式程式設計接口(api)。每一個api的注釋都以形如java.lang.string的類名開始。(java.lang包的重要性将在第4章給出解釋。)類名之後是一個或多個方法的名字、解釋和參數描述。

在這裡,一般不列出某個類的所有方法,而是選擇一些最常用的方法,并以簡潔的方式給予描述。完整的方法清單請參看聯機文檔(請參看3.6.8節)。

這裡還列出了所給類的版本号。如果某個方法是在這個版本之後添加的,就會給出一個單獨的版本号。

java.lang.string 1.0

char charat (int index)

傳回給定位置的代碼單元。除非對底層的代碼單元感興趣,否則不需要調用這個方法。

int codepointat(int index) 5.0

傳回從給定位置開始的碼點。

int offsetbycodepoints(int startindex, int cpcount) 5.0

傳回從startindex代碼點開始,位移cpcount後的碼點索引。

int compareto(string other)

按照字典順序,如果字元串位于other之前,傳回一個負數;如果字元串位于other之後,傳回一個正數;如果兩個字元串相等,傳回0。

intstream codepoints() 8

将這個字元串的碼點作為一個流傳回。調用toarray将它們放在一個數組中。

new string(int[] codepoints, int offset, int count) 5.0

用數組中從offset開始的count個碼點構造一個字元串。

boolean equals(object other)

如果字元串與other相等,傳回true。

boolean equalsignorecase(string other)

如果字元串與other相等(忽略大小寫),傳回true。

boolean startswith(string pref?ix)

boolean endswith(string suff?ix)

如果字元串以suff?ix開頭或結尾,則傳回true。

int index0f(string str)

int index0f(string str, int fromindex)

int index0f(int cp)

int index0f(int cp, int fromindex)

傳回與字元串str或代碼點cp比對的第一個子串的開始位置。這個位置從索引0或fromindex開始計算。如果在原始串中不存在str,傳回-1。

int lastindex0f(string str)

int lastindex0f(string str, int fromindex)

int lastindex0f(int cp)

int lastindex0f(int cp, int fromindex)

傳回與字元串str或代碼點cp比對的最後一個子串的開始位置。這個位置從原始串尾端或fromindex開始計算。

int length( )

傳回字元串的長度。

int codepointcount(int startindex, int endindex) 5.0

傳回startindex和endindex-1之間的代碼點數量。沒有配成對的代用字元将計入代碼點。

string replace(charsequence oldstring,charsequence newstring)

傳回一個新字元串。這個字元串用newstring代替原始字元串中所有的oldstring。可以用string或stringbuilder對象作為charsequence參數。

string substring(int beginindex)

string substring(int beginindex, int endindex)

傳回一個新字元串。這個字元串包含原始字元串中從beginindex到串尾或endindex–1的所有代碼單元。

string tolowercase( )

string touppercase( )

傳回一個新字元串。這個字元串将原始字元串中的大寫字母改為小寫,或者将原始字元串中的所有小寫字母改成了大寫字母。

string trim( )

傳回一個新字元串。這個字元串将删除了原始字元串頭部和尾部的空格。

string join(charsequence delimiter, charsequence... elements) 8

傳回一個新字元串,用給定的定界符連接配接所有元素。

注釋:在api注釋中,有一些charsequence類型的參數。這是一種接口類型,所有字元串都屬于這個接口。第6章将介紹更多有關接口類型的内容。現在隻需要知道隻要看到一個charsequence形參,完全可以傳入string類型的實參。

3.6.8 閱讀聯機api文檔

正如前面所看到的,string類包含許多方法。而且,在标準庫中有幾千個類,方法數量更加驚人。要想記住所有的類和方法是一件不太不可能的事情。是以,學會使用線上api文檔十分重要,從中可以查閱到标準類庫中的所有類和方法。api文檔是jdk的一部分,它是html格式的。讓浏覽器指向安裝jdk的docs/api/index.html子目錄,就可以看到如圖3-2所示的螢幕。

圖3-2 api文檔的三個窗格

可以看到,螢幕被分成三個窗框。在左上方的小窗框中顯示了可使用的所有包。在它下面稍大的窗框中列出了所有的類。點選任何一個類名之後,這個類的api文檔就會顯示在右側的大窗框中(請參看圖3-3)。例如,要獲得有關string類方法的更多資訊,可以滾動第二個窗框,直到看見string連結為止,然後點選這個連結。

接下來,滾動右面的窗框,直到看見按字母順序排列的所有方法為止(請參看圖3-4)。點選任何一個方法名便可以檢視這個方法的較長的描述(參見圖3-5)。例如,如果點選comparetoignorecase連結,就會看到comparetoignorecase方法的描述。

圖3-3 string類的描述

提示:馬上在浏覽器中将docs/api/index.html頁面建一個書簽。

圖3-4 string類方法的小結

圖3-5 string方法的較長的描述

3.6.9 建構字元串

有些時候,需要由較短的字元串建構字元串,例如,按鍵或來自檔案中的單詞。采用字元串連接配接的方式達到此目的效率比較低。每次連接配接字元串,都會建構一個新的string對象,既耗時,又浪費空間。使用stringbuilder類就可以避免這個問題的發生。

如果需要用許多小段的字元串建構一個字元串,那麼應該按照下列步驟進行。首先,建構一個空的字元串建構器:

當每次需要添加一部分内容時,就調用append方法。

在需要建構字元串時就調用tostring方法,将可以得到一個string對象,其中包含了建構器中的字元序列。

注釋:在jdk5.0中引入stringbuilder類。這個類的前身是stringbuffer,其效率稍有些低,但允許采用多線程的方式執行添加或删除字元的操作。如果所有字元串在一個單線程中編輯(通常都是這樣),則應該用stringbuilder替代它。這兩個類的api是相同的。

下面的api注釋包含了stringbuilder類中的重要方法。

java.lang.stringbuilder 5.0

stringbuilder()

構造一個空的字元串建構器。

int length()

傳回建構器或緩沖器中的代碼單元數量。

stringbuilder append(string str)

追加一個字元串并傳回this。

stringbuilder append(char c)

追加一個代碼單元并傳回this。

stringbuilder appendcodepoint(int cp)

追加一個代碼點,并将其轉換為一個或兩個代碼單元并傳回this。

void setcharat(int i,char c)

将第i個代碼單元設定為c。

stringbuilder insert(int offset,string str)

在offset位置插入一個字元串并傳回this。

stringbuilder insert(int offset,char c)

在offset位置插入一個代碼單元并傳回this。

stringbuilder delete(int startindex,int endindex)

删除偏移量從startindex到-endindex-1的代碼單元并傳回this。

string tostring()

傳回一個與建構器或緩沖器内容相同的字元串。

3.7 輸入輸出

為了增加後面示例程式的趣味性,需要程式能夠接收輸入,并以适當的格式輸出。當然,現代的程式都使用gui收集使用者的輸入,然而,編寫這種界面的程式需要使用較多的工具與技術,目前還不具備這些條件。主要原因是需要熟悉java程式設計語言,是以隻要有簡單的用于輸入輸出的控制台就可以了。第10章~第12章将詳細地介紹gui程式設計。

3.7.1 讀取輸入

前面已經看到,列印輸出到“标準輸出流”(即控制台視窗)是一件非常容易的事情,隻要調用system.out.println即可。然而,讀取“标準輸入流”system.in就沒有那麼簡單了。要想通過控制台進行輸入,首先需要構造一個scanner對象,并與“标準輸入流”system.in關聯。

(構造函數和new操作符将在第4章中詳細地介紹。)

現在,就可以使用scanner類的各種方法實作輸入操作了。例如,nextline方法将輸入一行。

在這裡,使用nextline方法是因為在輸入行中有可能包含空格。要想讀取一個單詞(以空白符作為分隔符),就調用

要想讀取一個整數,就調用nextint方法。

與此類似,要想讀取下一個浮點數,就調用nextdouble方法。

在程式清單3-2的程式中,詢問使用者姓名和年齡,然後列印一條如下格式的消息:

最後,在程式的最開始添加上一行:

scanner類定義在java.util包中。當使用的類不是定義在基本java.lang包中時,一定要使用import訓示字将相應的包加載進來。有關包與import訓示字的較長的描述請參看第4章。

程式清單3-2 inputtest/inputtest.java

注釋:因為輸入是可見的,是以scanner類不适用于從控制台讀取密碼。java se 6特别引入了console類實作這個目的。要想讀取一個密碼,可以采用下列代碼:

為了安全起見,傳回的密碼存放在一維字元數組中,而不是字元串中。在對密碼進行處理之後,應該馬上用一個填充值覆寫數組元素(數組處理将在3.10節介紹)。

采用console對象處理輸入不如采用scanner友善。每次隻能讀取一行輸入,而沒有能夠讀取一個單詞或一個數值的方法。

java.util.scanner 5.0

scanner (inputstream in)

用給定的輸入流建立一個scanner對象。

string nextline( )

讀取輸入的下一行内容。

string next( )

讀取輸入的下一個單詞(以空格作為分隔符)。

int nextint( )

double nextdouble( )

讀取并轉換下一個表示整數或浮點數的字元序列。

boolean hasnext( )

檢測輸入中是否還有其他單詞。

boolean hasnextint( )

boolean hasnextdouble( )

檢測是否還有表示整數或浮點數的下一個字元序列。

java.lang.system 1.0

static console console( ) 6

如果有可能進行互動操作,就通過控制台視窗為互動的使用者傳回一個console對象,否則傳回null。對于任何一個通過控制台視窗啟動的程式,都可使用console對象。否則,其可用性将與所使用的系統有關。

java.io.console 6

static char[] readpassword(string prompt, object...args)

static string readline(string prompt, object...args)

顯示字元串prompt并且讀取使用者輸入,直到輸入行結束。args參數可以用來提供輸入格式。有關這部分内容将在下一節中介紹。

3.7.2 格式化輸出

可以使用system.out.print(x)将數值x輸出到控制台上。這條指令将以x對應的資料類型所允許的最大非0數字位數列印輸出x。例如:

列印

如果希望顯示美元、美分等符号,則有可能會出現問題。

在早期的java版本中,格式化數值曾引起過一些争議。慶幸的是,java se 5.0沿用了c語言庫函數中的printf方法。例如,調用

可以用8個字元的寬度和小數點後兩個字元的精度列印x。也就是說,列印輸出一個空格和7個字元,如下所示:

在printf中,可以使用多個參數,例如:

每一個以%字元開始的格式說明符都用相應的參數替換。格式說明符尾部的轉換符将訓示被格式化的數值類型:f表示浮點數,s表示字元串,d表示十進制整數。表3-5列出了所有轉換符。

表3-5 用于printf的轉換符

轉換符 類  型 舉  例 轉換符 類  型 舉  例

d 十進制整數 159 s 字元串 hello

x 十六進制整數 9f c 字元 h

o 八進制整數 237 b 布爾 true

f 定點浮點數 15.9 h 散列碼 42628b2

e 指數浮點數 1.59e+01 tx或tx 日期時間(t強制大寫) 已經過時,應當改為使用java.time類,參見卷Ⅱ第6章

g 通用浮點數 — % 百分号 %

a 十六進制浮點數 0x1.fccdp3 n 與平台有關的行分隔符 —

另外,還可以給出控制格式化輸出的各種标志。表3-6列出了所有的标志。例如,逗号标志增加了分組的分隔符。即

可以使用多個标志,例如,“%, ( .2f”使用分組的分隔符并将負數括在括号内。

表3-6 用于printf的标志

标  志 目  的 舉  例

+ 列印正數和負數的符号 +3333.33

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

0 數字前面補0 003333.33

- 左對齊 |3333.33  |

( 将負數括在括号内 (3333.33)

, 添加分組分隔符 3,333.33

#(對于f格式) 包含小數點 3,333.

#(對于x或0格式) 添加字首0x或0 0xcafe

$ 給定被格式化的參數索引。例如,%1$d,%1$x将以十進制和十六進制格式列印第1個參數 159  9f

< 格式化前面說明的數值。例如,%d%<x以十進制和十六進制列印同一個數值 159  9f

注釋:可以使用s轉換符格式化任意的對象。對于任意實作了formattable接口的對象都将調用formatto方法;否則将調用tostring方法,它可以将對象轉換為字元串。在第5章中将讨論tostring方法,在第6章中将讨論接口。

可以使用靜态的string.format方法建立一個格式化的字元串,而不列印輸出:

基于完整性的考慮,下面簡略地介紹printf方法中日期與時間的格式化選項。在新代碼中,應當使用卷Ⅱ第6章中介紹的java.time包的方法。不過你可能會在遺留代碼中看到date類和相關的格式化選項。格式包括兩個字母,以t開始,以表3-7中的任意字母結束。

例如,

這條語句将用下面的格式列印目前的日期和時間:

表3-7 日期和時間的轉換符

轉換符 類  型 舉  例

c 完整的日期和時間 mon feb 09

18:05:19 pst

2015

f iso 8601日期 2015-02-09

d 美國格式的日期(月/日/年) 02/09/2015

t 24小時時間 18:05:19

r 12小時時間 06:05:19 pm

r 24小時時間沒有秒 18:05

y 4位數字的年(前面補0) 2015

y 年的後兩位數字(前面補0) 15

c 年的前兩位數字(前面補0) 20

b 月的完整拼寫 february

b或h 月的縮寫 feb

m 兩位數字的月(前面補0) 02

d 兩位數字的日(前面補0) 09

e 兩位數字的日(前面不補0) 9

a 星期幾的完整拼寫 monday

a 星期幾的縮寫 mon

j 三位數的年中的日子(前面補0),在001到366之間 069

h 兩位數字的小時(前面補0),在0到23之間 18

k 兩位數字的小時(前面不補0),在0到23之間 18

i 兩位數字的小時(前面補0),在0到12之間 06

l 兩位數字的小時(前面不補0),在0到12之間 6

m 兩位數字的分鐘(前面補0) 05

s 兩位數字的秒(前面補0) 19

l 三位數字的毫秒(前面補0) 047

n 九位數字的毫微秒(前面補0) 047000000

p 上午或下午的标志 pm

z 從gmt起,rfc822數字位移 -0800

z 時區 pst

s 從格林威治時間1970-01-01 00:00:00起的秒數 1078884319

q 從格林威治時間1970-01-01 00:00:00起的毫秒數 1078884319047

從表3-7可以看到,某些格式隻給出了指定日期的部分資訊。例如,隻有日期或月份。如果需要多次對日期操作才能實作對每一部分進行格式化的目的就太笨拙了。為此,可以采用一個格式化的字元串指出要被格式化的參數索引。索引必須緊跟在%後面,并以$終止。例如,

還可以選擇使用<标志。它訓示前面格式說明中的參數将被再次使用。也就是說,下列語句将産生與前面語句同樣的輸出結果:

提示:參數索引值從1開始,而不是從0開始,%1$...對第1個參數格式化。這就避免了與0标志混淆。

現在,已經了解了printf方法的所有特性。圖3-6給出了格式說明符的文法圖。

圖3-6 格式說明符文法

注釋:許多格式化規則是本地環境特有的。例如,在德國,組分隔符是句号而不是逗号,monday被格式化為montag。在卷Ⅱ第5章中将介紹如何控制應用的國際化行為。

3.7.3 檔案輸入與輸出

要想對檔案進行讀取,就需要一個用file對象構造一個scanner對象,如下所示:

如果檔案名中包含反斜杠符号,就要記住在每個反斜杠之前再加一個額外的反斜杠: “c:\\mydirectory\\myf?ile.txt”。

注釋:在這裡指定了utf-8字元編碼,這對于網際網路上的檔案很常見(不過并不是普遍适用)。讀取一個文本檔案時,要知道它的字元編碼——更多資訊參見卷Ⅱ第2章。如果省略字元編碼,則會使用運作這個java程式的機器的“預設編碼”。這不是一個好主意,如果在不同的機器上運作這個程式,可能會有不同的表現。

現在,就可以利用前面介紹的任何一個scanner方法對檔案進行讀取。

要想寫入檔案,就需要構造一個printwriter對象。在構造器中,隻需要提供檔案名:

如果檔案不存在,建立該檔案。可以像輸出到system.out一樣使用print、println以及printf指令。

警告:可以構造一個帶有字元串參數的scanner,但這個scanner将字元串解釋為資料,而不是檔案名。例如,如果調用:

這個scanner會将參數作為包含10個字元的資料:‘m’,‘y’,‘f’等。在這個示例中所顯示的并不是人們所期望的效果。

注釋:當指定一個相對檔案名時,例如,“myf?ile.txt”,“mydirectory/myf?ile.txt”或“../myf?ile.txt”,檔案位于java虛拟機啟動路徑的相對位置。如果在指令行方式下用下列指令啟動程式:

啟動路徑就是指令解釋器的目前路徑。然而,如果使用內建開發環境,那麼啟動路徑将由ide控制。可以使用下面的調用方式找到路徑的位置:

如果覺得定位檔案比較煩惱,則可以考慮使用絕對路徑,例如:“c:\\mydirectory\\ myf?ile.txt”或者“/home/me/mydirectory/myf?ile.txt”。

正如讀者所看到的,通路檔案與使用system.in和system.out一樣容易。要記住一點:如果用一個不存在的檔案構造一個scanner,或者用一個不能被建立的檔案名構造一個printwriter,那麼就會發生異常。java編譯器認為這些異常比“被零除”異常更嚴重。在第7章中,将會學習各種處理異常的方式。現在,應該告知編譯器:已經知道有可能出現“輸入/輸出”異常。這需要在main方法中用throws子句标記,如下所示:

現在讀者已經學習了如何讀寫包含文本資料的檔案。對于更加進階的技術,例如,處理不同的字元編碼、處理二進制資料、讀取目錄以及寫壓縮檔案,請參看卷Ⅱ第2章。

注釋:當采用指令行方式啟動一個程式時,可以利用shell的重定向文法将任意檔案關聯到system.in和system.out:

這樣,就不必擔心處理ioexception異常了。

scanner(file f)

構造一個從給定檔案讀取資料的scanner。

scanner(string data)

構造一個從給定字元串讀取資料的scanner。

java.io.printwriter 1.1

printwriter(string f?ilename)

構造一個将資料寫入檔案的printwriter。檔案名由參數指定。

java.nio.f?ile.paths 7

static path get(string pathname)

根據給定的路徑名構造一個path。

3.8 控制流程

與任何程式設計語言一樣,java使用條件語句和循環結構确定控制流程。本節先讨論條件語句,然後讨論循環語句,最後介紹看似有些笨重的switch語句,當需要對某個表達式的多個值進行檢測時,可以使用switch語句。

c++注釋:java的控制流程結構與c和c++的控制流程結構一樣,隻有很少的例外情況。沒有goto語句,但break語句可以帶标簽,可以利用它實作從内層循環跳出的目的(這種情況c語言采用goto語句實作)。另外,還有一種變形的for循環,在c或c++中沒有這類循環。它有點類似于c#中的foreach循環。

3.8.1 塊作用域

在深入學習控制結構之前,需要了解塊(block)的概念。

塊(即複合語句)是指由一對大括号括起來的若幹條簡單的java語句。塊确定了變量的作用域。一個塊可以嵌套在另一個塊中。下面就是在main方法塊中嵌套另一個語句塊的示例。

但是,不能在嵌套的兩個塊中聲明同名的變量。例如,下面的代碼就有錯誤,而無法通過編譯:

c++注釋:在c++中,可以在嵌套的塊中重定義一個變量。在内層定義的變量會覆寫在外層定義的變量。這樣,有可能會導緻程式設計錯誤,是以在java中不允許這樣做。

3.8.2 條件語句

在java中,條件語句的格式為

這裡的條件必須用括号括起來。

與絕大多數程式設計語言一樣,java常常希望在某個條件為真時執行多條語句。在這種情況下,應該使用塊語句(block statement),形式為

當yoursales大于或等于target時,将執行括号中的所有語句(請參看圖3-7)。

注釋:使用塊(有時稱為複合語句)可以在java程式結構中原本隻能放置一條(簡單)語句的地方放置多條語句。

在java中,更一般的條件語句格式如下所示(請參看圖3-8):

  

    圖3-7 if語句的流程圖           圖3-8 if/else語句的流程圖

其中else部分是可選的。else子句與最鄰近的if構成一組。是以,在語句

中else與第2個if配對。當然,用一對括号将會使這段代碼更加清晰:

重複地交替出現if...else if...是一種很常見的情況(請參看圖3-9)。例如:

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

3.8.3 循環

當條件為true時,while循環執行一條語句(也可以是一個語句塊)。一般格式為

如果開始循環條件的值就為false,則while循環體一次也不執行(請參看圖3-10)。

圖3-10 while語句的流程圖

程式清單3-3中的程式将計算需要多長時間才能夠存儲一定數量的終身俸,假定每年存入相同數量的金額,而且利率是固定的。

程式清單3-3 retirement/retirement.java

在這個示例中,增加了一個計數器,并在循環體中更新目前的累積數量,直到總值超過目标值為止。

(千萬不要使用這個程式安排退休計劃。這裡忽略了通貨膨脹和所期望的生活水準。)

while循環語句首先檢測循環條件。是以,循環體中的代碼有可能不被執行。如果希望循環體至少執行一次,則應該将檢測條件放在最後。使用do/while循環語句可以實作這種操作方式。它的文法格式為:

這種循環語句先執行語句(通常是一個語句塊),再檢測循環條件;然後重複語句,再檢測循環條件,以此類推。在程式清單3-4中,首先計算退休賬戶中的餘額,然後再詢問是否打算退休:

隻要使用者回答“n”,循環就重複執行(見圖3-11)。這是一個需要至少執行一次的循環的很好示例,因為使用者必須先看到餘額才能知道是否滿足退休所用。

程式清單3-4 retirement2/retirement2.java

3.8.4 确定循環

for循環語句是支援疊代的一種通用結構,利用每次疊代之後更新的計數器或類似的變量來控制疊代次數。如圖3-12所示,下面的程式将數字1~10輸出到螢幕上。

   

     圖3-11 do/while語句的流程圖        ???圖3-12 for語句的流程圖

for語句的第1部分通常用于對計數器初始化;第2部分給出每次新一輪循環執行前要檢測的循環條件;第3部分訓示如何更新計數器。

與c++一樣,盡管java允許在for循環的各個部分放置任何表達式,但有一條不成文的規則:for語句的3個部分應該對同一個計數器變量進行初始化、檢測和更新。若不遵守這一規則,編寫的循環常常晦澀難懂。

即使遵守了這條規則,也還有可能出現很多問題。例如,下面這個倒計數的循環:

警告:在循環中,檢測兩個浮點數是否相等需要格外小心。下面的for循環

可能永遠不會結束。由于舍入的誤差,最終可能得不到精确值。例如,在上面的循環中,因為0.1無法精确地用二進制表示,是以,x将從9.999 999 999 999 98跳到10.099 999 999 999 98。

當在for語句的第1部分中聲明了一個變量之後,這個變量的作用域就為for循環的整個循環體。

特别指出,如果在for語句内部定義一個變量,這個變量就不能在循環體之外使用。是以,如果希望在for循環體之外使用循環計數器的最終值,就要確定這個變量在循環語句的前面且在外部聲明!

另一方面,可以在各自獨立的不同for循環中定義同名的變量:

for循環語句隻不過是while循環的一種簡化形式。例如,

可以重寫為:

程式清單3-5給出了一個應用for循環的典型示例。這個程式用來計算抽獎中獎的機率。例如,如果必須從1~50之間的數字中取6個數字來抽獎,那麼會有(50×49×48×47×46×45)/(1×2×3×4×5×6)種可能的結果,是以中獎的幾率是1/15 890 700。祝你好運!

程式清單3-5 lotteryodds/lotteryodds.java

一般情況下,如果從n個數字中抽取k個數字,就可以使用下列公式得到結果。

下面的for循環語句計算了上面這個公式的值:

注釋:3.10.1節将會介紹“通用for循環”(又稱為for each循環),這是java se 5.0新增加的一種循環結構。

3.8.5 多重選擇:switch語句

在處理多個選項時,使用if/else結構顯得有些笨拙。java有一個與c/c++完全一樣的switch語句。

例如,如果建立一個如圖3-13所示的包含4個選項的菜單系統,可以使用下列代碼:

switch語句将從與選項值相比對的case标簽處開始執行直到遇到break語句,或者執行到switch語句的結束處為止。如果沒有相比對的case标簽,而有default子句,就執行這個子句。

警告:有可能觸發多個case分支。如果在case分支語句的末尾沒有break語句,那麼就會接着執行下一個case分支語句。這種情況相當危險,常常會引發錯誤。為此,我們在程式中從不使用switch語句。

如果你比我們更喜歡switch語句,編譯代碼時可以考慮加上-xlint:fallthrough選項,如下所示:

這樣一來,如果某個分支最後缺少一個break語句,編譯器就會給出一個警告消息。

如果你确實正是想使用這種“直通式”(fallthrough)行為,可以為其外圍方法加一個标注@suppresswarnings("fallthrough")。這樣就不會對這個方法生成警告了。(标注是為編譯器或處理java源檔案或類檔案的工具提供資訊的一種機制。我們将在卷Ⅱ的第8章詳細讨論标注。)

圖3-13 switch語句的流程圖

case标簽可以是:

類型為char、byte、short或int的常量表達式。

枚舉常量。

從java se 7開始,case标簽還可以是字元串字面量。

當在switch語句中使用枚舉常量時,不必在每個标簽中指明枚舉名,可以由switch的表達式值确定。例如:

3.8.6 中斷控制流程語句

盡管java的設計者将goto作為保留字,但實際上并沒有打算在語言中使用它。通常,使用goto語句被認為是一種拙劣的程式設計風格。當然,也有一些程式員認為反對goto的呼聲似乎有些過分(例如,donald knuth就曾編著過一篇名為《structured programming with goto statements》的著名文章)。這篇文章說:無限制地使用goto語句确實是導緻錯誤的根源,但在有些情況下,偶爾使用goto跳出循環還是有益處的。java設計者同意這種看法,甚至在java語言中增加了一條帶标簽的break,以此來支援這種程式設計風格。

下面首先看一下不帶标簽的break語句。與用于退出switch語句的break語句一樣,它也可以用于退出循環語句。例如,

在循環開始時,如果years > 100,或者在循環體中balance≥goal,則退出循環語句。當然,也可以在不使用break的情況下計算years的值,如下所示:

但是需要注意,在這個版本中,檢測了兩次balance < goal。為了避免重複檢測,有些程式員更加偏愛使用break語句。

與c++不同,java還提供了一種帶标簽的break語句,用于跳出多重嵌套的循環語句。有時候,在嵌套很深的循環語句中會發生一些不可預料的事情。此時可能更加希望跳到嵌套的所有循環語句之外。通過添加一些額外的條件判斷實作各層循環的檢測很不友善。

這裡有一個示例說明了break語句的工作狀态。請注意,标簽必須放在希望跳出的最外層循環之前,并且必須緊跟一個冒号。

如果輸入有誤,通過執行帶标簽的break跳轉到帶标簽的語句塊末尾。對于任何使用break語句的代碼都需要檢測循環是正常結束,還是由break跳出。

注釋:事實上,可以将标簽應用到任何語句中,甚至可以應用到if語句或者塊語句中,如下所示:

是以,如果希望使用一條goto語句,并将一個标簽放在想要跳到的語句塊之前,就可以使用break語句!當然,并不提倡使用這種方式。另外需要注意,隻能跳出語句塊,而不能跳入語句塊。

最後,還有一個continue語句。與break語句一樣,它将中斷正常的控制流程。continue語句将控制轉移到最内層循環的首部。例如:

如果n<0,則continue語句越過了目前循環體的剩餘部分,立刻跳到循環首部。

如果将continue語句用于for循環中,就可以跳到for循環的“更新”部分。例如,下面這個循環:

如果n<0,則continue語句跳到count++語句。

還有一種帶标簽的continue語句,将跳到與标簽比對的循環首部。

提示:許多程式員容易混淆break和continue語句。這些語句完全是可選的,即不使用它們也可以表達同樣的邏輯含義。在本書中,将不使用break和continue。

3.9 大數值

如果基本的整數和浮點數精度不能夠滿足需求,那麼可以使用java.math包中的兩個很有用的類:biginteger和bigdecimal。這兩個類可以處理包含任意長度數字序列的數值。biginteger類實作了任意精度的整數運算,bigdecimal實作了任意精度的浮點數運算。

使用靜态的valueof方法可以将普通的數值轉換為大數值:

遺憾的是,不能使用人們熟悉的算術運算符(如:+和*)處理大數值。而需要使用大數值類中的add和multiply方法。

c++注釋:與c++不同,java沒有提供運算符重載功能。程式員無法重定義+和*運算符,使其應用于biginteger類的add和multiply運算。java語言的設計者确實為字元串的連接配接重載了+運算符,但沒有重載其他的運算符,也沒有給java程式員在自己的類中重載運算符的機會。

程式清單3-6是對程式清單3-5中彩機率程式的改進,使其可以采用大數值進行運算。假設你被邀請參加抽獎活動,并從490個可能的數值中抽取60個,這個程式将會得到中彩機率1/716395843461995557415116222540092933411717612789263493493351013459481104668848。祝你好運!

程式清單3-6 bigintegertest/bigintegertest.java

在程式清單3-5中,用于計算的語句是

如果使用大數值,則相應的語句為:

api java.math.biginteger 1.1

biginteger add(biginteger other)

biginteger subtract(biginteger other)

biginteger multiply(biginteger other)

biginteger divide(biginteger other)

biginteger mod(biginteger other)

傳回這個大整數和另一個大整數other的和、差、積、商以及餘數。

int compareto(biginteger other)

如果這個大整數與另一個大整數other相等,傳回0;如果這個大整數小于另一個大整數other,傳回負數;否則,傳回正數。

static biginteger valueof(long x)

傳回值等于x的大整數。

java.math.biginteger 1.1

bigdecimal add(bigdecimal other)

bigdecimal subtract(bigdecimal other)

bigdecimal multiply(bigdecimal other)

bigdecimal divide(bigdecimal other,roundingmode mode) 5.0

傳回這個大實數與另一個大實數other的和、差、積、商。要想計算商,必須給出舍入方式(rounding mode)。roundingmode.half_up是在學校中學習的四舍五入方式(即,數值0到4舍去,數值5到9進位)。它适用于正常的計算。有關其他的舍入方式請參看api文檔。

int compareto(bigdecimal other)

如果這個大實數與另一個大實數相等,傳回0;如果這個大實數小于另一個大實數,傳回負數;否則,傳回正數。

static bigdecimal valueof(long x)

static bigdecimal valueof(long x,int scale)

傳回值為x或x / 10scale的一個大實數。

3.10 數組

數組是一種資料結構,用來存儲同一類型值的集合。通過一個整型下标可以通路數組中的每一個值。例如,如果a是一個整型數組,a[i]就是數組中下标為i的整數。

在聲明數組變量時,需要指出數組類型(資料元素類型緊跟[])和數組變量的名字。下面聲明了整型數組a:

不過,這條語句隻聲明了變量a,并沒有将a初始化為一個真正的數組。應該使用new運算符建立數組。

這條語句建立了一個可以存儲100個整數的數組。數組長度不要求是常量:new int[n]會建立一個長度為n的數組。

注釋:可以使用下面兩種形式聲明數組

大多數java應用程式員喜歡使用第一種風格,因為它将類型int[](整型數組)與變量名分開了。

這個數組的下标從0~99(不是1~100)。一旦建立了數組,就可以給數組元素指派。例如,使用一個循環:

建立一個數字數組時,所有元素都初始化為0。boolean數組的元素會初始化為false。對象數組的元素則初始化為一個特殊值null,這表示這些元素(還)未存放任何對象。初學者對此可能有些不解。例如,

會建立一個包含10個字元串的數組,所有字元串都為null。如果希望這個數組包含空串,可以為元素指定空串:

警告:如果建立了一個100個元素的數組,并且試圖通路元素a[100](或任何在0~99之外的下标),程式就會引發“array index out of bounds”異常而終止執行。

要想獲得數組中的元素個數,可以使用array.length。例如,

一旦建立了數組,就不能再改變它的大小(盡管可以改變每一個數組元素)。如果經常需要在運作過程中擴充數組的大小,就應該使用另一種資料結構——數組清單(array list)有關數組清單的詳細内容請參看第5章。

3.10.1 for each循環

java有一種功能很強的循環結構,可以用來依次處理數組中的每個元素(其他類型的元素集合亦可)而不必為指定下标值而分心。

這種增強的for循環的語句格式為:

定義一個變量用于暫存集合中的每一個元素,并執行相應的語句(當然,也可以是語句塊)。collection這一集合表達式必須是一個數組或者是一個實作了iterable接口的類對象(例如arraylist)。有關數組清單的内容将在第5章中讨論,有關iterable接口的内容将在第9章中讨論。

列印數組a的每一個元素,一個元素占一行。

這個循環應該讀作“循環a中的每一個元素”(for each element in a)。java語言的設計者認為應該使用諸如foreach、in這樣的關鍵字,但這種循環語句并不是最初就包含在java語言中的,而是後來添加進去的,并且沒有人打算廢除已經包含同名(例如system.in)方法或變量的舊代碼。

當然,使用傳統的for循環也可以獲得同樣的效果:

但是,for each循環語句顯得更加簡潔、更不易出錯(不必為下标的起始值和終止值而操心)。

注釋:for each循環語句的循環變量将會周遊數組中的每個元素,而不需要使用下标值。

如果需要處理一個集合中的所有元素,for each循環語句對傳統循環語句所進行的改進更是叫人稱贊不已。然而,在很多場合下,還是需要使用傳統的for循環。例如,如果不希望周遊集合中的每個元素,或者在循環内部需要使用下标值等。

提示:有個更加簡單的方式列印數組中的所有值,即利用arrays類的tostring方法。調用arrays.tostring(a),傳回一個包含數組元素的字元串,這些元素被放置在括号内,并用逗号分隔,例如,“[2,3,5,7,11,13]”。要想列印數組,可以調用

3.10.2 數組初始化以及匿名數組

在java中,提供了一種建立數組對象并同時賦予初始值的簡化書寫形式。下面是一個例子:

請注意,在使用這種語句時,不需要調用new。

甚至還可以初始化一個匿名的數組:

這種表示法将建立一個新數組并利用括号中提供的值進行初始化,數組的大小就是初始值的個數。使用這種文法形式可以在不建立新變量的情況下重新初始化一個數組。例如:

這是下列語句的簡寫形式:

注釋:在java中,允許數組長度為0。在編寫一個結果為數組的方法時,如果碰巧結果為空,則這種文法形式就顯得非常有用。此時可以建立一個長度為0的數組:

注意,數組長度為0與null不同。

3.10.3 數組拷貝

在java中,允許将一個數組變量拷貝給另一個數組變量。這時,兩個變量将引用同一個數組:

圖3-14顯示了拷貝的結果。如果希望将一個數組的所有值拷貝到一個新的數組中去,就要使用arrays類的copyof方法:

第2個參數是新數組的長度。這個方法通常用來增加數組的大小:

如果數組元素是數值型,那麼多餘的元素将被指派為0;如果數組元素是布爾型,則将指派為false。相反,如果長度小于原始數組的長度,則隻拷貝最前面的資料元素。

c++注釋:java數組與c++數組在堆棧上有很大不同,但基本上與配置設定在堆(heap)上的數組指針一樣。也就是說,

不同于

而等同于

java中的[ ]運算符被預定義為檢查數組邊界,而且沒有指針運算,即不能通過a加1得到數組的下一個元素。

3.10.4 指令行參數

前面已經看到多個使用java數組的示例。每一個java應用程式都有一個帶string arg[]參數的main方法。這個參數表明main方法将接收一個字元串數組,也就是指令行參數。

例如,看一看下面這個程式:

如果使用下面這種形式運作這個程式:

args數組将包含下列内容:

這個程式将顯示下列資訊:

c++注釋:在java應用程式的main方法中,程式名并沒有存儲在args數組中。例如,當使用下列指令運作程式時

args[0]是“-h”,而不是“message”或“java”。

3.10.5 數組排序

要想對數值型數組進行排序,可以使用arrays類中的sort方法:

這個方法使用了優化的快速排序算法。快速排序算法對于大多數資料集合來說都是效率比較高的。arrays類還提供了幾個使用很便捷的方法,在稍後的api注釋中将介紹它們。

程式清單3-7中的程式用到了數組,它産生一個抽彩遊戲中的随機數值組合。假如抽彩是從49個數值中抽取6個,那麼程式可能的輸出結果為:

要想選擇這樣一個随機的數值集合,就要首先将數值1,2,…,n存入數組numbers中:

而用第二個數組存放抽取出來的數值:

現在,就可以開始抽取k個數值了。math.random方法将傳回一個0到1之間(包含0、不包含1)的随機浮點數。用n乘以這個浮點數,就可以得到從0到n-1之間的一個随機數。

下面将result的第i個元素設定為numbers[r]存放的數值,最初是r+1。但正如所看到的,numbers數組的内容在每一次抽取之後都會發生變化。

現在,必須確定不會再次抽取到那個數值,因為所有抽彩的數值必須不相同。是以,這裡用數組中的最後一個數值改寫number[r],并将n減1。

關鍵在于每次抽取的都是下标,而不是實際的值。下标指向包含尚未抽取過的數組元素。

在抽取了k個數值之後,就可以對result數組進行排序了,這樣可以讓輸出效果更加清晰:

程式清單3-7 lotterydrawing/lotterydrawing.java

java.util.arrays 1.2

static string tostring(type[] a)  5.0

傳回包含a中資料元素的字元串,這些資料元素被放在括号内,并用逗号分隔。

參數:a 類型為int、long、short、char、byte、boolean、f?loat或double的數組。

static type copyof(type[] a, int length) 6

static type copyofrange(type[] a, int start, int end) 6

傳回與a類型相同的一個數組,其長度為length或者end-start,數組元素為a的值。

   start 起始下标(包含這個值)。

   end 終止下标(不包含這個值)。這個值可能大于a.length。在這種情況下,結果為0或false。

   length 拷貝的資料元素長度。如果length值大于a.length,結果為0或false;否則,數組中隻有前面length個資料元素的拷貝值。

static void sort(type[] a)

采用優化的快速排序算法對數組進行排序。

static int binarysearch(type[] a, type v)

static int binarysearch(type[] a, int start, int end, type v)  6

采用二分搜尋算法查找值v。如果查找成功,則傳回相應的下标值;否則,傳回一個負數值r。-r-1是為保持a有序v應插入的位置。

參數:a 類型為int、long、short、char、byte、boolean、f?loat或double的有序數組。

   end 終止下标(不包含這個值)。

   v 同a的資料元素類型相同的值。

static void f?ill(type[] a, type v)

将數組的所有資料元素值設定為v。

   v 與a資料元素類型相同的一個值。

static boolean equals(type[] a, type[] b)

如果兩個數組大小相同,并且下标相同的元素都對應相等,傳回true。

參數:a、b 類型為int、long、short、char、byte、boolean、f?loat或double的兩個數組。

3.10.6 多元數組

多元數組将使用多個下标通路數組元素,它适用于表示表格或更加複雜的排列形式。這一節的内容可以先跳過,等到需要使用這種存儲機制時再傳回來學習。

假設需要建立一個數值表,用來顯示在不同利率下投資$10,000會增長多少,利息每年兌現,而且又被用于投資(見表3-8)。

表3-8 不同利率下的投資增長情況

10% 11% 12% 13% 14% 15%

10 000.00 10 000.00 10 000.00 10 000.00 10 000.00 10 000.00

11 000.00 11 100.00 11 200.00 11 300.00 11 400.00 11 500.00

12 100.00 12 321.00 12 544.00 12 769.00 12 996.00 13 225.00

13 310.00 13 676.31 14 049.28 14 428.97 14 815.44 15 208.75

14 641 00 15 180.70 15 735.19 16 304.74 16 889.60 17 490.06

16 105.10 16 850.58 17 623.42 18 424 .35 19 254.15 20 113.57

17 715.61 18 704.15 19 738.23 20 819.52 21 949.73 23 130.61

19 487.17 20 761.60 22 106.81 23 526.05 25 022.69 26 600.20

21 435.89 23 045.38 24 759.63 26 584.44 28 525.86 30 590.23

23 579.48 25 580.37 27 730.79 30 040.42 32 519.49 35 178.76

可以使用一個二維數組(也稱為矩陣)存儲這些資訊。這個數組被命名為balances。

在java中,聲明一個二維數組相當簡單。例如:

與一維數組一樣,在調用new對多元數組進行初始化之前不能使用它。在這裡可以這樣初始化:

另外,如果知道數組元素,就可以不調用new,而直接使用簡化的書寫形式對多元數組進行初始化。例如:

一旦數組被初始化,就可以利用兩個方括号通路每個元素,例如,balances[i][j]。

在示例程式中用到了一個存儲利率的一維數組interest與一個存儲餘額的二維數組balances。一維用于表示年,另一維用于表示利率,最初使用初始餘額來初始化這個數組的第一行:

然後,按照下列方式計算其他行:

程式清單3-8給出了完整的程式。

注釋:for each循環語句不能自動處理二維數組的每一個元素。它是按照行,也就是一維數組處理的。要想通路二維數組a的所有元素,需要使用兩個嵌套的循環,如下所示:

提示:要想快速地列印一個二維數組的資料元素清單,可以調用:

輸出格式為:

程式清單3-8 compoundinterest/compoundinterest.java

3.10.7 不規則數組

到目前為止,讀者所看到的數組與其他程式設計語言中提供的數組沒有多大差別。但實際存在着一些細微的差異,而這正是java的優勢所在:java實際上沒有多元數組,隻有一維數組。多元數組被解釋為“數組的數組。”

例如,在前面的示例中,balances數組實際上是一個包含10個元素的數組,而每個元素又是一個由6個浮點數組成的數組(請參看圖3-15)。

圖3-15 一個二維數組

表達式balances[i]引用第i個子數組,也就是二維表的第i行。它本身也是一個數組,balances[i][j]引用這個數組的第j項。

由于可以單獨地存取數組的某一行,是以可以讓兩行交換。

還可以友善地構造一個“不規則”數組,即數組的每一行有不同的長度。下面是一個典型的示例。在這個示例中,建立一個數組,第i行第j列将存放“從i個數值中抽取j個數值”産生的結果。

由于j不可能大于i,是以矩陣是三角形的。第i行有i + 1個元素(允許抽取0個元素,也是一種選擇)。要想建立一個不規則的數組,首先需要配置設定一個具有所含行數的數組。

接下來,配置設定這些行。

在配置設定了數組之後,假定沒有超出邊界,就可以采用通常的方式通路其中的元素了。

程式清單3-9給出了完整的程式。

c++注釋:在c++中,java聲明

也不同于

而是配置設定了一個包含10個指針的數組:

然後,指針數組的每一個元素被填充了一個包含6個數字的數組:

慶幸的是,當建立new double[10][6]時,這個循環将自動地執行。當需要不規則的數組時,隻能單獨地建立行數組。

程式清單3-9 lotteryarray/lotteryarray.java

現在,已經看到了java語言的基本程式結構,下一章将介紹java中的面向對象的程式設計。