牛刀小試 - 詳細總結Java-IO流的使用

流的概念和作用


流是一組有順序的,有起點和終點的字節集合,是對數據傳輸的總稱或抽象。即數據在兩設備間的傳輸稱爲流。

流的本質是數據傳輸,根據數據傳輸的不同特性將流抽象封裝成不同的類,方便更直觀的進行數據操作。

 

IO流的分類

  • 根據處理數據類型的不同分爲:字符流和字節流

  • 根據數據流向不同分爲:輸入流和輸出流

輸入流和輸出流

所謂輸入流和輸出流,實際是相對內存而言。

  • 將外部數據讀取到Java內存當中就是所謂的輸入流

  • 而將Java內存當中的數據寫入存放到外部設備當中,就是所謂的輸出流

字符流和字節流

實際上最根本的流都是字節流,因爲計算機上數據存儲的最終形式實際上都是字節。

而字符流的由來是因爲:不同文字的數據編碼不同,所以纔有了對字符進行高效操作的流對象。

本質其實就是基於字節流讀取時,去查了指定的碼錶。 所以,字節流和字符流的區別在於:

  • 讀寫單位不同:字節流以字節(8bit)爲單位,字符流以字符爲單位,根據碼錶映射字符,一次可能讀多個字節。

  • 處理對象不同:字節流能處理所有類型的數據(如圖片、avi等),而字符流只能處理字符類型的數據。

通過一張關係圖來了解一下Java中的IO流構成:

通過該圖,我們除了可以看到IO流體系下,數量衆多的具體子類。

還可以注意到:這些子類的命名有一個規律,那就是:

字符流體系當中的頂層類是Reader和Writer;字節流體系當中的頂層類是InputStream和OutputStream。

而它們對應的體系下的子類的命名中,都以該頂層類的命名作爲其後綴,而前綴則說明了具體的特徵。


正確選擇合適的IO對象

在上面的Java-IO體系構成圖當中,我們已經發現這個體系中擁有衆多特定的子類,那麼,我們應該如何在如此衆多的選擇中選取最適合的IO對象使用呢?

究其根本,在衆多的子類當中,其實落實下來對於數據的操作方法基本上都很類似,都離不開對於數據的讀(read())寫(write())方法。

所以,只要瞭解不同IO流對象的自身特性之後,基本上就可以按照以下的步驟,來根據實際需求選取最合適的IO流對象使用:

(1)、明確你要做的操作是讀還是寫(也就是明確操作的是數據源還是數據目的)

  • 如果是要讀取數據(操作數據源):InputStream或Reader

  • 如果是要寫入數據(操作數據目的):OutputSteam或Writer

(2)、明確要處理的數據是否是純文本數據

  • 如果數據爲純文本:Reader或Writer (字符流對象操作文本數據效率更高)

  • 否則使用:InputStream或OutputSteam

(3)、明確源或目的的具體設備

  • 硬盤(外部文件) - File

  • 內存 - 數組

  • 網絡數據 - Socket流

(4)、明確是否需要額外功能


假設以一個小的需求:“請複製一個txt文件當中的內容至新的txt文件當中”爲例。我們來驗證一下上面所說的步驟:

(1)、複製一個文件,自然讀寫操作都要涉及到。所以這裏數據源可選:InputStream或Reader,數據目的可選:OutputSteam或Writer。

(2)、數據源是一個txt文件,其內容是純文本數據。於是我們可以進一步縮小範圍,數據源被確定爲:Reader;同理,數據目的爲:Writer。

(3)、明確數據涉及的具體設備,我們已經注意到關鍵詞“文件”。那麼對於具體的流對象的選擇,莫數據源FileReader,數據目的FileWriter莫屬了。

到了這裏,要選取的IO對象已經明確了,剩下的工作無非就是通過對象調用對應的讀寫方法來操作數據了。


除此之外,我們說到,有時還需要明確的是:你是否需要一些額外的功能。例如:

如果你想要提高操作數據的效率,可以選擇緩衝區流對象(Buffered...)來對原有的流對象進行裝飾;

或者,假如你操作的數據源是字節流,而數據目的爲字符流,那麼可以通過轉換流對象(InputStreamReader/OutputStreamWriter),

來讓輸入流與輸出流轉換達成一致,從而提高讀寫操作的效率。


舉個例子說:要獲取用戶在控制檯輸入的數據,存放到一個文本文件當中去。

這個時候,數據源爲系統的輸入流:System.in,該輸入流屬於字節流InputStream體系中。

