“挑三揀四”地學一學Java I/O

沉默王二發表於2019-03-16

古人云:“讀書破萬卷,下筆如有神”。也就是說,只有大量的閱讀,寫作的時候才能風生水起——寫作意味著輸出(的知識傳播給他人),而讀書意味著輸入(從他人的知識中汲取營養)。

對於Java I/O來說,I意味著Input(輸入),O意味著Output(輸出)。讀書寫作並非易事,而建立一個好的I/O系統更是一項艱難的任務。

01、資料流之位元組與字元

Java所有的I/O機制都是基於資料流進行的輸入輸出。資料流可分為兩種:

1)位元組流,未經加工的原始二進位制資料,最小的資料單元是位元組

2)字元流,經過一定編碼處理後符合某種格式規定的資料,最小的資料單元是字元——佔用兩個位元組。

OutputStreamInputStream用來處理位元組流;WriterReader用來處理字元流;OutputStreamWriter可以把OutputStream轉換為WriterInputStreamReader可以把InputStream轉換為Reader

Java的設計者為此設計了眾多的類,見下圖。

InputStream、OutputStream及Reader、Writer類

看到這麼多類,你一定感覺頭暈目眩。反正我已經看得不耐煩了。搞這麼多類,看起來頭真的大——這也從側面說明實際的應用場景各有各的不同——你也完全不用擔心,因為實際專案當中,根本就不可能全用到(我就沒用過SequenceOutputStream)。

我建議你在學習的時候要掌握一種“挑三揀四”的能力——學習自己感興趣的、必須掌握的、對能力有所提升的知識。切不可囫圇吞棗,強迫自己什麼都學。什麼都學,最後的結果可能是什麼都不會。

字元流是基於位元組流的,因此,我們先來學習一下位元組流的兩個最基礎的類——OutputStreamInputStream,它們是必須要掌握的。

1)OutputStream

OutputStream提供了4個非常有用的方法,如下。

  • public void write(byte b[]):將陣列b中的位元組寫到輸出流。
  • public void write(byte b[], int off, int len):將陣列b的從偏移量off開始的len個位元組寫到輸出流。
  • public void flush() : 將資料緩衝區中資料全部輸出,並清空緩衝區。
  • public void close() : 關閉輸出流並釋放與流相關的系統資源。

其子類ByteArrayOutputStreamBufferedOuputStream最為常用(File相關類放在下個小節)。

①、ByteArrayOutputStream通常用於在記憶體中建立一個位元組陣列緩衝區,資料被“臨時”放在此緩衝區中,並不會輸出到檔案或者網路套接字中——就好像一箇中轉站,負責把輸入流中的資料讀入到記憶體緩衝區中,你可以呼叫它的toByteArray()方法來獲取位元組陣列。

來看下例。

public static byte[] readBytes(InputStream in, long length) throws IOException {
	ByteArrayOutputStream bo = new ByteArrayOutputStream();
	byte[] buffer = new byte[1024];
	int read = 0;
	while (read < length) {
		int cur = in.read(buffer, 0, (int) Math.min(1024, length - read));
		if (cur < 0) {
			break;
		}
		read += cur;
		bo.write(buffer, 0, cur);
	}
	return bo.toByteArray();
}
複製程式碼

ByteArrayOutputStream的責任就是把InputStream中的位元組流“一字不差”的讀出來——這個工具方法很重要,很重要,很重要——可以解決粘包的問題。

②、BufferedOuputStream實現了一個緩衝輸出流,可以將很多小的資料快取為一個大塊的資料,然後一次性地輸出到檔案或者網路套接字中——這裡的“緩衝”和ByteArrayOutputStream的“緩衝”有著很大的不同——前者是為了下一次的一次性輸出,後者就是單純的為了緩衝,不存在輸出。

來看下例。

protected void write(byte[] data) throws IOException {
	out.write(intToByte(data.length));
	out.write(data);
	out.flush();
	out.close();
}

