Java程式設計(2021春)——第五章輸入輸出筆記與思考

Java程式設計(2021春)——第五章輸入輸出筆記與思考

本章概覽:

異常處理簡介

程式運行時,環境、操作等可能出現各種錯誤、故障,我們希望程式具有容錯能力,給出錯誤資訊。面向對象的程式設計里有異常處理機制,即,將程式的主要邏輯和容錯處理邏輯分開,發現異常的地方不一定是處理異常的地方。

輸入/輸出流的概念

流:Java將資訊的輸入輸出看作程式的流動,輸出流就是將數據從程式空間輸出到別的空間的通道;輸入流同理。

文件讀寫

目錄

5.1.1-5.1.2 異常處理的概念

異常處理就是程式的一種容錯機制,在程式運行過程中。如果遇到用戶或環境的錯誤,程式要有能力處理這些錯誤,並且從錯誤中恢復出來繼續執行,或者至少告訴用戶發生了什麼樣的錯誤,並且在程式結束之前做好善後工作。

異常的基本概念

  1. 又稱例外,是特殊的運行錯誤對象。
  2. Java中聲明了很多異常類,每個異常類都代表了一種運行錯誤,類中包含了該運行錯誤的資訊處理錯誤的方法
  3. 每當Java程式運行中發生一個可識別的運行錯誤時,即該錯誤有一個異常類與之對應時,系統都會產生一個相應的該異常類的對象,即產生一個異常

在Java中出現異常時,處理異常的辦法有兩種:一種是本Java程式不處理異常,但需要聲明一下不處理異常但是將異常拋出,如果整個程式不處理,就會拋出到運行環境,即Java虛擬機,然後會給出一些資訊並終止程式;第二種辦法是在自己的程式中捕獲異常並處理異常。一般而言,第二種方式會多一些。

Java異常處理機制的優點

  1. 將錯誤處理程式碼從常規程式碼中分離出來。
  2. 將錯誤按類型和差別分組。
  3. 對無法預測的錯誤的捕獲和處理。
  4. 克服了傳統方法的錯誤資訊有限的問題,即,可以擴展錯誤資訊。
  5. 把錯誤傳播給調用堆棧,可以讓上級的調用者模組處理資訊。

錯誤的分類

根據錯誤的嚴重程度不同,可以分為兩類:

錯誤:

  1. 致命性的,程式無法處理,多數情況無法從錯誤中恢復並繼續運行。
  2. Error類是所有錯誤類的父類。

異常

  1. 非致命性的,可編製程式捕獲和處理。
  2. Exception類是所有異常類的父類。

異常的分類

非檢查型異常

  1. 不期望程式捕獲的異常,在方法中不需要聲明,編譯器也不進行檢查。
  2. 都繼承自RuntimeException
  3. 不要求捕獲和聲明的原因:引發RuntimeException的操作在Java應用程式中會頻繁出現。例如:若每次使用對象時,都必須編寫異常處理程式碼來檢查null引用,則整個應用程式很快將變成龐大的try-catch塊;它們表示的問題不一定作為異常處理,如:可以在除法運算時檢查\(0\)值,而不使用ArithmeticException&&可以在引用前測試控值等。

檢查型異常

  1. 其它類型的異常。
  2. 如果被調用的方法拋出一個類型為E的檢查型異常,那麼調用者必須捕獲E或者也聲明拋出E(或者E的一個父類),對此,編譯器要進行檢查。

Java預定義的一些常見異常

非檢查型異常

  1. ArithmeticException:整數除法中除數為\(0\)
  2. NullPointerException:訪問的對象還沒有實例化。
  3. NegativeArraySzieException:創建數組時元素個數是負數。
  4. ArrayIndexOutOfBoundsException:訪問數組元素時,數組下標越界。

檢查型異常

  1. ArrayStoreException:程式試圖向數組中存取錯誤類型的數據。
  2. FileNoteFoundException:試圖存取一個並不存在的文件。
  3. IOException:通常的I/O錯誤。

例:非檢查型異常——數組越界異常

public class HelloWorld {
	public static void main(String[] args) {
		int i = 0;
		String greetings[] = { "HelloWorld!", "No,I mean it!", "HeELLO WORLD!!" };
		while (i < 4) {
			System.out.println(greetings[i]);
			i++;
		}
	}
}

在上述程式碼中,我們故意製造了數組越界,greetings[]數組只有三個元素但我們卻訪問到了第四個,以下是eclipse IDE運行結果:

HelloWorld!
No,I mean it!
HELLO WORLD!!
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
	at HelloWorld.main(HelloWorld.java:6)

可以注意到,主方法中沒有聲明拋出這種異常,也沒有捕獲這種異常,編譯器也沒有進行強制性檢查,即,並不要求處理/拋出這個異常。

5.1.3-5.1.5 異常的處理

檢查型異常的處理

聲明拋出異常

  1. 不在當前方法內處理異常,可以使用throws子句聲明將異常拋出到調用方法中。
  2. 如果所有的方法都選擇了拋出此異常,最後JVM(Java虛擬機)將捕獲它,輸出相關的錯誤資訊,並且終止程式的運行。

捕獲異常

  1. 使用try{}catch{}塊,捕獲所發生的異常,並進行相應的處理。

異常處理示意圖

image

如上圖,Method1調用了Method2Method2調用Method3Method3再調用Method4。如果在Method4中探測到了異常發生,不去處理這個異常,就會沿著棧的方向向上拋出,即拋給Method4的調用者Method3Method3如果繼續不處理這個異常就會拋給Method2;如果Method2不處理,則會繼續拋給Method1。如果Method1打算處理,則應當捕獲異常並且處理。