但實際上我們知道用戶通過鍵盤輸入的數據其實就是純文本數據,所以我們最好選擇使用字符流Reader來處理數據,效率更高。

這個時候就可以使用轉換流對象InputStreamReader來將原本的字節流轉換爲字符流來處理:

InputStreamReader isr = newInputStreamReader(System.in);

而同時我們希望加入緩衝區技術,從而使效率進一步提升,最終的代碼就變爲了:

BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));


IO流的具體應用


1、輸入輸出流的常用操作

首先就以“複製文件”爲例,分別看一看字符流和字節流的常用操作。

通過字節流對象來完成上述操作,代碼爲:

package com.tsr.j2seoverstudy.io;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

/*
 * 通過字節流對象:FileInputStream,完成文件內容複製
 */
public class ByteStreamDemo {
	private static final String LINE_SEPARATOR = System
			.getProperty("line.separator");
	private static final int BUFFER_SIZE = 1024;

	public static void main(String[] args) {
		writeContentInFile();

		copyFile_1();
		
		copyFile_2();
	}

	static void writeContentInFile() {
		// 聲明輸出流對象.
		FileOutputStream fos = null;
		// 指定輸出內容
		String content = "first" + LINE_SEPARATOR + "second";
		try {
			// 構造流對象,指定輸出文件
			fos = new FileOutputStream("byteStream_1.txt");
			// 向指定文件中寫入指定內容
			fos.write(content.getBytes());
		} catch (FileNotFoundException e) {
			System.out.println("未查找到對應文件!");
		} catch (IOException e) {
			System.out.println("書寫異常!");
		} finally {
			if (fos != null)
				try {
					fos.close();
				} catch (IOException e) {
					System.out.println("關閉流對象異常.");
				}
		}
	}

	static void copyFile_1() {
		// 輸入流對象
		FileInputStream fis = null;
		// 輸出流對象
		FileOutputStream fos = null;

		try {
			fis = new FileInputStream("byteStream_1.txt");
			fos = new FileOutputStream("byteStream_1_copy.txt");

			int position = 0;
			while ((position = fis.read()) != -1) {
				fos.write(position);
				fos.flush();
			}
		} catch (FileNotFoundException e) {
		} catch (IOException e) {
		} finally {
			try {
				if (fis != null)
					fis.close();
				if (fos != null)
					fos.close();
			} catch (IOException e) {
				// TODO: handle exception
			}
		}

	}

	static void copyFile_2() {
		// 輸入流對象
		FileInputStream fis = null;
		// 輸出流對象
		FileOutputStream fos = null;

		try {
			fis = new FileInputStream("byteStream_1.txt");
			fos = new FileOutputStream("byteStream_1_copy.txt");

			byte[] buffer = new byte[BUFFER_SIZE];
			int length = 0;
			while ((length = fis.read(buffer)) != -1) {
				fos.write(buffer,0,length);
				fos.flush();
			}
		} catch (FileNotFoundException e) {
		} catch (IOException e) {
		} finally {
			try {
				if (fis != null)
					fis.close();
				if (fos != null)
					fos.close();
			} catch (IOException e) {
				// TODO: handle exception
			}
		}

	}
}

而通過字節流的代碼爲:

package com.tsr.j2seoverstudy.io;

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

public class CharacterStreamDemo {
	private static final String LINE_SEPARATOR = System
			.getProperty("line.separator");
	private static final int BUFFER_SIZE = 1024;

	public static void main(String[] args) {
		writeContentInFile();

		//copyFile_1();

		copyFile_2();
	}

	private static void copyFile_2() {
		FileWriter fw = null;
		FileReader fr = null;
		try {
			fw = new FileWriter("characterStream_1_copy.txt");
			fr = new FileReader("characterStream_1.txt");

			int length = 0;
			char [] buffer =  new char[BUFFER_SIZE];
			while ((length = fr.read(buffer)) != -1) {
				fw.write(buffer,0,length);
				fw.flush();
			}
		} catch (IOException e) {
			// TODO: handle exception
		} finally {
			try {
				if (fr != null)
					fr.close();
				if (fw != null)
					fw.close();
			} catch (IOException e) {
				// TODO: handle exception
			}
		}

	}

	private static void copyFile_1() {
		FileWriter fw = null;
		FileReader fr = null;
		try {
			fw = new FileWriter("characterStream_1_copy.txt");
			fr = new FileReader("characterStream_1.txt");

			int position = 0;
			while ((position = fr.read()) != -1) {
				fw.write(position);
			}
		} catch (IOException e) {
			// TODO: handle exception
		} finally {
			try {
				if (fr != null)
					fr.close();
				if (fw != null)
					fw.close();
			} catch (IOException e) {
				// TODO: handle exception
			}
		}

	}

