00 15Java高級之字節流與字符流

1 流的基本概念

在java.io包裏面File類是唯一一個與文件本身有關的程序處理類,但是File類只能夠操作文件本身而不能能夠操作文件的內容,或者說在實際的開發之中IO操作的核心意義在於:輸入與輸出操作。而對於程序而言,輸入與輸出可能來自於不同的環境,例如:通過電腦連接服務器上進行瀏覽的時候,實際上此時客戶端發出了一個信息,而後服務器接收到此信息之後進行迴應處理。

對於服務器或者是客戶端而言,實質上傳遞的就是一種數據流的處理形式,而所謂的數據流指的就是字節數據。而對於這種流的處理形式在java.io包裏面提供有兩類支持:
(1)字節處理流:OutputStream(輸出字節流)、InputStream(輸入字節流);
(2)字符處理流:Writer(輸出字符流)、Reader(輸入字符流)。
所有的流操作都應該採用如下統一的步驟進行,下面以文件處理的流程爲例:
(1)如果現在要進行的是文件的讀寫操作,則一定要通過File類找到一個文件路徑;
(2)通過字節流或字符流的子類爲父類對象實例化;
(3)利用字節流或字符流中的方法實現數據的輸入與輸出操作;
(4)流的操作屬於資源操作,資源操作必須進行關閉處理;

2 OutputStream字節輸出流

字節數據是以Byte類型爲主實現的操作,在進行字節內容輸出的時候可以使用OutputStream類完成,這個類的基本定義如下:
public abstract class OutputStream extends Object implements Closeable, Flushable
首先可以發現這個類實現了兩個接口,於是基本的對應關係如下:
(1)Closeable

public interface Closeable extends AutoCloseable

(2)Flushable

public interface Flushable


OutputStream類定義的是一個公共的輸出操作標準,而在這個操作標準裏面一共定義有三個內容輸出的方法。
(1)輸出單個字節數據:public abstract void write​(int b) throws IOException
(2)輸出一組字節:public void write​(byte[] b) throws IOException
(3)輸出部分字節數據public void write​(byte[] b, int off, int len) throws IOException

但是需要注意的一個核心問題在於:OutputStream類畢竟是一個抽象類,而這個抽象類如果要想獲得實例化對象,按照傳統的認識應該通過子類實例的向上轉型完成,如果說現在要進行的是文件處理操作,則可以使用FileOutputStream子類。

因爲最終都需要發生向上轉型的處理關係,所以對於此時的FileOutputStream子類核心的關注點就可以放在構造方法上。
(1)【覆蓋】構造方法:public FileOutputStream​(File file) throws FileNotFoundException
(2)【追加】構造方法:public FileOutputStream​(File file, boolean append) throws FileNotFoundException
範例:使用OutputStream類實現內容的輸出

package cn.victor.demo;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class JavaAPIDemo {
	
	public static void main(String[] args) throws IOException {
		File file = new File("d:" + File.separator + "hy" + File.separator + "love" + File.separator + "lks.txt");
		if(!file.getParentFile().exists()) {
			file.getParentFile().mkdirs();
		}
		
		OutputStream output = new FileOutputStream(file, true);
		
		String str = "lks loves hhy\r\n";
		
		output.write(str.getBytes());
		output.close();
	}
	
}

本程序是採用了最爲標準的形式實現了輸出的操作處理,並且在整體的處理之中,只是創建了文件的父目錄,但是並沒有創建文件,而在執行後會發現文件可以自動幫助用戶創建。另外需要提醒的是,由於OutputStream子類也屬於AutoCloseable接口子類,所以對於close()方法也可以簡化使用。
範例:自動關閉處理

