Java學習總結之Java IO系統(二)

本文接着Java學習總結之Java IO系統(一),我們繼續總結Java IO系統的相關知識。

字符流(Writer、Reader)

Java提供了兩個操作字符的字符流基類,分別是Writer和Reader。先來了解兩個用於讀寫文件的字符流FileReader(字符輸入流)和FileWriter(字符輸出流):

FileReader 
FileReader類從InputStreamReader類繼承而來。該類按字符讀取流中數據。可以通過以下幾種構造方法創建需要的對象。
在給定從中讀取數據的 File 的情況下創建一個新 FileReader。

FileReader(File file)

在給定從中讀取數據的 FileDescriptor 的情況下創建一個新 FileReader。

FileReader(FileDescriptor fd) 

在給定從中讀取數據的文件名的情況下創建一個新 FileReader。

FileReader(String fileName) 

創建FIleReader對象成功後,可以參照以下列表裏的方法操作文件。

FileReader.png

 

FileWriter 
FileWriter 類從 OutputStreamWriter 類繼承而來。該類按字符向流中寫入數據。可以通過以下幾種構造方法創建需要的對象。
在給出 File 對象的情況下構造一個 FileWriter 對象。

FileWriter(File file)

在給出 File 對象的情況下構造一個 FileWriter 對象。

 FileWriter(File file, boolean append)

構造與某個文件描述符相關聯的 FileWriter 對象。

FileWriter(FileDescriptor fd)

在給出文件名的情況下構造 FileWriter 對象,它具有指示是否掛起寫入數據的 boolean 值。

FileWriter(String fileName, boolean append)

創建FileWriter對象成功後,可以參照以下列表裏的方法操作文件。

 

FileWriter.png

字符流的操作比字節流操作方便一點,就是可以直接輸出字符串。不在用再像之前那樣進行字節轉換操作了。使用字符流默認情況下依然是覆蓋已有的文件,如果想追加的話,則直接在FileWriter上增加一個可追加的標記即可。

下面的例子演示了FileReader和FileWriter的使用

import java.io.*;
public class FileRead{
   public static void main(String args[])throws IOException{
      File file = new File("Hello1.txt");
      // 創建文件
      file.createNewFile();
      // creates a FileWriter Object
      FileWriter writer = new FileWriter(file); 
      // 向文件寫入內容
      writer.write("This\n is\n an\n example\n"); 
      writer.flush();
      writer.close();
      //創建 FileReader 對象
      FileReader fr = new FileReader(file); 
      char [] a = new char[50];
      fr.read(a); // 從數組中讀取內容
      for(char c : a)
          System.out.print(c); // 一個個打印字符
      fr.close();
   }
}

字節-字符轉換流(OutputStreamWriter、InputStreamReader)

在整個IO包中,實際上就是分爲字節流和字符流,但是除了這兩個流之外,還存在了一組字節流-字符流的轉換類。

InputStreamReader 
InputStreamReader是字節流通向字符流的橋樑,它使用指定的charset讀取字節並將其解碼爲字符。它擁有一個InputStream類型的變量,並繼承了Reader,使用了對象的適配器模式,如圖所示:

InputStreamReader.jpg

 

根據InputStream的實例創建InputStreamReader的方法有4種:
1.根據默認字符集創建
InputStreamReader(InputStream in)
2.使用給定字符集創建
InputStreamReader(InputStream in, Charset cs)
3.使用給定字符集解碼器創建
InputStreamReader(InputStream in, CharsetDecoder dec)
4.使用指定字符集名字符串創建
InputStreamReader(InputStream in, String charsetName)

後面的3個構造函數都制定了一個字符集,最後一個是最簡單的,可以直接指定字符集的名稱來創建,例如UTF-8等。
注意:在對文件進行讀寫操作時,默認使用的是項目的編碼,如果要讀寫其他編碼方式的文件,要在構造輸入輸出流時指定對應的編碼。這一般通過字節-字符轉換流完成。

每次調用InputStreamReader中的一個read()方法都會導致從底層輸入流讀取一個或多個字節。要啓用從字節到字符的有效轉換,可以提前從底層流讀取更多的字節,使其超過滿足當前讀取操作所需的字節。共有3個可用的read()方法:

int read(); //讀取單個字符
int read(char[] cbuf, int offset, int length);
//將字符讀入數組中的某一部分
boolean ready(); //判斷此流是否已經準備好用於讀取

使用字符流的形式讀取字節流的文件,代碼如下:


import java.io.* ;
public class InputStreamReaderDemo01{
    public static void main(String args[]) throws Exception{
        File f = new File("d:" + File.separator + 
        "test.txt") ;   
        Reader reader = new InputStreamReader
        (new FileInputStream(f)) ;  
        // 將字節流變爲字符流
        char c[] = new char[1024] ;
        int len = reader.read(c) ;  // 讀取
        reader.close() ;    // 關閉
        System.out.println(new String(c,0,len)) ;
    }
};

OutputStreamWriter
OutputStreamWriter是字符流通向字節流的橋樑,可使用指定的charset將要寫入流中的字符編碼成字節。因此,它擁有一個OutputStream類型的變量,並繼承了Writer,使用對象的適配器模式,如圖所示:

OutputStreamWriter.jpg

 

根據OutputStream的實例創建OutputStreamWriter的方法有4種:

1.根據默認字符集創建
OutputStreamReader(OutputStream out)

2.使用給定字符集創建
OutputStreamReader(OutputStream out, Charset cs)

3.使用給定字符集解碼器創建
OutputStreamReader(OutputStream out, CharsetDecoder dec)

4.使用指定字符集名字符串創建
OutputStreamReader(OutputStream out, Stroutg charsetName)

後面的3個構造函數都制定了一個字符集,最後一個是最簡單的,可以直接指定字符集的名稱來創建,例如UTF-8等。

每次調用write()方法都會導致在給定字符(或字符集)上調用編碼轉換器。在寫入底層輸出流之前,得到的這些字節將在緩衝區中累積。可以指定此緩衝區的大小,不過,默認的緩衝區對多數用途來說已足夠大。注意,傳遞給write()方法的字符沒有緩衝。共有3個可用的write()方法:
void write(char[] cbuf, int off, int len); //寫入字符數組的某一部分
void write(int c); //寫入單個字符
void write(String str, int off, int len); //寫入字符串的某一部分