拋出異常的例子

public void openThisFile(String fileName) throws java.io.FileNotFoundException{
    //code for method
}
public void getCustomerInfo()throws java.io.FileNotFoundException{
    //do something
    this.openThisFile("customer.txt");
    //do something
}

如果在openThisFile中拋出了FileNotFoundException異常,getCustomerInfo將停止執行,並將此異常傳送給它的調用者。

捕獲異常的語法

try{
    statement(s)
}catch(exceptiontype name){
    statement(s)
}finally{
    statement(s)
}

說明:

  1. try語句,其後跟有可能產生異常的程式碼塊

  2. catch語句,其後跟隨異常處理語句,通常都要用到兩個方法:

    getMessage():返回一個字元串,對發生的異常進行描述。

    printStackTrace():給出方法的調用序列,一直到異常的產生位置。

  3. finally()語句:不論在try程式碼段是否產生異常,finally後的程式程式碼段都會被執行,通常在這裡釋放記憶體以外的其他資源。

注意事項

如果並列有多個catch語句捕獲多個異常,則一般的異常類型放在後面特殊的放在前面。比如說我們需要捕獲的異常,其中有超類有子類的話,那我們應該首先捕獲子類類的異常,再捕獲超類類型的異常

生成異常對象

三種方式

  1. 由Java虛擬機生成。
  2. 由Java類庫中的某些類生成。
  3. 在自己寫的程式中生成和拋出異常對象。

拋出異常對象都是通過throw語句來實現,異常對象必須是Throwable或其子類的實例:

  1. throw new ThrowableObject();

  2. ArithmeticException e = new ArithmeticException();
    throw e;
    

例:生成異常對象

class ThrowTest{
    public static void main(String[] args){
        try{
            throw new ArithmeticException();
        }catch(ArithmeticException ae){
            System.out.println(ae);
        }
        try{
            throw new ArrayIndexOutOfBoundsException();
        }catch(ArrayIndexOutOfBoundsException ai){
            System.out.println(ai);
        }
        try{
            throw StringIndexOutOfBoundsException();
        }catch(StringIndexOutOfBoundsException si){
            System.out.println(si);
        }
    }
}

以上可見,我們可以主動生成異常對象,然後拋出。

運行結果:

java.lang.ArithmeticException
java.lang.ArrayIndexOutOfBoundsException
java.lang.StringIndexOutOfBoundsException

聲明自己的異常類

  1. 自定義的所有異常類都必須是Exception的子類。

  2. 聲明語法如下:

    public class MyExceptionName extends SuperclassOfMyException{
    	public MyExceptionName(){
            super("Some string explaining exception");//調用超類生成方法
    	}
    }
    

5.2 輸入輸出流的概念

在Java中,將資訊的輸入輸出抽象為資訊的流動,輸入流是資訊從程式空間之外的地方流入程式空間內的「通道」;輸出流是將資訊從程式空間輸送到程式空間之外。

預定義的I/O流類

從流的方向劃分

  1. 輸入流
  2. 輸出流

從流的分工劃分

  1. 節點流:真正訪問文件,進行輸入輸出操作的流。
  2. 處理流:在節點流的基礎上,對資訊進行加工、轉換、處理等操作的流。

從流的內容劃分(java.io包的頂級層次結構)

  1. 面向字元的流:專門用於處理字元數據。
  2. 面向位元組的流:用於一般目的的輸入輸出。

image

我們用於讀寫的流都要繼承自這四個超類

面向字元的流

面向字元的流針對字元數據的特點專門進行了優化,提供專門針對字元的處理。

  1. 源或目標通常是文本文件。

  2. 實現內部格式和文本文件中的外部格式之間的轉換。

    內部格式:16-bit char數據類型

    外部格式:UTF(Universal character set Transformation Format),被很多人稱為Universal Text Format;包括ASCII碼和非ASCII碼,比如,斯拉夫(\(Cyrillic\))字元,希臘字元,亞洲字元等。

面向字元的抽象流類——ReaderWriter

  1. java.io包中所有字元流的抽象超類。

  2. Reader提供了輸入字元的API

  3. Writer提供了輸出字元的API

  4. 它們的子類又可以分為兩大類

    節點流:從數據源讀入數據或往目的地寫出數據。

    處理流:對數據執行某種處理。

  5. 多數程式使用這兩個抽象類的一系列子類來讀入/寫出文本資訊

    例如:FileReader/FileWriter用來讀/寫文本文件。

面向字元的流

image

陰影部分為節點流,其他為處理流

面向位元組的抽象流類——InputStreamOutputStream

面向位元組的流是用來處理非文本文件的輸入輸出的。事實上,大多數文件都不是文本文件,如聲音、影片等等。即使有一些數據既可以存儲為文本,又可以存儲為二進位,存儲為二進位都要節省空間很多,而且,傳輸二進位文件時間上也會節省。因此,當我們的數據不是給人讀的或者要進行進一步處理,我們往往會選擇以二進位形式輸出,這樣比較節省時間,在存儲介質上也比較節省空間。

  1. 是用來處理位元組流的抽象基類,程式使用這兩個類的子類來讀寫位元組資訊

  2. 分為兩部分

    節點流

    處理流

標準輸入輸出流對象

  1. System類的靜態成員變數

  2. 包括

    System.inInputStream類型的,代表標準輸入流,默認狀態對應於鍵盤輸入

    System.outPrintStream類型的,代表標準輸出流,默認狀態對應於顯示器輸出

    System.errPrintStream類型的,代表標準錯誤資訊輸出流,默認狀態對應於顯示器輸出