package cn.victor.demo;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class JavaAPIDemo {
	
	public static void main(String[] args){
		File file = new File("d:" + File.separator + "hy" + File.separator + "love" + File.separator + "lks.txt");
		if(!file.getParentFile().exists()) {
			file.getParentFile().mkdirs();
		}
		
		try(OutputStream output = new FileOutputStream(file, true)){
			String str = "lks loves hhy\r\n";
			
			output.write(str.getBytes());
		}catch(IOException e) {
			e.printStackTrace();
		}
	}
	
}

是否使用自動的關閉取決於你項目的整體結構,另外還需要注意的是,整個的程序裏面最終是輸出了一組的字節數據,但是千萬不要忘記了,OutputStream類之中定義的輸出方法一共有三個。

3 InputStream字節輸入流

與OutputStream類對應的一個流就是字節輸入流,InputStream類主要實現的就是字節數據讀取,該類定義如下:

public abstract class InputStream extends Object implements Closeable

在InputStream類裏面定義有如下的幾個核心方法:
(1)讀取單個字節數據(如果已經讀取到底了,返回-1):public abstract int read() throws IOException
(2)讀取一組字節數據(返回讀取的個數,如果數據讀取到底了,返回-1):public int read​(byte[] b) throws IOException
(3)讀取一組字節數據的部分內容:public int read​(byte[] b, int off, int len) throws IOException

InputStream類屬於一個抽象類,這時應該依靠它的子類來實例化對象,如果要從文件讀取一定使用FileInputStream子類,對於子類而言只關心對父類對象實例化,其構造方法如下:

範例:讀取數據

package cn.victor.demo;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

public class JavaAPIDemo {
	
	public static void main(String[] args) throws IOException{
		File file = new File("d:" + File.separator + "hy" + File.separator + "love" + File.separator + "lks.txt");
		if(!file.getParentFile().exists()) {
			file.getParentFile().mkdirs();
		} 
		
		InputStream input = new FileInputStream(file);
		byte[] bytes = new byte[1024];
		while(true) {
			int i = input.read();
			if(i == -1) {
				break;
			}
			System.out.print((char) i);
		}
		input.close();
	}
	
}

對於字節輸入流裏面最爲麻煩的問題就在於:使用read()方法讀取的時候只能夠以字節數組爲主進行接收。

特別需要注意的是從JDK 1.9開始在InputStream類裏面增加了一個新的方法:
範例:新方法

package cn.victor.demo;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class JavaAPIDemo {
	
	public static void main(String[] args) throws IOException{
		File file = new File("d:" + File.separator + "hy" + File.separator + "love" + File.separator + "lks.txt");
		if(!file.getParentFile().exists()) {
			file.getParentFile().mkdirs();
		}
		
		InputStream input = new FileInputStream(file);
		byte[] bytes = new byte[1024];
		bytes = input.readAllBytes();
		System.out.println(new String(bytes));
		input.close();
	}
	
}

如果你現在要讀取的內容很大的時候,那麼這種讀取會導致程序崩潰。

4 Writer字符輸出流

使用Output字節輸出流進行數據輸出的時候使用的都是字節類型的數據,而很多情況下字符串的輸出是比較方便的,所以對於java.io包而言,在JDK 1.1的時候又推出了字符輸出流:Writer,這個類的定義如下:

public abstract class Writer
extends Object
implements Appendable, Closeable, Flushable


在Writer類裏面提供有許多的輸出操作方法,重點來看兩個:
(1)輸出字符數組:public void write​(char[] cbuf) throws IOException
(2)輸出字符串:public void write​(String str) throws IOException

範例:使用Writer輸出

package cn.victor.demo;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;

public class JavaAPIDemo {
	
	public static void main(String[] args) throws IOException{
		File file = new File("d:" + File.separator + "hy" + File.separator + "love" + File.separator + "lks.txt");
		if(!file.getParentFile().exists()) {
			file.getParentFile().mkdirs();
		}
		Writer writer = new FileWriter(file);
		String str = "lks loves hhy.";
		
		writer.write(str);
		writer.append(" forever!");
		writer.close();
	}
	
}

