1. 程式人生 > >面試必備:詳解Java I/O流,掌握這些就可以說精通了?

面試必備:詳解Java I/O流,掌握這些就可以說精通了?

@TOC

Java IO概述

IO就是輸入/輸出。Java IO類庫基於抽象基礎類InputStream和OutputStream構建了一套I/O體系,主要解決從資料來源讀入資料和將資料寫入到目的地問題。我們把資料來源和目的地可以理解為IO流的兩端。當然,通常情況下,這兩端可能是檔案或者網路連線。

我們用下面的圖描述下,加深理解:

從一種資料來源中通過InputStream流物件讀入資料到程式記憶體中

在這裡插入圖片描述

當然我們把上面的圖再反向流程,就是OutputStream的示意了。

在這裡插入圖片描述

其實除了面向位元組流的InputStream/OutputStream體系外,Java IO類庫還提供了面向字元流的Reader/Writer體系。Reader/Writer繼承結構主要是為了國際化,因為它能更好地處理16位的Unicode字元。

在學習是這兩套IO流處理體系可以對比參照著學習,因為有好多相似之處。

要理解總體設計

剛開始寫IO程式碼,總被各種IO流類搞得暈頭轉向。這麼多IO相關的類,各種方法,啥時候能記住。

其實只要我們掌握了IO類庫的總體設計思路,理解了它的層次脈絡之後,就很清晰。知道啥時候用哪些流物件去組合想要的功能就好了,API的話,可以查手冊的嘛。

首先從流的流向上可以分為輸入流InputStream或Reader,輸出流OutputStream或Writer。任何從InputStream或Reader派生而來的類都有read()基本方法,讀取單個位元組或位元組陣列;任何從OutputSteam或Writer派生的類都含有write()的基本方法,用於寫單個位元組或位元組陣列。

從操作位元組還是操作字元的角度,有面向位元組流的類,基本都以XxxStream結尾,面向字元流的類都以XxxReader或XxxWriter結尾。當然這兩種型別的流是可以轉化的,有兩個轉化流的類,這個後面會說到。

一般在使用IO流的時候會有下面類似程式碼:

1 FileInputStream inputStream = new FileInputStream(new File("a.txt"));
2 BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);

這裡其實是一種裝飾器模式的使用,IO流體系中使用了裝飾器模式包裝了各種功能流類。不瞭解裝飾器模式的看下這篇【詳解設計模式】-裝飾者模式

在Java IO流體系中FilterInputStream/FilterOutStreamFilterReader/FilterWriter就是裝飾器模式的介面類,從該類向下包裝了一些功能流類。有DataInputStream、BufferedInputStream、LineNumberInputStream、PushbackInputStream等,當然還有輸出的功能流類;面向字元的功能流類等。

下面幾張圖描述了整個IO流的繼承體系結構

InputStream流體系

在這裡插入圖片描述

OutputStream流體系

在這裡插入圖片描述

Reader體系

在這裡插入圖片描述

Writer體系

在這裡插入圖片描述

最後再附加一張表加深印象:

在這裡插入圖片描述

File其實是個工具類

File類其實不止是代表一個檔案,它也能代表一個目錄下的一組檔案(代表一個檔案路徑)。下面我們盤點一下File類中最常用到的一些方法

 1File.delete() 刪除檔案或資料夾目錄。
2File.createNewFile() 建立一個新的空檔案。
3File.mkdir() 建立一個新的空資料夾。
4File.list() 獲取指定目錄下的檔案和資料夾名稱。
5File.listFiles() 獲取指定目錄下的檔案和資料夾物件。
6File.exists() 檔案或者資料夾是否存在
7
8String   getAbsolutePath()   // 獲取絕對路徑
9long   getFreeSpace()       // 返回分割槽中未分配的位元組數。
10String   getName()         // 返回檔案或資料夾的名稱。
11String   getParent()         // 返回父目錄的路徑名字串;如果沒有指定父目錄,則返回 null。
12File   getParentFile()      // 返回父目錄File物件
13String   getPath()         // 返回路徑名字串。
14long   getTotalSpace()      // 返回此檔案分割槽大小。
15long   getUsableSpace()    //返回佔用位元組數。
16int   hashCode()             //檔案雜湊碼。
17long   lastModified()       // 返回檔案最後一次被修改的時間。
18long   length()          // 獲取長度,位元組數。
19boolean canRead()  //判斷是否可讀
20boolean canWrite()  //判斷是否可寫
21boolean isHidden()  //判斷是否隱藏
22
23
24// 成員函式
25static File[]    listRoots()    // 列出可用的檔案系統根。
26boolean    renameTo(File dest)    // 重新命名
27boolean    setExecutable(boolean executable)    // 設定執行許可權。
28boolean    setExecutable(boolean executable, boolean ownerOnly)    // 設定其他所有使用者的執行許可權。
29boolean    setLastModified(long time)       // 設定最後一次修改時間。
30boolean    setReadable(boolean readable)    // 設定讀許可權。
31boolean    setReadable(boolean readable, boolean ownerOnly)    // 設定其他所有使用者的讀許可權。
32boolean    setWritable(boolean writable)    // 設定寫許可權。
33boolean    setWritable(boolean writable, boolean ownerOnly)    // 設定所有使用者的寫許可權。
34