例如:將字節的文件輸出流,以字符的形式輸出。代碼如下:

import java.io.* ;
public class OutputStreamWriterDemo01{
    public static void main(String args[]) throws Exception{    
        File f = new File("d:" + File.separator + "test.txt");
        Writer out = new OutputStreamWriter
        (new FileOutputStream(f)) ; // 字節流變爲字符流
        out.write("hello world!!") ;    
           // 使用字符流輸出
        out.close() ;
    }
};

特別說明:OutputStreamWriter是字符流到字節流的橋樑,這不表示OutputStreamWriter接收一個字符流並將其轉換爲字節流,恰恰相反,其接收的OutputStream是一個字節流,而它本身是一個字符流。那爲什麼說它是字符流到字節流的橋樑呢? 
我們以文件操作爲例,之前已經提到,在內存中數據是以字符形式存在的,而在文件中數據是以字節形式保存的。所以在內存中的字符數據需要通過OutputStreamWriter變爲字節流才能保存在文件之中,讀取的時候需要將讀入的字節流通過InputStreamReader變爲字符流
但OutputStreamWriter和InputStreamReader都是字符流,也就是說,OutputStreamWriter以字符輸出流形式操作了字節的輸出流,但實際上還是以字節的形式輸出。而InputStreamReader,雖然以字符輸入流的形式操作,但實際上還是使用的字節流輸入,也就是說,傳輸或者是從文件中讀取數據的時候,文件中真正保存的數據永遠是字節。

change.png

 

輸入流和輸出流要指定相同的字符集才能避免亂碼!

FileWriter和FileReader的說明 
從JDK文檔中可以知道FileOutputStream是OutputStream的直接子類,FileInputStream也是InputStream的直接子類,但是在字符流文件的兩個操作類卻有一些特殊,FileWriter並不直接是Writer的子類,而是轉換流OutputStreamWriter的子類,而FileReader也不直接是Reader的子類,而是轉換流InputStreamReader的子類,那麼從這兩個類的繼承關係就可以清楚的發現,不管是是使用字節流還是字符流實際上最終都是以字節形式操作輸出流的。

緩衝流(BufferedReader和BufferedWriter、BufferedInputStream和BufferedOutputStream)

緩衝流是一系列處理流(包裝流),目的是爲了提高I/O效率,它們爲I/O提供了內存緩衝區,這是一種常見的性能優化,增加緩衝區的兩個目的:
(1)允許Java的I/O一次不只操作一個字符,這樣提高了整個系統的性能
(2)由於有緩衝區,使得在流上執行skip、mark和reset方法都成爲可能。
如果沒有緩衝區,每次調用 read() 或 write()方法都會對文件進行讀或寫字節,在文件和內存之間發生字節和字符的轉換,這是極其低效的。

BufferedReader 
BufferedReader是一個包裝類,是爲了提高讀效率提供的,其可以接收一個Reader,然後用readLine()逐行讀入字符流,直到遇到換行符爲止(相當於反覆調用Reader類對象的read()方法讀入多個字符)。
因此,建議用 BufferedReader 包裝所有其 read() 操作可能開銷很高的 Reader(如 FileReader 和 InputStreamReader),如:

Buffer.png

BufferedReader1.png

BufferedReader2.png

markSupported 判斷該輸入流能支持 mark 和 reset 方法。mark 用於標記當前位置,readlimit 制定可以重新讀取的最大字節數,如果標記後讀取的字節數不超過 readlimit 可以用 reset 回到標誌位置重複讀取。

BufferedWriter 
同理建議用BfferedWriter包裝所有其write()操作可能開銷很高的Writer(如FileWriter和OutputStreamWriter)

BufferedWriter.png

 

BufferedInputStream 
BufferedInputStream用於包裝其他較爲緩慢的InputStream
構造方法摘要

  • BufferedInputStream(InputStream in)
    創建一個使用默認大小輸入緩衝區的緩衝字節輸入流
  • BufferedInputStream(InputStream in, int size)
    創建一個使用指定大小輸入緩衝區的緩衝字節輸入流

方法摘要

  • public int read();
    從該輸入流中讀取一個字節
  • public int read(byte[] b,int off,int len);
    從此字節輸入流中給定偏移量處開始將各字節讀取到指定的 byte 數組中。

BufferedOutputStream 
BufferedOutputStream用於包裝其他較爲緩慢的OutputStream
構造方法摘要

  • BufferedOutputStream(OutputStream out);
    創建一個使用默認大小輸入緩衝區的緩衝字節輸出流
  • BufferedOutputStream(OutputStream out,int size);
    創建一個使用默認大小輸入緩衝區的緩衝字節輸出流

方法摘要

  • public void write(int b);
    向輸出流中輸出一個字節
  • public void write(byte[] b,int off,int len);
    將指定 byte 數組中從偏移量 off 開始的 len 個字節寫入此緩衝的輸出流。
  • public void flush();
    刷新此緩衝的輸出流。這迫使所有緩衝的輸出字節被寫出到底層輸出流中。

其他 
(1)緩衝輸入流BufferedInputSTream除了支持read和skip方法意外,還支持其父類的mark和reset方法;
(2)BufferedReader提供了一種新的ReadLine方法用於讀取一行字符串(以\r或\n分隔);
(3)BufferedWriter提供了一種新的newLine方法用於寫入一個行分隔符;
(4)對於輸出的緩衝流,BufferedWriter和BufferedOutputStream,寫出的數據會先在緩衝區(由緩衝流提供的一個字節數組,是不可見的)中緩存,直到緩衝區滿了會自動寫數據到輸出流,如果緩衝區未滿,可以使用flush方法將會使緩衝區的數據強制寫出。關閉輸出流也會造成緩衝區中殘留的數據被寫出。注意BufferedReader和BufferedInputStream沒有flush方法,因爲flush只用於輸出到文件時。