使用Writer輸出的最大優勢在於可以直接利用字符串完成。Writer是字符流,字符處理的優勢在於中文數據上。

5 Reader字符輸入流

Reader是實現字符輸出流的一種類型,其本身屬於一個抽象類,這個類的定義如下:

public abstract class Reader
extends Object
implements Readable, Closeable


Reader類裏面並沒有像Writer類一樣提供有整個字符串的輸入處理操作,只能夠使用字符數組來實現接收:
(1)接收數據:public int read​(char[] cbuf) throws IOException

範例:實現數據讀取

package cn.victor.demo;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;

public class JavaAPIDemo {
	
	public static void main(String[] args) throws IOException{
		File file = new File("d:" + File.separator + "hy" + File.separator + "love" + File.separator + "lks.txt");
		if(!file.getParentFile().exists()) {
			file.getParentFile().mkdirs();
		}
		Reader reader = new FileReader(file);
		char[] data = new char[1024];
		int len = reader.read(data);
		System.out.println(new String(data, 0, len));
		reader.close();
	}
	
}

字符流讀取的時候只能夠按照數組的形式來實現處理操作。

6 字節流與字符流的區別

現在通過一系列的分析已經可以清楚字節流與字符流的基本操作了,但是對於這兩類流依然是存在有區別的,重點來分析下輸出的處理操作。在使用OutputStream和Writer輸出的最後發現都使用了close()方法進行了關閉處理。

在使用OutputStream類輸出的時候如果沒有使用close()方法關閉輸出流發現內容依然可以實現正常的輸出,但是如果在使用Writer的時候沒有使用close()方法關閉輸出流,那麼這個時候內容將無法進行輸出,因爲Writer使用到了緩衝區。當使用了close()方法的時候實際上會出現有強制刷新緩衝區的情況,所以這個時候會將內容進行輸出,如果沒有關閉,那麼將無法進行輸出操作,所以此時如果在不關閉的情況下要想將全部的內容輸出可以使用flush()方法強制性清空。
範例:使用Writer並強制性清空

package cn.victor.demo;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;

public class JavaAPIDemo {
	
	public static void main(String[] args) throws IOException{
		File file = new File("d:" + File.separator + "hy" + File.separator + "love" + File.separator + "lks.txt");
		if(!file.getParentFile().exists()) {
			file.getParentFile().mkdirs();
		}
		Writer writer = new FileWriter(file);
		String str = "lks loves hhy.";
		writer.write(str);
		writer.flush();
	}
	
}

字節流在進行處理的時候並不會使用到緩衝區,而字符流會使用到緩衝區。另外使用緩衝區的字符流更加適合於中文數據的處理,所以在日後的程序開發之中,如果要涉及到包含中文信息的輸出一般都會使用字符流處理,但是從另外一方面來講,字節流和字符流的基本處理形式是相似的,由於IO很多情況下都是進行數據的傳輸使用(二進制)。

7 轉換流

所謂的轉換流指的是可以實現字節流與字符流操作的功能轉換,例如:進行輸出的時候OutputStream需要將內容變爲字節數組後纔可以輸出,而Writer可以直接輸出字符串,這一點是方便的,所以很多人就認爲需要提供有一種轉換的機制來實現不同流類型的轉換操作,爲此在java.io包裏面提供有兩個類:InputStreamReader、OutputStreamWriter。
(1)InputStreamReader
|——定義:public class InputStreamReader extends Reader
|——構造方法:public InputStreamReader​(InputStream in)

(2)OutputStreamWriter
|——定義:public class OutputStreamWriter extends Writer
|——構造方法:public OutputStreamWriter​(OutputStream out)

通過類的繼承結構和構造方法可以發現,所謂的轉換處理就是將接收到的字節流對象通過向上轉型變爲字符流對象。
範例:觀察轉換

package cn.victor.demo;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;

public class JavaAPIDemo {
	
