Web全棧~26.IO

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時,應該幾乎總是在它的外面包上對應的緩衝類

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