打印流(PrintStream、PrintWriter)

在整個IO包中,打印流是輸出信息最方便的類,主要包含字節打印流(PrintStream)和字符打印流(PrintWriter)。打印流提供了非常方便的打印功能,可以打印任何的數據類型,例如:小數、整數、字符串等等。
相較OutputStream在輸出時的各種麻煩(比如要將String轉爲byte[]才能輸出)打印流中可以方便地進行輸出。

PrintStream 
1、public PrintStream(File file) throws FileNotFoundException
//構造方法 通過一個File對象實例化PrintStream類

2、public PrintStream(OutputStream out)
//構造方法 接收OutputStream對象,實例化PrintStream類

3、public PrintStream printf(Locale l, String format, Object ...arg)
//普通方法 根據指定的Locale進行格式化輸出

4、public PrintStream printf(String format,Object ... arg)
//普通方法 根據本地環境進行格式化輸出

5、public void print(boolean b)
//普通方法 此方法被重載很多次,輸出任意數據

6、public void println(boolean b)
//普通方法 此方法被重載很多次,輸出任意數據後換行

打印流的好處:在PrintStream中定義的構造方法中可以清楚的發現有一個構造方法可以直接接收OutputStream類的實例,這是因爲與OutputStream相比起來,PrintStream可以更加方便的輸出數據,這就好比將OutputStream重新包裝了一下,使之輸出更加方便。

PrintWriter 
構造方法

//使用指定文件創建不具有自動行刷新的新 PrintWriter
public PrintWriter(File file);

//創建具有指定文件和字符集且不帶自動刷行新的新 PrintWriter
public PrintWriter(File file,String csn);

//根據現有的 OutputStream 創建不帶自動行刷新的新PrintWriter
public PrintWriter(OutputStream out);

//通過現有的 OutputStream 創建新的 PrintWriter(具有自動行刷新)
public PrintWriter(OutputStream out,boolean autoFlush);

//創建具有指定文件名稱且不帶自動行刷新的新PrintWriter
public PrintWriter(String fileName);

//創建具有指定文件名稱和字符集且不帶自動行刷新的PrintWriter
public PrintWriter(String fileName,String csn);

//創建新 PrintWriter(具有自動行刷新)
public PrintWriter(Writer out,boolean autoFlush)

常用方法 
//打印boolean值
public void print(boolean b)
//打印 boolean 值,然後終止該行
public void println(boolean x)

//打印字符
public void print(char c)
//打印字符,然後終止該行
public void println(char x)

//打印字符數組
public void print(char[] s)
//打印字符數組,然後終止該行
public void println(char[] x)

//打印 double 精度浮點數
public void print(double d)
//打印 double 精度浮點數,然後終止該行
public void println(double x)

//打印一個浮點數
public void print(float f)
//打印浮點數,然後終止該行
public void println(float x)

//打印整數
public void print(int i)
//打印整數,然後終止該行
public void println(int x)

//打印 long 整數
public void print(long l)
//打印 long 整數,然後終止該行
public void println(long x)

//打印對象
public void print(Object obj)
//打印 Object,然後終止該行
public void println(Object x)

//打印字符串。如果參數爲 null,則打印字符串 "null"
public void print(String s)
//打印 String,然後終止該行
public void println(String x)

//通過寫入行分隔符字符串終止當前行
public void println()

//使用指定格式字符串和參數將一個格式化字符串寫入此 writer 中。如果啓用自動刷新,則調用此方法將刷新輸出緩衝區
public PrintWriter format(Locale l,String format,Object... args)

//使用指定格式字符串和參數將一個格式化字符串寫入此 writer 中。如果啓用自動刷新,則調用此方法將刷新輸出緩衝區
public PrintWriter format(String format,Object... args)

//將指定字符添加到此 writer
public PrintWriter append(char c)
//將指定的字符序列添加到此 writer
public PrintWriter append(CharSequence csq)
//將指定字符序列的子序列添加到此 writer
public PrintWriter append(CharSequence csq,int start,int end)

//寫入字符數組
public void write(char[] buf)
//寫入字符數組的某一部分
public void write(char[] buf,int off,int len)
//寫入單個字符
public void write(int c)
//寫入字符串
public void write(String s)
//寫入字符串的某一部分
public void write(String s,int off,int len)

提示:由於BufferedWriter沒有PrintWriter使用靈活,所以在實際的操作中,我們往往會使用PrintWriter/BufferedReader這種組合。

內存操作流

之前的程序中,輸出輸入都是在內存和文件之間進行的,當然,輸入輸出也可以不訪問文件,只在內存中進行。也就是把數據的輸入源和輸出目的地從文件改成了byte數組、char數組或字符串。

字節數組流(ByteArrayInputStream、ByteArrayOutputStream)

ByteArrayInputStream的主要功能是完成將byte數組的內容寫入到內存之中,而ByteArrayOutputStream的主要功能是將內存中的數據輸出到byte數組。

test.png

 

ByteArrayInputStream 
字節數組輸入流從內存中的一個字節數組讀取字節到內存,這個字節數組就是數據的輸入源。創建字節數組輸入流對象有以下幾種方式。
接收字節數組作爲參數創建:

ByteArrayInputStream bArray = 
new ByteArrayInputStream(byte [] b);

另一種創建方式是接收一個字節數組,和兩個整型變量 off、len,off表示第一個讀取的字節,len表示讀取字節的長度,即將字節數組中從off開始的len個字節讀入該輸入流

ByteArrayInputStream bArray = new
ByteArrayInputStream(byte []b,int off,int len)

成功創建字節數組輸入流對象後,可以參見以下列表中的方法,對流進行讀操作或其他操作。

ByteArrayInputStream.png

 

ByteArrayOutputStream 
字節數組輸出流在內存中創建一個字節數組緩衝區,所有發送到輸出流的數據保存在該字節數組緩衝區中,可以用toByteArray()得到該字節數組,也可以用toString()得到緩衝區內容轉換得到的字符串。創建字節數組輸出流對象有以下幾種方式。
下面的構造方法創建一個32字節(默認大小)的緩衝區。