按類型輸入/輸出數據

  1. printf方法

    System.out.printf("%-12s is %2d long",name,l);

    System.out.printf("value = %2.2F",value);

    %n平台無關的換行標誌。

  2. Scanner

    如果我們知道二進位文件中存放了一些數值類的數據,並且知道依次存放了什麼數據,就可以用Scanner對象按照類型讀取。

    Scanner s = new Scanner(System.in);:構造Scanner對象時,需要用另一個輸入流做參數,因為Scanner不是直接訪問磁碟文件進行輸出的,實際上是一個處理流,對輸入流讀取的資訊進行轉換,賦予類型特徵的流。

    int n = s.nextInt();:調用對象的方法。

    還有下列方法:nextByte()nextDouble()nextFloat()nextLine()nextLongnextShort()

例:標準輸入/輸出重定向(複製文件)

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;

public class Redirecting {
	public static void main(String[] args) throws IOException {
		BufferedInputStream in = new BufferedInputStream(new FileInputStream("Redirecting.java"));// 構造一個輸入流備用,這個輸入流直接關聯到磁碟文件,此處使用java源程式碼
		PrintStream out = new PrintStream(new BufferedOutputStream(new FileOutputStream("test.out")));// 構造一個輸出流對象,這個輸出流對象直接關聯到磁碟文件,此處隨便起一個名字,text.out自然也是文本文件
		System.setIn(in);// 調用setIn方法,將標準輸入流重新定向到in
		System.setOut(out);// 調用setOut方法,將標準輸出流重新定向到out
		System.setErr(out);// 關聯標準輸出錯誤流
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));// 構造有緩衝的輸入流,可以使輸入的效率更高一些,但是BufferedReader本身並不執行讀磁碟文件的操作,只是一個處理流,因此我們需要一個真正讀取文件資訊的節點流作為參數來構造緩衝的BufferedReader,因此將System.in作為參數傳給InputStreamReader,InputStreamReader是面向位元組的流和面向字元的流的橋樑。此處的System.in已經是重定向到磁碟文件的輸入流了
		String s;
		while ((s = br.readLine()) != null) {
			System.out.println(s);//out經過了重定向,所以輸出到磁碟文件
		}
		in.close();
		out.close();
	}
}

5.3.1 寫文本文件

例:創建文件並寫入若干行文本

import java.io.*;

public class FileWriterTester {
	public static void main(String[] args) throws IOException {
		// main 方法中聲明拋出IO異常
		String fileName = "Hello.txt";//相對路徑名,在當前工作文件夾下的文件
		FileWriter writer = new FileWriter(fileName);//創建FileWriter輸出流對象writer
		writer.write("Hello!\n");
		writer.write("This is my first text file,\n");
		writer.write("You can see how this is done.\n");
		writer.write("輸入一行中文也可以\n");
		writer.close();//關閉文件
	}
}

兩個注意點:

  1. 換行符\n不具有跨平台的性質,在不同平台下可能有不同的解釋
  2. 每次運行該程式,可以發現每次刪除了舊文件(Hello.txt),重新創建了Hello.txt

例:寫入文本文件,處理IO異常

import java.io.FileWriter;
import java.io.IOException;

public class FileWriterTester {
	public static void main(String[] args) {
		String fileName = "Hello.txt";
		try {
			FileWriter writer = new FileWriter(fileName, true);
			writer.write("Hello!\n");
			writer.write("This is my first text file,\n");
			writer.write("You can see how this is done");
			writer.write("輸入一行中文也可以\n");
			writer.close();
		} catch (IOException iox) {
			System.out.println("Problem writing" + fileName);
		}
	}
}

補充知識

  1. FileWriter類,參考自菜鳥教程

    FileWriter(File file)構造一個FileWriter對象。

    FileWriter(File file, boolean append)參數file:要寫入數據的file對象;參數append:如果append為append,則將位元組寫入文件末尾處,相當於追加資訊,如果append為false,則寫入文件開始處。

說明

  1. 運行此程式,會發現在原文件內容後面又追加了重複的內容,這就是將構造方法的第二個參數append設置為true的效果。
  2. 如果將文件屬性人為更改為只讀屬性,再運行本程式,就會出現IO錯誤,程式將轉入catch塊中,並給出出錯資訊(終端)。

BufferedWriter

FileWriterBufferedWriter類都用用於輸出字元流,包含的方法幾乎完全一樣,但是BufferedWriter多提供了一個newLine()方法用於換行。

  1. 不同的系統對文字的換行方式不同,newLine()方法可以輸出在當前電腦上正確的換行符。
  2. BufferedWriter為緩衝輸出流,可以起到緩衝作用,提高輸出效率。

例:寫入文本文件,使用BufferedWriter

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class FileWriterTester {
	public static void main(String[] args) throws IOException {
		String fileName = "Hello.txt";
		BufferedWriter out = new BufferedWriter(new FileWriter(fileName));
		out.write("Hello!");
		out.newLine();
		out.write("This is another text file using BufferedWriter");
		out.newLine();
		out.write("So I can us a common way to start a new line");
		out.close();
	}
}

5.3.2-讀文本文件

讀文本文件相關的類

FileReader

  1. 從文本文件中讀取字元
  2. 繼承自Reader抽象類的子類InputStreamReader

BufferedReader

  1. 讀文本文件的緩衝器類
  2. 具有readLine()方法,可以對換行符進行鑒別,一行一行地讀取輸入流中的內容
  3. 繼承自Reader

