用 Java 入門物件導向程式設計

Object 是指讓某個可以獨立運作的程式區段,變得像與你相互對立的「對象」,讓你可以與它溝通交流、協力合作。而不是企圖把整個世界搬到程式裡面,成為各個互相對等的「存在」,因為這樣做叫 things,而不是 object。


程式語言有分簡約語言和魔幻語言,而 Java 就是簡約的物件導向程式語言。

在 1990 年代,比 C++ 容易入門物件導向程式設計的 Java,令人喜愛的程度,就跟現在的 Python 一樣~

當時我們的想法是:「用 Java 學物件導向程式設計,再適合不過。」興起一股大家來學物件導向程式語言的風氣。

然而,現在 Java 跟 Python、Go、.NET Core 相比,只剩伺服端系統在用,沒人願意用 Java 寫應用端程式,讓我對自己的 Java 教學起了改版的念頭:「與其用現代的 Java 跟 Python 和 Go 拚搏卻是在誤人子弟,不如用傳統的 Java 教怎麼入門物件導向程式設計。」


前言

原《爪哇即溶咖啡》改版為《用 Java 入門物件導向程式設計》,重點變成「物件導向」,而不是「Java 程式設計」。

因此我把 Java 8 以後的內容刪除,改為 Java 7 之前的純物件導向,所以本文就 Java 程式設計來說是過時的:「你無法從本文學到正確的 Java 語言。」

但對研究物件導向程式設計來說,用復古的 Java 7 寫法,概念會更清晰、觀念會更清楚!雖然我這麼做弊大於利,但我還是覺得不得不做這樣錯謬的事,《用 Java 入門物件導向程式設計》是和《用 Pascal 傳承程式》同系列的單元,而不是和《用 C# 堆砌程式》《用 Python 快寫程式》《用 PHP 網住程式》同系列。

總之,請不要用本資料學 Java 程式設計:「我還把三分之二對了解物件導向沒幫助的 API 範例砍光,你根本學不到什麼。」本文是用 Java 來入門物件導向的!範例精簡到只剩三分之一,反而方便你反覆練習寫 Java 程式,實際感受 Java 物件導向程式設計是怎麼回事~


目錄


焦點話題

從 C++ 到 Java 的物件導向程式設計

物件導向設計原則(SOLID)… Wikipedia
物件導向設計模式 … 菜鸟教程
MVC 軟體架構 … Wikipedia
重構(Refactoring)… 林維展的學習筆記
Scrum 與敏捷開發 … Microsoft 365 Team:如何為您的專案選擇最佳敏捷做法

是抽象還是想像?
為何會把物件導向拿來對照真實世界的物件?
不要違反 Java 的物件導向思維
如何寫出好的程式?什麼是好的設計?
用 UML 進行物件導向系統分析與設計,用 Java 進行物件導向程式設計。
Java 不再免費?


應用範例

String(不可變物件的字串)
StringBuilder(可變字串)
格式化字串
Regular Expression

陣列
Collections 速成(各式資料結構)
亂數

資料夾與檔案的管理
純文字文件的寫入與讀取
以二進制格式讀寫資料
將 Java 物件轉為檔案形式(序列化)

命令列介面的程式設計
調用作業系統的命令
Swing 快速入門
變更視窗樣式
對話視窗
JScrollPane
JTable

日期與時間
計時器
暫停一段時間
列印

複製物件
Observer pattern(觀察者設計模式)


其它功能

javac
jar
JavaScript 引擎:Rhino
Tomcat + JSP + SQL 快速入門


從 C++ 到 Java 的物件導向程式設計

在 90 年代,C++ 是物件導向程式設計的首選,它的 class 語法簡單又直覺,軟體開發人員很容易用物件的概念組織程式。但 C++ 相容 C 語言,因此很多人在使用物件導向程式設計的同時,摻雜了程序導向的程式設計,久而久之破壞了物件導向程式設計的穩固性。

如果程式碼不能穩固地使用,就失去物件導向的核心利益了!這樣還不如繼續用 C 語言以程序導向寫好結構化程式設計,何必浪費將程式碼拆成一個個類別的時間?

隨著網際網路興起,能在不同作業系統執行的 Applet 成了程式設計的熱門話題,人們開始學習 Java 語言,並注意到它參考的是 C++ 語法,它將物件導向的部分發揚光大,然後剔除程序導向的部分,讓人們可以享受用 C++ 語法寫出優雅物件導向程式的樂趣,於是用 Java 語言學習物件導向程式設計成了第二個熱門話題!

事實上,在當時用 Java 寫應用程式並不是好主意,執行速度太慢。但學習 Java 程式設計卻是件很有意義的事,寫不好 C++ 物件導向程式設計的人,可以在 Java 學會如何正確使用物件導向語法。結果,網路上最流行的不是教你寫 Applet,而是教你學 Java 語言,展開了 Java 成為 2000 年代最受歡迎程式語言的序章~


類別(Class)

Java 規定所有程式都必須以類別來撰寫,所以即使只想宣告一個整數型態的變數,也必須這麼寫:


將類別建立成物件,並存取變數的方式如下:


5
main() 是 Java 內定的函式,Java 虛擬機器只要看到這個名稱的函式,就會自動執行裡面的程式碼。static 表示不用建立物件也能使用,也就是說不用 new 一個 Main 物件就能呼叫 main()。main() 一定要宣告為 public 表示公開使用,否則 Java 虛擬主機會找不到它。

Java 跟 C++ 一樣,將類別變數和函式稱為成員(member),但 C++ 將類別裡面的變數和函式稱為成員變數和成員函式,Java 改稱欄位(field)和方法(method)。

物件導向將呼叫物件的函式,稱為傳遞訊息(message passing)。程式只透過物件之間的訊息傳遞,來組織軟體,就叫物件導向程式設計。1

Java 的類別也引進了 C++ 的建構子、封裝、繼承、多型、覆載、多載、抽象…等物件導向基本功能,下面將逐一介紹。


建構子(Constructor)

跟 C++ 一樣,名稱與類別一樣,且沒有傳回值,就是建構子:


如果有傳回值,像是 int、String、void,則是函式,而不是建構子。

建構子可以有參數,藉由參數的不同,來為類別設計多個建構子。

沒有建構子的話,Java 會自動建立一個呼叫 super() 函式的建構子。已經有建構子的話,就沒有自動呼叫 super() 的建構子,所以需要執行父類別的建構子時要記得要自己呼叫。

Java 虛擬機器有一套垃圾收集系統,會自動將浪費記憶體資源的物件釋放掉,所以不需要解構子(destructor)。雖然 Java 有 finalize() 函式,但這並不是解構子,而是垃圾收集決定回收該物件時自動執行的函式。


封裝(Encapsulation)

Java 引進了 C++ 的 public、private、protected,用來規劃類別的成員是公開存取還是限制存取:


變數 i 使用 private 表示只有自己能存取,函式 add 使用 public 表示別的物件也能使用。

比較特別的是,Java 規定類別使用 public 的話,存放該類別的檔案,檔名必須與類別名稱一樣。

protected 表示開放給子類別存取。

不使用存取權限的話,表示同套件的類別彼此間都能存取。雖然 Java 預設使用這種存取權限,但建議物件導向新手明確宣告 public 和 private,否則你會覺得整個設計一團亂、在組織程式元件的規劃上失去控制權。只有物件導向專家,才懂得用預設的存取權限,將物件用群組化的方式來設計,平常人會一團糟。


繼承(Inheritance)

Java 使用 extends 指令來繼承類別:


沒有使用 extends 時,Java 會自動繼承 java.lang.Object 物件。

在 C++ 使用多重繼承時,常常產生成員歸屬問題,所以 Java 只能繼承一個類別。Java 使用多重實作介面(interface)的方式,來表達多重繼承的關係。介面只宣告函式名稱,不實作函式功能,所以沒有責任歸屬的問題,一律由實作的類別負責。


多型(Polymorphism)

Java 跟 C++ 一樣,透過繼承讓不同子類別有共同的行為能力,再藉由子類別覆寫(override)父類別的函式,來實現多型的程式設計。

不一樣的是,Java 預設所有方法都是 virtual,所以子類別的方法只要同名、同參數,就能覆寫父類別的方法,這也是 Java 之所以稱為簡約語言的主要原因:


BBB
CCC

或許,不是每個用過 C++ 的人都了解多型的意義,所以這裡還是補充一下觀念。

多型就是以同樣的訊息來表達不同的內容,或者以同樣的行為來進行不同的動作。舉個例子,你可以反覆用「你叫什麼名字」這行為向不同人發問,然後每個人會反應自己的狀況。