OutputStream bOut = new ByteArrayOutputStream();

另一個構造方法創建一個大小爲n字節的緩衝區。

OutputStream bOut = new ByteArrayOutputStream(int n)

成功創建字節數組輸出流對象後,可以參見以下列表中的方法,對流進行寫操作或其他操作。

ByteArrayOutputStream.png

 

下面的例子演示了ByteArrayInputStream 和 ByteArrayOutputStream的使用:


import java.io.* ;
public class ByteArrayDemo01{
    public static void main(String args[]){
        String str = "HELLOWORLD" ;     // 定義一個字符串,全部由大寫字母組成
        ByteArrayInputStream bis = null ;   // 內存輸入流
        ByteArrayOutputStream bos = null ;  // 內存輸出流
        bis = new ByteArrayInputStream(str.getBytes()) ;    // 向內存中輸入內容
        bos = new ByteArrayOutputStream() ; // 準備從內存ByteArrayInputStream中讀取內容
        int temp = 0 ;
        while((temp=bis.read())!=-1){
            char c = (char) temp ;  // 讀取的數字變爲字符
            bos.write(Character.toLowerCase(c)) ;   // 將字符變爲小寫
        }
        // 所有的數據就全部都在ByteArrayOutputStream中
        String newStr = bos.toString() ;    // 取出內容
        try{
            bis.close() ;
            bos.close() ;
        }catch(IOException e){
            e.printStackTrace() ;
        }
        System.out.println(newStr) ;
    }
}

字符數據流(CharArrayReader、CharArrayWriter)

CharArrayReader、CharArrayWriter和ByteArrayInputStream、ByteArrayOutputStream類似,只不過後者是字節數組輸入流,而前者是字符數組輸入流。
CharArrayReader 
構造方法摘要

  • CharArrayReader(char buf[]);
    使用傳入的buf構造CharArrayReader
  • CharArrayReader(char buf[], int offset, int length);
    使用傳入的buf的一部分構造CharArrayReader

方法摘要

  • void close(); 關閉此流
  • void mark(int readAheadLimit); 標記當前流讀取的位置
  • void markSupport(); 檢測此流是否支持標記
  • int read(); 讀取一個字符、並以整數形式返回
  • int read(char[] b, int off, int len); 將buf中len個字符讀取到下標從off開始的b中、返回讀取的字符個數
  • boolean ready(); 查看CharArrayReader是否可讀。
  • void reset(); 將此流開始位置重置到最後一次調用mark是流的讀取位置
  • long skip(long n); 丟棄buf中n個字符、返回實際丟棄的字符個數

CharArrayWriter 
構造方法摘要

  • public CharArrayWriter()
    使用默認的buf大小創建CharArrayWriter。
  • public CharArrayWriter(int initialSize)
    使用指定的buf大小創建CharArrayWriter。

方法摘要

  • CharArrayWriter append(CharSequence csq)
    將一串有序字符序列寫入buf中
  • CharArrayWriter append(CharSequence csq, int start, int end)
    將一串有序字符序列的一部分寫入buf中
  • CharArrayWriter append(char c) 將一個字符寫入buf中
  • void close() 關閉此流(沒有效果,因爲不訪問文件)
  • void flush() flush此流(沒有效果,因爲不訪問文件)
  • void reset() 清空buf、重頭開始
  • int size() 查看當前buf中字符總數
  • char[] toCharArray() 將buf中內容轉換成char[]
  • String toString() 將buf中字符轉換成String返回
  • void write(int c) 寫入一個字符。
  • void write(char c[], int off, int len)
    將一個char[]的一部分寫入buf中、若buf滿、擴容。
  • void write(String str, int off, int len)
    將一個字符串寫入buf中、滿自動擴容
  • void writeTo(Writer out)
    將buf中現有的字節寫入到另一個輸出字符流out中

例子:

 public static void main(String[] args) throws IOException {
        String str = "Hello world!";

        // 構建字符輸入流
        CharArrayReader reader = new CharArrayReader(str.toCharArray());

        // 從字符輸入流讀取字符
        char[] chars = new char[1024];
        int len = reader.read(chars);
        System.out.println(new String(chars, 0, len));
    }
        //構建字符輸出流
    CharArrayWriter writer = new CharArrayWriter(1024 * 1024);

        // 將字符串寫入到CharArrayWriter
        String msg = "hello world!!!22121";
        writer.write(msg.toCharArray());

        System.out.println(writer.toString());

        writer.close();

字符串流(StringReader、StringWriter)

字符串流和字符數據流基本一樣,只是把char[]數組換成了String,在此不贅述。

合併流(SequenceInputStream、SequenceOutputStream)

SequenceInputStream 
有些情況下,當我們需要從多個輸入流中向程序讀入數據。此時,可以使用合併流,將多個輸入流合併成一個SequenceInputStream流對象。
SequenceInputStream會將與之相連接的流集組合成一個輸入流並從第一個輸入流開始讀取,直到到達文件末尾,接着從第二個輸入流讀取,依次類推,直到到達包含的最後一個輸入流的文件末 尾爲止。 合併流的作用是將多個源合併合一個源。

合併流.png

 

構造方法
public SequenceInputStream(InputStream s1,InputStream s2)
使用兩個輸入流對象實例化本類對象。

示例:


import java.io.File ;
import java.io.SequenceInputStream ;
import java.io.FileInputStream ;
import java.io.InputStream ;
import java.io.FileOutputStream ;
import java.io.OutputStream ;
public class SequenceDemo{
    public static void main(String args[]) throws Exception {   
    // 所有異常拋出
        InputStream is1 = null ;        // 輸入流1
        InputStream is2 = null ;        // 輸入流1
        OutputStream os = null ;        // 輸出流
        SequenceInputStream sis = null ;    // 合併流
        is1 = new FileInputStream("d:" + File.separator + "a.txt");
        is2 = new FileInputStream("d:" + File.separator + "b.txt");
        os = new FileOutputStream("d:" + File.separator + "ab.txt");
        sis = new SequenceInputStream(is1,is2) ;
        // 實例化合並流
        int temp = 0 ;  // 接收內容
        while((temp=sis.read())!=-1){   // 循環輸出
            os.write(temp) ;    // 保存內容
        }
        sis.close() ;   // 關閉合並流
        is1.close() ;   // 關閉輸入流1`
        is2.close() ;   // 關閉輸入流2
        os.close() ;    // 關閉輸出流
    }
};