	private static void writeContentInFile() {
		FileWriter fw = null;
		String content = "first" + LINE_SEPARATOR + "second";
		try {
			fw = new FileWriter("characterStream_1.txt");
			fw.write(content);
		} catch (IOException e) {
			// TODO: handle exception
		} finally {
			try {
				if (fw != null) {
					fw.close();
				}
			} catch (IOException e) {
				// TODO: handle exception
			}
		}

	}
}
通過上述的兩個例子中,主要想要說明的幾點是:

1.可以看到對於所謂的字節流和字符流之間,對於數據的輸入輸出操作的方法使用,實際上大同小異的。


2.首先可以看到的不同是,字節流文件對象在向外部文件寫入內容時,必須通過單個int型的值或byte數組的形式。

而字符流則可以通過int值,char型數組寫入之外,還可以直接將字符串對象(String)形式寫入。


3.對於流對象的讀入數據的操作,都有兩種方式:

第一種方式是,每次讀取單個數據:字節流每次讀取單個字節,字符流每次讀入單個字符(2個字節)。

但同樣都返回一個int型的值。字符流返回的這個值實際就讀取到的字符在Unicode碼錶中的位置。返回值爲-1則代表數據讀取完畢。

第二種方式是,每次讀取批量的數據到一個指定類型的緩衝區數組當中,從而提高了讀取效率。不同的是:

字節流能夠接受的緩衝區數組類型爲byte型數組,而字符流接受的類型爲char型數組。

它們也會返回一個int型的值,該值代表當前讀取操作讀取到的數據的實際長度,同樣返回-1代表讀取完畢。


4、調用完成流對象的寫入操作之後,一定要記得使用flush刷新該流對象的緩衝,才能真正的將數據寫入到外部文件。

flush和close的區別在於:flush用於刷新流的緩衝,但不關閉該流對象。

調用close方法關閉流對象,並且之前會自動的調用一次flush刷新緩衝。


5、通過上述幾點的描述,我們也進一步驗證了,當操作數據爲純文本時,應當選取字符流,因爲效率更高。

但如果數據爲非純文本,例如賦值一張圖片操作時,就只能選擇字節流來完成。


2、緩衝區流對象

我們前面已經說過了,Java中IO體系下的各個類的命名都有很強的目的性。

所以顧名思義,緩衝區流對象就是指該體系下前綴爲buffered的幾個類。

緩衝區流對象,最大的好處在於:提高數據讀寫操作的效率。


如果僅僅是瞭解對於緩衝區流對象的使用方法,實際上並不複雜,其方式就是:

創建一個緩衝區流對象,然後將需要提高操作效率的流對象作爲構造參數傳遞給創建的該緩衝區流對象就搞定了。

public class BufferedDemo {
	public static void main(String[] args) {
         try {
			FileWriter fw = new FileWriter("buffer.txt");
			BufferedWriter bw = new BufferedWriter(fw);
			
			bw.write("緩衝區流對象用於調高讀寫效率");
			bw.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
         
	}
}
而我們需要了解的是,緩衝區流對象究竟是怎樣實現提高效率的目的的呢?

在前面我們用於說明輸出流的使用時,你可能已經看到了這樣的代碼:

			int length = 0;
			char [] buffer =  new char[BUFFER_SIZE];
			while ((length = fr.read(buffer)) != -1) {
				fw.write(buffer,0,length);
				fw.flush();
			}
沒錯,這裏我們將輸入流讀取到的指定數量的數據先存放在一個指定類型的數組當中。

然後再將存放在數組中的數據取出,進行寫入操作。這裏數組就起到了一個臨時存儲的作用,也就是所謂的緩衝區。

而Java中的緩衝區流對象,其實現原理其實也是這樣。只不過他們將這些較複雜的操作封裝了起來,形成一個單獨的類,更加方便使用。


瞭解了原理,其實我們自己也可以定義所謂的緩衝區流對象。例如:

public class MyBufferedReader extends Reader {

	private Reader r;
	
	//定義一個數組作爲緩衝區。
	private char[] buf = new char[1024];
	