需要注意的是,不同系統對檔案路徑的分割符表是不一樣的,比如Windows中是“\”,Linux是“/”。而File類給我們提供了抽象的表示File.separator,遮蔽了系統層的差異。因此平時在程式碼中不要使用諸如“\”這種代表路徑,可能造成Linux平臺下程式碼執行錯誤。

下面是一些示例:

根據傳入的規則,遍歷得到目錄中所有的檔案構成的File物件陣列

 1public class Directory {
2    public static File[] getLocalFiles(File dir, final String regex){
3        return dir.listFiles(new FilenameFilter() {
4            private Pattern pattern = Pattern.compile(regex);
5            public boolean accept(File dir, String name) {
6                return pattern.matcher(new File(name).getName()).matches();
7            }
8        });
9    }
10
11    // 過載方法
12    public static File[] getLocalFiles(String path, final String regex){
13        return getLocalFiles(new File(path),regex);
14    }
15
16    public static void main(String[] args) {
17        String dir = "d:";
18        File[] files = Directory.getLocalFiles(dir,".*\\.txt");
19        for(File file : files){
20            System.out.println(file.getAbsolutePath());
21        }
22    }
23}

輸出結果:

1d:\\1.txt
2d:\\新建文字文件.txt

上面的程式碼中dir.listFiles(FilenameFilter ) 是策略模式的一種實現,而且使用了匿名內部類的方式。

【詳解設計模式】-策略模式

Java內部類超詳細總結(含程式碼示例)

上面的例子是《Java 程式設計思想》中的示例,這本書中的每個程式碼示例都很經典,Bruce Eckel大神把面向物件的思想應用的爐火純青,非常值得細品。

InputStream和OutputStream

InputStream是輸入流,前面已經說到,它是從資料來源物件將資料讀入程式內容時,使用的流物件。通過看InputStream的原始碼知道,它是一個抽象類,

1public abstract class InputStream  extends Object  implements Closeable
2

提供了一些基礎的輸入流方法:

 1//從資料中讀入一個位元組,並返回該位元組,遇到流的結尾時返回-1
2abstract int read() 
3
4//讀入一個位元組陣列,並返回實際讀入的位元組數,最多讀入b.length個位元組,遇到流結尾時返回-1
5int read(byte[] b)
6
7// 讀入一個位元組陣列,返回實際讀入的位元組數或者在碰到結尾時返回-1.
8//b:代表資料讀入的陣列, off:代表第一個讀入的位元組應該被放置的位置在b中的偏移量,len:讀入位元組的最大數量
9int read(byte[],int off,int len)
10
11// 返回當前可以讀入的位元組數量,如果是從網路連線中讀入,這個方法要慎用,
12int available() 
13
14//在輸入流中跳過n個位元組,返回實際跳過的位元組數
15long skip(long n)
16
17//標記輸入流中當前的位置
18void mark(int readlimit) 
19
20//判斷流是否支援打標記,支援返回true
21boolean markSupported() 
22
23// 返回最後一個標記,隨後對read的呼叫將重新讀入這些位元組。
24void reset() 
25
26//關閉輸入流,這個很重要,流使用完一定要關閉
27void close()
28

直接從InputStream繼承的流,可以發現,基本上對應了每種資料來源型別。

功能
ByteArrayInputStream 將位元組陣列作為InputStream
StringBufferInputStream 將String轉成InputStream
FileInputStream 從檔案中讀取內容
PipedInputStream 產生用於寫入相關PipedOutputStream的資料。實現管道化
SequenceInputStream 將兩個或多個InputStream物件轉換成單一的InputStream
FilterInputStream 抽象類,主要是作為“裝飾器”的介面類,實現其他的功能流

OutputStream是輸出流的抽象,它是將程式記憶體中的資料寫入到目的地(也就是接收資料的一端)。看下類的簽名:

1public abstract class OutputStream implements Closeable, Flushable {}