SequenceOutputStream 
同SequenceInputStream,區別在於合併的是兩個OutputStream,在此不贅述。

數據操作流(DataInputStream、DataOutputStream)

DataInputStream 
數據輸入流允許應用程序以與機器無關方式從底層輸入流中讀取Java 8種基本數據類型,方法命名爲readXxx。
下面的構造方法用來創建數據輸入流對象。

DataInputStream dis = new DataInputStream(InputStream in);

另一種創建方式是接收一個字節數組,和兩個整形變量 off、len,off表示第一個讀取的字節,len表示讀取字節的長度。

DataInputStream dis = new DataInputStream(byte[] a,int off,int len);

DataInputStream.png

DataOutputStream 
數據輸出流允許應用程序以與機器無關方式將Java 8種基本數據類型寫到底層輸出流,方法命名爲writeXxx。
下面的構造方法用來創建數據輸出流對象。

DataOutputStream out = new DataOutputStream(OutputStream  out);

創建對象成功後,可以參照以下列表給出的方法,對流進行寫操作或者其他操作。

DataOutputStream.png

 

下面的例子演示了DataInputStream和DataOutputStream的使用,該例從文本文件test.txt中讀取5行,並轉換成大寫字母,最後保存在另一個文件test1.txt中。

import java.io.*;

public class Test{
   public static void main(String args[])throws IOException{

      DataInputStream d = new DataInputStream(new
                               FileInputStream("test.txt"));

      DataOutputStream out = new DataOutputStream(new
                               FileOutputStream("test1.txt"));

      String count;
      while((count = d.readLine()) != null){
          String u = count.toUpperCase();
          System.out.println(u);
          out.writeBytes(u + "  ,");
      }
      d.close();
      out.close();
   }
}

對象流(ObjectInputStream、ObjectOutputStream)

序列化與反序列化

Java序列化是指把Java對象轉換爲字節序列的過程,而Java反序列化是指把字節序列恢復爲Java對象的過程。

 

序列化:對象序列化的最主要的用處就是在傳遞和保存對象的時候,保證對象的完整性和可傳遞性。序列化是把對象轉換成有序字節流,以便在網絡上傳輸或者保存在本地文件中。序列化後的字節流保存了Java對象的狀態以及相關的描述信息。序列化機制的核心作用就是對象狀態的保存與重建

反序列化:客戶端從文件中或網絡上獲得序列化後的對象字節流後,根據字節流中所保存的對象狀態及描述信息,通過反序列化重建對象。

實現序列化的前提

只有實現了 Serializable 或 Externalizable 接口的類的對象才能被序列化,否則拋出異常!

注意:Serializable接口和Cloneable接口一樣是一個標記接口,即沒有任何方法的接口。但只有一個類實現了Serializable接口,它才能被序列化爲二進制流進行傳輸,否則會拋出NotSerializableException異常。一個類如果實現了Serializable接口,其子類自動實現序列化,不需要顯式實現 Serializable 接口。

Serializable 和 Externalizable 接口的關係和區別
① Externalizable接口是Serializable接口的子接口,Serializable是一個空的標記接口,而Externalizable接口中定義了兩個方法,如下:

public interface Externalizable extends Serializable { 
    public void writeExternal(ObjectOutput out) throws IOException; 
    public void readExternal(ObjectInput in) 
                        throws IOException, ClassNotFoundException; 
} 

② 若實現Serializble接口,可選擇性實現 readObject(ObjectInputStream in) 和 writeObject(ObjectOutputSteam out),如果實現了就參與自定義序列化和反序列化過程,未實現就參與默認序列化和反序列化過程;若實現Externalizable接口則必須實現readExternal(ObjectInput in) 和 writeExternal(ObjectOutput out) 方法。

③ 若實現Externalizable接口,反序列化時會調用被序列化類的無參構造器去創建一個新的對象,然後再將被保存對象的字段的值分別填充到新對象中。所以此時被序列化類必須定義一個無參構造器。

④ 比較而言,Externalizable更爲高效, 但Serializable更加靈活,其最重要的特色就是可以自動序列化,因此使用廣泛。所以一般只有在對效率要求較高的情況下才會考慮Externalizable,但通常情況下Serializable使用的更多。

實現Java對象序列化與反序列化的方法

假定一個User類,它的對象需要序列化,可以有如下三種方法:

若 User 類僅僅實現了 Serializable 接口,則可以按照以下方式進行序列化和反序列化:

ObjectOutputStream 採用默認的序列化方式,對 User 對象的非 transient 的實例變量進行序列化。
ObjcetInputStream 採用默認的反序列化方式,對對 User 對象的非 transient 的實例變量進行反序列化。

若User類僅僅實現了Serializable接口,並且還定義了 readObject(ObjectInputStream in) 和writeObject(ObjectOutputSteam out),則採用以下方式進行序列化與反序列化:

ObjectOutputStream 調用 User 對象的 writeObject(ObjectOutputStream out) 的方法進行自定義序列化。
ObjectInputStream 會調用 User 對象的 readObject(ObjectInputStream in) 的方法進行自定義反序列化。

若User類實現了 Externalnalizable 接口,且 User 類必須實現 readExternal(ObjectInput in) 和 writeExternal(ObjectOutput out) 方法,則按照以下方式進行序列化與反序列化:

ObjectOutputStream 調用 User 對象的 writeExternal(ObjectOutput out)) 的方法進行序列化。
ObjectInputStream 會調用User對象的 readExternal(ObjectInput in) 的方法進行反序列化。

定義一個可被序列化的類:

public class User implements Serializable{
    private String username ;    
    private String password;
        private String gender;      
    public User(String username,String password,String gender){ 
        this.username = username;
                this.password = password;
                this.gender = gender;
    }
    public String toString(){
        return "用戶名:" + this.username + ";密碼:" 
            + this.password + ";性別:" + this.gender ;
    }
}