這項語法特性是物件導向程式設計與程序導向程式設計差異最大的地方,以往只能事先寫死程式碼來執行各種動作,有了多型會將程式碼設計成可擴充的元件,事後想要什麼再新增一個元件去用,靈活很多。


多載(Overload)

在 C++,類別的成員函式有個很棒的功能,就是函式名稱可以重複,只要參數不同,就可以視為不同函式。Java 也支援這項功能,讓物件導向程式設計更能表達語意:


AAA
Hello
Hello, world!


其它特性

除了上述對 C++ 語法的改進,Java 也對物件導向程式設計做了重大改進!像是廢除指標運算能力的參照(reference)、不用再寫解構式的垃圾回收(garbage collection)、介面與實作分離的 abstract 和 interface…這些影響物件導向發展的特性,都是 Java 開創的!

加上 Java 包山包海的程式庫(Java API)、沙盒般的安全性2,能用 Java 寫應用程式,真是幸福的事~

但 Java 也有失策的地方,像是過於仰賴「所有類別都繼承自 java.lang.Object」的特性,想讓物件導向程式的語法盡可能簡化,所以沒有 C++ 的 template 和重載運算子。結果拖到 Java SE 5 和 Java SE 7 又陸續追加 C++ 這些語法功能,加與不加都造成災情。


[1] 不過柯達控告 Java 抄襲他所收購王安實驗室的「某一應用向另一應用請求協助」專利勝訴,Sun Microsystems 以 9200 萬美元和解後,就很少人這麼解釋物件導向程式設計了。
[2] Java SE 7 連環爆出漏洞前,安全性真的是 Java 訴求的特性之一。常常拿 Java 應用程式比 Windows 原生程式安全來說嘴的我,當年被打臉打得很腫。


是抽象還是想像?

不要把現實世界的物體,搬到物件導向的程式裡。


想像

坦白說,物件導向程式設計的開發手法,其實還是跟程序導向一樣,可以重複利用的,就加以模組化,只是從函式改成類別來模組化你的程式,能夠更靈活管理一大群模組罷了!

亦即,物件導向絕對不是用來對照現實世界的開發手法!

學過「封裝」「繼承」「多型」依然不知道物件導向程式設計是怎樣的開發手法,就是因為想把現實世界搬到程式設計;但那不叫「抽象」,而叫「想像」。下場通常是過兩三年了,還無法建立起能夠彰顯物件導向精神的開發手法。

要建立讓自己感覺到漂亮的物件導向開發手法,應該回過頭來讓程式設計去像現實世界,這樣才叫「抽象」。或者說,你應該抽象的對象是依然是「程式」本身,而不是「現實世界的物體」,錯這一步就全盤通輸。

在傳統程式設計裡,根據程式的需求,分割出函式來使用,這就是一種針對程式本身所進行的抽象了!現在你只需要把函式改用類別來設計,就能在往後修改程式時,發現到程式具備的穩固性,不會因為大量修改而破壞系統的運作。千萬不要以為把現實事物寫在程式世界裡面叫做抽象!

所以「物件導向程式設計」並不複雜,只是一種能夠更靈活管理模組的程式語言而已,是牽扯上「物件導向專案管理」的手法才導致「物件導向」變成像玄學一樣詭譎的議題。因此,當你在初學「物件導向」感到迷惘時,請跳脫「物件導向分析」「物件導向設計」,剩下「物件導向編程」就好,單純地從程式設計的觀點,來理解物件導向的基本精神,會比較有效率。

但並不是拋棄「物件導向分析」「物件導向設計」,而是原本正常的學習過程,就應該從「程式設計」本身建立起物件導向的開發手法,然後為了「明文規範」這樣的開發手法,才將這些擴展為「物件導向分析」「物件導向設計」。1


抽象

在破除了把「想像」當「抽象」的魔障後,接著進一步來描述抽象的手法。

讓某個可以獨立運作的程式區段,變得像某個「對象」可以與它溝通交流,然後協力運作,這就是 object 了!

object 這一詞所表示的「物件」,是可以與你「相對立」的東西,而不是與你「相對等」的存在。

把程式碼變成與你相對立的東西,讓你可以對它進行操作,就是物件導向程式設計的精隨。讓某個程式區段變成可以穩固操作,而且沒有副作用,是很快樂的事!這件事就像從化學排列組織中調配出新的藥方,可以有效治療某個症狀,但身體卻沒有副作用一樣,設計物件導向程式的時候,都會感受到這樣的快樂2

如果你企圖把整個世界搬到程式裡面,成為各個相對等的存在,那叫 things,而不是 object。把明明不真實存在的事情搬到程式裡面,然後說服自己說:「這些是物件。」你發揮的是想像,並不是抽象。把確實存在的程式碼,設計成相對立的東西,讓你可以更穩固、有效地重複使用它,才是物件導向真正所要的抽象精神。


[1] 一開始就從「專案管理手法」來理解「物件導向」是怎麼回事,通常是博士班的研究生在幹的事,他們專長原本就是再複雜的學問都能釐清。但如果沒有釐清複雜事務的高人智商,從「程式設計」本身來掌握「物件導向開發手法」其實也沒有比較慢,何苦作賤自己。
[2] 如果你感受不到,表示你設計出來不是相對立的物件,而是一段普通的程式碼。


為何會把物件導向拿來對照真實世界的物件?

「把現實世界搬到程式設計」的物件導向觀念,容易讓你誤入歧途,但為何大多數人正在這麼做?


為了物件而物件,或多或少在兌現獨立個體的設計~

物件導向程式設計語言並不是讓你把 Ball(球)、Court(球場)、Stand(球架)、Player(球員)、Rule(規則)、Score(計分)鉅細靡遺搬到程式裡面,企圖堆砌出一個想像世界的技術。但當你這麼做時,卻相當貼近物件導向程式設計的精神了:「每個物體都保持獨立性,球是球、場地是場地,球不必關心場地座落在哪或造價多少來製造,場地也不必關心球是哪個品牌或哪種顏色來設計。」

所以,你並不是不能建立 Ball、Stand、Player、Score…這些類別,只是我們並非為了把所有真實世界存在的物體,通通搬進程式碼裡面,讓我們的程式世界應有盡有,而建立出這些類別。我們是為了方便整理「可獨立運作的程式碼」才建立這些類別,讓外界只能接觸該個體公諸於世的功能特性使用它,讓它始終就是那樣的個體,不會時常因外界的需求條件變成另一種個體。

因此,不要為了物件而物件似的,瘋狂比照現實世界的事物,建立一堆類別。而是要讓不同個體能獨立於外物,讓某塊程式碼照自己需求條件發展,不受外界影響往後的設計,而寫成類別來管理。否則,即使你把所有事物都搬進程式碼裡面,卻彼此牽扯不清:「球會挑南北場地、場地會挑球的顏色。」那你的物件導向程式設計不管怎麼寫,永遠都不會有成熟的一天~


做為最高道德標準,不要陷入迷思。

以籃球來講,有室內球跟室外球的設計之分,這就是在挑場地了!

這時,我們繼續堅持保有類別不受外界影響的獨立性,不讓該類別被修改設計,以免時常演變成另一種不一樣的類別。只要費心思考,依然能夠不破壞程式碼獨立性,最小限度地「附加」公諸於世的新功能特性上去,而不是破壞設計。

但也不要陷入迷思,以為類別應該完全禁止受外界影響,我們的目標是:「以免『時常』演變成另一種不一樣的類別」,並不是『絕對』不能。

如果真的該設計一顆「拿到戶外打就會爆掉的室內籃球」,那就放手去設計這顆可以當炸彈的籃球吧 XDDD


不要違反 Java 的物件導向思維

因為 Java 不是純正的物件導向程式語言,而是一款規範說應該怎樣使用物件導向程式設計的語言,所以不要違背 Java 制定的規則。


Java 並不適合做為「學術派物件導向程式設計」的語言

你可能會想藉由 Java 來建立一套自己的物件導向思維,但真的不建議幹這種事,你會發現很多事都不能做。像是鉅細靡遺 import 所有用到的套件(甚至連 lang 都寫進來)、全面使用 this 叫用自身屬性與方法、不宣告 reference(參照)直接 new 一個 object(物件)…我知道這麼做一定有自己的想法,但在 Java 幹這些事,吃虧的份要比佔便宜來得大。


因為 Java 並非完整的物件導向程式語言

Java 一向自稱是完全物件導向的程式語言,而不是「完整的」物件導向程式語言。

這差異在於,Java 只是規定你必須用物件導向的結構來開發程式,因此使用 Java 語言設計出來的程式,確實是完全物件導向的結構。

不過,Java 卻把物件導向的做法給寫死,要求你照 Java 那一套物件導向思維來設計程式。而許多不在 Java 物件導向思維的語法功能,則不會納入 Java 語言。