public static byte[] intToByte(int num)
{
    byte[] data = new byte[4];
    for (int i = 0; i < data.length; i++)
    {
        data[3-i] = (byte)(num % 256);
        num = num / 256;
    }
    return data;
}
複製程式碼

使用BufferedOuputStream的時候,一定要記得呼叫flush()方法將資料從緩衝區中全部輸出。使用完畢後,呼叫close()方法關閉輸出流,釋放與流相關的系統資源。

2)InputStream

InputStream也提供了4個非常有用的方法,如下。

  • public int read(byte b[]):讀取b.length個位元組的資料放到陣列b中,返回值是讀取的位元組數。
  • public int read(byte b[], int off, int len):從輸入流中最多讀取len個位元組的資料,存放到偏移量為off的陣列b中。
  • public int available():返回輸入流中可以讀取的位元組數。
  • public int close() :使用完後,對開啟的流進行關閉。

其子類BufferedInputStream(緩衝輸入流)最為常用,效率最高(當我們不確定讀入的是大資料還是小資料)。

無緩衝流上的每個讀取請求通常會導致對作業系統的呼叫以讀取所請求的位元組數——進行系統呼叫的開銷非常大。但緩衝輸入流就不一樣了,它通過對內部緩衝區執行(例如)高達8k位元組的大量讀取,然後針對緩衝區的大小再分配位元組來減少系統呼叫的開銷——效能會提高很多。

使用示例如下。

先來看一個輔助方法byteToInt,把位元組轉換成int。

public static int byteToInt(byte[] b) {
	int num = 0;
	for (int i = 0; i < b.length; i++) {
		num*=256;
		num+=(b[i]+256)%256;
	}
	return num;
}
複製程式碼

再來看如何從輸入流中,根據指定的長度contentLength來讀取資料。readBytes()方法在之前已經提到過。

BufferedInputStream in = new BufferedInputStream(socket.getInputStream());

byte[] tmpByte = new byte[4];
// 讀取四個位元組判斷訊息長度
in.read(tmpByte, 0, 4);
// 將byte轉為int
int contentLength = byteToInt(tmpByte);

byte[] buf = null;
if (contentLength > in.available()) {
	// 之前提到的方法
	buf = readBytes(in, contentLength);
} else {
	buf = new byte[contentLength];
	in.read(buf, 0, contentLength);

	// 發生粘包了
	if (in.available() > 0) {
	}
}
複製程式碼

我敢保證,只要你搞懂了位元組流,字元流也就不在話下——所以,我們在此略過字元流。

02、File類

前面我們瞭解到,資料有兩種格式:位元組與字元。那麼這些資料從哪裡來,又存往何處呢?

一個主要的方式就是從物理磁碟上進行讀取和儲存,磁碟的唯一最小描述就是檔案。也就是說上層應用程式只能通過檔案來操作磁碟上的資料,檔案也是作業系統和磁碟驅動器互動的一個最小單元。

在Java中,通常用File類來操作檔案。當然了,File不止是檔案,它也是資料夾(目錄)。File類儲存了檔案或目錄的各種後設資料資訊(檔名、檔案長度、最後修改時間、是否可讀、當前檔案的路徑名等等)。

通過File類以及檔案輸入輸出流(FileInputStreamFileOutputStream),可以輕鬆地建立、刪除、複製檔案或者目錄。

這裡,我提供給你一個實用的檔案工具類——FileUtils。

package com.cmower.common.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Lists;

/**
 * 檔案操作工具類
 * 實現檔案的建立、刪除、複製、壓縮、解壓以及目錄的建立、刪除、複製、壓縮解壓等功能
 */
public class FileUtils extends org.apache.commons.io.FileUtils {
	
	private static Logger logger = LoggerFactory.getLogger(FileUtils.class);

	/**
	 * 複製單個檔案,如果目標檔案存在,則不覆蓋
	 * @param srcFileName 待複製的檔名
	 * @param descFileName 目標檔名
	 * @return 如果複製成功,則返回true,否則返回false
	 */
	public static boolean copyFile(String srcFileName, String descFileName) {
		return FileUtils.copyFileCover(srcFileName, descFileName, false);
	}