以後此類的對象就可以被序列化了。變爲二進制byte流。

serialVersionUID

在對象進行序列化或反序列化操作的時候,要考慮類版本的問題,如果序列化後修改了該類,反序列化就則就有可能造成異常。所以在序列化操作中引入了一個serialVersionUID的常量,可以通過此常量來驗證版本的一致性,在進行反序列化時,JVM會把傳來的字節流中的serialVersionUID與本地相應實體(類)的serialVersionUID進行比較,如果相同就認爲是一致的,可以進行反序列化,否則就會出現版本不一致的異常。
更多見:Java對象序列化爲什麼要使用SerialversionUID

ObjectOutputStream(序列化)

步驟一:創建一個對象輸出流,它可以包裝一個其它類型的目標輸出流,如文件輸出流:

ObjectOutputStream oos = 
              new ObjectOutputStream(new FileOutputStream("D:\\object.out"));

步驟二:通過對象輸出流的writeObject()方法寫對象:

oos.writeObject(new User("xuliugen", "123456", "male"));

ObjectInputStream(反序列化)

步驟一:創建一個對象輸入流,它可以包裝一個其它類型輸入流,如文件輸入流:

ObjectInputStream ois= new ObjectInputStream(new FileInputStream("object.out"));

步驟二:通過對象輸出流的readObject()方法讀取對象:

User user = (User) ois.readObject();

序列化與反序列化示例

爲了更好地理解Java序列化與反序列化,舉一個簡單的示例如下:

public class SerialDemo {

    public static void main(String[] args) throws IOException, 
                                                  ClassNotFoundException {
        //序列化
        FileOutputStream fos = new FileOutputStream("object.out");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        User user1 = new User("Steven1997", "123456", "male");
        oos.writeObject(user1);
        oos.flush();
        oos.close();
        
        //反序列化
        FileInputStream fis = new FileInputStream("object.out");
        ObjectInputStream ois = new ObjectInputStream(fis);
        User user2 = (User) ois.readObject();
        System.out.println(user2);
        //反序列化的輸出結果爲:用戶名:Steven1997;密碼:123456;性別:male
    }
}

public class User implements Serializable{
    private String username ;    
    private String password;
        private String gender;      
    public User(String username,String password,String gender){ 
        this.username = username;
                this.password = password;
                this.gender = gender;
    }
    public String toString(){
        return "用戶名:" + this.username + ";密碼:" 
                          + this.password + ";性別:" + this.gender ;
    }
}

序列化只針對對象的非靜態變量,對於 static 、transient 修飾的變量和成員方法不進行序列化。
當一個對象的實例變量引用其他對象,序列化該對象時也把引用對象進行序列化。

transient關鍵字

當使用Serializable接口實現序列化操作時,如果一個對象中的某個屬性不希望被JVM默認序列化的話,則可以使用transient關鍵字進行聲明。如果用transient聲明一個實例變量,當對象存儲時,它的值不需要維持,而會保持默認值。換句話來說就是,用transient關鍵字標記的成員變量不參與JVM的默認序列化過程

更多序列化知識見:
Java序列化心得(一):序列化設計和默認序列化格式的問題 
Java序列化心得(二):自定義序列化 
Java IO操作——對象序列化(Serializable接口、ObjectOutputStream、以及與Externalizable接口的用法和區別)

拓展:ArrayList 中存儲數據的數組是用 transient 修飾的,因爲這個數組是動態擴展的,並不是所有的空間都被使用,因此就不需要所有的內容都被序列化。通過重寫序列化和反序列化方法,使得可以只序列化數組中有內容的那部分數據。

private transient Object[] elementData;

壓縮流

在日常的使用中經常會使用到像WinRAR或WinZIP這樣的壓縮文件,通過這些軟件可以把一個很大的文件進行壓縮以方便傳輸。
在JAVA中 爲了減少傳輸時的數據量也提供了專門的壓縮流,可以將文件或文件夾壓縮成ZIP、JAR、GZIP等文件的格式。
具體見大牛博客:
http://blog.csdn.net/u013087513/article/details/52151227

管道流(PipedOutputStream、PipedInputStream)

管道流的作用是可以進行兩個線程間的通訊,分爲管道輸出流(PipedOutputStream)、管道輸入流(PipedInputStream)。如果要想進行管道輸出,則必須把輸出流連在輸入流之上,在PipedOutputStream中有一個方法用於連接管道:
public void connect(PipedInputStream snk) throws IOException

管道流.png

例子如下:


import java.io.* ;
class Send implements Runnable{         // 線程類
    private PipedOutputStream pos = null ;  // 管道輸出流
    public Send(){
        this.pos = new PipedOutputStream() ;    // 實例化輸出流
    }
    public void run(){
        String str = "Hello World!!!" ; // 要輸出的內容
        try{
            this.pos.write(str.getBytes()) ;
        }catch(IOException e){
            e.printStackTrace() ;
        }
        try{
            this.pos.close() ;
        }catch(IOException e){
            e.printStackTrace() ;
        }
    }
    public PipedOutputStream getPos(){  // 得到此線程的管道輸出流
        return this.pos ;   
    }
}


class Receive implements Runnable{
    private PipedInputStream pis = null ;   // 管道輸入流
    public Receive(){
        this.pis = new PipedInputStream() ; // 實例化輸入流
    }
    public void run(){
        byte b[] = new byte[1024] ; // 接收內容
        int len = 0 ;
        try{
            len = this.pis.read(b) ;    // 讀取內容
        }catch(IOException e){
            e.printStackTrace() ;
        }
        try{
            this.pis.close() ;  // 關閉
        }catch(IOException e){
            e.printStackTrace() ;
        }
        System.out.println("接收的內容爲:" + new String(b,0,len)) ;
    }
    public PipedInputStream getPis(){
        return this.pis ;
    }
}


public class PipedDemo{
    public static void main(String args[]){
        Send s = new Send() ;
        Receive r = new Receive() ;
        try{
            s.getPos().connect(r.getPis()) ;    // 連接管道
        }catch(IOException e){
            e.printStackTrace() ;
        }
        new Thread(s).start() ; // 啓動線程
        new Thread(r).start() ; // 啓動線程
    }
}