	//定義一個指針用於操作這個數組中的元素。當操作到最後一個元素後,指針應該歸零。	
	private int pos = 0;	
	
	
	//定義一個計數器用於記錄緩衝區中的數據個數。 當該數據減到0,就從源中繼續獲取數據到緩衝區中。
	private int count = 0;
	
	
	MyBufferedReader(Reader r){
		this.r = r;
	}
	
	/**
	 * 該方法從緩衝區中一次取一個字符。 
	 * @return
	 * @throws IOException
	 */
	public int myRead() throws IOException{
		
		if(count==0){
			count = r.read(buf);
			pos = 0;
		}
		if(count<0)
			return -1;
		
		char ch = buf[pos++];
		
		count--;
		
		return ch;
		
		/*
		//1,從源中獲取一批數據到緩衝區中。需要先做判斷,只有計數器爲0時,才需要從源中獲取數據。
		if(count==0){
			count = r.read(buf);
			
			if(count<0)
				return -1;
			
			//每次獲取數據到緩衝區後,角標歸零.
			pos = 0;
			char ch = buf[pos];
			
			pos++;
			count--;
			
			return ch;
			
		}else if(count>0){
			
			char ch = buf[pos];
			
			pos++;
			count--;
			
			return ch;
			
		}*/
		
	}
	
	public String myReadLine() throws IOException{
		
		StringBuilder sb = new StringBuilder();
		
		int ch = 0;
		while((ch = myRead())!=-1){
			
			if(ch=='\r')
				continue;
			if(ch=='\n')
				return sb.toString();
			//將從緩衝區中讀到的字符,存儲到緩存行數據的緩衝區中。
			sb.append((char)ch);
			
		}		
		
		if(sb.length()!=0)
			return sb.toString();
		return null;
	}

	public void myClose() throws IOException {
		
		r.close();
	}

	@Override
	public int read(char[] cbuf, int off, int len) throws IOException {
		
		return 0;
	}

	@Override
	public void close() throws IOException {
	}
}


3、轉換流

轉換流是指InputStreamReader和OutputStreamWriter。顧名思義,也就是指用於在字符流與字節流之間做轉換工作的流對象。

那麼,我麼已經知道了字符流與字節流之間的本質區別就是:字節流實際是通過查指定的編碼表,一次提取對應數量的字節轉換爲字符。

所以,既然是轉換流,自然就離不開一個重要的概念:編碼。而在轉換流對象的構造器中,也可以看到提供了指定編碼形式的構造器(默認編碼爲unicode)。


那麼,以我們在上面提到過的需求 “將用戶從鍵盤輸入的值,存儲到一個外部txt文件當中”爲例,通過轉換流的實現方式爲:

package com.tsr.j2seoverstudy.io;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;

public class CastStreamDemo {
	public static void main(String[] args) {
		// 獲取系統輸入流
		InputStream in = System.in;
		// 因爲用戶從鍵盤輸入的數據肯定是純文本,所以爲了提高效率,我們將其轉換爲字符流處理
	    BufferedReader reader = new BufferedReader(new InputStreamReader(in));
	    
	    try {
	    	//構造輸出流
			FileWriter fw = new FileWriter("castSteam.txt");
			BufferedWriter writer = new BufferedWriter(fw);
			
			String line = null;
			//每次讀取單行數據寫入外部文件
			while((line = reader.readLine())!= null){
				//如果用戶輸入quit,代表退出當前程序
				if(line.equals("quit"))
					break;
				writer.write(line);
				writer.newLine();
				writer.flush();
			}
		} catch (IOException e) {
		}
	}
}

4、File類

提到IO流,就不得不提到一個與之緊密相連的類的使用,也就是File類。

File類封裝了一系列對於文件或文件夾的操作方法,包括:創建,刪除,重命名,獲取文件信息等等。

我們依然通過一段代碼,來看一看File類的常用操作:

package com.tsr.j2seoverstudy.io;

import java.io.File;
import java.io.IOException;
import java.text.DateFormat;
import java.util.Date;

public class FileDemo {

	public static void main(String[] args) {
		creatNewFile();
		deleteAllFile(new File("E:\\FileTest"));
		getFileInfo();
	}

	/*
	 * 創建文件或文件夾
	 */
	public static void creatNewFile() {
		// 創建單個文件夾
		File folder = new File("E:\\FileTest");
		if (!folder.exists()) {// 判斷指定文件夾是否已經存在
			boolean b = folder.mkdir();
			System.out.println("文件夾創建是否成功?" + b);
		}
		// 創建多級文件目錄
		File folders = new File("E:\\FileTest\\1\\2\\3\\4");
		if (!folders.exists()) {
			boolean b = folders.mkdirs();
			System.out.println("多級文件目錄創建是否成功?" + b);
		}
		// 創建文件
		File txt = new File("E:\\FileTest\\file.txt");
		if (!txt.exists()) {
			try {
				boolean b = txt.createNewFile();
				System.out.println("txt文件是否創建成功?" + b);
			} catch (IOException e) {
			}
		}

	}