例:讀文本文件並顯示

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class BuffereddReaderTester {
	public static void main(String[] args) {
		String fileName = "Hello.txt", line;
		//文本訪問操作有可能產生異常,因此用try-catch塊
        try {
			BufferedReader in = new BufferedReader(new FileReader(fileName));
			line = in.readLine();
			while (line != null) {
				System.out.println(line);
				line = in.readLine();
			}
			in.close();
		} catch (IOException iox) {
			System.out.println("Problem reading " + fileName);
		}
	}
}
說明
  1. 運行該程式,螢幕上將逐行顯示Hello.txt文件中的內容。

  2. FileReader對象:創建後將打開文件,如果文件不存在,會拋出一個IOException

  3. FileReader類的readLine()方法:從一個面向字元的輸入流中讀取一行文本。如果其中不再有數據,返回null

  4. Reader類的read()方法:也可以用來判別文件結束。該方法返回的一個表示某字元的int型整數,如果讀到文件末尾,返回-1。據此,可修改本例中的讀文件部分:

    int c;
    while((c = in.read()) != -1){
        System.out.print((char)c);
    }
    
  5. close()方法:為了作業系統可以更為有效地利用有限的資源,應該在讀取完畢後,調用該方法。

例:文件的複製

  1. 指定源文件和目標文件名,將源文件的內容複製到目標文件。調用方式為(命令行操作):

    java copy sourceFile destinationFile

    共包括兩個類

  2. CopyMaker 以下返回值均為boolean類型,成功為true,不成功為false

    1. private boolean openFiles() 打開文件
    2. private boolean copyFiles() 真正拷貝複製文件
    3. private boolean closeFiles() 關閉文件
    4. public boolean copy(String src, String dst) 對外的方法,其餘三個方法均輔助本方法實現而不對外可見
  3. FileCopy

    main()

CopyMaker類構造

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class CopyMaker {
	String sourceName, destName;
	// 以下兩個引用將要指向讀寫
	BufferedReader source;
	BufferedWriter dest;
	String line;

	private boolean openFiles() {
		// try-catch捕獲異常
		try {
			source = new BufferedReader(new FileReader(sourceName));
		} catch (IOException iox) {
			System.out.println("Problem opening " + sourceName);
			return false;
		}
		try {
			dest = new BufferedWriter(new FileWriter(destName));
		} catch (IOException iox) {
			System.out.println("Problem opening " + destName);
			return false;
		}
		return true;
	}

	private boolean copyFiles() {
		try {
			line = source.readLine();
			while (line != null) {
				dest.write(line);
				dest.newLine();// 跨平台換行符
				line = source.readLine();
			}
		} catch (IOException iox) {
			System.out.println("Problem reading or writing");
			return false;
		}
		return true;
	}

	private boolean closeFiles() {
		boolean retVal = true;// 滿足但出口,符合結構化程式設計思想
		try {
			source.close();
		} catch (IOException iox) {
			System.out.println("Problem closing " + sourceName);
			retVal = false;
		}
		try {
			dest.close();
		} catch (IOException iox) {
			System.out.println("Problem closing " + destName);
			retVal = false;
		}
		return retVal;
	}

	public boolean copy(String src, String dst) {
		sourceName = src;
		destName = dst;
		// 若返回值結果為真說明三個操作都正確完成了,否則說明出現了錯誤
		return openFiles() && copyFiles() && closeFiles();
	}
}

FileCopy類構造

public class FileCopy {
	public static void main(String[] args) {
		if (args.length == 2) {// 參數個數正確
			new CopyMaker().copy(args[0], args[1]);
		} else {
			System.out.println("Please Enter File Names");
		}
		// eclipse輸入命令行參數的方法可以參考//blog.csdn.net/weixin_43896318/article/details/101846956
	}
}

5.3.3-寫二進位文件

二進位文件的寫比文本文件的寫快很多;同樣的資訊以二進位文本存儲通常比文本文件小很多;有些時候我們的數據本身不是純文本,無法存儲為文本文件,因此需要以二進位形式存儲到二進位文件中。

抽象類OutputStream

派生類FileOutputStream

  1. 用於一般目的輸出(非字元輸出);
  2. 用於成組位元組輸出

派生類DataOutputStream

  1. 具有寫各種基本數據類型的方法
  2. 將數據寫到另一個輸出流(是處理流,並不直接執行寫操作,而是對數據按照類型處理,然後將數據傳給另外的輸出流,有其他輸出流負責真正的寫操作)
  3. 在所有的電腦平台上使用同樣的數據格式
  4. 其中的size方法可以作為計數器,統計寫入的位元組數

DataOutputStream類的成員

名稱 說明
public DataOutputStream(OutputStream out) 構造函數,參數為一個OutputStream對象作為其底層的輸出對象
protected int written 私有屬性,代表當前已寫出的位元組數
public void fulsh() 沖刷此數據流,使流內的數據都被寫出
public final int size() 返回私有變數written的值,即已經寫出的位元組數
public void write(int b) 向底層輸出流輸出int變數的低8位,執行後,記錄寫入位元組數的計數器+1
public final void writeBoolean(boolean b) 寫出一個布爾數,true為1,false為0,執行後計數器增加1
public final void writeByte(int b) public final void writeByte(int b)int參數的低8位寫入,捨棄高24位,計數器增加1
public void writeBytes(String s) 字元串中的每個字元被丟掉高8位寫入流中,計數器增加寫入的位元組數,即字元個數
public final void writeChar(int c) 將16-bit字元寫入流中,高位在前,計數器增加2
public void writeDouble(double v) 寫雙精度數,計數器增加8
public void writeFloat(float f) 寫單精度數,計數器增加4
public void writeInt(int I) 寫整數,計數器增加4
public void writeLong(long I) 寫長整數,計數器增加8
public final void writeShort(int s) 寫短整數,計數器增加2