	public static void main(String[] args) throws IOException{
		File file = new File("d:" + File.separator + "hy" + File.separator + "love" + File.separator + "lks.txt");
		if(!file.getParentFile().exists()) {
			file.getParentFile().mkdirs();
		}
		OutputStream output = new FileOutputStream(file);
		Writer writer = new OutputStreamWriter(output);
		writer.write("hello");
		writer.close();
	}
	
}

講解轉換流的主要目的基本上不是爲了讓開發者去記住它,而是知道有這樣一種功能,但同時更多的是需要進行結構的分析處理。通過之前的字節流與字符流的一系列分析之後你會發現OutputStream類有FileOutputStream直接之類、InputStream類有FileInputStream直接子類,但是來觀察一下FileWriter、FileReader類繼承關係。

實際上所謂的緩存指的都是程序中間的一道處理緩衝區。

8 綜合實戰:文件拷貝

在操作系統裏面有一個copy命令,這個命令的主要功能是可以實現文件的拷貝處理,現在要求模擬這個命令,通過初始化參數輸入拷貝的源文件路徑與拷貝的目標路徑實現文件的拷貝處理。
需求分析:
(1)需要實現文件的拷貝操作,那麼這種拷貝就有可能拷貝各種類型的文件,所以肯定使用字節流;
(2)在進行拷貝的時候可能需要考慮到大文件的拷貝問題;
實現方案:
(1)使用InputStream將全部要拷貝的內容直接讀取到程序裏面,而後一次性的輸出到目標文件。
|——如果拷貝文件過大,程序會崩潰。
(2)採用部分拷貝,讀取一部分輸出一部分,如果現在要採用第二種方法,核心的操作方法:
|——InputStream:public int read​(byte[] b) throws IOException
|——OutputStream:public void write​(byte[] b, int off, int len) throws IOException
範例:實現文件拷貝處理

package cn.victor.demo;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

class FileUtil{
	private File srcFile;
	private File desFile;
	
	public FileUtil() {}
	
	public FileUtil(String srcFile, String desFile) {
		this(new File(srcFile), new File(desFile));
	}
	
	public FileUtil(File srcFile, File desFile) {
		this.srcFile = srcFile;
		this.desFile = desFile;
	}
	
	public boolean copy()throws IOException{
		InputStream input = null;
		OutputStream output = null;
		
		if( !srcFile.getParentFile().exists()) {
			return false;
		}
		
		if( !desFile.getParentFile().exists() ) {
			desFile.getParentFile().mkdirs();
		}
		
		try {
			input = new FileInputStream(srcFile);
			output = new FileOutputStream(desFile);
			byte datas[] = new byte[1024];
			int len = -1;
			while((len = input.read(datas)) != -1) {
				output.write(datas, 0, len);
			}
			return true;
		}catch(IOException e) {
			throw e;
		}finally {
			if( input != null ) {
				input.close();
			}
			
			if( output != null ) {
				output.close();
			}
		}
	}
}

public class JavaAPIDemo {
	
	public static void main(String[] args) throws IOException{
		if( args.length != 2 ) {
			System.out.println("Syntax Error. Please input ...");
		}
		boolean flag = new FileUtil(args[0], args[1]).copy();
		if(flag) {
			System.out.println("success!");
		}else {
			System.out.println("fail!");
		}
		
	}
	
}

但是需要注意的是,以上的做法是屬於文件拷貝的最原始的實現,而從JDK 1.9開始InputStream和Reader類中都追加了有數據轉存的處理方法:
(1)InputStream:public long transferTo​(OutputStream out) throws IOException
(2)Reader:public long transferTo​(Writer out) throws IOException
範例:使用轉存的方式處理

package cn.victor.demo;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

class FileUtil{
	private File srcFile;
	private File desFile;
	
	public FileUtil() {}
	
	public FileUtil(String srcFile, String desFile) {
		this(new File(srcFile), new File(desFile));
	}
	