	/*
	 * 刪除文件,分爲兩種情況:
	 * 1、刪除單個文件或刪除刪除空的文件目錄,這種情況很簡單,直接調用delete方法就搞定了
	 * 2、但是當要刪除指定目錄下的所有內容(包括該目錄下的文件以及其子目錄和子目錄下的文件), 這時就需要涉及深度的遍歷,通過遞歸來完成。
	 */
	public static void deleteAllFile(File dir) {
		File[] files = dir.listFiles();
		System.out.println(files.length);
		// 遍歷file對象
		for (File file : files) {
			// 如果是文件夾
			if (file.isDirectory()) {
				// 遞歸
				deleteAllFile(file);
			} else {// 否則直接刪除文件
				file.delete();
			}
		}
		// 刪除文件夾
		dir.delete();
	}
	
	//重命名
    public static void renameToDemo() {
		
		File f1 = new File("c:\\9.mp3");
		
		File f2 = new File("d:\\aa.mp3");
		
		boolean b = f1.renameTo(f2);
		
		System.out.println("b="+b);
	}
	
	//文件對象的一些常用判斷方法
	public static void decideFile(){
		File folder = new File("E:\\FileTest");
		//判斷文件是否存在
		boolean b = folder.exists();
		//判斷是不是文件夾
		boolean b1 = folder.isDirectory();
		//判斷是不是文件
		boolean b2 = folder.isFile();
		//判斷是不是隱藏文件
		boolean b3 = folder.isHidden();
		//測試指定路徑是不是絕對路徑
		boolean b4 = folder.isAbsolute();
	}
	
	//獲取File對象信息
	public static void getFileInfo(){
		File file = new File("E:\\FileTest\\");
		
		String name = file.getName();//返回由此抽象路徑名錶示的文件或目錄的名稱
		
		String absPath = file.getAbsolutePath();//絕對路徑。
		
		String path = file.getPath();//將此抽象路徑名轉換爲一個路徑名字符串。
		
		long len = file.length();//返回由此抽象路徑名錶示的文件的長度
		
		long time = file.lastModified();//最後修改時間
		
		Date date = new Date(time);
		
		DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG,DateFormat.LONG);
		
		String str_time = dateFormat.format(date);
		//返回此抽象路徑名父目錄的路徑名字符串;如果此路徑名沒有指定父目錄,則返回 null
		String parentPath = file.getParent();
		
		System.out.println("parent:"+parentPath);
		System.out.println("name:"+name);
		System.out.println("absPath:"+absPath);
		System.out.println("path:"+path);
		System.out.println("len:"+len);
		System.out.println("time:"+time);
		System.out.println("str_time:"+str_time);
	}
}
對於file的使用,需要值得注意的就是:

關於對file對象的刪除方法,如果指定的路徑所代表的是一個文件目錄,並且該目錄下還存在其它文件或子目錄。

那麼該對象代表的文件夾是無法被刪除的 ,原理很簡單:你可以試着通過cmd命令行刪除這樣的文件夾,當然是行不通的。

所以如果要刪除這樣的文件目錄,通常會通過遞歸對該文件目錄下的內容進行深度的遍歷,逐次將所有內容刪除後,才能刪除該文件夾。


而關於通過file對象的list或listFile方法進行文件內容的遍歷時,還可以通過自定義攔截器(如FileFilter)來進行有條件的遍歷。

例如:遍歷出所有後綴爲".txt"的文件。


5、通過Properties,生成你自己的屬性文件

在實際的開發中,通常都會通過一系列的屬性文件來記錄我們軟件的相關信息或者客戶的使用情況。

例如:我們開發了一個軟件,可以提供免費試用5次。當試用次數用完後,就必須付費購買正版。

public class PropertiesDemo {

	/**
	 * @param args
	 * @throws IOException 
	 * @throws Exception 
	 */
	public static void main(String[] args) throws IOException  {
		
		getAppCount();
		
	}
	