	/**
	 * 複製單個檔案
	 * @param srcFileName 待複製的檔名
	 * @param descFileName 目標檔名
	 * @param coverlay 如果目標檔案已存在,是否覆蓋
	 * @return 如果複製成功,則返回true,否則返回false
	 */
	public static boolean copyFileCover(String srcFileName,
			String descFileName, boolean coverlay) {
		File srcFile = new File(srcFileName);
		// 判斷原始檔是否存在
		if (!srcFile.exists()) {
			logger.debug("複製檔案失敗,原始檔 " + srcFileName + " 不存在!");
			return false;
		}
		// 判斷原始檔是否是合法的檔案
		else if (!srcFile.isFile()) {
			logger.debug("複製檔案失敗," + srcFileName + " 不是一個檔案!");
			return false;
		}
		File descFile = new File(descFileName);
		// 判斷目標檔案是否存在
		if (descFile.exists()) {
			// 如果目標檔案存在,並且允許覆蓋
			if (coverlay) {
				logger.debug("目標檔案已存在,準備刪除!");
				if (!FileUtils.delFile(descFileName)) {
					logger.debug("刪除目標檔案 " + descFileName + " 失敗!");
					return false;
				}
			} else {
				logger.debug("複製檔案失敗,目標檔案 " + descFileName + " 已存在!");
				return false;
			}
		} else {
			if (!descFile.getParentFile().exists()) {
				// 如果目標檔案所在的目錄不存在,則建立目錄
				logger.debug("目標檔案所在的目錄不存在,建立目錄!");
				// 建立目標檔案所在的目錄
				if (!descFile.getParentFile().mkdirs()) {
					logger.debug("建立目標檔案所在的目錄失敗!");
					return false;
				}
			}
		}

		// 準備複製檔案
		// 讀取的位數
		int readByte = 0;
		InputStream ins = null;
		OutputStream outs = null;
		try {
			// 開啟原始檔
			ins = new FileInputStream(srcFile);
			// 開啟目標檔案的輸出流
			outs = new FileOutputStream(descFile);
			byte[] buf = new byte[1024];
			// 一次讀取1024個位元組,當readByte為-1時表示檔案已經讀取完畢
			while ((readByte = ins.read(buf)) != -1) {
				// 將讀取的位元組流寫入到輸出流
				outs.write(buf, 0, readByte);
			}
			logger.debug("複製單個檔案 " + srcFileName + " 到" + descFileName
					+ "成功!");
			return true;
		} catch (Exception e) {
			logger.debug("複製檔案失敗:" + e.getMessage());
			return false;
		} finally {
			// 關閉輸入輸出流,首先關閉輸出流,然後再關閉輸入流
			if (outs != null) {
				try {
					outs.close();
				} catch (IOException oute) {
					oute.printStackTrace();
				}
			}
			if (ins != null) {
				try {
					ins.close();
				} catch (IOException ine) {
					ine.printStackTrace();
				}
			}
		}
	}

	/**
	 * 複製整個目錄的內容,如果目標目錄存在,則不覆蓋
	 * @param srcDirName 源目錄名
	 * @param descDirName 目標目錄名
	 * @return 如果複製成功返回true,否則返回false
	 */
	public static boolean copyDirectory(String srcDirName, String descDirName) {
		return FileUtils.copyDirectoryCover(srcDirName, descDirName,
				false);
	}