於是 Java 語言確實是完全物件導向的程式設計,但卻是會讓你綁手綁腳的物件導向程式語言,因為它物件導向的機制不足,你心中思維的物件導向架構,有可能無法在 Java 程式語言建立起來,只能被迫改變自己真正想要的結構來通過 Java 認定的規範,因此稱不上是一款完整的物件導向程式語言


解決之道

若真要架構自己的物件導向規範,物件導向語法強大的 C++ 反而是比較適合的選擇。

但有時候我們不是為了開發軟體而在眾多語言中選擇 Java,而是為了跑 Java 所以試圖在上頭開發應用程式,沒得選擇。既然用 Java 程式語言,那與其建立自己的一套物件導向學術思維,不如遵循 Java 預設的物件導向學術思維~

那為何在物件導向的領域,Java 比 C++ 來得熱門?因為 C++ 的語法功能強悍到難以駕馭,剛上路的物件導向程式設計師,不曉得該怎樣整合得宜這群語法結構。反而規定你只能怎樣使用物件導向的 Java 是最好入門的選擇!


不盡然是缺點

雖然 Java 稱不上是完整的物件導向程式語言,但像這樣把物件導向的做法給規範成固定幾種範本,有個好處就是每個人寫出來的程式會變得類似,有助於團隊的開發。

Java 原本就不是自比 Perl 這類「There's more than one way to do it(不只一種做法來做一件事)」的萬能型程式語言,反而是希望做法減到最低,能在特定幾個領域提升開發效率的程式語言。


如何寫出好的程式?什麼是好的設計?


好的程式,不是寫來彰顯技術,而是讓沒技術的人也讀得懂~

漸漸熟悉程式設計以後,首先,最容易發生的,就是質疑自己寫的程式不夠水準,然後開始思考,該怎樣寫出好的程式?

而思考的方向往往是,怎樣寫才能讓自己顯得高竿一些?不希望寫出來的程式碼,讓人家看了以後,認為你像初學者般笨拙。

於是可能傾向於這樣寫程式:


然而,這種純粹只是節省行數或者字數,對執行效能沒有實際提升的寫法,並不叫做好的程式。1

真正好的程式,是寧可多花幾行程式,讓閱讀你程式碼的人,能輕鬆自在地理解你想要傳達的想法:


所以,別再為「怎樣寫才叫好的程式」這種問題感到疑惑,最笨拙的寫法,往往就是最好的程式。

自認為高竿的程式碼,但沒人明白是在做什麼事,其實是最差勁的程式!能故意用最笨拙的方式,寫出連小朋友看了以後,都大概知道是在做什麼的程式碼,才是好程式。2


除了好的程式,還要有好的設計。

那麼,把上面的範例,寫得像下面載明得更仔細呢?


這是好是壞,還真的見仁見智…

上一節範例

來看兩個範例的差異,上一節認為好的範例,只是將艱澀的計算結果保存在變數裡面,好賦予一個名稱,讓我們能夠辨識這是怎樣一筆資料,然後剩下的部分保持不變。

因為 newInstance().newDocumentBuilder().parse(xmlfile) 各個串接的 method,從字義上已經很明顯表達出是建立 DocumentBuilderFactory 與 DocumentBuilder,所以就不進一步為這些操作取名為 factorybuilder

API 公開的操作介面會說話,這是相當重要的關鍵,懂得這招對你的設計會有幫助!

至於 document 則是只用到一次,另外開設變數來命名的話,反而讓 element 的存在失焦,所以上一節認為好的範例選擇省略,如果會用到第二次的話再說。

從語意上來看,上一節範例其實更有表達力,基本上「我個人認為」它是比較好的風格。

這一節範例

但並不能說這一節範例做過頭,因為就設計上來看,它是為了日後的修改,所刻意讓出修改空間的風格。

上一節認為好的範例常見於中國人,這一節的範例常見於台灣人。而我們常常聽到業界認為中國的程式設計師開發能力很強,但屬於個人戰力,所以讚賞台灣的程式開發人員比較懂設計,適合團隊工作,差別就在這裡。3

業界偏好事先就能周到地保留修改空間的程式風格,更勝於直覺、有效傳達語意的寫作風格。

能寫出好的程式,不一定就能產生好的設計!好的程式,必須易於閱讀,而好的設計,必須易於修改。然而,易於閱讀的程式雖然對易於修改很有幫助,但那只是其中一環而已,易於修改的好設計並不是光靠可讀性就能達成。甚至為了易於修改,像是大量使用物件導向設計模式的話,反而可讀性會降低。所以好的程式跟好的設計其實是不一樣的議題,我們必須另外努力去做,才能達成!


[1] 除非你真的放著 Java API 不用,例如不知道有 collections,結果自己土法煉鋼敲了數十行程式落落長,才屬於需要降低行數來改善設計的新手。但這不是因為你需要節省行數的關係,而是你真的多寫了冤枉的數十行程式來。
[2] 目的是寫出小朋友也看得懂在做什麼的程式碼,而不是小朋友也寫得出來的程式碼;不要搞錯方向。例如我們看得懂什麼叫精彩的武俠小說,但不見得自己也寫得出來一樣。程式設計是一種創作,它充滿我們個人的想法在裡面,我們要努力的是傳達自己想法給他人,而不是努力制定應用文的範本讓內容千篇一律。
[3] 這篇文章是 2000 年代寫的,已不符現在的事實。在 2010 年代後期,中國開發大型專案的經驗遠比台灣豐富,現在無論開發還是設計,都比台灣程式設計師高好幾個檔次。過去台灣是靠 1990 年代為中小企業開發應用軟體累積了經驗,普遍比 2000 年代的中國程式設計師有更多軟體工程的實踐,但也就僅止於此,我們很少有大型專案的經驗。


Java 不再免費?

Java SE 11 和 Java SE 8u211 起,從 Oracle 官網下載的 Java 不再免費,只供企業付費使用。要免費使用 Java,請從其他地方下載 OpenJDK。


Java 8u211、Java 11

官方下載的 JDK 已經不是免費使用,變成商用授權軟體

Java SE 11 和 Java SE 8 Update 211 開始,全面使用 OTN 授權協議(Oracle Technology Network License Agreement),取代舊的 BCL 授權協議(Oracle Binary Code License Agreement),Oracle JDK 確定不再免費,屬於向官方取得授權然後商業使用的軟體。

但純粹只是 Oracle 自己的 JDK 不再免費,並非 Java 不再免費!Oracle 已為一般免費使用者鋪好 OpenJDK 的路,只要改用 OpenJDK,依然跟之前一樣,可以免費使用 Java 技術開發應用程式、商轉企業系統。為此 Oracle 還釋出許多 Java 的專利給 OpenJDK,讓兩者在技術上保持一致。

這裡有個坑,Oracle JDK 依然是免費下載的,但只能評估和測試用!若只在自己電腦使用,Oracle 會視為評估和測試用途,不會向你收錢。但只要是在有營運的公司內部使用 Oracle JDK 開發或執行 Java 程式,就要與 Oracle 洽談合約!所以請不要在工作上使用 Oracle JDK 開發程式,也不要在公司網站採用 Oracle JDK 運行程式~

換句話說,Oracle JDK 並不是過去的 Java SE Development Kit,而是 Oracle 另外自己用獨家專利打造的 JDK 產品,是給企業用的,而不是給大家下載使用的免費軟體資源。過去的 Java SE Development Kit,已遷移到 OpenJDK,繼續讓大家能下載並免費使用 Java!

但 Java 依然是可以免費使用的技術

其實還是跟以前一樣,Java 有免費使用和商業使用兩種!只是 Oracle 不再提供免費使用的版本,把免費使用的版本移交到 OpenJDK 而已。

所以嚴格來說,變的只有 Oracle 的業務,Java 程式語言可以免費使用的情況並沒有變!你依然可以像過去那樣免費使用 Java,只是要改用 OpenJDK,不要再從 Oracle 網站下載 JDK。

其實這樣的改變,反而更能保障免費使用 Java 的開發者和企業!過去 Oracle 提供付費和免費兩種 Java 時,想免費使用卻會誤入商業使用的陷阱,現在 Oracle 只提供付費的 Java,免費使用的 Java 由 OpenJDK 提供,付費版和免費版界線分明,使用者不用再擔心有哪些陷阱。

坦白說這招走得漂亮!一方面 Oracle 能用自己獨家專利的高性能 JDK 從 Java 賺錢,實現 Oracle 一直想從 Java 開設業務來賺錢的心願,一方面又讓免費使用的 OpenJDK 更完善,讓想免費使用 Java 的人更方便往沒有付費陷阱的 OpenJDK 遷移~


