1. 程式人生 > >Java 之路 (二十) -- Java I/O 上(BIO、檔案、資料流、如何選擇I/O流、典型用例)

Java 之路 (二十) -- Java I/O 上(BIO、檔案、資料流、如何選擇I/O流、典型用例)

前言

Java 的 I/O 類庫使用 這個抽象概念,代表任何有能力產出資料的資料來源物件或者是有能力接收資料的接收端物件。 遮蔽了實際的 I/O 裝置中處理資料的細節。

資料流是一串連續不斷的資料的集合,簡單理解的話,我們可以把 Java 資料流當作是 管道里的水流。我們只從一端供水(輸入流),而另一端出水(輸出流)。對輸入端而言,只關心如何寫入資料,一次整體全部輸入還是分段輸入等;對於輸出端而言,只關心如何讀取資料,,無需關心輸入端是如何寫入的。

對於資料流,可以分為兩類:

  1. 位元組流:資料流中最小的資料單元是位元組(二進位制資料)
  2. 字元流:資料流中最小的資料單元是字元(Unicode 編碼,一個字元佔兩個位元組)

1. 概述

對於 Java.io 包核心心其實就是如下幾個類:InputStream、OutputStream、Writer、Reader、File、(RandomAccessFile)。只要熟練掌握這幾個類的使用,那麼 io 部分就掌握的八九不離十了。

對於上面的幾個類,又可以如下分類:

  1. 檔案:File、RandomAccessFile
  2. 位元組流:InputStream、OutputStream
  3. 字元流:Writer、Reader

簡單介紹一下這幾個類:

  1. File:用於描述檔案或者目錄資訊,通常代表的是 檔案路徑 的含義。
  2. RandomAccessFile:隨機訪問檔案
  3. InputStream:位元組流寫入,抽象基類。
  4. OutputStream:位元組流輸出,抽象基類。
  5. Reader:字元流輸入,抽象基類
  6. Writer:字元流輸出,抽象基類

2. 檔案

2.1 File

File - 檔案和目錄路徑名的抽象表示。它既可以指代檔案,也可以代表一個目錄下的一組檔案。當指代檔案集時,可以呼叫 list() 方法,返回一個字元陣列,代表目錄資訊。

下面簡單列舉 File 的使用:

1. 讀取目錄

public class TestFile {
    public static void main(String[] args) {
        File path = new
File("./src/com/whdalive/io"); String[] list; list = path.list(); for (String dirItem : list) { System.out.println(dirItem); } } } /**輸出 TestFile.java */

2. 建立目錄

public class TestFile {
    public static void main(String[] args) {
        File file = new File("D://test1/test2/test3");
        file.mkdirs();

        System.out.println(file.isDirectory());
    }
}

/**輸出
true
*/

需要注意 mkdir() 和 mkdirs() 方法的區別

mkdir() 建立一個資料夾

mkdirs() 建立當前資料夾以及其所有父資料夾

3. 刪除目錄或檔案

public class TestFile {
    public static void main(String[] args) {
        File file = new File("D://test1");
        deleteFolder(file);
    }
    private static void deleteFolder(File folder) {
        File[] files = folder.listFiles();
        if (files!=null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    deleteFolder(file);
                }else {
                    file.delete();
                }
            }
        }
        folder.delete();
    }
}

2.2 RandomAccessFile

RandomAccessFile 是一個完全獨立的類,它和其他 I/O 類別有著本質不同的行為,它適用於記錄由大小已知的記錄組成的檔案,因此可以將記錄從一處轉移到另一處,然後讀取或者修改記錄。

在 java SE 4 中,它的大多數功能由 nio 儲存對映檔案所取代,因此該類實際上用的不多了。

3. 資料流

資料流相關類的派生關係如圖所示,四個基本的類為 InputStream、OutputStream、Writer、Reader,其餘類都是這四個類派生出來的。

3.1 位元組流

3.1.1 InputStream

InputStream 是所有位元組流輸入的抽象基類,作用是用來表示那些從不同資料來源產生輸入的類。這些資料來源包括:

  1. 位元組陣列
  2. String 物件
  3. 檔案
  4. 管道
  5. 一個由其他種類的流組成的序列,以便我們可以將他們收集合併到一個流內
  6. 其他資料來源,比如網路連結等。

每種資料來源都有一個對應的 InputStream 子類,如下:

功能 如何使用
ByteArrayInputStream 允許將記憶體的緩衝區當作 InputStream 使用 作為一種資料來源:將其與 FilterInputStream 物件相連以提供有用介面
StringBufferInputStream(棄用) 將 String 轉換成 InputStream 作為一種資料來源:將其與 FilterInputStream 物件相連以提供有用介面
FileInputStream 用於從檔案讀取資訊 作為一種資料來源:將其與 FilterInputStream 物件相連以提供有用介面
PipedInputStream 產生用於寫入相關 PipedOutputStream 的資料。實現”管道化“概念 作為多執行緒中資料來源:將其與 FilterInputStream 物件相連以提供有用介面
SequenceInputStream 將兩個或多個 InputStream 物件轉換成單一 InputStream 作為一種資料來源:將其與 FilterInputStream 物件相連以提供有用介面
FilterInputStream 抽象類,作為裝飾器介面。其中裝飾器為其他的 InputStream 類提供游泳功能 見↓

FilterInputStream 類的設計採用了裝飾器模式,FilterInputStream 類是所有裝飾器類的基類,為被裝飾的物件提供通用介面,它的子類可以控制特定輸入流,以及修改內部 InputStream 的行為方式:是否緩衝,是否保留讀過的行,是否把單一字元回退輸入流等。

功能 如何使用
DataInputStream 與 DataOutputStream 搭配使用,因此可以按照可移植方式從流讀取基本資料型別 包含用於讀取基本型別資料的全部介面
BufferedInputStream 防止每次讀取時都得進行實際寫操作。代表”使用緩衝區“ 與介面物件搭配
LineNumberInputStream(已棄用) 跟蹤輸入流中的行號 僅增加了行號,因此可能要與介面物件搭配使用
PushbackInputStream 具有”能彈出一個位元組的緩衝區“。因此可以將讀到的最後一個字元回退 通常作為編譯器的掃描器,包含在內是因為 Java 編譯器的需要,我們幾乎不會用到。

3.1.2 OutputStream

該類同樣作為位元組輸出流的抽象基類,其類別決定了輸出所要去往的目標:位元組陣列、檔案或管道。

功能 如何使用
ByteArrayOutputStream 在記憶體中建立緩衝區,所有送往”流“的資料都要放置在此緩衝區 用於指定資料的目的地:將其與 FilterInputStream 物件相連以提供有用介面
FileOutputStream 用於將資訊寫至檔案 用於指定資料的目的地:將其與 FilterInputStream 物件相連以提供有用介面
PipedOutputStream 任何寫入其中的資訊都會自動作為相關 PipedInputStream 的輸出。實現管道化概念 用於指定多執行緒的資料的目的地:將其與 FilterInputStream 物件相連以提供有用介面
FiflterOutputStream 抽象類,作為裝飾器的介面。其中裝飾器為其他 OutputStream 提供有用功能 見↓

同樣的,FilterOutputStream 也是裝飾器模式:

功能 如何使用
DataOutputStream 與 DataInputStream 搭配使用,因此可以按照可移植方式向流寫入基本資料型別 包含用於寫入基本型別資料的全部介面
PrintStream 用於產生格式化輸出。其中 DataOutputStream 處理資料的儲存,PrintStream 處理顯示 可以用 boolean 值顯示是否在每次換行時清空緩衝區。等等
BufferedOutputStream 代表”使用緩衝區“。可以呼叫 flush() 清空緩衝區 與介面物件搭配。

3.1.3 序列化

關於序列化物件的輸入和輸出流:

  1. 物件的輸出流ObjectOutputStream   
  2. 物件的輸入流: ObjectInputStream

使用:

物件的輸出流將指定的物件寫入到檔案的過程,就是將物件序列化的過程,物件的輸入流將指定序列化好的檔案讀出來的過程,就是物件反序列化的過程。既然物件的輸出流將物件寫入到檔案中稱之為物件的序列化,那麼可想而知物件所對應的class必須要實現Serializable介面。

示例:

User.java

public class User implements Serializable{

    private static final long serialVersionUID = 1L;
    String uid;
    String pwd;

    public User(String uid,String pwd) {
        // TODO Auto-generated constructor stub
        this.uid = uid;
        this.pwd = pwd;
    }

    @Override
    public String toString() {
        // TODO Auto-generated method stub
        return "id = " + this.uid + ", pwd = " + this.pwd;
    }
}

解釋一下 serialVersionUID 這個成員變數的作用:

它是用來記錄 class 檔案的版本資訊,是 JVM 通過類的資訊來算出的一個數字,如果我們不顯式指定它,當序列話之後我們把這個 User 類改變了,比如增加一個方法,這時 serialVersionUID 的值也會隨之改變,這樣序列化檔案中記錄的 serialVersionUID 和專案中的不一致,就找不到對應的類來反序列化。

而當我們顯式指定 serialVersionUID 的值後,JVM 就不會再計算這個 class 的 serialVersionUID 了,這樣我們不用擔心序列化後改變原始檔後無法反序列化的問題了。

TestFile.java

