[程式設計的壹貳參肆]程式語言的基礎知識(III) — I/O、檔案讀取及例外處理
[前言]
這週要分享給大家程式語言的輸入輸出(I/O)以及例外處理,要開始寫輸出輸入的程式之前,我們要先有一點計算機基本架構的概念,我們的計算機硬體基本架構包含以下五個單元:
● 輸入單元:就像是我們身體的感官,用來接收外部訊息,傳送至大腦的記憶單元
● 輸出單元:就像是我們的四肢,用來執行大腦發出的任務而在計算機架構裡負責大腦中樞的功能就是我們的主機,裡面有:
● 記憶單元:主記憶體負責短期的記憶儲存,像是RAM、ROM
● 運算單元:負責記憶單元資料的算數運算及邏輯運算
● 控制單元:負責發出控制訊號控制輸出單元及運算單元讓計算機能夠正常運作
這篇文章會講解計算機架構裡的輸入輸出單元在JAVA的I/O的處理,分為兩種,一種是Console視窗的I/O處理,一種是檔案的I/O處理,當然也可以透過其他介面來做I/O的處理,就看大家寫的程式是哪種應用了!另外因為許多的I/O處理都有做一些例外處理(Exceptions),能夠在程式執行發生可預期的錯誤時,不讓程式崩潰(Crash),下面是這篇文章提及的內容:
Java的I/O處理大部分都是定義在java.io.*,透過數據流(data streams)、序列(Serialization)還有檔案系統(File System)進行系統通訊、週邊設備通訊及檔案讀取。
另外系統通訊的I/O是定義在java.lang.* 這個package裡面,如我們之前範例程式一直使用的System.out.print(),這個package裡面有太多基本的API及Class類別(String, Double, etc.),因此Java在預設狀況下直接將他import進來。
而Java I/O處理又分為兩種,一種是針對二進位數據來做存取的字節流(byte stream),進行Byte的操作;另一種是char的讀取的字符流(character stream),直接做文字(text, string)的操作,因此接下來提到的I/O 的 API都會有兩種不同格式讀取,大家千萬不要搞混了,像我一開始學習程式的時候想說怎麼又多出了好幾種方式而搞了好久傻傻分不清!
[Console視窗的I/O]
Console視窗有些人會稱為Terminal、Command Line,聽到這三者其實都是一樣的東西,在Mac、Linux搜尋Terminal或是在Windows 搜尋Cmd看到”命令提示字元”都可以開啟Console視窗!!!之前推薦的Visual Studio Code也有自帶Terminal:
Console視窗的輸出在學習Java的一開始就一直有使用到,那就是System.out.println()和System.out.print(),System.out是Java 的標準輸出流,主要用於輸出到主機環境或是顯示訊息給使用者看,而兩個的差異在於前者印出數據時最後會加上換行符號,後者並不會換行,可以看到下列範例13, 14行:
System.out沒有區分字節流或是字流,一律都是字元字串,當然也可以用2進位或其他進位法表示,如果我們是要輸出程式資訊給"人”看的就不會選擇用計算機的進位法(這時候會用到字串的格式化format,先做個預告下篇文章就會提到型態轉換及字串格式化囉!)
另外介紹兩個我覺得很常使用也很好用的API,就是System.out.append(),可以將資料一個byte一個byte顯示,假如我們今天是要輸出到檔案,在某些時候來說,我們會希望儲存二進位方式,會比較節省電腦空間!!
在24行的System.out.format()我會覺得使用起來比println()和print(),因為它可以不用一直在意中間的加號,程式會看起來較為乾淨整齊!!
Console視窗的輸入是指使用者能夠在Console視窗透過鍵盤或是其他來自主機或使用者的輸入源讀入數據流,讓我們的程式使用。這邊介紹三種方式從Console 讀取數據流:
● System.in.read()
● BufferedReader()
● Scanner()
而輸入這件事情以鍵盤輸入來當範例,機器以及文件之間彼此通訊都是以1010來溝通,主機收到二進位資料後,我們撰寫的程式如果正在執行並且有讀取的動作,讀到的也會是1010這種二進位碼。
下面是一個System.in.read()的簡單範例,因為”enter”對於Java輸入是一個結束字元,“enter” 按下後資料存放到Buffer暫存,用System.in.read()可以讀取Buffer內到”enter” 以前(包含)的一個字元(0~255),因此用一個while迴圈不斷讀取,直到Buffer內沒有資料:
執行結果如右:
可以看到輸入完“abcde”後按下 ”enter”,Buffer內有資料,while迴圈內的read()讀取Buffer內資料並且輸出到視窗
接著繼續等待”enter”,Buffer內讀取到”q”後跳離迴圈,結束程式
這邊進行強迫轉型可以看到字元”a” 的 ascii code 為97,可參考ASCII Table來做比對!
不過因為System.in.read()是一個字節一個字節讀取,在Oracle 的文件中,建議開發者使用類別 InputStreamReader來做讀取,不僅可以將字節轉換為字串,提前從Buffer中讀取資料,在使用上也能夠指定編碼的格式集。為了能夠提高效率,使用這個類別官方建議能夠搭配另一個類別BufferedReader:
執行結果如右:
一樣可以看到輸入完”abcde”後按下 ”enter”,Buffer內有資料,while迴圈內的read()讀取Buffer,讀取到”q”後跳離迴圈,結束程式
InputStreamReader可以設定其他編碼來解析輸入的內容,我們必須使用Java提供的一個charset類別來做轉換,這樣接收到的1010二進位碼就可以依據不同的table來對應到不同的charset,Standard charsets如下:
下面程式範例是使用charset ”UTF-8" 編碼,以及改用BufferedReader 的 readLine() 讀取整個字串(“enter” 輸入之前的):
程式執行結果:
上面提到的方法都是僅能讀取資料,沒有幫忙做資料的解析的API,其實有時候我們會有一些目標的資料,像是整數、浮點數,或是已經有規劃好的輸入資料格式(像日期2020/08/01),我們就可以使用Scanner來讀取
在Java5之後提供了新的類別Scanner,Scanner被包在java.util.Scanner,使用時要記得先import 這個library!也可以使用import java.util.*,將util內全部模組都匯入
常用的函式:
● nextInt():取得使用者輸入或是檔案中的整數值(以空格、tab分隔),返回整數值
● nextFloat():取得使用者輸入或是檔案中的浮點數值(以空格、tab分隔),返回浮點數值
● nextLine():取得使用者輸入或是檔案中的整行字串(以”\n”分隔),返回字串
● next():取得使用者輸入或是檔案中的字串(以空格、tab分隔),返回字串
● hasNext():確認是否有符合格式(pattern)的字串,返回布林值(boolean)
● useDelimiter(String patternStr): 以patternStr做為分隔字元,返回Scanner,可使用next()逐一讀取相符格式字串
● findInLine(String patternStr):以patternStr做為目標格式字串(這邊是/),返回MatchResult,相符的部分儲存成group,可透過MatchResult的groupCount()方法取得符合數量;MatchResult的group(index)方法取得相符的部分
[檔案讀取]
Java 的檔案讀取有兩種模式 — 字元操作、字節操作,又分為讀取及寫入兩個動作:
上面表格四個都是在java.io內但是不同的類別,在使用時需要先實體化(使用new 這個關鍵字)一個物件才能夠使用裡面的函式及屬性,類別可以通過不同建構方式來實體化物件,下面是四個類別常用的建構子:
這邊我們可以發現幾件事情:
1. FileOutputStream 和 FileWriter是用來寫入資料到檔案中,因此他們的建構子又多了一個,可以決定我們這次的寫入是加入到檔案的尾端或是整個覆蓋掉檔案
2. FileInputStream 和 FileOutputStream 並不用指定charset,因為這兩個類別是以字節byte 來做操作,而FileReader 和 FileWriter 是以字元做操作
3. FileWriter 和 FileReader在沒設定charset時,是使用作業系統平台的預設字集,像Mac就是”MacRoman”,Linux是”UTF-8",如果你的程式會在不同作業系統中使用,就必須要注意這一點!
下面是FileOutputStream 和 FileInputStream 的範例:
首先,我們先看到第9, 10 行,這兩行是創建一個FileOutputStream 物件 及File 物件,待會要來寫入資料到檔案,因為 FileOutputStream有不同的建構子,所以這兩行也可以這樣寫:
第11行被註解起來則是另一個建構子,來決定寫入時是要覆蓋還是要插入到檔案最後;再來我們看第12~17行再次示範了字元char 與 ASCII code 的轉換,因為FileOutputStream 接受的是byte 類型的資料,因此我們將’a’這個字元轉換為byte,夠過陣列,將一串資料寫入data.txt,最後必須用close()將stream 關閉,避免造成系統資源的損耗
第24行開始我們試著讀出剛剛寫入的檔案內的資料,並且輸出到console視窗
FileInputStream 一樣有兩個建構子,在這邊就不多做舉列了,第27行是獲取檔案的大小,單位是byte,而因為FileInputStream是以byte來操作,所以我們寫一個for迴圈來逐一獲取資料,底下是27行之後 Method1 讀取檔案的流程圖:
Method2 則是將檔案資料一次讀取出來,不分特殊符號一律讀出dataRead陣列大小的資料,dataRead是16個整數,讀就會只讀到16個字元
因此通常我們不會使用Method2一次讀出檔案,不僅程式緩存的陣列需要夠大,我們通常也不知道檔案的大小,浪費空間也有抓不到完整資料的可能!
接著我們來看字元讀取操作的FileWriter 和 FileReader的範例:
這邊我們讀寫與上一個範例同樣的檔案,使用FileWriter(String path)這個建構子,寫入兩行a b c d(有分行符號\n,下個範例也會用到)
第18行開始是FileReader 的Method1:建立緩存的陣列以及FileReader物件,fileReader.read()一次讀一個字元,如果有資料則回傳資料,沒有則回傳-1,利用這個規則我們寫出一個while迴圈來重複讀取,直到讀到檔案的結束回傳-1就離開while迴圈
Method2 則是將檔案資料一次讀取出來,不分特殊符號一律讀出str陣列大小的資料
FileReader一樣也可以透過BufferedReader 來讀取一整行,看下面範例第17行開始,BufferedReader.readLine()回傳的是字串物件,若為null則讀到到案結尾,離開while 迴圈:
[IO 效能比較]
我們先看一樣都可以line by line 讀取的 Scanner 和 BufferedReader,為了公平起見,我準備了一個10124行的檔案,讓Scanner 和 BufferedReader 執行一樣次數的讀取操作,接著用System.currentTimeMillis()來獲取時間,在for迴圈讀取了10124行完成後,將時間相減,可以看到底下Terminal 的輸出結果,Scanner的執行時間比BufferedReader 慢了將近20倍!!!
啊!這也是因為如同之前提過的Scanner 會做一些資料的解析動作,不過如果沒有要使用到字串的匹配功能的話,之後為了演算法或是其他應用的執行速度好,大家還是使用BufferedReader執行效率會比較高唷!!
接著我們來看一下同樣的檔案用Byte及char讀取效果會是如何!同樣我們也比較兩種類別 — FileInputStream 和 FileReader,讀取的檔案有10124行共91124個字,可以看到Terminal裡的執行結果,FileReader比FileInputStream快了三倍,不過他們都比readLine來得慢,這是因為byte(char) by byte(char) 檔案讀取次數相對多上很多(91124個字元 / 10124行)
想自己驗證程式的人也可以到下列連結複製程式碼來執行,相信會更加印象深刻的!https://github.com/PHOEBEHAUNG/MediumFile/tree/master/JAVA/CompareIO
[例外 Exception]
相信大家在這篇文章的所有程式範例都看到了一個陌生的東西 — try-catch或是 throw IOException,這是JVM設計用來捕捉可預期的錯誤,不讓我們的程式崩潰,在很多程式語言都可以看到這個語法
當執行程式時,很可能會產生不同的錯誤及例外,我們可以從文件中知道Java的錯誤資訊類別都是包含在Trowable這個類別:
Throwable又分為兩類 —Error & Exception ;
- Checked Exception: Compiler 會要求必須檢查的Exception,亦即如果沒有在程式中做捕捉(catch)的動作,將會無法編譯通過
- Unchecked Exception:Compiler不建議處理的Exception,因為發生這種例外通常是已經無法補救的,像是RuntimeException和Error,通常由程式設計師在程式中來避免
try-catch(-finally)的語法如下(橘色底部分):
當程式A有例外發生時,由catch捕捉到並取得例外資訊,以利開發者做處理,而finally 可以視情況來決定要不要處理,通常如果有開檔案或是創建物件,可以在finally這邊做資源的釋放
try-catch-finally程式流程動畫:
Throwable 應用範例:
try-catch 實際應用:
先用一個簡單的範例,當我們想要將輸入的String字串轉型為數字來做計算時,可能會有不是數字的輸入,這時候如果沒有做例外捕捉會發生什麼事情呢:
我們可以看到執行的時候在Integer.parseInt(num)遇到一個例外(NumberFormatException)發生了,導致我們在第6行之後的程式完全沒有執行,接著我們看看加上try-catch後的效果:
太好了!!即使我們的使用者手殘輸錯,我們也不會去罵他們,因為我們的程式並不會因此崩潰退出,反而會提示他們輸入錯誤,這真是對開發者的一大福音啊!!!
不過在這邊補充一點,也不能夠太過依賴try-catch來避免程式崩潰,因為try-catch機制是將例外訊息放在stack中堆疊的,也就是說,太多的例外發生時,也會導致程式所佔的記憶體不斷的增加,所以同時還是要養成自己寫程式預防例外發生的狀況!
下面再提供一個用Java來做伺服器語言的應用範例,因為在做Database的通訊時很容易遇到連接不上或是語法不對的狀況,在這邊JVM也給出了可捕捉的例外處理SQLException:
這個禮拜講的是比較偏向語法跟理論的東西,其實我比較喜歡講演算法的介紹,但還是希望看我文章的人能夠基本熟悉就能做更多的應用了!
這篇文章說明了Java的I/O、檔案讀取、例外處理的用法,下週的文章是關於JAVA的型態轉換和格式化字串,以下也有一些QA測試大家有沒有吸收囉!
[QA]
● 計算機基本架構共有哪五個單元?
● System.out.println()和 System.out.print()有什麼不同?
● “y” 的 ascii code 為多少?? 試著到ASCII Table找找看
● 將 InputStreamReader 的數據流丟到 BufferedReader可以如何讀取數據??
● Scanner 比起 BufferedReader多了什麼樣方便的功能?
● FileInputStream 和 FileWriter 操作的數據格式一樣嗎?分別是什麼?
● try-catch 用途為何?可以應用在哪裡?
喜歡我的文章的人也記得幫我按個拍手、分享,覺得很不錯的可以幫我拍個50下!
也要快點追蹤我的 FB粉絲專頁 — 飛比尋常的程式設計世界 ,不會太頻繁出現在你的塗鴉牆騷擾你,好文章生產需要一點時間,有錯誤或想討論的都歡迎留言給我唷!那就下次見拉!