	/**
	 * 複製整個目錄的內容 
	 * @param srcDirName 源目錄名
	 * @param descDirName 目標目錄名
	 * @param coverlay 如果目標目錄存在,是否覆蓋
	 * @return 如果複製成功返回true,否則返回false
	 */
	public static boolean copyDirectoryCover(String srcDirName,
			String descDirName, boolean coverlay) {
		File srcDir = new File(srcDirName);
		// 判斷源目錄是否存在
		if (!srcDir.exists()) {
			logger.debug("複製目錄失敗,源目錄 " + srcDirName + " 不存在!");
			return false;
		}
		// 判斷源目錄是否是目錄
		else if (!srcDir.isDirectory()) {
			logger.debug("複製目錄失敗," + srcDirName + " 不是一個目錄!");
			return false;
		}
		// 如果目標資料夾名不以檔案分隔符結尾,自動新增檔案分隔符
		String descDirNames = descDirName;
		if (!descDirNames.endsWith(File.separator)) {
			descDirNames = descDirNames + File.separator;
		}
		File descDir = new File(descDirNames);
		// 如果目標資料夾存在
		if (descDir.exists()) {
			if (coverlay) {
				// 允許覆蓋目標目錄
				logger.debug("目標目錄已存在,準備刪除!");
				if (!FileUtils.delFile(descDirNames)) {
					logger.debug("刪除目錄 " + descDirNames + " 失敗!");
					return false;
				}
			} else {
				logger.debug("目標目錄複製失敗,目標目錄 " + descDirNames + " 已存在!");
				return false;
			}
		} else {
			// 建立目標目錄
			logger.debug("目標目錄不存在,準備建立!");
			if (!descDir.mkdirs()) {
				logger.debug("建立目標目錄失敗!");
				return false;
			}

		}

		boolean flag = true;
		// 列出源目錄下的所有檔名和子目錄名
		File[] files = srcDir.listFiles();
		for (int i = 0; i < files.length; i++) {
			// 如果是一個單個檔案,則直接複製
			if (files[i].isFile()) {
				flag = FileUtils.copyFile(files[i].getAbsolutePath(),
						descDirName + files[i].getName());
				// 如果拷貝檔案失敗,則退出迴圈
				if (!flag) {
					break;
				}
			}
			// 如果是子目錄,則繼續複製目錄
			if (files[i].isDirectory()) {
				flag = FileUtils.copyDirectory(files[i]
						.getAbsolutePath(), descDirName + files[i].getName());
				// 如果拷貝目錄失敗,則退出迴圈
				if (!flag) {
					break;
				}
			}
		}

		if (!flag) {
			logger.debug("複製目錄 " + srcDirName + " 到 " + descDirName + " 失敗!");
			return false;
		}
		logger.debug("複製目錄 " + srcDirName + " 到 " + descDirName + " 成功!");
		return true;

	}

	/**
	 * 
	 * 刪除檔案,可以刪除單個檔案或資料夾
	 * 
	 * @param fileName 被刪除的檔名
	 * @return 如果刪除成功,則返回true,否是返回false
	 */
	public static boolean delFile(String fileName) {
 		File file = new File(fileName);
		if (!file.exists()) {
			logger.debug(fileName + " 檔案不存在!");
			return true;
		} else {
			if (file.isFile()) {
				return FileUtils.deleteFile(fileName);
			} else {
				return FileUtils.deleteDirectory(fileName);
			}
		}
	}

	/**
	 * 
	 * 刪除單個檔案
	 * 
	 * @param fileName 被刪除的檔名
	 * @return 如果刪除成功,則返回true,否則返回false
	 */
	public static boolean deleteFile(String fileName) {
		File file = new File(fileName);
		if (file.exists() && file.isFile()) {
			if (file.delete()) {
				logger.debug("刪除檔案 " + fileName + " 成功!");
				return true;
			} else {
				logger.debug("刪除檔案 " + fileName + " 失敗!");
				return false;
			}
		} else {
			logger.debug(fileName + " 檔案不存在!");
			return true;
		}
	}