例:將int寫入文件

import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;

//將三個int型數字255/0/-1寫入數據文件data1.dat
public class FileOutputstreamTester {
	public static void main(String[] args) {
		String fileName = "data1.dat";
		int value0 = 255;
		int value1 = 0;
		int value2 = -1;
		try {
			// FileOutputStream是原生位元組流,只能識別寫出的是位元組,不能識別int,double
			// DataOutputStream可以將位元組處理成某種類型的數據
			DataOutputStream out = new DataOutputStream(new FileOutputStream(fileName));
			out.writeInt(value0);
			out.writeInt(value1);
			out.writeInt(value2);
			out.close();
			System.out.println("Finished writing");
		} catch (IOException iox) {
			System.out.println("Problem writing " + fileName);
		}
	}
}

BufferedOutputStream

用法示例:

DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(fileName)));

說明:

  1. BufferedOutputStream是處理流,用於緩衝,構造BufferedOutputStream需要輸出流,在本例中是FileOutputStream,以其為參數構造BufferedOutputStream;本例中繼續以BufferedOutputStream為對象構造DataOutputStream對象,既可以緩衝又可以寫入。
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class BufferedOutputStreamTester {
	// 沒有捕獲異常所以聲明拋出了異常
	public static void main(String[] args) throws IOException {
		String fileName = "mixedTypes.dat";
		// 原生位元組輸出流->緩衝
		DataOutputStream dataOut = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(fileName)));
		dataOut.writeInt(0);
		// dataOut.size()返回的是一共寫了多少位元組而非當前
		System.out.println(dataOut.size() + "bytes have been written.");
		dataOut.writeDouble(31.2);
		System.out.println(dataOut.size() + "bytes have been written.");
		dataOut.writeBytes("JAVA");
		System.out.println(dataOut.size() + "bytes have been written.");
		dataOut.close();
	}
}

輸出:

4bytes have been written.
12bytes have been written.
16bytes have been written.

例:向文件寫入一個位元組並讀取

public class FileOutputStreamTester2 {
	public static void main(String[] args) throws IOException {
		DataOutputStream out = new DataOutputStream(new FileOutputStream("trytry.dat"));
		out.writeByte(-1);// 應該寫入2個F(8個1)
		out.close();
		DataInputStream in = new DataInputStream(new FileInputStream("trytry.dat"));
		int a = in.readByte();
		System.out.println(Integer.toHexString(a));
		System.out.println(a);
		in.skip(-1);// 往後一個位置,以便下面重新讀出
		a = in.readUnsignedByte();
		System.out.println(Integer.toHexString(a));
		System.out.println(a);
		in.close();
	}
}

輸出:

ffffffff
-1
ff
255

5.3.4-讀二進位文件

例:讀取二進位文件中的3個int型數字並相加

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class DataInputStreamTester {
	public static void main(String[] args) {
		String fileName = "data1.dat";
		int sum = 0;
		try {
			DataInputStream instr = new DataInputStream(new BufferedInputStream(new FileInputStream(fileName)));
			sum += instr.readInt();
			sum += instr.readInt();
			sum += instr.readInt();
			System.out.println("The sum is: " + sum);
			instr.close();
		} catch (IOException iox) {
			System.out.println("Problem reading " + fileName);
		}
	}
}

輸出:

254

說明:

  1. 一般喜歡在同一文件中寫一個類型的數據
  2. 可以利用try-catch讀取未知個數的數

例:通過捕獲異常控制讀取結束

public class DataInputStreamTester {
	public static void main(String[] args) {
		String fileName = "data1.dat";
		long sum = 0;
		try {
			DataInputStream instr = new DataInputStream(new BufferedInputStream(new FileInputStream(fileName)));
			try {
				while (true) {
					sum += instr.readInt();
				}
			} catch (EOFException eof) {
				System.out.println("The sum is:" + sum);
				instr.close();
			}
		} catch (IOException iox) {
			System.out.println("IO Problems with " + fileName);
		}
	}
}
//使用了嵌套try-catch塊

例:用位元組流讀取文本文件

import java.io.FileInputStream;
import java.io.IOException;

public class InputStreamTester {
	public static void main(String[] args) throws IOException {
		FileInputStream s = new FileInputStream("Hello.txt");
		int c;
		while ((c = s.read()) != -1) {
			System.out.write(c);
		}
		s.close();
	}
}
//說明:read()方法讀取一個位元組,轉化為[0,255]間的一個整數,返回一個int。如果讀到了文件末尾,則返回-1.
//write(int)方法寫一個位元組的低8位,忽略高24位(高低需要看大端小端)

讀寫位元組

DataOutputStreamwriteByte方法

  1. public final void writeByte(int b) throws IOException
  2. int的最不重要位元組寫入輸出流

DataInputStreamreadUnsignedByte方法

  1. public final int readUnsignedFile()Throws IOException
  2. 從輸入流中讀取1位元組存入int的最不重要位元組。