提供了基礎方法相比輸入流來說簡單多了,主要就是write寫方法(幾種過載的方法)、flush沖刷和close關閉。

 1// 寫出一個位元組的資料
2abstract void write(int n)
3
4// 寫出位元組到資料b
5void write(byte[] b)
6
7// 寫出位元組到陣列b,off:代表第一個寫出位元組在b中的偏移量,len:寫出位元組的最大數量
8void write(byte[] b, int off, int len)
9
10//沖刷輸出流,也就是將所有緩衝的資料傳送到目的地
11void flush()
12
13// 關閉輸出流
14void close()
15

同樣地,OutputStream也提供了一些基礎流的實現,這些實現也可以和特定的目的地(接收端)對應起來,比如輸出到位元組陣列或者是輸出到檔案/管道等。

功能
ByteArrayOutputStream 在記憶體中建立一個緩衝區,所有送往“流”的資料都要放在此緩衝區
FileOutputStream 將資料寫入檔案
PipedOutputStream 和PipedInputStream配合使用。實現管道化
FilterOutputStream 抽象類,主要是作為“裝飾器”的介面類,實現其他的功能流

使用裝飾器包裝有用的流

Java IO 流體系使用了裝飾器模式來給哪些基礎的輸入/輸出流新增額外的功能。這寫額外的功能可能是:可以將流緩衝起來提高效能、是流能夠讀寫基本資料型別等。

這些通過裝飾器模式新增功能的流型別都是從FilterInputStream和FilterOutputStream抽象類擴充套件而來的。可以再返回文章最開始說到IO流體系的層次時,那幾種圖加深下印象。

FilterInputStream型別

功能
DataInputStream 和DataOutputStream搭配使用,使得流可以讀取int char long等基本資料型別
BufferedInputStream 使用緩衝區,主要是提高效能
LineNumberInputStream 跟蹤輸入流中的行號,可以使用getLineNumber、setLineNumber(int)
PushbackInputStream 使得流能彈出“一個位元組的緩衝區”,可以將讀到的最後一個字元回退

FilterOutStream型別

功能
DataOutputStream 和DataInputStream搭配使用,使得流可以寫入int char long等基本資料型別
PrintStream 用於產生格式化的輸出
BufferedOutputStream 使用緩衝區,可以呼叫flush()清空緩衝區

大多數情況下,其實我們在使用流的時候都是輸入流和輸出流搭配使用的。目的就是為了轉移和儲存資料,單獨的read()對我們而言有啥用呢,讀出來一個位元組能幹啥?對吧。因此要理解流的使用就是搭配起來或者使用功能流組合起來去轉移或者儲存資料。

Reader和Writer

Reader是Java IO中所有Reader的基類。ReaderInputStream類似,不同點在於,Reader基於字元而非基於位元組。

Writer是Java IO中所有Writer的基類。與ReaderInputStream的關係類似,Writer基於字元而非基於位元組,Writer用於寫入文字,OutputStream用於寫入位元組。

ReaderWriter的基礎功能類,可以對比InputStreamOutputStream來學習。

面向位元組 面向字元
InputStream Reader
OutputStream Writer
FileInputStream FileReader
FileOutputStream FileWriter
ByteArrayInputStream CharArrayReader
ByteArrayOutputStream CharArrayWriter
PipedInputStream PipedReader
PipedOutputStream PipedWriter
StringBufferInputStream(已棄用) StringReader
無對應類 StringWriter

有兩個“介面卡” 流型別,它們可以將位元組流轉化成位元組流。這就是InputStreamReader 可以將InputStream轉成為Reader,OutputStreamWriter可以將OutputStream轉成為Writer。

介面卡類,位元組流轉字元流

在這裡插入圖片描述

當然也有類似位元組流的裝飾器實現方式,給字元流新增額外的功能或這說是行為。這些功能字元流類主要有:

  • BufferedReader
  • BufferedWriter
  • PrintWriter
  • LineNumberReader
  • PushbackReader

System類中的I/O流

想想你的第一個Java程式是啥?我沒猜錯的話,應該是 hello world。

1System.out.println("hello world")

簡單到令人髮指,今天就說說標準的輸入/輸出流。

在標準IO模型中,Java提供了System.in、System.out和System.error。

先說System.in,看下原始碼

1public final static InputStream in

是一個靜態域,未被包裝過的InputStream。通常我們會使用BufferedReader進行包裝然後一行一行地讀取輸入,這裡就要用到前面說的介面卡流InputStreamReader

1public class SystemInReader {
2    public static void main(String[] args)