	/**
	 * 
	 * 刪除目錄及目錄下的檔案
	 * 
	 * @param dirName 被刪除的目錄所在的檔案路徑
	 * @return 如果目錄刪除成功,則返回true,否則返回false
	 */
	public static boolean deleteDirectory(String dirName) {
		String dirNames = dirName;
		if (!dirNames.endsWith(File.separator)) {
			dirNames = dirNames + File.separator;
		}
		File dirFile = new File(dirNames);
		if (!dirFile.exists() || !dirFile.isDirectory()) {
			logger.debug(dirNames + " 目錄不存在!");
			return true;
		}
		boolean flag = true;
		// 列出全部檔案及子目錄
		File[] files = dirFile.listFiles();
		for (int i = 0; i < files.length; i++) {
			// 刪除子檔案
			if (files[i].isFile()) {
				flag = FileUtils.deleteFile(files[i].getAbsolutePath());
				// 如果刪除檔案失敗,則退出迴圈
				if (!flag) {
					break;
				}
			}
			// 刪除子目錄
			else if (files[i].isDirectory()) {
				flag = FileUtils.deleteDirectory(files[i]
						.getAbsolutePath());
				// 如果刪除子目錄失敗,則退出迴圈
				if (!flag) {
					break;
				}
			}
		}

		if (!flag) {
			logger.debug("刪除目錄失敗!");
			return false;
		}
		// 刪除當前目錄
		if (dirFile.delete()) {
			logger.debug("刪除目錄 " + dirName + " 成功!");
			return true;
		} else {
			logger.debug("刪除目錄 " + dirName + " 失敗!");
			return false;
		}

	}

	/**
	 * 建立單個檔案
	 * @param descFileName 檔名,包含路徑
	 * @return 如果建立成功,則返回true,否則返回false
	 */
	public static boolean createFile(String descFileName) {
		File file = new File(descFileName);
		if (file.exists()) {
			logger.debug("檔案 " + descFileName + " 已存在!");
			return false;
		}
		if (descFileName.endsWith(File.separator)) {
			logger.debug(descFileName + " 為目錄,不能建立目錄!");
			return false;
		}
		if (!file.getParentFile().exists()) {
			// 如果檔案所在的目錄不存在,則建立目錄
			if (!file.getParentFile().mkdirs()) {
				logger.debug("建立檔案所在的目錄失敗!");
				return false;
			}
		}

		// 建立檔案
		try {
			if (file.createNewFile()) {
				logger.debug(descFileName + " 檔案建立成功!");
				return true;
			} else {
				logger.debug(descFileName + " 檔案建立失敗!");
				return false;
			}
		} catch (Exception e) {
			e.printStackTrace();
			logger.debug(descFileName + " 檔案建立失敗!");
			return false;
		}

	}

	/**
	 * 建立目錄
	 * @param descDirName 目錄名,包含路徑
	 * @return 如果建立成功,則返回true,否則返回false
	 */
	public static boolean createDirectory(String descDirName) {
		String descDirNames = descDirName;
		if (!descDirNames.endsWith(File.separator)) {
			descDirNames = descDirNames + File.separator;
		}
		File descDir = new File(descDirNames);
		if (descDir.exists()) {
			logger.debug("目錄 " + descDirNames + " 已存在!");
			return false;
		}
		// 建立目錄
		if (descDir.mkdirs()) {
			logger.debug("目錄 " + descDirNames + " 建立成功!");
			return true;
		} else {
			logger.debug("目錄 " + descDirNames + " 建立失敗!");
			return false;
		}

	}

	/**
	 * 寫入檔案
	 * @param file 要寫入的檔案
	 */
	public static void writeToFile(String fileName, String content, boolean append) {
		try {
			FileUtils.write(new File(fileName), content, "utf-8", append);
			logger.debug("檔案 " + fileName + " 寫入成功!");
		} catch (IOException e) {
			logger.debug("檔案 " + fileName + " 寫入失敗! " + e.getMessage());
		}
	}

	/**
	 * 寫入檔案
	 * @param file 要寫入的檔案
	 */
	public static void writeToFile(String fileName, String content, String encoding, boolean append) {
		try {
			FileUtils.write(new File(fileName), content, encoding, append);
			logger.debug("檔案 " + fileName + " 寫入成功!");
		} catch (IOException e) {
			logger.debug("檔案 " + fileName + " 寫入失敗! " + e.getMessage());
		}
	}
	