類似地,還有管道字符流 PipedReader 和 PipedWriter,用法基本相同,在此不贅述。

回退流(PushbackInputStream和PushbackReader)

在Java IO中所有的數據都是採用順序的讀取方式,即對於一個輸入流來講都是採用從頭到尾的順序讀取的,如果在輸入流中某個不需要的內容被讀取進來,則只能通過程序將這些不需要的內容處理掉,爲了解決這樣的處理問題,在Java中提供了一種回退輸入流(PushbackInputStream、PushbackReader),可以把讀取進來的某些數據重新回退到輸入流的緩衝區之中。

回退流.png

回退流分爲字節回退流和字符回退流,我們以字節回退流PushbackInputStream爲例。

PushbackInputStream.png

對於回退操作來說,提供了三個unread()的操作方法,這三個操作方法與InputStream類中的read()方法是一一對應的。

例子如下,內存中使用ByteArrayInputStream,把內容設置到內存之中:


import java.io.ByteArrayInputStream ;
import java.io.PushbackInputStream ;
public class PushInputStreamDemo{
   public static void main(String args[]) throws Exception {   // 所有異常拋出
       String str = "www.baidu.com" ;      // 定義字符串
       PushbackInputStream push = null ;       // 定義回退流對象
       ByteArrayInputStream bai = null ;       // 定義內存輸入流
       bai = new ByteArrayInputStream(str.getBytes()) ;    // 實例化內存輸入流
       push = new PushbackInputStream(bai) ;   // 從內存中讀取數據
       System.out.print("讀取之後的數據爲:") ;
       int temp = 0 ; 
       while((temp=push.read())!=-1){  // 讀取內容
           if(temp=='.'){  // 判斷是否讀取到了“.”
               push.unread(temp) ; // 放回到緩衝區之中
               temp = push.read() ;    // 再讀一遍
               System.out.print("(退回"+(char)temp+")") ;
           }else{
               System.out.print((char)temp) ;  // 輸出內容
           }
       }
   }
};

選擇合適的IO流

1.首先,明確IO流中有兩個主要的體系,即 InputStream、OutputStream和Reader、Writer。其次,明確數據的來源和數據將要到達的目的地。

2.明確將要操作的數據是否是純文本數據。如果數據源是純文本數據選Reader;數據源不是純文本數據選擇InputStream。如果數據目的地是純文本數據就選擇Writer;如果不是則選擇OutputStream。

3.明確具體的設備。即數據源是從哪個設備來的:是硬盤就加File;是鍵盤用System.in(是一個InputStream對象);是內存用數組;是網絡用Socket流。同樣目的是哪個設備:是硬盤就加File;是鍵盤用System.out(是一個PrintStream對象);是內存用數組;是網絡用Socket流。

4.明確是否還需要其他額外功能呢,例如:
①是否需要較高的效率,即是否需要使用緩衝區,是就加上Buffered;
②是否需要轉換,是就使用轉換流,InputStreamReader 和OutputStreamWriter。

例子:

1.jpg

 

2.jpg

System類對IO的支持(out、err、in)

System類的常量
System表示系統類,實際上在Java中也對IO給予了一定的支持
1、public static final PrintStream out
//常量 對應系統標準輸出,一般是顯示器
2、public static final PrintStream err
//常量 錯誤信息輸出
3、public static final InputStream in
//常量 對應標準輸出,一般是鍵盤

使用static final關鍵字聲明的變量是全局常量,只要是常量,則所有的單詞字母必須全部大寫,按照現在的標準:
System.OUT —> System.out

System.out

使用System.out輸出的時候就是將輸出的位置定義在了顯示器之中。FileOutputStream是定位在文件裏,而System.out是定位在屏幕上輸出。PrintStream就是OutputStream的子類。


import java.io.OutputStream ;
import java.io.IOException ;
public class SystemDemo01{
    public static void main(String args[]){
        OutputStream out = System.out ;// 此時的輸出流是向屏幕上輸出
        try{
            out.write("hello world!!!".getBytes()) ;    // 向屏幕上輸出
        }catch(IOException e){
            e.printStackTrace() ;   // 打印異常
        }
        try{
            out.close() ;   // 關閉輸出流
        }catch(IOException e){
            e.printStackTrace() ;
        }
    }
}

System1.png

System.err

System.err 表示的是錯誤的標準輸出,如果程序中出現了錯誤的話,則直接使用System.err進行輸出即可。程序如下:


public class SystemDemo02{
    public static void main(String args[]){
        String str = "hello" ;      // 聲明一個非數字的字符串
        try{
            System.out.println(Integer.parseInt(str)) ; // 轉型
        }catch(Exception e){
            System.err.println(e) ;
        }
    }
};

System2.png

使用System.out輸出錯誤如下:


public class SystemDemo03{
    public static void main(String args[]){
        String str = "hello" ;      // 聲明一個非數字的字符串
        try{
            System.out.println(Integer.parseInt(str)) ; // 轉型
        }catch(Exception e){
            System.out.println(e) ;
        }
    }
};

System3.png

System.out 和System.err 的區別:System.out和System.err都是PrintStream的實例化對象,而且通過代碼可以發現,兩者都可以輸出錯誤信息,但是一般來講System.out是將信息顯示給用戶看,是正常的信息顯示,而System.err的正好相反是不希望用戶看到的,會直接在後臺打印,是專門顯示錯誤的。
一般來講,如果要輸出錯誤信息的時候最好不要使用System.out而是直接使用System.err 這一點只能從其概念上劃分。

System4.png

 

System.in

System.in實際上是一個鍵盤的輸入流,其本身是InputStream類型的對象。那麼,此時就可以利用此方式完成從鍵盤讀取數據的功能。
InputStream對應的是輸入流,輸入流的話肯定是從指定位置讀取的,之前使用的是FileInputStream,是從文件中讀取的。