例:文件複製

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class CopyBytes {
	public static void main(String[] args) {
		DataInputStream instr;
		DataOutputStream outstr;
		if (args.length != 2) {
			System.out.println("Please enter file names");
			return;
		}
		try {
			instr = new DataInputStream(new BufferedInputStream(new FileInputStream(args[0])));
			outstr = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(args[1])));
			try {
				int data;
				while (true) {
					data = instr.readUnsignedByte();
					outstr.writeByte(data);
				}
			} catch (EOFException eof) {
				outstr.close();
				instr.close();
				return;
			}
		} catch (FileNotFoundException nfx) {//先捕獲比較具體的文件未找到異常
			System.out.println("Problem opening files");
		} catch (IOException iox) {//再捕獲比較一般的異常
			System.out.println("IO Problems");
		}
	}
}

5.3.5-File

File類中存儲一些文件相關的資訊,並提供了管理文件的一些操作

File類的作用

  1. 創建、刪除文件
  2. 重命名文件
  3. 判斷文件的讀寫許可權是否存在
  4. 設置和查詢文件的最近修改時間
  5. 構造文件流可以使用File類的對象作為參數

例:File類舉例

//創建文件Hello.txt,如果存在則刪除舊文件,不存在則直接創建新的
import java.io.File;

public class FileTester {
	public static void main(String[] args) {
		File f = new File("Hello.txt");
		if (f.exists()) {
			f.delete();
		} else {
			try {
				f.createNewFile();
			} catch (Exception e) {
				System.out.println(e.getMessage());
			}
		}
	}
}

運行結果:

  1. 因為在前面的例子中已經創建了Hello.txt,所以第一次運行將刪除這個文件
  2. 第二次運行則又創建了一個此名的空文件

分析:

  1. 在試圖打開文件之前,可以使用File類的isFile方法來確定File對象是否代表一個文件而非目錄
  2. 還可以通過exists方法判斷同名文件或路徑是否存在,進而採取正確的方法,以免造成誤操作

例:改進的文件複製程式

之前的複製文件例子中,有以下幾個問題沒有考慮

  1. 複製的目標文件是否又同名文件存在,會不會衝掉原文件
  2. 源文件是否存在
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class NewCopyBytes {
	public static void main(String[] args) {
		DataInputStream instr;
		DataOutputStream outstr;
		if (args.length != 2) {
			System.out.println("Please enter file names");
			return;
		}
		// 確定文件都存在,否則給用戶提示
		File inFile = new File(args[0]);
		File outFile = new File(args[1]);
		if (outFile.exists()) {
			System.out.println(args[1] + " already exists");
			return;
		}
		if (!inFile.exists()) {
			System.out.println(args[0] + " does not exist");
			return;
		}
		try {
			instr = new DataInputStream(new BufferedInputStream(new FileInputStream(args[0])));
			outstr = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(args[1])));
			try {
				int data;
				while (true) {
					data = instr.readUnsignedByte();
					outstr.writeByte(data);
				}
			} catch (EOFException eof) {
				outstr.close();
				instr.close();
				return;
			}
		} catch (FileNotFoundException nfx) {
			System.out.println("Problem opening files");
		} catch (IOException iox) {
			System.out.println("IO Problems");
		}
	}
}

5.3.6-處理壓縮文件

壓縮流類

GZIPOutputStreamZipOutputStream

可分別把數據壓縮成GZIP格式和ZIP格式

GZIPInputStreamZipInputStream

可分別把壓縮成GZIP格式和ZIP格式的數據解壓縮恢復原狀

例:壓縮和解壓縮Gzip文件

//將文本文件Hello.txt壓縮為文件test.gz,再解壓該文件,顯示其中內容,並另存為newHello.txt
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

public class GZIPTester {
	public static void main(String[] args) throws IOException {
		// 構建原生位元組文件輸入流對象
		FileInputStream in = new FileInputStream("Hello.txt");
		// 在普通輸入流外接GZIPOutputStream對象
		GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream("test.gz"));
		System.out.println("Writing compressing file from Hello.txt to test.gz");
		// 以下實現文件複製
		int c;
		while ((c = in.read()) != -1) {
			out.write(c);// 寫壓縮文件
		}
		in.close();
		out.close();
		System.out.println("Reading file from test.gz to monitor");
		// InputStreamReader:面向位元組的流和面向字元的流的橋樑 BufferedReader實現按行讀
		BufferedReader in2 = new BufferedReader(
				new InputStreamReader(new GZIPInputStream(new FileInputStream("test.gz"))));
		String s;
		while ((s = in2.readLine()) != null) {
			System.out.println(s);
		}
		in2.close();
		System.out.println("Writing decompression to newHello.txt");
		GZIPInputStream in3 = new GZIPInputStream(new FileInputStream("test.gz"));
		FileOutputStream out2 = new FileOutputStream("newHello.txt");
		while ((c = in3.read()) != -1) {
			out2.write(c);
		}
		in3.close();
		out2.close();
	}
}

運行結果

  1. 首先生成了壓縮文件test.gz
  2. 再讀取顯示其中的內容,和Hello.txt中的內容完全一樣
  3. 解壓縮文件newHello.txtHello.txt中的內容也完全相同

說明

  1. read()方法讀取一個位元組,轉化為\([0,255]\)之間的一個整數,返回一個int。如果讀到了文件末尾,則返回\(-1\)
  2. write(int)方法寫一個位元組的低\(8\)位,忽略了高\(24\)

例:Zip文件壓縮與解壓縮

//從命令行輸入若干個文件名,將所有文件壓縮為"test.zip",再從此壓縮文件中解壓並顯示
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