Java 17

免費送你三年商用許可

隨著新的 LTS 版 Java 問世,Oracle 基於 NFTC 授權協議(Oracle No-Fee Terms and Conditions)制定新的 Java 免費授權協議,允許在期限內免費商用 Oracle JDK 17!

免費的期限是三年,即下一個 LTS 版本推出後滿一年截止。這段期間將 Oracle JDK 17 用於公司營運,不需要向 Oracle 商談合約付授權金,等於談妥三年合約了!

期限到了必須付費,否則合約到期,不允許繼續商用 Oracle JDK 17!

照授權協議的內容,可免費商用的只有 Oracle JDK 17 與修補版本如 Oracle JDK 17.0.1,並不表示 Oracle JDK 18 也能免費商用,要等 Oracle JDK 18 出來,看使用的授權協議是否相同、或者變動的部分不影響商用,才能確定是否免費。

但 Oracle JDK 依然是商用授權軟體

僅 Oracle JDK 17 適用新的 NFTC 授權協議,Oracle JDK 8 和 Oracle JDK 11 依然使用 OTN 授權協議,請勿貿然下載最新的 Oracle JDK 8 和 Oracle JDK 11 在商用上。

所以,並不能說 Oracle JDK 成為免費軟體,Oracle JDK 仍舊是商用授權軟體,只是 Oracle 免費提供三年商用合約而已。

要免費商用 Java 技術,依然是 OpenJDK 才有保障,不建議轉換到法律關係複雜的 Oracle JDK 17!

想商用 Oracle JDK 17 的話,要有「換約」的觀念,當個知道怎麼守法律條文的人,積極了解與關心 Oracle 是否每一版或每一年都擬定不一樣的合約出來,及時遵照授權協議的變動去商用 Oracle JDK,以免誤觸法律陷阱。如果你不願關心法律和合約的事,請不要使用 Oracle JDK!以為就此免費,卻變成違法使用 Oracle JDK,反而付出慘痛的代價。


String

請把 String 視為進入 Java 程式設計的第一課,它比你所想的還要不簡單!

你應該對 String 的種種功能感到興奮、有趣,而不是覺得怎麼光一個 String 就這麼麻煩…否則勸你還是趁早放棄物件導向程式語言,因為可以操控物件的各式各樣功能來完成任務,是物件導向程式設計人員最基本的樂趣~

對學習物件導向程式設計來說,學習怎麼用 String,要比學習各式設計類別的花招來得重要!因為物件導向程式設計的重點,是用物件來設計程式,類別只是達成這目的的手段而已。

藉由這一章的 String 還有下一章的 StringBuilder,了解「操作物件」是怎麼回事,然後我們也設法把自己的程式,都設計成像 String 這樣的物件來用,我告訴你,這就是物件導向程式設計!1


比較字串內容

有三種,而且比較的原理不同,請務必釐清箇中差異,否則依賴比對字串來動作的程式將設計失敗:


兩物件指向同一筆資料。
兩物件傳回的資料相同。
兩物件的資料相同。


從字串中尋找文字所在位置


3
5
-1

找不到的話傳回 -1,因此這個功能也常常用來檢查字串中有沒有特定文字,例如:

if(a.indexOf("#")<0){
 doSomething();
}

另外還有類似的 lastIndexOf() 可用,差別在於改從字串尾端往前尋找,但結果不變。


以 Regular Expression 比對資料

雖然 indexOf() 就可用來檢查字串中有沒有特定資料,但主要任務並非如此,所以可以選用真正專門用來執行這樣任務的 matches(),它可用 Regular Expression 語法來檢查資料。

底下範例用 Regular Expression 設定條件,檢查字串的資料是否符合 Hex 寫法的 RGB 值:


true
false


取得某個位置的字元


D
因為要指定第一個字元的話是 0,所以指定為 3 所獲得的字元為 D。


擷取字串


CD
不是很直覺的參數,容易誤以為是第三個字元到第五個字元,但其實第二個參數是「終點」,並不是「從…到…」,要特別記住。

也可以不用第二個參數,這樣則是從指定的某個字元開始擷取往後的所有字串內容。


混搭 indexOf() 與 substring()

底下範例從 [password=abc123] 這組字串中2,擷取出 password 與 abc123。


password
abc123


分離字串


CCC
上面的程式可以簡化成:


這招尤其適合用來快速取得 CSV 格式中想要的資料!特別是以純文字文件的「行」為 row,以 split() 的陣列索引值為 column,一個「資料表」很簡單就實現了。


替換字串中的文字資料


HE--O
值得注意的是,b 雖然是 HE--O,但 a 依然是 HELLO。


以 Regular Expression 一次替換多種條件

如果想替換掉的文字不只一個,又不想多寫一行程式來替換,可以使用 Regular Expression 做為條件。下面的範例,一行就將 HELLO 中的 L 和 O 全部替換為 - 符號:


HE---


連接字串


ABCDEFGHIJKLMNOP
ABCDEFG

這個範例值得注意的是,b 雖然獲得連接了字串的內容,但 a 本身的資料並未改變。


削掉頭尾空白


 ABCDEFG
ABCDEFG


轉換為大小寫


TWIDEEM CIVS
twideem civs

有時候需要檢查字串內容時,先全部轉為大寫或小寫,再來處理會比較適合。所以這個功能雖然只適用於英文字母,但還是用得到,尤其是設計登入介面,處理英文字母的帳號。


將數值轉為字串


579
123456

如果想將兩個數值資料用 + 串起來成為文字,會變成兩個數值進行加法運算,而不是串接。這時就適合用 valueOf() 功能。


將物件型態的字串轉輸出為基本型態 char 的陣列


BCDEFGH
適合逐一針對字串裡面每個字元進行運算的場合。


字串的 Unicode 值


猜 \u731c 猜
猜 \u731c 猜
我 \u6211 我
是 \u662f 是
誰 \u8ab0 誰


直接對字串資料操作物件功能

以 "" 括住的資料,本身就是 String 物件了,因此可以直接對其呼叫 Stirng 的功能:


3


[1] 物件導向程式設計的重點,並不是實作與介面分離,而是有沒有把程式運作的細節都裝在物件裡面,讓程式碼看上去整片都是用操作物件的方式來完成。介面分離到照樣把細節攤在眼前、把處理細節的程式碼混夾在操作物件之間,依然不是物件導向程式設計!
[2] 這組字串的結構,是過去 Windows 95/98 時代很常見到「INI 組態檔」資料格式。當時應用軟體常常這樣的格式,將軟體的設定值寫在純文字文件來保存。


StringBuilder、StringBuffer

由於 Java 的 String 有「內容不可變」的性質,當內容變更時,採取重新 new 一個物件做為新內容的做法,因此為了避免 new 太多 String,而提供 StringBuilder 來彌補。

既然 StringBuilder 是特別用來變更內容用的,因此有更豐富的文字修改能力,可以方便開發者編輯字串內容。但不支援 Regular Expression,因此不是用來取代 String 的,而是輔助用的,適合用在插入與刪除的場合。

最後,StringBuffer 其實與 StringBuilder 一樣,只差在支援多執行緒;如果設計上沒用到多執行緒,那使用 StringBuilder 有更佳的執行效能。


加入字串


ABCDEFG


插入字串


ABCXYZDEFG


輸出字串

StringBuilder 並非 String,因此不能將它代入 String 的參數,必須使用 toString() 將內容以 String 輸出:


ABCDEFG


取得字元


D


擷取字串


CD


更改字元


ABC-EFG
注意!既然是字元,第二參數用的是單引號,而不是雙引號,否則將編譯錯誤。


更改字串


A-G
注意,與 String 的 replace() 參數用法不一樣。


將字串順序反過來


GFEDCBA


刪除單一字元


ABDEFG


刪除連續字串


AG
注意!跟 String 的 substring() 一樣,參數看起來並不直覺,第二個參數是終點(到哪裡為止),所以 (1,6) 而不是從第二個字元刪到第七個字元。


清空字串

StringBuilder 並沒有 clear() 這類操作方式,只好用 setLength(0) 將字串長度設為 0,以資料歸零的做法來折衷:


HIJKLMNOP


削掉頭尾空白


ABCDEFG


從字串中尋找文字所在位置


3


取得字串長度


7


格式化字串


Example 1


123


Example 2


1 + 1 = 2


Example 3

System.out.printf() 跑的其實就是 Formatter:


1 + 1 = 2
1 + 1 = 2


Regular Expression

雖然 Java 的 String 可以進行 Regular Expression(正規表示式),但正式的做法是使用 java.util.regex.Pattern,它需要混搭物件,所以稍嫌麻煩,但也因此賦予許多靈巧的機制,可以設計出較有彈性的程式結構。