	public FileUtil(File srcFile, File desFile) {
		this.srcFile = srcFile;
		this.desFile = desFile;
	}
	
	public boolean copy()throws IOException{
		InputStream input = null;
		OutputStream output = null;
		
		if( !srcFile.getParentFile().exists()) {
			return false;
		}
		
		if( !desFile.getParentFile().exists() ) {
			desFile.getParentFile().mkdirs();
		}
		
		try {
			input = new FileInputStream(srcFile);
			output = new FileOutputStream(desFile);
			long len = input.transferTo(output);
			System.out.println(len);
			return true;
		}catch(IOException e) {
			throw e;
		}finally {
			if( input != null ) {
				input.close();
			}
			
			if( output != null ) {
				output.close();
			}
		}
	}
}

public class JavaAPIDemo {
	
	public static void main(String[] args) throws IOException{
		if( args.length != 2 ) {
			System.out.println("Syntax Error. Please input ...");
		}
		long start = System.currentTimeMillis();
		boolean flag = new FileUtil(args[0], args[1]).copy();
		long end = System.currentTimeMillis();
		System.out.println(end - start);
		if(flag) {
			System.out.println("success!");
		}else {
			System.out.println("fail!");
		}
		
	}
	
}

此時千萬注意程序的運行版本問題。那麼如果說現在對此程序要求進一步擴展,可以實現一個文件目錄的拷貝,一旦進行文件目錄的拷貝還需要拷貝所有的子目錄中的文件。
範例:文件夾拷貝

package cn.victor.demo;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

class FileUtil{
	private File srcFile;
	private File desFile;
	
	public FileUtil() {}
	
	public FileUtil(String srcFile, String desFile) {
		this(new File(srcFile), new File(desFile));
	}
	
	public FileUtil(File srcFile, File desFile) {
		this.srcFile = srcFile;
		this.desFile = desFile;
	}
	
	public boolean copyDir() throws IOException {
		if(!this.srcFile.getParentFile().exists()) {
			return false;
		}
		this.copyImpl(this.srcFile);
		return true;
	}
	
	public void copyImpl(File srcFile) throws IOException {
		if( srcFile.isDirectory() ) {
			File result[] = srcFile.listFiles();
			if(result != null) {
				for(int i = 0; i < result.length; i++) {
					copyImpl(result[i]);
				}
			}
		}else {
			String newFilePath = srcFile.toString().replace(this.srcFile.getPath() + File.separator, "");
			File newFile = new File(this.desFile, newFilePath);
			this.copy(srcFile, newFile);
		}
	}
	
	public boolean copy(File srcFile, File desFile) throws IOException {
		if( !desFile.getParentFile().exists() ) {
			desFile.getParentFile().mkdirs();
		}
		
		InputStream input = null;
		OutputStream output = null;
		
		try {
			input = new FileInputStream(srcFile);
			output = new FileOutputStream(desFile);
			long len = input.transferTo(output);
			return true;
		}catch(IOException e) {
			throw e;
		}finally {
			if( input != null ) {
				input.close();
			}
			
			if( output != null ) {
				output.close();
			}
		}
	}
	
	public boolean copyFile()throws IOException{
		
		if( !this.srcFile.getParentFile().exists()) {
			return false;
		}
		return copy(this.srcFile, this.desFile);
		
	}
}

public class JavaAPIDemo {
	
	public static void main(String[] args) throws IOException{
		if( args.length != 2 ) {
			System.out.println("Syntax Error. Please input ...");
		}
		long start = System.currentTimeMillis();
		boolean flag = new FileUtil(args[0], args[1]).copyDir();
		long end = System.currentTimeMillis();
		System.out.println(end - start);
		if(flag) {
			System.out.println("success!");
		}else {
			System.out.println("fail!");
		}
		
	}
	
}

本程序是IO核心代碼,本程序可以理解,整個的IO處理機制就非常容易理解了。

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