	public static void getAppCount() throws IOException{
		
		//將配置文件封裝成File對象。
		File confile = new File("count.properties");
		
		if(!confile.exists()){
			confile.createNewFile();
		}
		
		FileInputStream fis = new FileInputStream(confile);
		
		Properties prop = new Properties();
		
		prop.load(fis);
		
		
		//從集合中通過鍵獲取次數。		
		String value = prop.getProperty("time");
		//定義計數器。記錄獲取到的次數。
		int count =0;
		if(value!=null){
			count = Integer.parseInt(value);
			if(count>=5){
				throw new RuntimeException("您的使用次數已滿5次,若喜歡我們的軟件,請到指定網站購買正版.");
			}
		}
		count++;
		
		//將改變後的次數重新存儲到集合中。
		prop.setProperty("time", count+"");
		
		FileOutputStream fos = new FileOutputStream(confile);
		
		prop.store(fos, "");
		
		fos.close();
		fis.close();
		
		
	}

}
Properties實際上是容器類HashTable的子類,而Properties 類表示的是一個持久化的屬性集。

Properties可保存在流中或從流中加載。屬性列表中每個鍵及其對應值都是一個字符串。

也就是說,這個特殊的子類基本上都是用於與IO流相關的操作,用於一些常規數據的持久化。


它的使用過程大概就是:

通過load方法用於接收一個輸入流對象,也就是指從輸入流中讀取屬性列表(鍵和元素對)。

當讀取結束輸入流中的屬性列表後,就可以通過getProperty獲取對應鍵的值或通過setProperty方法修改對應鍵的值。

當你修改完接收的屬性列表當中的值之後,就可以通過store方法將此Properties 表中的屬性列表(鍵和元素對)寫入對應的輸出流,以更新屬性文件。

注:Java編寫的軟件的屬性文件通常都使用“.properties” 作爲文件格式後綴。


6、序列流


序列流的基本應用

來看一看JDK API說明文檔中對於序列輸入流SequenceInputStream的說明:

SequenceInputStream 表示其他輸入流的邏輯串聯。它從輸入流的有序集合開始,並從第一個輸入流開始讀取,直到到達文件末尾。

接着從第二個輸入流讀取,依次類推,直到到達包含的最後一個輸入流的文件末尾爲止。


通俗的說,也就是使用該流對象,可以將多個流對象當中的內容進行有序的合併(序列化)。

舉例來說,假設我們現在有三個txt文件:a.txt、b.txt、c.txt。而我們需要將這個三個txt文件中的內容合併到一個txt文件當中。

按照基本的思路來說,我們應當先構造3個輸入流對象分別關聯3個不同的txt文件,然後依次將它們的內容寫入到新的txt文件當中。

而通過序列流的使用,我們就可以直接將這個3個輸入流進行序列化,直接寫入新的文件。

public class SequenceInputStreamDemo {
	public static void main(String[] args) throws IOException {
		String[] fileNames = { "a", "b", "c" };
		ArrayList<FileInputStream> al = new ArrayList<FileInputStream>();
		for (int x = 0; x < 3; x++) {
			al.add(new FileInputStream(fileNames[x] + ".txt"));
		}

		Enumeration<FileInputStream> en = Collections.enumeration(al);
		SequenceInputStream sis = new SequenceInputStream(en);

		FileOutputStream fos = new FileOutputStream("d.txt");

		byte[] buf = new byte[1024];

		int len = 0;

		while ((len = sis.read(buf)) != -1) {
			fos.write(buf, 0, len);
		}

		fos.close();
		sis.close();
	}
}
對於序列流的使用,需要注意的也就是,其接受的需要進行序列化的流對象的容器類型必須是:Enumeration

文件的切割與合併

很多網站在上傳文件的時候,因爲文件過大會影響傳輸,所以都會選擇對文件做切割。

也就是說將一個大的文件,分割爲多個較小的文件。在Java中,就可以通過序列流來實現這樣的功能:

public class FileCuttingUtil {
	private static final int SIZE = 1024 * 1024;

	/**
	 * @param args
	 * @throws Exception
	 */
	public static void main(String[] args) throws Exception {

		File file = new File("F:\\Audio\\KuGou\\All in My Head.mp3");
		File parts = new File("E:\\PartFiles");
		splitFile(file);

		mergeFile(parts);
	}