基本用法


123


取得成功匹配結果的開頭位置與終點


3
6

注意開頭位置是「第幾個字元開始」,但終點卻不是「第幾個字元結束」,而是「到哪裡為止」。


取代字串


ABC---


完整匹配

有時候希望更簡單就能以嚴密的方式,比對整個字串是否能通過驗證。如下範例,Pattern 所 matcher() 的字串,必須從頭到尾都符合 compile() 設定的條件,才會傳回 true:


true
底下示範無法通過規範:


false


切割字串

使用 Pattern 的 split() 可將字串依照 compile() 所建立的規則,切割為好幾段的字串,而切割出來字串,以 String 陣列傳回:


ABC
123
:)


陣列、java.util.Arrays


陣列

陣列是程式設計語言都有的語法功能,用來連續建立同樣型態的資料。

建立陣列

空白陣列:


含初始值的陣列:


匿名陣列:


5

存取陣列


DDD

陣列長度


3

巡訪陣列

傳統做法:


aaa
bbb
ccc

Java SE 5 新增的語法 For-each loop:


aaa
bbb
ccc


java.util.Arrays

陣列本身不是物件,因此沒有足夠的功能可以操作。為了更進一步靈活使用陣列,Java 提供了 Arrays。

它更讓人驚豔的是採用高效率的演算法來執行工作,提升解決問題的能力!

代入陣列到資料群集

想要將陣列依序放入群集時,通常會寫如下的程式:


對於這樣的動作,Arrays 設計更高效率的演算法,寫法如下:


注意!使用 Arrays.asList() 取出的資料,必須改用 List.addAll() 一次存入。

擷取陣列、複製陣列


AAA
BBB
CCC

擷取完整長度的陣列,就變成複製陣列了。

擷取資料還有 copyOfRange(陣列, 起點, 終點) 可用。

比對陣列


兩陣列所含資料相同

填滿陣列


XXX
XXX
XXX

排序資料


1
2
9

依自訂的比較規則排序資料


C
B
A

打亂排序的資料


每次執行結果不一樣:
C
B
A

搜尋資料

排序過的陣列,可以使用 binarySearch(陣列, 資料)binarySearch(陣列, 起點, 終點, 資料) 搜尋資料,找到的話會傳回索引值。


Collections 速成

如果你懂得根據資料的處理方式,善用合適的 Collection,可以簡化程式碼的結構,提升可讀性。例如既要過濾重複資料、又要排序資料,那直接用 TreeSet 一次搞定,要比先用 HashSet 過濾,再轉存到 ArrayList 讓 java.util.Collections.sort() 排序來得乾淨俐落!

反過來,明明 Java 已經提供這些高效能演算法的資料結構,卻還自己土法煉鋼去處理,變成要敲二三十行的程式碼,錯失只需兩三行程式就能搞定的快感,而且處理時間可能要多兩三倍。


使用時機

陣列用途:

ArrayList允許重複資料、照先後順序排列、適合讀取資料。
LinkedList允許重複資料、照先後順序排列、適合更動資料。
HashSet濾除重複資料、特殊演算順序。
LinkedHashSet濾除重複資料、照先後順序排列。
TreeSet濾除重複資料、自動排序。

關聯表用途:

HashMap鍵/值對資料、特殊演算順序。
LinkedHashMap鍵/值對資料、照先後順序排列。
TreeMap鍵/值對資料、自動排序。

巡訪資料:

Iterator使用 hasNext() 和 next() 巡訪資料。

舊版做法:

Vector建議用 ArrayList 替代。
Stack建議用 LinkedList 替代。
Hashtable建議用 HashMap 替代。
Enumeration建議用 Iterator 替代。

範例

先以 ArrayList 為例:


aaa
bbb
ccc

再以 TreeSet 為例:


4
8


補充

為什麼使用 List list=new ArrayList() 或 Set set=new HashSet() 的方式來宣告參照?這是為了往後隨時可以抽換成其它類別。當我們發現所選的群集在處理資料並不適任時,就可以由 ArrayList 抽換成 LinkedList、或由 HashSet 抽換成 LinkedHashSet,而不影響程式其它部分。

當然,還可以更進一步!如果希望將來可以在 List 與 Set 中替換,那可以改用 Collection 介面,因為它是 List 與 Set 的共通介面:


上例是用 HashSet 實作,現在直接換成 ArrayList,但其它地方都不需要修改:


然而,這樣做是需要犧牲的!例如 Collection 介面並沒有 List 介面的 get(int),因此雖然可以從 Set 介面替換為 List 介面,但既然使用 Collection 介面,就不認得 get(int),因此只能用 iterator() 來巡訪資料1

這是理所當然的,既然要能隨時互相替換,那就應該用彼此都有的方式來操作。

如何在「初寫的方便性」與「修改的靈活性」之間做抉擇,端看我們對需求的掌握程度。


進階

java.util.Collections 提供許多 static 的 method,方便我們處理 Collection 的資料:

binarySearch(List, Object)搜尋 List 內容的 Object 元素。
copy(List dest, List src)複製 src 的元素給 dest。
enumerarion(Collection)傳回舊式的 Enumerarion 迭代器。
fill(List, Object)將 List 內容填滿為 Object。
max(Collection)傳回 Collection 中最大的元素。
min(Collection)傳回 Collection 中最小的元素。
reverse(List)顛倒 List 內容的排列順序。
shuffle(List)亂排 List 的內容。
sort(List)排序 List 的內容。
swap(List, int i, int j)交換 List 中 i 與 j 位置兩筆元素。

簡單示範其中的 shuffle,設計用電腦抽籤來決定聯盟球隊選秀順位的程式:


每次執行結果都不一樣:

三商虎
味全龍
俊國熊
兄弟象
時報鷹
統一獅


補充

用來處理陣列的 java.util.Arrays,對 Collections 也有幫助,底下是利用 Arrays 產生 List 的範例,程式相當簡潔:


AAA
BBB
CCC

如果可以再寫成 List<String> list = {"AAA","BBB","CCC"} 就好了,可惜 Java 不支援。


[1] 頂多用 toArray() 轉成陣列再處理。


亂數


從 0 到 2 之間,隨機產生一個數值。
你可能會感到疑惑說,預設的計算結果並不直覺,為何設定 3,並不是 0 到 3 或 1 到 3,偏偏是 0 到 2?

是不直覺沒錯,但這樣有個好處是,能直接處理「陣列」或「群集」的資料,因為這些結構在資料長度為 3 的時候,卻是從 0 到 2 去存取。可能設計 API 的人認為這種情況居多,所以預設這樣的值吧?

自己變通一下,把得到數值加 1,即為 1 到 3 之間的亂數了。

最小值到最大值的話:

new Random().nextInt(最大值-最小值)+最小值;

用起來跟 Math.round(Math.random()*(最大值-最小值))+最小值; 一樣,為什麼不提供 nextInt(最小值, 最大值) 把這細節裝在物件裡面?確實 java.util.Random 的設計不夠物件導向,但卻是不錯的反教材:「我們不該像 java.util.Random 這樣,設計一個其實重複的類別,我們應該設計進一步抽象掉細節層次的類別。」


資料夾與檔案的管理


基本範例


其它常見功能

canExecute()是否為可執行檔。
canRead()是否可讀取。
canWrite()是否可寫入。
compareTo(File)合併檔案。
getFreeSpace()取得所在磁碟的剩餘容量,單位是 byte。
getName()取得檔案名稱。
getParent()取得當前資料夾。
getPath()取得完整路徑。
getTotalSpace()取得所在磁碟的容量,單位是 byte。
getUsableSpace()取得所在磁碟的可用容量,單位 byte。
isDirectory()是否為資料夾。
isFile()是否為檔案。
isHidden()是否為隱藏檔。
length()取得檔案大小,單位是 byte。
listFiles()以 File 陣列傳回檔案。
setExecutable(boolean)設定執行權限。
setLastModified(long)設定檔案修改日期。
setReadable(boolean)設定讀取權限。
setReadOnly()設定唯讀。
setWritable(boolean)設定寫入權限。
toURL()以 java.net.URL 輸出。


純文字文件的寫入與讀取

檔案讀寫是 Java 最讓人津津樂道的物件導向經典案例!它有最基本功能的物件方便使用,事後用俗稱 decorator pattern(裝飾者模式)的設計,可以不斷堆加功能上去來滿足需求。

請在本章節好好看 Java 怎麼做,然後用同樣套路強化我們的類別!


直接套用作業系統預設格式來讀寫文字的 FileWriter 與 FileReader