import java.io.InputStream ;
public class SystemDemo04{
    public static void main(String args[]) throws Exception {   // 所有異常拋出
        InputStream input = System.in ; // 從鍵盤接收數據
        byte b[] = new byte[1024] ; // 開闢空間,接收數據
        System.out.print("請輸入內容:") ;    // 提示信息
        int len = input.read(b) ;   // 接收數據
        System.out.println("輸入的內容爲:" + new String(b,0,len)) ;
        input.close() ; // 關閉輸入流
    }
};

但是以上的操作存在如下問題:
問題一:指定了輸入數據的長度,如果現在輸入的數據超過了長度範圍,只能輸入部分的數據。
問題二:如果byte數組是奇數的話,則還可能出現中文亂碼的情況,因爲一個字符是兩個字節。

可以通過標誌位的方式避免指定byte數組大小來解決。實例如下:


import java.io.InputStream ;
public class SystemDemo05{
    public static void main(String args[]) throws Exception {   // 所有異常拋出
        InputStream input = System.in ; // 從鍵盤接收數據
        StringBuffer buf = new StringBuffer() ; // 使用StringBuffer接收數據
        System.out.print("請輸入內容:") ;    // 提示信息
        int temp = 0 ;      // 接收內容
        while((temp=input.read())!=-1){
            char c = (char) temp ;  // 將數據變爲字符
            if(c=='\n'){    // 退出循環,輸入回車表示輸入完成
                break ;
            }
            buf.append(c) ; // 保存內容
        }
        System.out.println("輸入的內容爲:" + buf) ;
        input.close() ; // 關閉輸入流
    }
}

但這種方法讀取中文還是會亂碼,這是因爲每讀取一個字節就將其轉爲字符,字母和數字都是佔1個字節 可以正常顯示。但是如果是中文的話,就相當於每讀取到一個字節就是半個字符就進行轉化,所以導致亂碼的錯誤。
最好的輸入方式是將全部輸入的數據暫時存放在一塊內存之上,之後一次性的從內存中讀取數據,這樣所有數據就整體只讀了一次,則不會造成亂碼,而且也不會受到長度的限制。

System5.png

上述功能可以通過BufferedReader實現。

輸入輸出重定向

從之前的操作中知道System.out、System.err、System.in三個常量的作用,但是通過System類也可以改變System.in的輸入流來源,以及System.out和System.err兩個輸出流的輸出位置。
1、public static void setOut(PrintStream out)
//普通方法 重定向標準輸出流
2、public static void setErr(PrintStream err)
//普通方法 重定向標準錯誤輸出流
3、public static void setIn(InputStream in)
//普通方法 重定向標準輸入流

爲System.out輸出重定向


import java.io.File ;
import java.io.FileOutputStream ;
import java.io.PrintStream ;
public class SystemDemo06{
    public static void main(String args[]) throws Exception {
        System.setOut(
            new PrintStream(
                new FileOutputStream("d:" + 
                    File.separator + "red.txt"))) ; // System.out輸出重定向
        System.out.print("hello") ; // 輸出時,不再向屏幕上輸出
        System.out.println(",world") ;
    }
};

System.out是希望用戶看得到信息,一旦有錯誤,最好保存起來。


import java.io.File ;
import java.io.FileOutputStream ;
import java.io.PrintStream ;
public class SystemDemo07{
    public static void main(String args[]){
        String str = "hello" ;      // 聲明一個非數字的字符串
        try{
            System.out.println(Integer.parseInt(str)) ; // 轉型
        }catch(Exception e){
            try{
                System.setOut(
                    new PrintStream(
                        new FileOutputStream("d:"
                            + File.separator + "err.log"))) ;   // 輸出重定向
            }catch(Exception e1){
            
            }
            System.out.println(e) ;
        }
    }
};

通過此操作就可以完成錯誤的重定向,保存錯誤日誌。

爲System.err重定向

利用System.err向屏幕上輸出信息,此時,爲了方便起見,使用內存操作流。


import java.io.ByteArrayOutputStream ;
import java.io.PrintStream ;
public class SystemDemo08{
    public static void main(String args[]) throws Exception{    // 所有異常拋出
        ByteArrayOutputStream bos = null ;      // 聲明內存輸出流
        bos = new ByteArrayOutputStream() ;     // 實例化
        System.setErr(new PrintStream(bos)) ;   // 輸出重定向
        System.err.print("hello") ; // 錯誤輸出,不再向屏幕上輸出
        System.err.println("world") ;           // 向內存中輸出
        System.out.println(bos) ;   // 輸出內存中的數據
    }
};

一般不建議去修改System.err的輸出位置,因爲這樣的信息都不太希望用戶可以看見。

爲System.in重定向

默認情況下System.in是指鍵盤輸入,也可以通過setIn()方法,將其輸入流的位置改變,例如,現在從文件中讀取。


import java.io.FileInputStream ;
import java.io.InputStream ;
import java.io.File ;
public class SystemDemo09{
    public static void main(String args[]) throws Exception{    // 所有異常拋出
        System.setIn(new FileInputStream("d:"
            + File.separator + "demo.txt")) ;   // 設置輸入重定向
        InputStream input = System.in ; // 從文件中接收數據
        byte b[] = new byte[1024]   ;// 開闢空間,接收數據
        int len = input.read(b) ;   //接收
        System.out.println("輸入的內容爲:" + new String(b,0,len)) ;
        input.close() ; // 關閉輸入流
    }
};

總結

三個常量的使用:

  • System.out是希望用戶可以看見的信息。用IDE(Eclipse)的話錯誤信息使用黑顏色顯示的。
  • System.err 是不希望用戶可以看見的信息。則在IDE中將以紅色的文字顯示錯誤信息。
  • System.in 對應鍵盤輸入。
  • 以上三個常量的輸入、輸出都可以重定向,但是一般建議只修改setOut的重定向。
  • System.in讀取的時候會出現中文亂碼的問題,則可以通過BufferedReader完成讀取功能。
  • 小編推薦一個學Java的學習社區【 戳我立即加入 】,無論你是大牛還是小白,是想轉行還是想入行都可以來了解一起進步一起學習!裙內有開發工具,很多幹貨和技術資料分享!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章