Web全棧~26.IO
上一期
Java處理文件的常見方法
IO流
在Java中,文件是視爲輸入輸出(Input/Output , IO)設備的一種。Java使用基本統一的概念來處理所有的IO,包括鍵盤、顯示終端、網絡等。這個統一的概念稱之爲流。而流則有輸入流和輸出流之分。
輸入流就是可以從中獲取數據,輸入流的實際提供者可以是鍵盤、文件、網絡等。輸出流就是可以向其中寫入數據,輸出流的實際目的地也可以是顯示終端、文件、網絡等。所以在我們後來學習Socket編程的時候就會經常的使用IO流。
Java的IO的基本類大多位於包java.io中。類InputStream表示輸入流,OutputStream表示輸出流,而FileInputStream表示文件輸入流,FileOutputStream表示文件輸出流。那麼,有了流的概念,就也有了很多面向流的代碼,比如說對流做加密、壓縮、計算信息摘要、以及計算檢驗和等,這些代碼呢接受的參數和返回的結果都是抽象的流,它們構成了一個協作體系,這類似於之前介紹的接口概念、面向接口的編程,以及容器類協作體系。一些實際上不是IO的數據源和目的地也轉換爲了流,以方便參與這種協作,比如字節數組,也包裝爲了流ByteArrayInputStream和ByteArrayOutputStream。
裝飾器設計模式
基本的流按字節讀寫,沒有緩衝區,這不方便使用。Java解決這個問題的方法是使用裝飾器設計模式。在Java中也有很多的裝飾類,有兩個基類:過濾器輸入流FileInputStream和過濾器輸出流FileOutputStream。過濾器其實並沒有改變流的本質,只是在流的基礎上增加了些功能。
BufferedInputStream和BufferedOutputStream對流起緩衝裝飾。
DataInputStream和DataOutput-Stream。可以按8種基本類型和字符串對流進行讀寫
GZIPInputStream、ZipInputStream、GZIPOutput-Stream和ZipOutputStream。可以對流進行壓縮和解壓縮
PrintStream可以將基本類型、對象輸出爲其字符串表示。
Reader/Writer
以InputStream/OutputStream爲基類的流基本都是以二進制形式處理數據的,不能夠方便地處理文本文件,沒有編碼的概念,能夠方便地按字符處理文本數據的基類是Reader和Writer
FileReader和FileWriter讀寫文件。
BufferedReader和BufferedWriter起緩衝裝飾
CharArrayReader和CharArrayWriter將字符數組包裝爲Reader/Writer
StringReader和StringWriter將字符串包裝爲Reader/Writer
InputStreamReader和OutputStreamWriter將InputStream/OutputStream轉換爲Reader/Writer。
PrintWriter將基本類型、對象輸出爲其字符串表示
序列化和反序列化
簡單來說,序列化就是將內存中的Java對象持久保存到一個流中,反序列化就是從流中恢復Java對象到內存。序列化和反序列化主要有兩個用處:一是對象狀態持久化,二是網絡遠程調用,用於傳遞和返回對象。
Java主要通過接口Serializable和類ObjectInputStream/ObjectOutputStream提供對序列化的支持,基本的使用是比較簡單的,但也有一些複雜的地方。不過,Java的默認序列化有一些缺點,比如,序列化後的形式比較大、浪費空間,序列化/反序列化的性能也比較低,更重要的問題是,它是Java特有的技術,不能與其他語言交互。
二進制文件和字節流
InputStream/OutputStream
IO流的基類,抽象類
FileInputStream/FileOutputStream
輸入源和輸出目標是文件的流
ByteArrayInputStream/ByteArrayOutputStream
輸入源和輸出目標是字節數組的流
DataInputStream/DataOutputStream
裝飾類,按基本類型和字符串而非只是字節讀寫流
BufferedInputStream/BufferedOutputStream
裝飾類,對輸入輸出流提供緩衝功能。
InputStream/OutputStream
InputStream
public int read(byte b[]) throws IOException
讀入的字節放入參數數組b中,第一個字節存入b[0],第二個存入b[1],以此類推,一次最多讀入的字節個數爲數組b的長度,但實際讀入的個數可能小於數組長度,返回值爲實際讀入的字節個數。如果剛開始讀取時已到流結尾,則返回-1;否則,只要數組長度大於0,該方法都會盡力至少讀取一個字節,如果流中一個字節都沒有,它會阻塞,異常出現時也是拋出IOException。該方法不是抽象方法,InputStream有一個默認實現,主要就是循環調用讀一個字節的read方法,但子類如FileInputStream往往會提供更爲高效的實現。
批量讀取還有一個更爲通用的重載方法
public int read(byte b[],int off,int len) throws IOException
讀入的第一個字節放入b[off],最多讀取len個字節,read(byte b[])就是調用了該方法。流讀取結束後,應該關閉,以釋放相關資源。不管read方法是否拋出了異常,都應該調用close方法,所以close方法通常應該放在finally語句內。close方法自己可能也會拋出IOException,但通常可以捕獲並忽略。
OutputStream
public abstract void write(int b) throws IOException
向流中寫入一個字節,參數類型雖然是int,但其實只會用到最低的8位。這個方法是抽象方法,具體子類必須實現,FileInputStream會調用本地方法。
public void write(byte b[]) throws IOException
public void write(byte b[],int off,int len) throws IOException
在第二個方法中,第一個寫入的字節是b[off],寫入個數爲len,最後一個是b[off+len-1],第一個方法等同於調用write(b,0,b.length);。OutputStream的默認實現是循環調用單字節的write()方法,子類往往有更爲高效的實現,FileOutpuStream會調用對應的批量寫本地方法。
public void flush() throws IOException
public void close() throws IOException
flush方法將緩衝而未實際寫的數據進行實際寫入,比如,在BufferedOutputStream中,調用flush方法會將其緩衝區的內容寫到其裝飾的流中,並調用該流的flush方法。基類OutputStream沒有緩衝,flush方法代碼爲空。
可能會認爲,調用flush方法會強制確保數據保存到硬盤上,但實際上不是這樣,FileOutputStream沒有緩衝,沒有重寫flush方法,調用flush方法沒有任何效果,數據只是傳遞給了操作系統,但操作系統什麼時候保存到硬盤上,這是不一定的。要確保數據保存到了硬盤上,可以調用FileOutputStream中的特有方法
close方法一般會首先調用flush方法,然後再釋放流佔用的系統資源。同InputStream一樣,close方法一般應該放在finally語句內。
FileInputStream/FileOutputStream
FileOutputStream
public FileOutputStream(File file,boolean append)throws FileNotFoundException
public FileOutputStream(String name) throws FileNotFoundException
File類型的參數file和字符串的類型的參數name都表示文件路徑,路徑可以是絕對路徑,也可以是相對路徑,如果文件已存在,append參數指定是追加還是覆蓋,true表示追加,false表示覆蓋,第二個構造方法沒有append參數,表示覆蓋。new一個FileOutputStream對象會實際打開文件,操作系統會分配相關資源。如果當前用戶沒有寫權限,會拋出異常SecurityException,它是一種RuntimeException。如果指定的文件是一個已存在的目錄,或者由於其他原因不能打開文件,會拋出異常FileNotFoundException,它是IOException的一個子類。
代碼示例
public class Test {
public static void main(String[] args) throws IOException {
OutputStream outputStream = new FileOutputStream("G:/HTML/Java/fileTest/alvin.txt");
try{
String data = "hello world java";
byte[] bytes = data.getBytes(Charset.forName("UTF-8"));
outputStream.write(bytes);
}finally {
outputStream.close();
}
}
}
OutputStream只能以byte或byte數組寫文件,爲了寫字符串,我們調用String的get-Bytes方法得到它的UTF-8編碼的字節數組,再調用write()方法,寫的過程放在try語句內,在finally語句中調用close方法。
FileInputStream
public FileInputStream(String name) throws FileNotFoundException
public FileInputStream(File file) throws FileNotFoundException
參數與FileOutputStream類似,可以是文件路徑或File對象,但必須是一個已存在的文件,不能是目錄。new一個FileInputStream對象也會實際打開文件,操作系統會分配相關資源,如果文件不存在,會拋出異常FileNotFoundException,如果當前用戶沒有讀的權限,會拋出異常SecurityException。
public static void main(String[] args) throws IOException {
InputStream inputStream = new FileInputStream("G:/HTML/Java/fileTest/alvin.txt");
try{
byte[]buf = new byte[1024];
int n = inputStream.read(buf);
String data = new String(buf,0,n,"UTF-8");
System.out.println(data);
}finally {
inputStream.close();
}
}
讀入到的是byte數組,我們使用String的帶編碼參數的構造方法將其轉換爲了String。這段代碼假定一次read調用就讀到了所有內容,且假定字節長度不超過1024。爲了確保讀到所有內容,可以逐個字節讀取直到文件結束
public static void main(String[] args) throws IOException {
InputStream inputStream = new FileInputStream("G:/HTML/Java/fileTest/alvin.txt");
try{
byte[]buf = new byte[1024];
int n = -1;
int index = 0;
while((n = inputStream.read()) != -1){
buf[index++] = (byte)n;
}
}finally {
inputStream.close();
}
}
在沒有緩衝的情況下逐個字節讀取性能很低,可以使用批量讀入且確保讀到結尾
public static void main(String[] args) throws IOException {
InputStream inputStream = new FileInputStream("G:/HTML/Java/fileTest/alvin.txt");
try{
byte[]buf = new byte[1024];
int off = 0;
int n = 0;
while((n = inputStream.read(buf,off,1024-off))!= -1){
off += n;
}
String data = new String(buf,0,off,"UTF-8");
System.out.println(data);
}finally {
inputStream.close();
}
}
ByteArrayInputStream/ByteArrayOutputStream
這裏輸入源和輸出目標都是字節數組
ByteArrayOutputStream
ByteArrayOutputStream的輸出目標是一個byte數組,這個數組的長度是根據數據內容動態擴展的。
public ByteArrayOutputStream()
public ByteArrayOutputStream(int size)
第二個構造方法中的size指定的就是初始的數組大小,如果沒有指定,則長度爲32。在調用write方法的過程中,如果數組大小不夠,會進行擴展,擴展策略同樣是指數擴展,每次至少增加一倍。
ByteArrayOutputStream有如下方法,可以方便地將數據轉換爲字節數組或字符串
public synchronized byte[]toByteArray()
/**
toString()方法使用系統默認編碼。
ByteArrayOutputStream中的數據也可以方便地寫到另一個OutputStream:
**/
public synchronized String toString()
public synchronized String toString(String charsetName)
public synchronized void writeTo(OutputStream out) throws IOException
size方法返回當前寫入的字節個數。reset方法重置字節個數爲0,reset後,可以重用已分配的數組。
public synchronized int size()
public synchronized void reset()
使用ByteArrayOutputStream,我們可以改進前面的讀文件代碼,確保將所有文件內容讀入
public static void main(String[] args) throws IOException {
InputStream inputStream = new FileInputStream("G:/HTML/Java/fileTest/alvin.txt");
try{
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int n = 0;
while((n = inputStream.read(buf)) != -1){
byteArrayOutputStream.write(buf,0,n);
}
String data = byteArrayOutputStream.toString();
System.out.println(data);
}finally {
inputStream.close();
}
}
讀入的數據先寫入ByteArrayOutputStream中,讀完後,再調用其toString方法獲取完整數據。
ByteArrayInputStream
ByteArrayInputStream將byte數組包裝爲一個輸入流,是一種適配器模式
public ByteArrayInputStream(byte buf[])
public ByteArrayInputStream(byte buf[],int offset,int length)
構造方法以buf中offset開始的length個字節爲背後的數據。ByteArrayInput-Stream的所有數據都在內存,支持mark/reset重複讀取。
DataInputStream/DataOutputStream
DataOutputStream
DataOutputStream是裝飾類基類FilterOutputStream的子類,FilterOutputStream是Output-Stream的子類。它接受一個已有的OutputStream,基本上將所有操作都代理給了它。DataOutputStream實現了DataOutput接口,可以以各種基本類型和字符串寫入數據
void writeBoolean(boolean v) throws IOException;
void writeInt(int v) throws IOException;
void writeUTF(String s) throws IOException;
writeBoolean:寫入一個字節,如果值爲true,則寫入1,否則0。
writeInt:寫入4個字節,最高位字節先寫入,最低位最後寫入。
writeUTF:將字符串的UTF-8編碼字節寫入,這個編碼格式與標準的UTF-8編碼略有不同,不過,我們不用關心這個細節。
與FilterOutputStream一樣,DataOutputStream的構造方法也是接受一個已有的Output-Stream
public DataOutputStream(OutputStream out)
代碼案例(寫)
class Student{
private int son;
private String name;
private Double score;
public Student(int son, String name, Double score) {
this.son = son;
this.name = name;
this.score = score;
}
public int getSon() {
return son;
}
public String getName() {
return name;
}
public Double getScore() {
return score;
}
}
public class Test {
public static void main(String[] args) throws IOException {
List<Student> list = new ArrayList<>();
Student A = new Student(1,"alvin",77.5);
Student B = new Student(2,"bob",88.5);
Student C = new Student(3,"clear",90.0);
list.add(A); list.add(B); list.add(C);
DataOutputStream dataOutputStream = new DataOutputStream(new FileOutputStream("G:/HTML/Java/fileTest/alvin.txt"));
try{
dataOutputStream.writeInt(list.size());
for(Student s : list){
dataOutputStream.writeInt(s.getSon());
dataOutputStream.writeUTF(s.getName());
dataOutputStream.writeDouble(s.getSon());
}
}finally {
dataOutputStream.close();
}
}
}
DataInputStream
DataInputStream是裝飾類基類FilterInputStream的子類,FilterInputStream是Input-Stream的子類。DataInputStream實現了DataInput接口,可以以各種基本類型和字符串讀取數據
boolean readBoolean() throws IOException;
int readInt() throws IOException;
String readUTF() throws IOException;
在讀取時,DataInputStream會先按字節讀進來,然後轉換爲對應的類型。
代碼案例(讀)
public static void main(String[] args) throws IOException {
DataInputStream dataInputStream = new DataInputStream(new FileInputStream("G:/HTML/Java/fileTest/alvin.txt"));
try{
int size = dataInputStream.readInt();
List<Student> list = new ArrayList<>();
for(int i = 0; i < size; i++){
Student student = new Student(dataInputStream.readInt(),dataInputStream.readUTF(),dataInputStream.readDouble());
list.add(student);
}
System.out.println(list.toString());
}finally {
dataInputStream.close();
}
}
BufferedInputStream/BufferedOutputStream
FileInputStream/FileOutputStream是沒有緩衝的,按單個字節讀寫時性能比較低,雖然可以按字節數組讀取以提高性能,但有時必須要按字節讀寫,怎麼解決這個問題呢?方法是將文件流包裝到緩衝流中。BufferedInputStream內部有個字節數組作爲緩衝區,讀取時,先從這個緩衝區讀,緩衝區讀完了再調用包裝的流讀
public BufferedInputStream(InputStream in)
public BufferedInputStream(InputStream in,int size)
size表示緩衝區大小,如果沒有,默認值爲8192。除了提高性能,BufferedInputStream也支持mark/reset,可以重複讀取。與BufferedInputStream類似,BufferedOutputStream的構造方法也有兩個,默認的緩衝區大小也是8192,它的flush方法會將緩衝區的內容寫到包裝的流中。在使用FileInputStream/FileOutputStream時,應該幾乎總是在它的外面包上對應的緩衝類