為了方便程式設計師在不同作業系統,都能以相同的程式寫法,來存取純文字檔案的資料,Java 提供 FileWriter 與 FileReader,它會直接用作業系統預設的字元編碼與換行格式,來寫入與讀取純文字資料!

寫入純文字文件







注意!這個範例沒有建立空白新檔的動作,所以你必須事先準備好檔名為 sample.txt 的純文字文件,否則無法執行程式。

讀取純文字文件


如果讀取的 sample.txt 是前一個範例所產生,程式執行結果為:






提升檔案讀寫效率的 BufferedWriter 與 BufferedReader

使用 FileWriter 與 FileReader 是直接對硬碟的檔案進行讀寫,如果讀寫頻繁的話,因為硬碟往往是電腦中速度最慢的裝置,應用程式的效率可能會被拖慢。因此 Java 又另外提供了 BufferedWriter 與 BufferedReader,可以先將硬碟檔案的資料緩存在速度較快記憶體來讀寫,提高應用程式的執行效率!

不只如此,BufferedWriter 與 BufferedReader 提供了更容易使用的方式來讀寫文字,所以很少人直接用 FileWriter 和 FileReader,寧願多一道程序捆入 BufferedWriter 和 BufferedReader。

寫入純文字文件


猜猜我是誰
注意!這個範例沒有建立空白新檔的動作,所以你必須事先準備好檔名為 sample.txt 的純文字文件,否則無法執行程式。

讀取純文字文件


如果讀取的 sample.txt 是前一個範例所產生,程式執行結果為:

猜猜我是誰


使用 FileOutputStream 與 FileInputStream 指定字元編碼

常常聽到 FileWriter 和 FileReader 跨平台的說法!其實是「檔案讀寫」這個程式動作跨平台,而不是文字檔案本身跨平台。例如在預設使用 Big5 編碼的 Windows 寫入純文字文件,拿到預設使用 UTF-8 編碼的 Linux 碼讀取,還是會有亂碼的情況發生。所以我們應該掌握如何指定以某字元編碼來讀寫純文字文件的做法:


猜猜我是誰


以二進制格式讀寫資料


DataInputStream、DataOutputStream

與「純文字文件」不同的是,「二進制檔案」無法用「記事本」或「文書軟體」開啟來閱讀,會看到一堆亂碼。有些人不希望保存資料的檔案能夠直接被閱讀與修改,或者希望提高檔案的資料處理效率而使用它,屬於進階應用的檔案保存手法。

寫入


將產生名為 data.dat、內容為二進制格式的檔案。

讀取


輸出結果:

AAA
BBB
CCC

補充

除了 writeUTF() 和 readUTF(),還有更多像是 writeInt() 和 readInt() 之類的功能可操作。

FileInputStream 和 FileOutputStream 通常會用 java.io.BufferedInputStream() 和 java.io.BufferedOutputStream() 包起來,以提升執行效率。


RandomAccessFile

RandomAccessFile 能進行 DataInputStream 與 DataOutputStream 一樣的工作效果,但只需要建立一個物件~

Example 1


將產生名為 data.dat、內容為二進制格式的檔案,並輸出如下結果:

123
ABC
true

Example 2


將產生名為 data.dat、內容為二進制格式的檔案,並輸出如下結果:

true
123
ABC

使用 seek() 必須注意資料長度的問題,例如在 seek(0) 使用了 writeInt() 的話,因為整數是 byte[4] 長度,所以必須至少從 seek(4) 開始才行,否則等於從原先資料的中間插入新資料,把原先資料給破壞掉。

特別是 writeUTF(),長度不固定,文字越多就越長,這時 seek() 要保持足夠間距來應付才行!至於長度的計算,公式是「目前 seek() + 字數 + 2」,所以要取得本範例的 boolean 資料時 seek 為 4+3+2。


FileInputStream、FileOutputStream

前面範例是透過 Java 將資料以二進制格式寫入資料,往後再用 Java 讀出來用。但如果要讀取的,是像可執行檔、圖片、音樂、或其它程式產生的二進制檔案內容,那使用 FileInputStream 和 FileOutputStream 比較適合。

讀取


如此 bytes 即為讀取到的二進制資料。

寫入

以二進制格式讀取檔案內容,再轉存為另一個檔案,這招等於複製檔案!


將 Java 物件轉為檔案形式

Java 能夠將程式執行中的「物件」,以檔案的形式儲存起來;這樣的過程術語叫「序列化」。當然,我們可將序列化的檔案還原為物件,善用序列化,可以讓我們寫出更優雅的物件導向程式設計,例如遊戲進度的存檔會更直覺、物件深層複製也能用這招來解決。


基本範例

Test.java

Main.java


Hello


進階項目

如果物件某些狀態不想被序列化的話,可以加上 transient 指令:

Test.java

Main.java


Null


命令列介面的程式設計

應用程式(Application)一般分為「視窗軟體(Window)」的「圖型化使用者介面(GUI; Graphical User Interface)」,以及「主控台指令(Console)」的「命令列介面(CLI; Command Line Interface)」。本文將介紹如何用 Java 設計指令模式的程式~


輸入

底下示範 Java 如何取得使用者用鍵盤輸入的資料~

Example 1-1

第一個範例雖然不常用,但比較簡單,它用來取得使用者按下的是哪個鍵:


a  (假設使用者輸入時敲的是 a 鍵)
97 (結果輸出 a 的 ASCII 十進位碼)

Example 1-2

第二個範例來看如何取得使用者輸入的文字,雖然它可用第三個範例的寫法來取代…


aaa (假設使用者輸入時敲了 aaa)
aaa (輸出結果為 aaa)

Example 1-3

第三個範例,將使用者輸入的資料分解成字串和整數等資料型態來使用,方便我們處理輸入的資料,避免寫一堆落落長的程式來擷取與轉換:


請輸入參數與設定值:
double 200 (這是使用者的輸入情況)
400

Scanner 還有更多轉換或取出資料的功能,請參考 Java API Specification。

Scanner 是 Java SE 5.0 開始支援的物件,它比過去的 java.io.StreamTokenizer 操作簡單、功能更強,而且不只可以引入 System.in,其實也可以引入 String 和 File。自從有了 Scanner,就很少人再用 StreamTokenizer 和 StringTokenizer 了~

不只如此,你可以發現這個範例比上一個範例簡潔多了!確實,自從有了 Scanner,也很少人再用 InputStreamReader 和 BufferedReader 來設計命令列介面程式的輸入功能了!


輸出

命令列模式的輸出很簡單,使用 System.out.print() 能直接將資料以字串輸出,System.out.println() 一樣,但會自動換行。System.out.format() 提供了格式化輸出的功能,但因為這是仿效 C 語言的 printf() 而來,所以又提供 System.out.printf() 來呼叫它:

Example 2-1


AAABBB
CCC

Example 2-2


Hello, world!
更多 printf() 格式化參數:

%d輸出十進位整數
%o輸出八進位整數
%x輸出十六進位整數
%f輸出十進位小數
%g輸出科學標記法
%a輸出十六進位指數
%c輸出字元
%s輸出字串
%b輸出布林值
%n換行
%數字$第幾個參數
%數字固定佔用寬度
%.數字設定小數點位數

Example 2-3


222

Example 2-4


  A  BB CCC
AAA BBB   C

Example 2-5


123.46


參數

在命令列下達程式指令時,經常都能尾隨一些參數,底下示範做法:

Example 3-1


java Main aaa bbb ccc
aaa
bbb
ccc

Example 3-2


java Main hello
哈囉

Example 3-3


java Main
命令語法不正確

java Main /create text.txt
已建立 text.txt 檔案

java Main /rename text.txt image.img
已將 text.txt 檔案改名為 image.img


調用作業系統的命令


[*] Runtime.getRuntime().exec() 已被列為 deprecated,不建議使用。


Swing 快速入門


建立視窗


顯示結果:

插圖


加入按鈕


顯示結果:

插圖 插圖


你已經快速入門了~

依樣畫葫蘆,陸續加入像是:文字標籤的 JLabel、文字區域的 JTextArea、單行輸入欄的 JTextField、密碼輸入欄位 JPasswordField…等等各式組件,就能打造出視窗了!

接著再查 API 看看各個元件有哪些功能可用,然後深入研究 java.awt.event 的其它事件處理,就能熟悉 Java 的視窗應用程式設計!



變更視窗樣式


Java 內建視窗樣式

底下程式示範將視窗更換為 Metal 樣式:


顯示結果:

插圖

其它內建的視窗樣式還有如下,請自行嘗試:

javax.swing.plaf.gtk.GTKLookAndFeel
javax.swing.plaf.mac.MacLookAndFeel
javax.swing.plaf.motif.MotifLookAndFeel
javax.swing.plaf.nimbus.NimbusLookAndFeel
javax.swing.plaf.windows.WindowsClassicLookAndFeel
javax.swing.plaf.windows.WindowsLookAndFeel

另外,Windows 與 MacOS 並非跨平台的樣式,遇上相異的作業系統無法使用,會改用預設的樣式。因此顧慮跨平台問題的話,也可考慮以下自動處理跨平台問題的樣式:

UIManager.getSystemLookAndFeelClassName()
UIManager.getCrossPlatformLookAndFeelClassName()


Steel Theme

Java SE 5.0 時,推出新的 Metal 風格,叫做 Ocean Theme,較為明亮。J2SE 1.4 時,Metal 風格叫做 Steel Theme,比較暗沉,而且元件的層次之間沒什麼立體感,辨識度較差。

由於 Java SE 5.0 開始將 Ocean 做為 Metal 風格的預設主題,因此如果你想切換回 Steel 主題,程式碼如下:


插圖


第三方視窗樣式

網路可以找到不少 Swing 的視窗樣式可以套用,首先推薦 JTatto,因為下載這個套件,就能切換十數種樣式1,而且每種樣式都經過精心配置,每樣都比其他人的樣式精美。請至官網下載:

http://www.jtattoo.net

目前為止 JTatto 可用的樣式如下:

com.jtattoo.plaf.acryl.AcrylLookAndFeel
com.jtattoo.plaf.aero.AeroLookAndFeel
com.jtattoo.plaf.aluminium.AluminiumLookAndFeel
com.jtattoo.plaf.bernstein.BernsteinLookAndFeel
com.jtattoo.plaf.fast.FastLookAndFeel
com.jtattoo.plaf.graphite.GraphiteLookAndFeel
com.jtattoo.plaf.hifi.HiFiLookAndFeel
com.jtattoo.plaf.luna.LunaLookAndFeel
com.jtattoo.plaf.mcwin.McWinLookAndFeel
com.jtattoo.plaf.mint.MintLookAndFeel
com.jtattoo.plaf.noire.NoireLookAndFeel
com.jtattoo.plaf.smart.SmartLookAndFeel
com.jtattoo.plaf.texture.TextureLookAndFeel

各樣式顯示結果依順序如下:

插圖
Acryl
插圖
Aero
插圖
Aluminium
插圖
Bernstein
插圖
Fast
插圖
Graphite
插圖
Hifi
插圖
Luna
插圖
Mcwin
插圖
Mint
插圖
Noire
插圖
Smart
插圖
Texture

[1] 可在執行期間切換,不用重新啟動程式。甚至同一個應用程式裡,顯示不同樣式的視窗。


對話視窗


訊息視窗

純粹跳出對話視窗顯示一下訊息,頂多搭配圖示方便使用者判斷訊息,有五種可用…

緊急通報


插圖

注意事項


插圖

警告標語


插圖

警訊提報


插圖

一般訊息


插圖


確認視窗

跳出詢問用的對話視窗,可根據傳回值,判斷使用者的回覆:

JOptionPane.YES_OPTION
JOptionPane.NO_OPTION
JOptionPane.CANCEL_OPTION
JOptionPane.OK_OPTION
JOptionPane.CLOSED_OPTION

依按鈕的配置,總共有四種…

OK


插圖

YES / NO


插圖

YES / NO / CANCEL


插圖

YES / CANCEL


插圖

補充範例

例如還蠻常用的情況是,用詢問視窗確認使用者是否真的要關閉:


輸入視窗

標準型(可設定標題列與圖示)


插圖

速用型(可輸入預設值)


插圖


檔案視窗

雖然有兩種方式可以呼叫不一樣的對話視窗,但其實只有標題列文字不一樣而已,取得檔案的行為一模一樣。為了方便研究,依然分開寫兩個範例如下:

儲存視窗


插圖

開啟視窗


插圖

可選取多個檔案

指定預設路徑

你可以預設檔案對話視窗的一開始所在的資料夾位置,如下介紹的是使用者的桌面位置:

篩選副檔名

檔案對話視窗通常可以篩選副檔名,底下範例介紹如何為檔案對話視窗增加只篩選出 XML 副檔名的功能。

XMLFileFilter.java

XMLFileChooser.java

Main.java

插圖


色彩選擇器


插圖


JScrollPane


捲動軸

底下示範為 JTextArea 添加捲動軸:


執行程式並貼上文字後:

插圖


其它設定

可用 scroll_pane.setHorizontalScrollBarPolicy() 設定垂直捲軸的顯示時機:

JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS (顯示)
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED (自動)
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER (隱藏)

可用 scroll_pane.setVerticalScrollBarPolicy() 設定水平捲軸的顯示時機:

JScrollPane.VERTICAL_SCROLLBAR_ALWAYS (顯示)
JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED (自動)
JScrollPane.VERTICAL_SCROLLBAR_NEVER (隱藏)

將元件做為 JScrollPane() 建構式參數的話,相當於 scroll_pane.setViewportView() 的功能,例如:JScrollPane scroll_pane = new JScrollPane(text_area)。


JTable


產生表格


顯示結果如下:

插圖


操作表格

針對上一節範例的 table_model 物件,進行「欄」「列」「資料」的相關操作。


讓表格能對應資料型別

表格可以根據資料型態的種類,作出不同的顯示:

插圖
文字向左對齊,數值向右對齊,布林值以核取方塊顯示。

先建立新的類別 MyTableModel.java 讓它繼承 DefaultTableModel,然後複寫 getColumnClass(),再將 Main.java 的 DefaultTableModel 改為 MyTableModel 即可。

MyTableModel.java

Main.java


日期與時間

java.util.* 裡的 Date 是個失敗的設計,它不夠精確、不能滿足各國在地化的需求、而且犯了神類別的錯,沒有彈性可以變更設計去滿足需求~

在 JDK1.1 看到的解決辦法是,改推 Calendar 取代 Date 大部份的功能,然後把 Date 該被 Calendar 取代的功能整排 deprecated 掉。這做法不是取代整個類別,而是把 Date 功能割掉當成新類別(就跟 Calendar 一樣新而且是不一樣的東西),不是推出 Calendar 後形成新舊兩類別(也就不能當作 Date 一樣的舊東西),理念之複雜不是一般程式設計師能理解的。

後來 Java 8 推出新的 java.time.* 能把事情做盡善盡美!然而,練習舊的、設計不良的 Date,要比新的 Instance 和 LocalDateTime 更能了解 Java 物件導向程式設計。從 Date 的錯誤設計到 Calendar 和 DateFormat 的失敗套路,我們可以學到很多東西,所以本文還是刊載舊的來練習。

只是練習,正式場合請不要用舊的寫法處理日期和時間 XDDD


取得日期與時間


執行結果隨電腦當時的時間與時區而不同:

2002/8/29 下午 12:34
只想輸出日期的話,可用 DateFormat.getDateInstance().format(date)) 輸出「2002/8/29」,只想輸出時間的話,可用 DateFormat.getTimeInstance().format(date)) 輸出「下午 12:34:56」。


分別取得年、月、日、週、時、分、秒


執行結果隨電腦當時的時間與時區而不同:

年:2002
月:8
日:29
週:星期四
午:下午
時:12
分:34
秒:56

有 SimpleDateFormat,想取得「年、月、日、週、時、分、秒」時,可別傻傻用 DateFormat 傳回的字串去剖析。


自訂日期與時間的輸出格式


執行結果隨電腦當時的時間與時區而不同:

2002 年 8 月 29 日 星期四 下午 12:34:56


指定時間


執行結果:

年:2002
月:8
日:29
週:4
時:12
分:34
秒:56

彆扭的是,Calendar 的一月是 0,所以取得月份時要加 1,設定月份時則用常數比較直覺。星期幾就複雜了,不同地區,有些星期日是每週的第一天,有些星期一是每週是第一天,導致台灣取得星期幾要減 1。


計時器


(一秒後,每兩秒輸出一個 * 符號。)


暫停一段時間


(一秒後)
Hello!

在某些場合,sleep() 是很頻繁使用的功能,每次都寫在 try 裡很噁心,整串程式碼都寫在 try 裡又很愚蠢,所以在需要用到 sleep() 的場合,不妨丟出例外就好,不要處理例外。

或者另外寫個 sleep() 函式處理好例外:

void sleep(int x){
 try{
  Thread.sleep(x * 1000);
 }
 catch(Exception e){
  e.printStackTrace();
 }
}

往後都改用這個 sleep() 函式。


列印

列印的原理有點怪,但講起來也很簡單:「用畫的。」

我覺得這是很有意思、也很有意義的範例,練習用不一樣的視野來開拓我們的設計。