	private static void splitFile(File file) throws IOException {

		// 用讀取流關聯源文件。
		FileInputStream fis = new FileInputStream(file);

		// 定義一個1M的緩衝區。
		byte[] buf = new byte[SIZE];

		// 創建目的。
		FileOutputStream fos = null;

		int len = 0;
		int count = 1;

		/*
		 * 切割文件時,必須記錄住被切割文件的名稱,以及切割出來碎片文件的個數。 以方便於合併。
		 * 這個信息爲了進行描述,使用鍵值對的方式。用到了properties對象
		 */
		Properties prop = new Properties();

		File dir = new File("E:\\partfiles");
		if (!dir.exists())
			dir.mkdirs();

		while ((len = fis.read(buf)) != -1) {

			fos = new FileOutputStream(new File(dir, (count++) + ".part"));
			fos.write(buf, 0, len);
			fos.close();
		}

		// 將被切割文件的信息保存到prop集合中。
		prop.setProperty("partcount", count + "");
		prop.setProperty("filename", file.getName());

		fos = new FileOutputStream(new File(dir, count + ".properties"));

		// 將prop集合中的數據存儲到文件中。
		prop.store(fos, "save file info");

		fos.close();
		fis.close();

	}

	public static void mergeFile(File dir) throws IOException {

		/*
		 * 獲取指定目錄下的配置文件對象。
		 */
		File[] files = dir.listFiles(new SuffixFilter(".properties"));

		if (files.length != 1)
			throw new RuntimeException(dir + ",該目錄下沒有properties擴展名的文件或者不唯一");
		// 記錄配置文件對象。
		File confile = files[0];

		// 獲取該文件中的信息================================================。

		Properties prop = new Properties();
		FileInputStream fis = new FileInputStream(confile);

		prop.load(fis);

		String filename = prop.getProperty("filename");
		int count = Integer.parseInt(prop.getProperty("partcount"));

		// 獲取該目錄下的所有碎片文件。 ==============================================
		File[] partFiles = dir.listFiles(new SuffixFilter(".part"));

		if (partFiles.length != (count - 1)) {
			throw new RuntimeException(" 碎片文件不符合要求,個數不對!應該" + count + "個");
		}

		// 將碎片文件和流對象關聯 並存儲到集合中。
		ArrayList<FileInputStream> al = new ArrayList<FileInputStream>();
		for (int x = 0; x < partFiles.length; x++) {

			al.add(new FileInputStream(partFiles[x]));
		}

		// 將多個流合併成一個序列流。
		Enumeration<FileInputStream> en = Collections.enumeration(al);
		SequenceInputStream sis = new SequenceInputStream(en);

		FileOutputStream fos = new FileOutputStream(new File(dir,filename));

		byte[] buf = new byte[1024];

		int len = 0;
		while ((len = sis.read(buf)) != -1) {
			fos.write(buf, 0, len);
		}

		fos.close();
		sis.close();
	}
}

class SuffixFilter implements FilenameFilter {

	private String suffix;

	public SuffixFilter(String suffix) {
		super();
		this.suffix = suffix;
	}

	@Override
	public boolean accept(File dir, String name) {

		return name.endsWith(suffix);
	}

}


利用IO流實現對象的持久化


舉例來說,我們一個程序中通常都會涉及到很多實體類(bean),大多時候我們都需要對這些實體類對象存放的數據進行數據持久化。

持久化的方式有很多,實際開發中最常用的可能就是類似通過xml文件或者數據庫的方式了。


那麼,當我們想通過IO流對象,將一個實體對象的數據保存到一個外部文件當中時。應當選擇什麼樣的流對象使用呢?

Java提供的兩個類分別是:ObjectInputStream與ObjectOutputStream。

其中ObjectOutputStream流對象用於:將對象序列化後寫入。

而ObjectInputStream則用於對那些經ObjectOutputStream序列化的數據進行反序列化。


你可能在最初學習Java時,就知道了一個關鍵字“transient”,但卻很少使用。

該關鍵字的使用都是關聯到對象序列化的,也就是說:一個對象中被transient修飾的字段,不會參與到對象的序列化工作。

該字段是瞬態的,它的值不會被持久化到外部文件當中。

舉例來說:你的某個實體類含有“密碼”字段,那麼你當然不希望如此隱私的信息暴露給外部。就可以通過transient修飾該字段。

注:使用該流對象進行序列化工作的類必須實現Serializable接口。

package com.tsr.j2seoverstudy.io;

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

public class ObjectStreamDemo {