public class TestFile {
    static User user ;
    public static void main(String[] args) {
        File file = new File("D://user.txt");
        writeObject(file);
        readObject(file);
    }
    private static void writeObject(File file) {
        user = new User("whdalive", "123...");
        try {
            FileOutputStream fOutputStream = new FileOutputStream(file);
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fOutputStream);

            objectOutputStream.writeObject(user);
            objectOutputStream.close();
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }
    private static void readObject(File file) {
        try {
            FileInputStream fInputStream = new FileInputStream(file);
            ObjectInputStream objectInputStream = new ObjectInputStream(fInputStream);
            User user = (User) objectInputStream.readObject();
            System.out.println("uid = " + user.uid + ", pwd = " + user.pwd);
        } catch (Exception e) {
            // TODO: handle exception
        }
    }
}

結果

//D://User.txt
 sr com.whdalive.io.User        L pwdt Ljava/lang/String;L uidq ~ xpt 123...t whdalive

//輸出結果
uid = whdalive, pwd = 123...

3.2 字元流

這裡由於 InputStream 和 Reader 類似,OutputStream 和 Writer 類似,只不過是面向的資料流不同,InputStream/OutputStream 是位元組流,而 Reader/Writer 是字元流。因此只需要記憶對應關係即可。

位元組流 字元流
InputStream Reader
介面卡:InputStreamReader
OutputStream Writer
介面卡:OutputStreamWriter
FileInputStream FileReader
FileOutputStream FileWriter
StringBufferInputStream(已過時) StringReader
無對應的類 StringWriter
ByteArrayInputStream CharArrayReader
ByteArrayOutputStream CharArrayWriter
PipedInputStream PipedReader
PipedOutputStream PipedWriter

以下是“過濾器”的對應:

過濾器 對應類
FilterInputStream FilterReader
FilterOutputStream FilterWriter
BufferedInputStream BufferedReader
BufferedOutputStream BufferedWriter
DataInputStream 使用DataInputStream(當需要使用 readline() 時,使用 BufferedReader
PirntStream PrintWriter
LineNumberInputStream(已棄用) LineNumberReader
StreamTokenizer StreamTokenizer(使用接收 Reader 的構造器)
PushbackInputStream PushbackReader

4. 如何選擇 I/O 流

  1. 輸入 vs 輸出
    1. 輸入:InputStream、Reader
    2. 輸出:OutputStream、Writer
  2. 位元組(音訊檔案、圖片、歌曲等) vs 字元(涉及到中文文字等)
    1. 位元組:InputStream、OutputStream
    2. 字元:Reader、Writer
  3. 資料來源和去處
    1. 檔案
      1. 讀:FileInputStream、FileReader
      2. 寫:FileOutputStream、FileWriter
    2. 陣列
      1. byte[]:ByteArrayInputStream、ByteArrayOutputStream
      2. char[]:CharArrayReader、CharArrayWriter
    3. String
      1. StringReader、StringWriter
  4. 標準I/O
    1. System.in
    2. System.out
    3. System.err
  5. 格式化輸出
    1. printStream、printWriter

5. 典型使用例項

5.1 標準輸入(鍵盤輸入)顯示到標準輸出(顯示器)

public class TestFile {
    public static void main(String[] args) {
        displayInput();
    }
    private static void displayInput() {
        String ch;
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        try {
            while ((ch =  in.readLine())!= null){
                System.out.println(ch);
            }
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }
}

5.2 將檔案內容列印到顯示器

public class TestFile {
    public static void main(String[] args) {
        displayFile();
    }
    private static void displayFile() {
        File file = new File(".\\src\\com\\whdalive\\io\\User.java");
        String string;
        StringBuilder sb = new StringBuilder();
        BufferedReader bufferedReader;
        try {
            bufferedReader = new BufferedReader(new FileReader(file));
            while((string = bufferedReader.readLine())!=null) {
                sb.append(string + "\n");
            }
            bufferedReader.close();
            System.out.println(sb.toString());
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }
}

5.3 將標準輸入儲存到檔案

public class TestFile {
    public static void main(String[] args) {
        copyScan();
    }
    private static void copyScan() {
        Scanner in = new Scanner(System.in);
        FileWriter out;
        String string;
        try {
            out = new FileWriter("D://log.txt");
            while(!(string = in.nextLine()).equals("Q")) {
                out.write(string + "\n");
            }
            out.flush();
            out.close();
            in.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

總結

關於 Java I/O 本篇還遠遠不是全部,本文只是簡單介紹了 BIO 的內容(即 JDK 1.0 就加入的 java.io 包)。雖然類的擴充套件性很好,但是代價也在此:實現一個輸入輸出,需要使用的類過多。儘管如此,只要分類記憶還是比較容易記住的,多學多用,掌握 Java I/O 不是什麼特別難的問題。

願本文對大家有所幫助。

共勉。