public class ZipOutputStreamTester {
	public static void main(String[] args) throws IOException {
		ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream("test.zip")));
		int len = args.length;
		for (int i = 0; i < len; i++) {
			System.out.println("Writing file " + args[i]);
			BufferedInputStream in = new BufferedInputStream(new FileInputStream(args[i]));
			out.putNextEntry(new ZipEntry(args[i]));
			int c;
			while ((c = in.read()) != -1) {
				out.write(c);
			}
			in.close();
		}
		out.close();
		System.out.println("Reading File");
		// 解壓縮流<-緩衝流<-二進位流
		ZipInputStream in2 = new ZipInputStream(new BufferedInputStream(new FileInputStream("test.zip")));
		ZipEntry ze;
		// 逐個文件判斷
		while ((ze = in2.getNextEntry()) != null) {
			System.out.println("Reading File " + ze.getName());
			int x;
			// 讀的同時解壓縮了
			while ((x = in2.read()) != -1) {
				System.out.write(x);
			}
			System.out.println();
		}
		in2.close();
	}
}

運行結果

  1. 在命令行輸入兩個文本文件名(eclipse也支援運行時輸入命令行參數,方法可自行百度)後,將生成test.zip文件
  2. 使用任意解壓軟體打開test.zip後可以看到被壓縮的兩個文件
  3. console里可以看到解壓後每個文件的內容
  4. 在資源管理器窗口中,可以使用任意解壓軟體解壓縮test.zip,可以恢復出和原來文件相同的兩個文本文件

例:解壓縮Zip文件,並恢復其原來的路徑

更多的情況,我們希望解壓文件並恢復其原來的目錄結構

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public class Unzip {
	byte doc[] = null;// 存儲解壓縮數據的緩衝位元組數組
	String Filename = null;// 壓縮文件名字字元串
	String UnZipPath = null;// 解壓縮路徑字元串

	public Unzip(String filename, String unZipPath) {
		this.Filename = filename;
		this.UnZipPath = unZipPath;
		this.setUnZipPath(this.UnZipPath);
	}

	public Unzip(String filename) {
		this.Filename = new String(filename);
		this.UnZipPath = null;
		this.setUnZipPath(this.UnZipPath);
	}

	private void setUnZipPath(String unZipPath) {
		if (unZipPath.endsWith("\\")) {
			this.UnZipPath = new String(unZipPath);
		} else {
			this.UnZipPath = new String(unZipPath + "\\");
		}
	}

	public void doUnZip() {
		try {
			ZipInputStream zipis = new ZipInputStream(new FileInputStream(Filename));
			ZipEntry fEntry = null;
			while ((fEntry = zipis.getNextEntry()) != null) {
				if (fEntry.isDirectory()) {// 是路徑則創建路徑
					checkFilePath(UnZipPath + fEntry.getName());// 判斷路徑是否存在
				} else {// 是文件則解壓縮文件
					String fname = new String(UnZipPath + fEntry.getName());
					try {
						FileOutputStream out = new FileOutputStream(fname);
						doc = new byte[512];// 一次多讀一些位元組
						int n;
						while ((n = zipis.read(doc, 0, 512)) != -1) {// 返回值是實際讀到的字元數,文件末尾即-1
							out.write(doc, 0, n);
						}
						out.close();
						out = null;
						doc = null;
					} catch (Exception ex) {

					}
				}
			}
			zipis.close();
		} catch (IOException ioe) {
			System.out.println(ioe);
		}
	}

	private void checkFilePath(String dirName) throws IOException {
		File dir = new File(dirName);
		if (!dir.exists()) {
			dir.mkdirs();// 創建所有缺失的目錄
		}
	}
}
public class UnZipTester {
	public static void main(String[] args) {
		String zipFile = args[0];// 第一個參數為zip文件名
		String unZipPath = args[1] + "\\";// 第二個參數為指定解壓縮路徑
		Unzip myZip = new Unzip(zipFile, unZipPath);
		myZip.doUnZip();
	}
}

5.3.7-對象序列化

如果有需要永久保留的資訊,則需要對象的序列化,即將對象整體寫入,再整體讀出。

ObjectInputStream/ObjectOutputStream

實現對象的讀寫

  1. 通過ObjectOutputStream把對象寫入磁碟文件
  2. 通過ObjectInputStream把對象讀入程式

不保存對象的transientstatic類型的變數

  1. transient修飾的變數不被保存
  2. static修飾的變數不屬於任何一個對象,因此也不被保存

對象想要實現序列化,其所屬的類必須實現Serializable介面(為了安全考慮)

ObjectOutputStream

  1. 必須通過另一個流構造OutputSteeam(也為處理流,不直接執行寫操作):

    // 首先構造一個FileOutputStream對象,直接和文件打交道
    FileOutputStream out = new FileOutputStream("theTime");
    // 再以其為參數,構造ObjectOutputStream對象,再用其對對象存檔
    ObjectOutputStream s = new ObjectOutputStream(out);
    s.writeObject("Today");
    s.writeObject(new Date());
    // 註:以上兩個對象已經實現Serializable介面
    s.flush();
    

ObjectInputStream

  1. 必須通過另一個流構造ObjectInputStream

    // 首先構造一個FileInputStream對象,直接和文件打交道
    FileInputStream in = new FileInputStream("theTime");
    // 再以其為參數,構造ObjectInputStream對象,再用其對對象存檔
    ObjectInputStream s = new ObjectInputStream(in);
    String today = (String)s.readObject();
    Date date = (Date)s.readObject();
    

Seriealizable

  1. Serizable介面的定義

    package java.io;
    public interface Serializable{
    	// there's nothing here!
    }
    

    事實上是一個空介面

  2. 實現Serializable介面的語句

    // 事實上只要implements這個介面即可,表明允許對象可以整體存入磁碟
    public class MyClass implements Serializable{
    	//...
    }
    
  3. 使用關鍵字transient可以阻止對象的某些成員被自動寫入文件