	/**
	 * 獲目錄下的檔案列表
	 * @param dir 搜尋目錄
	 * @param searchDirs 是否是搜尋目錄
	 * @return 檔案列表
	 */
	public static List<String> findChildrenList(File dir, boolean searchDirs) {
		List<String> files = Lists.newArrayList();
		for (String subFiles : dir.list()) {
			File file = new File(dir + "/" + subFiles);
			if (((searchDirs) && (file.isDirectory())) || ((!searchDirs) && (!file.isDirectory()))) {
				files.add(file.getName());
			}
		}
		return files;
	}
}
複製程式碼
  • public static boolean createFile(String descFileName):建立檔案。
  • public static boolean createDirectory(String descDirName):建立目錄。
  • public static boolean copyFile(String srcFileName, String descFileName):複製檔案。
  • public static boolean copyDirectory(String srcDirName, String descDirName):複製目錄。
  • public static boolean deleteFile(String fileName):刪除檔案。
  • public static boolean deleteDirectory(String dirName):刪除目錄。
  • public static void writeToFile(String fileName, String content, boolean append):向檔案中寫入內容。

03、網路套接字——Socket

雖然網路套接字(Socket)並不在java.io包下,但它和輸入輸出流密切相關。FileSocket是兩組主要的資料傳輸方式。

Socket是描述計算機之間完成相互通訊的一種抽象。可以把 Socket比作為兩個城市之間的交通工具,有了交通工具(高鐵、汽車),就可以在城市之間來回穿梭了。交通工具有多種,每種交通工具也有相應的交通規則。Socket也一樣,也有多種。大部分情況下,我們使用的都是基於TCP/IP的套接字——一種穩定的通訊協議。

Socket 通訊示例

假設主機A是客戶端,主機B是伺服器端。客戶端要與伺服器端通訊,客戶端首先要建立一個Socket例項,作業系統將為這個Socket例項分配一個沒有被使用的本地埠號,並建立一個套接字資料結構,直到這個連線關閉。

示例如下。

Socket socket = new Socket(serverIp, serverPort);
BufferedInputStream in = new BufferedInputStream(socket.getInputStream());
BufferedOutputStream out = new BufferedOutputStream(socket.getOutputStream());
複製程式碼

與之對應的,服務端需要建立一個ServerSocket例項,之後呼叫accept()方法進入阻塞狀態,等待客戶端的請求。當一個新的請求到來時,將為這個連線建立一個新的套接字資料結構。

示例如下。

ServerSocket server = new ServerSocket(port);
Socket socket = server.accept();
InputStream in = new BufferedInputStream(socket.getInputStream());
OutputStream out = new BufferedOutputStream(socket.getOutputStream());
複製程式碼

Socket一旦打通,就可以通過InputStreamOutputStream進行資料傳輸了。

04、壓縮

Java I/O 支援壓縮格式的資料流。在Socket通訊中,我常用GZIPOutputStreamGZIPInputStream來對資料流進行簡單地壓縮和解壓。

壓縮的好處就在於能夠減小網路傳輸中資料的體積。程式碼如下。

package com.cmower.common.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

/**
 * 壓縮解壓
 */
public class CompressionUtil {
	public static byte[] compress(byte[] data) throws IOException {

		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		byte[] result = null;
		GZIPOutputStream zos = new GZIPOutputStream(bos);
		zos.write(data);
		zos.finish();
		zos.flush();
		result = bos.toByteArray();
		zos.close();
		bos.close();
		return result;
	}

	public static byte[] deCompress(byte[] in) throws IOException {

		ByteArrayOutputStream outStream = new ByteArrayOutputStream();
		GZIPInputStream inStream = new GZIPInputStream(new ByteArrayInputStream(in));

		byte[] buf = new byte[1024];
		while (true) {
			try {
				int size = inStream.read(buf);
				if (size <= 0)
					break;
				outStream.write(buf, 0, size);
			} catch (Exception e) {
				e.printStackTrace();
				break;
			}
		}
		inStream.close();
		outStream.close();

		return outStream.toByteArray();
	}
}
複製程式碼

偷偷地告訴你:作者「沉默王二」的微信ID是:qing_gee,趁他沒有小號之前先加一波好友,佔個坑位再說!

相關文章