	/**
	 * @param args
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	public static void main(String[] args) throws IOException,
			ClassNotFoundException {

		/*
		 * 輸出結果爲:小強:0。
         * 可以看到因爲age被設爲瞬態,所以該屬性不會參與對象的序列化工作。
         * 當進行對象的反序列化時,則會取到一個該數據類型的默認初始化值
		 */
		writeObj();
		readObj();
	}

	public static void readObj() throws IOException, ClassNotFoundException {

		ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
				"obj.object"));
		// 對象的反序列化。
		Person p = (Person) ois.readObject();

		System.out.println(p.getName() + ":" + p.getAge());

		ois.close();

	}

	public static void writeObj() throws IOException, IOException {

		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
				"obj.object"));
		// 對象序列化。 被序列化的對象必須實現Serializable接口。
		oos.writeObject(new Person("小強", 30));

		oos.close();

	}

}

class Person implements Serializable {
	private static final long serialVersionUID = 1L;

	private String name;
	private transient int age;

	public Person(String name, int age) {
		this.name = name;
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}

}

管道流

管道流與其它IO流的最大不同之處在於:管道流是結合多線程使用的。

簡單的說,其特點就是:輸出管道流與輸入管道流存在於不同的線程之間,但它們之間能夠互相將管道連接起來。

也就是說,當我們在線程A中定義一個輸入管道流,用於接收在線程B當中的輸出管道流所書寫的數據信息。

package com.tsr.j2seoverstudy.io;

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

public class PipedStreamDemo {

	/**
	 * @param args
	 * @throws IOException
	 */
	public static void main(String[] args) throws IOException {

		PipedInputStream input = new PipedInputStream();
		PipedOutputStream output = new PipedOutputStream();

		input.connect(output);

		new Thread(new Input(input)).start();
		new Thread(new Output(output)).start();

	}

}

class Input implements Runnable {

	private PipedInputStream in;

	Input(PipedInputStream in) {
		this.in = in;
	}

	public void run() {

		try {
			byte[] buf = new byte[1024];
			int len = in.read(buf);

			String s = new String(buf, 0, len);

			System.out.println("s=" + s);
			in.close();
		} catch (Exception e) {
			// TODO: handle exception
		}

	}
}

class Output implements Runnable {
	private PipedOutputStream out;

	Output(PipedOutputStream out) {
		this.out = out;
	}

	public void run() {

		try {
			Thread.sleep(5000);
			out.write("我是線程B當中的輸出管道流,給你輸出信息了,線程A當中的兄弟!".getBytes());
		} catch (Exception e) {
			// TODO: handle exception
		}
	}
}


DataStream-操作基本數據類型的流


簡單的說,該流對象最大的特點就是對基本的輸入輸出流進行了一定的“裝飾”。

專門針對於Java當中的基本數據類型的操作方法進行了封裝,所以當操作的數據是基本數據類型時

就應當選擇該中流對象使用,因爲效率更高。

public class DataStreamDemo {

	private static void write() {
		try {
			DataOutputStream dos = new DataOutputStream(new FileOutputStream(
					"dataStream.txt"));
			dos.writeByte(0);
			dos.writeChar(0);
			dos.writeInt(0);
			dos.writeLong(0);
			dos.writeDouble(0);
			dos.writeFloat(0);
			dos.writeUTF("hello");
			dos.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	private static void read() {
		try {
			DataInputStream dis = new DataInputStream(new FileInputStream(
					"dataStream.txt"));
			dis.readByte();
			dis.readChar();
			dis.readInt();
			dis.readLong();
			dis.readFloat();
			dis.readDouble();
			dis.readUTF();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

操作內存數組的流


就是指:ByteArrayInputStream;ByteArrayOutputStream;CharArrayReader;CharArrayWriter;

與其它的輸入輸出流不同的是:該種流對象用於,當數據源和數據目的都位於內存當中的情況。

並且,這種類型的流對象調用close方法後,此類當中的方法還是可以被調用到。

public class ByteArrayStreamDemo {

	/**
	 * @param args
	 * @throws IOException 
	 */
	public static void main(String[] args) {

		ByteArrayInputStream bis = new ByteArrayInputStream("abcedf".getBytes());
		
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		
		int ch = 0;
		
		while((ch=bis.read())!=-1){
			bos.write(ch);
		}
		
		System.out.println(bos.toString());
	}

}


以上就是對Java IO體系當中常用的流對象和一些提供特殊功能的對象的常用操作做了一個總結。

我們之前已經說過了,無論IO體系下存在這麼多不同的具體子類實現,其根本無非就是根據數據傳輸的不同特性將流抽象封裝成不同的類。

所以,對於IO的掌握,重點還是瞭解不同的流對象的功能特點,從而根據實際需求選擇最合適的流對象。從而使代碼變得更簡單,使讀寫效率更高。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章