例:創建一個書籍對象輸出並讀出

import java.io.Serializable;

// 創建一個書籍對象,並把它輸出到一個文件book.dat中,然後再把該對象讀出來,在螢幕上顯示對象資訊
public class Book implements Serializable {
	// Serializable介面知識標誌對象可以被序列化,事實上裡面是空的
	int id;
	String name;
	String author;
	double price;

	public Book(int id, String name, String author, double price) {
		this.id = id;
		this.name = name;
		this.author = author;
		this.price = price;
	}
}

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SerializableTester {
	public static void main(String[] args) throws IOException, ClassNotFoundException {
		Book book = new Book(100032, "Java Programming Skills", "Wang Sir", 30.0);
		// 在普通的輸出流之外加一個ObjectOutputStream,以便可以寫出對象
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("book.dat"));
		oos.writeObject(book);
		oos.close();
		book = null;
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream("book.dat"));
		// 需要額外的文檔記錄我們究竟寫入了什麼對象
		book = (Book) ois.readObject();
		ois.close();
		System.out.println("ID is:" + book.id);
		System.out.println("name is:" + book.name);
		System.out.println("author is:" + book.author);
		System.out.println("price is:" + book.price);
	}
}

運行結果

ID is:100032
name is:Java Programming Skills
author is:Wang Sir
price is:30.0

Externalizable介面

  1. API中的說明為

    public interface Externalizable extends Serializable

    如果實現了Externalizable介面,就不必實現Serializable介面,因為Externalizable介面已經是Serializable的超介面

  2. 其中有兩個方法writeExternal()readExternal(),因此實現該介面類必須實現這兩個方法;可以在這兩個方法中按照我們自己的設計實現如何將對象寫入文件以及從文件中讀取對象,比如可以設計一些加密演算法來提高數據安全性

  3. ObjectInputStreamreadObject()方法調用對象所屬類的readExternal(),此時readObject()只作為標誌

5.3.8-隨機文件讀寫

Java將數據的輸入輸出都看作位元組流,因此對文件的隨機讀寫支援得並不很好,但是Java依然提供了文件讀寫有關的類,因此依舊可以實現隨機文件讀寫,只是略麻煩。

RandomAccessFile

  1. 可跳轉到文件的任意位置讀/寫數據

  2. 可在隨機文件中插入數據,而不破壞該文件的其他數據

  3. 實現了DataInputDataOutput介面,可使用普通的讀寫方法

  4. 有位置指示器,指向當前讀寫處的位置。剛打開文件時,文件指示器指向文件的開頭處。對文件指針顯示操作的方法有:

    1. int skipBytes(int n):把文件指針向前移動指定的n個位元組
    2. void seek(long):移動文件指針到指定的位置
    3. long getFilePointer:得到當前的文件指針
  5. 在登場記錄格式文件的隨機讀取時有很大的優勢,但僅限於操作文件,不能訪問五年其他IO設備,如網路、記憶體映像等

  6. 構造方法

    public RandomAccessFile(File file,String mode)
        	throws FileNotFoundException
    public RandomAccessFile(String name,String name)
        	throws FileNotFoundException
    
  7. 構造RandomAccessFile對象時,要指出操作:僅讀,還是讀寫

    new RandomAccessFile("farrago.txt","r");
    new RandomAccessFile("farrago.txt","rw");
    

RandomAccessFile類常用API

可以查看Java官方文檔

例:隨機文件讀寫

public class Employee {
	char[] name = { '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000' };
	int age;

	public Employee(String name, int age) throws IOException {
		if (name.toCharArray().length > 8) {
			System.arraycopy(name.toCharArray(), 0, this.name, 0, 8);
		} else {
			System.arraycopy(name.toCharArray(), 0, this.name, 0, name.toCharArray().length);
		}
		this.age = age;
	}
}
import java.io.RandomAccessFile;

public class RandomAccessFileTester {
	String Filename;

	public RandomAccessFileTester(String Filename) {
		this.Filename = Filename;
	}

	public void writeEmployee(Employee e, int n) throws Exception {
		RandomAccessFile ra = new RandomAccessFile(Filename, "rw");
		ra.seek(n * 20);
		for (int i = 0; i < 8; i++) {
			ra.writeChar(e.name[i]);
		}
		ra.writeInt(e.age);
		ra.close();
	}

	public void readEmployee(int n) throws Exception {
		char buf[] = new char[8];
		RandomAccessFile ra = new RandomAccessFile(Filename, "r");
		ra.seek(n * 20);
		for (int i = 0; i < 8; i++) {
			buf[i] = ra.readChar();
		}
		System.out.println("name:");
		System.out.println(buf);
		System.out.println("age:" + ra.readInt());
		ra.close();
	}

	public static void main(String[] args) throws Exception {
		RandomAccessFileTester t = new RandomAccessFileTester("temp/1.txt");
		Employee e1 = new Employee("zhangSantt", 23);
		Employee e2 = new Employee("李曉珊", 33);
		Employee e3 = new Employee("王華", 19);
		t.writeEmployee(e1, 0);
		t.writeEmployee(e3, 2);
		System.out.println("第一個僱員資訊:");
		t.readEmployee(0);
		System.out.println("第三個僱員資訊:");
		t.readEmployee(2);
		System.out.println("第二個僱員資訊:");
		t.readEmployee(1);
	}
}