事前準備

除非你嫌墨匣太多,否則建議安裝 PDFCreator 充當印表機,在練習列印的功能時,將列印結果以 PDF 文件來儲存。或者 Windows Vista 開始內建的 Microsoft XPS Document Writer 也有同樣功能。

這樣在執行列印功能時,就可以不用拿真的印表機來測試,列印的結果改用 PDF 文件或 XPS 文件來輸出與顯示:


基本列印的語法架構

底下範例是最簡單的列印1,會在紙張上印出 Hello 字串:


改變文字樣式、取得紙張大小與可列印範圍

你可以透過 Graphics 改變文字的字型、大小、顏色,或其它樣式。而透過 PageFormat 的 getWidth()、getHeight()、getImageableWidth()、getImageableHeight(),可以取得紙張範圍的大小,避免資料列印到外界去了,例如:


自訂紙張格式

或者使用 PageFormat 透過 Paper 自訂紙張尺寸,如下範例,它加大了 A4 紙張的可列印範圍,否則雖然紙張夠大,但允許的可列印範圍不夠,列印時超出範圍的部分會被切割掉:


複製物件

如果你想要複製一份物件,Object 提供了 clone(),每個繼承自 Object 的 class 都能用它來複製自己。

不過,任意就能複製一份別人的物件來用,並不是很安全的設計,所以後來改成要使用 clone() 的話,必須宣告願意被複製的物件是 Cloneable 介面,並將 protected 的 clone() 覆寫為 public,而且還要拋出 CloneNotSupportedException 例外。

簡單的事變得這麼複雜,著實令人厭煩!但先別急,正好就是這樣的例子,可以見識 Java 如何藉由物件導向應對變化,學到很多東西~


A.java


Main.java


123
abc


Observer pattern

Java 內建設計模式中的 Observer,它用來監視物件狀態,當物件狀態改變時,它會通知監視該物件的對象,讓監視者可以做出反應;本範例中 View 物件將監視 Model 物件。


範例一:被監視者在狀態改變時發出通知,讓監視者做出反應。

Model.java

View.java

Main.java


所監視物件更新了狀態!


範例二:取得被監視物件的狀態

Model.java

View.java


Main.java


所監視物件更新狀態為:hello


javac

基本格式:javac -參數 參數值 [-參數2 參數值2...] 原始碼


常用參數

-d指定編譯出來的類別,要存放在哪個資料夾。
-cp 或 -classpath指定類別或掛載第三方套件到 CLASSPATH 環境變數。
-encoding指定原始碼的字元編碼系統。

進階參數

-source以哪一版的 Java 語法來編譯原始碼。
-target以哪一版的 Java 架構來產生位元檔。

其它參數

-version查閱 JDK 的版本。
-verbose顯示更詳細的編譯錯誤訊息。
-Xlint顯示新版編譯器建議的新做法。
-nowarm停用警告訊息。
-g編譯出來的類別含完整除錯用資訊。
-g:none編譯出來的類別只含位元碼,連反組譯用的資訊都沒。
-O優化編譯,提升類別的執行效率。
-help顯示並說明更多其它的參數。

jar

JDK 提供 jar 指令工具,可將所有類別壓縮成一個檔案來使用。

如果懂得自訂 MANIFEST.MF 檔案的話,甚至能夠讓你的 *.jar 成為執行檔。


一個一個類別打包到 JAR 壓縮檔裡面。

jar cf 壓縮檔名稱.jar 類別一.class 類別二.class


將某個資料夾所有東西都打包到 JAR 壓縮檔裡面。

jar cf 壓縮檔名稱.jar -C 資料夾 .

注意 -C 是大寫!


只打包「子資料夾」到 JAR 壓縮檔裡面。

jar cf 壓縮檔名稱.jar -C 資料夾 ./子資料夾


如何執行 JAR 檔裡面的類別

假設你打包了一個 Application.jar,裡面有個 application 資料夾,這個資料夾裡面有 Main.class,那麼要執行 Application.jar 裡面的 Main.class 類別,方法是:

java -classpath Application.jar application.Main


Executable JAR

接續上一個「如何執行 JAR 檔裡面的類別」的案例,如果 application.Main 裡面有 main(),那麼在用 jar 打包時,可以多添加一個 e 參數將 application.Main 指定為入口(entrypoint):

jar cfe Application.jar application.Main Main.class

以後就能直接執行 jar 檔:

java -jar Application.jar

甚至有安裝 JRE 或 IcedTea 的話,直接在 Application.jar 用滑鼠點兩下,就能執行程式,跟 *.exe 使用起來沒兩樣!


[META-INF]/MANIFEST.MF

上一節「Executable JAR」之所以能讓 *.jar 檔案成為執行檔,是因為下達 e 參數後,JAR 裡面的 META-INF 資料夾有 MANIFEST.MF 檔案出現這項設定值:

Main-Class: application.Main

你也可以自己寫好 MANIFEST.MF,再打包到 JAR 裡面,尤其需要追加更多參數給時只能這麼做。

做法是使用 m 參數,以及你自己寫好的 MANIFEST.MF 檔案:

jar cfm Application.jar MANIFEST.MF Main.class

寫 MANIFEST.MF 時,注意最後要新增一個空行,否則會有檔案結尾辨識錯誤,而找不到參數的問題。


相關網站

Oracle JDK - 官方版 Java!已轉為商用授權軟體,必須取得合約才能合法使用。
Oracle OpenJDK - 官方版 OpenJDK,免費使用,商業用途也沒問題。
Adoptium - 原 AdoptOpenJDK,性能優化佳,是最受歡迎的 OpenJDK。
Amazon Corretto - 有 Java 之父 James Gosling 任職的 AWS 所推出 OpenJDK。
Zulu - 提供最多版本的 OpenJDK,有 BETA 版的最新 Java,還有仍在更新的 Java 7。
JavaFX - OpenJDK 不含 JavaFX,因為獨立成 Open JavaFX。
SWT - 原生元件的圖形使用者介面。
Groovy - 以 Java 語法為基礎,加以擴充各式先進語法機制進去的表述語言。
Scala - 基於 JVM 的函數式程式語言,但又提供比 Java 物件導向更純正的結構。
Kotlin - 可編譯為 Java 類別檔或 JavaScript 程式碼,且賦予資料型別的特性。

NetBeans - Java 官方推出的開發環境,以最友善的視覺化環境為優點。
Eclipse - 口碑第一的 Java 開發環境,但著重在整合開發不是編輯環境,已退流行。
JDeveloper - Oracle 開放免費下載使用,所整合功能最強悍的開發環境。
BlueJ - 以 UML 來檢視專案結構的整合式發展環境,藉以養成好的物件導向開發習慣。
jEdit - 雖然操作介面不夠方便,但編輯性能堪稱第一的文字編輯器。
Ant - 將編譯、測試、部署工作自動化的專案管理工具。
Maven - 軟體專案管理及自動構建工具。
Gradle - 基於 Ant 和 Maven 概念的專案自動化建構工具。

JUnit - 可實現「測試先行開發模式」的單元測試框架。
Struts - 符合 MVC 設計精神來開發 Web Application 的 Framework。
Spring - 全方位應用程式框架,可建立輕量級的 EJB 應用程式。
Hibernate - 能將物件的關係,對照為關聯式資料的開發框架,更有效結合資料庫。
Tomcat - 可執行 JSP 的 HTTP 伺服器系統。
Jakarta EE - 原 Java EE,企業級 Java 應用程式架構,移交給 Eclipse 而改名。
GlassFish - 企業級 Java 應用程式伺服環境。
Jetty - 基於 Java 的網頁伺服器和 Java Servlet 容器。
Apache Commons - 提供各式實用套件,除了 Java API,第二個想到的就是它。
dom4j - 比 Java API 簡單易用的 XML 解析器。
JDOM - 比 Java API 簡單易用的 XML 解析器。
jaxen - 能夠讓 DOM 以 XPath 存取節點。
XOM - 能將 Java 物件序列化為 XML 模型。
JCraft - 提供 JOrbis、JSch、JZlib、JRexec…等各式套件。
jPCT - 3D 引擎。

Java SE Documention - Java 官方使用說明書,含程式設計指南與 API 規格書。
The Java Tutorials - Java 官方教程,尤其是主題性介紹 API 用途,堪稱必讀!
The Java Source - Java 官方部落格,第一時間窺探技術動向。
Java Notes: Table of Contents - Java 程式設計技術參考辭典。
批踢踢 Java - 定期關注別人各式各樣的發問與回答,做為突破自己成長瓶頸的經歷。
維基百科「軟體開發」條目 - 軟體開發步驟、軟體開發模式……一窺大概。