Java 之路 (二十) -- Java I/O 上(BIO、文件、數據流、如何選擇I/O流、典型用例)

前言

Java 的 I/O 類庫使用 這個抽象概念,代表任何有能力產出數據的數據源對象或者是有能力接收數據的接收端對象。 屏蔽了實際的 I/O 設備中處理數據的細節(jié)。

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

對于數據流,可以分為兩類:

  1. 字節(jié)流:數據流中最小的數據單元是字節(jié)(二進制數據)
  2. 字符流:數據流中最小的數據單元是字符(Unicode 編碼,一個字符占兩個字節(jié))

1. 概述

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

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

  1. 文件:File、RandomAccessFile
  2. 字節(jié)流:InputStream、OutputStream
  3. 字符流:Writer、Reader

io 包內還有一些其他的類,涉及安全以及過濾文件描述符等等,這里重點只在 io 的輸入輸出,有興趣可以自行了解:https://docs.oracle.com/javase/9/docs/api/java/io/package-tree.html

簡單介紹一下這幾個類:

  1. File:用于描述文件或者目錄信息,通常代表的是 文件路徑 的含義。
  2. RandomAccessFile:隨機訪問文件
  3. InputStream:字節(jié)流寫入,抽象基類。
  4. OutputStream:字節(jié)流輸出,抽象基類。
  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. 創(chuàng)建目錄

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() 方法的區(qū)別

mkdir() 創(chuàng)建一個文件夾

mkdirs() 創(chuàng)建當前文件夾以及其所有父文件夾

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 字節(jié)流

3.1.1 InputStream

InputStream 是所有字節(jié)流輸入的抽象基類,作用是用來表示那些從不同數據源產生輸入的類。這些數據源包括:

  1. 字節(jié)數組
  2. String 對象
  3. 文件
  4. 管道
  5. 一個由其他種類的流組成的序列,以便我們可以將他們收集合并到一個流內
  6. 其他數據源,比如網絡鏈接等。

每種數據源都有一個對應的 InputStream 子類,如下:

功能 如何使用
ByteArrayInputStream 允許將內存的緩沖區(qū)當作 InputStream 使用 作為一種數據源:將其與 FilterInputStream 對象相連以提供有用接口
StringBufferInputStream(棄用) 將 String 轉換成 InputStream 作為一種數據源:將其與 FilterInputStream 對象相連以提供有用接口
FileInputStream 用于從文件讀取信息 作為一種數據源:將其與 FilterInputStream 對象相連以提供有用接口
PipedInputStream 產生用于寫入相關 PipedOutputStream 的數據。實現”管道化“概念 作為多線程中數據源:將其與 FilterInputStream 對象相連以提供有用接口
SequenceInputStream 將兩個或多個 InputStream 對象轉換成單一 InputStream 作為一種數據源:將其與 FilterInputStream 對象相連以提供有用接口
FilterInputStream 抽象類,作為裝飾器接口。其中裝飾器為其他的 InputStream 類提供游泳功能 見↓

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

功能 如何使用
DataInputStream 與 DataOutputStream 搭配使用,因此可以按照可移植方式從流讀取基本數據類型 包含用于讀取基本類型數據的全部接口
BufferedInputStream 防止每次讀取時都得進行實際寫操作。代表”使用緩沖區(qū)“ 與接口對象搭配
LineNumberInputStream(已棄用) 跟蹤輸入流中的行號 僅增加了行號,因此可能要與接口對象搭配使用
PushbackInputStream 具有”能彈出一個字節(jié)的緩沖區(qū)“。因此可以將讀到的最后一個字符回退 通常作為編譯器的掃描器,包含在內是因為 Java 編譯器的需要,我們幾乎不會用到。

3.1.2 OutputStream

該類同樣作為字節(jié)輸出流的抽象基類,其類別決定了輸出所要去往的目標:字節(jié)數組、文件或管道。

功能 如何使用
ByteArrayOutputStream 在內存中創(chuàng)建緩沖區(qū),所有送往”流“的數據都要放置在此緩沖區(qū) 用于指定數據的目的地:將其與 FilterInputStream 對象相連以提供有用接口
FileOutputStream 用于將信息寫至文件 用于指定數據的目的地:將其與 FilterInputStream 對象相連以提供有用接口
PipedOutputStream 任何寫入其中的信息都會自動作為相關 PipedInputStream 的輸出。實現管道化概念 用于指定多線程的數據的目的地:將其與 FilterInputStream 對象相連以提供有用接口
FiflterOutputStream 抽象類,作為裝飾器的接口。其中裝飾器為其他 OutputStream 提供有用功能 見↓

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

功能 如何使用
DataOutputStream 與 DataInputStream 搭配使用,因此可以按照可移植方式向流寫入基本數據類型 包含用于寫入基本類型數據的全部接口
PrintStream 用于產生格式化輸出。其中 DataOutputStream 處理數據的存儲,PrintStream 處理顯示 可以用 boolean 值顯示是否在每次換行時清空緩沖區(qū)。等等
BufferedOutputStream 代表”使用緩沖區(qū)“。可以調用 flush() 清空緩沖區(qū) 與接口對象搭配。

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 是字節(jié)流,而 Reader/Writer 是字符流。因此只需要記憶對應關系即可。

字節(jié)流 字符流
InputStream Reader<br />適配器:InputStreamReader
OutputStream Writer<br />適配器: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. 字節(jié)(音頻文件、圖片、歌曲等) vs 字符(涉及到中文文本等)
    1. 字節(jié):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 不是什么特別難的問題。

愿本文對大家有所幫助。

共勉。

?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容