面試必備:詳解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 CloseableFlushable {}

提供了基礎方法相比輸入流來說簡單多了,主要就是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) throws IOException {
3        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
4        String s;
5        while ((s = reader.readLine()) != null && s.length() != 0){
6            System.out.println(s);
7        }
8    }
9}

該程式等待會一直等待我們輸入,輸入啥,後面會接著輸出。輸入空字元串可以結束。

11
21
3123
4123

System.out是一個PrintStream流。System.out一般會把你寫到其中的數據輸出到控制台上。System.out通常僅用在類似命令行工具的控制台程式上。System.out也經常用於列印程式的調試資訊(儘管它可能並不是獲取程式調試資訊的最佳方式)。

System.err是一個PrintStream流。System.err與System.out的運行方式類似,但它更多的是用於列印錯誤文本。

可以將這些系統流重定向

儘管System.in, System.out, System.err這3個流是java.lang.System類中的靜態成員,並且已經預先在JVM啟動的時候初始化完成,你依然可以更改它們。

可以使用setIn(InputStream)、setOut(PrintStream)、setErr(PrintStream)進行重定向。比如可以將控制台的輸出重定向到文件中。

1OutputStream output = new FileOutputStream("d:/system.out.txt");
2PrintStream printOut = new PrintStream(output);
3System.setOut(printOut);

壓縮(ZIP文檔)

Java IO類庫是支援讀寫壓縮格式的數據流的。我們可以把一個或一批文件壓縮成一個zip文檔。這些壓縮相關的流類是按位元組處理的。先看下設計壓縮解壓縮的相關流類。

壓縮類 功能
CheckedInputStream getCheckSum()可以為任何InputStream產生校驗和(不僅是解壓縮)
CheckedOutputStream getCheckSum()可以為任何OutputStream產生校驗和(不僅是壓縮)
DeflaterOutputStream 壓縮類的基類
ZipOutputStream 繼承自DeflaterOutputStream,將數據壓縮成Zip文件格式
GZIPOutputStream 繼承自DeflaterOutputStream,將數據壓縮成GZIP文件格式
InflaterInputStream 解壓縮類的基類
ZipInputStream 繼承自InflaterInputStream,解壓縮Zip文件格式的數據
GZIPInputStream 繼承自InflaterInputStream,解壓縮GZIP文件格式的數據

表格中CheckedInputStream 和 CheckedOutputStream 一般會和Zip壓縮解壓過程配合使用,主要是為了保證我們壓縮和解壓過程數據包的正確性,得到的是中間沒有被篡改過的數據。

我們以CheckedInputStream 為例,它的構造器需要傳入一個Checksum類型:

1    public CheckedInputStream(InputStream in, Checksum cksum) {
2        super(in);
3        this.cksum = cksum;
4    }

而Checksum 是一個介面,可以看到這裡又用到了策略模式,具體的校驗演算法是可以選擇的。Java類庫給我提供了兩種校驗和演算法:Adler32 和 CRC32,性能方面可能Adler32 會更好一些,不過CRC32可能更準確。各有優劣吧。

好了,接下來看下壓縮/解壓縮流的具體使用。

將多個文件壓縮成zip包

 1public class ZipFileUtils {
2    public static void compressFiles(File[] files, String zipPath) throws IOException {
3
4        // 定義文件輸出流,表明是要壓縮成zip文件的
5        FileOutputStream f = new FileOutputStream(zipPath);
6
7        // 給輸出流增加校驗功能
8        CheckedOutputStream checkedOs = new CheckedOutputStream(f,new Adler32());
9
10        // 定義zip格式的輸出流,這裡要明白一直在使用裝飾器模式在給流添加功能
11        // ZipOutputStream 也是從FilterOutputStream 繼承下來的
12        ZipOutputStream zipOut = new ZipOutputStream(checkedOs);
13
14        // 增加緩衝功能,提高性能
15        BufferedOutputStream buffOut = new BufferedOutputStream(zipOut);
16
17        //對於壓縮輸出流我們可以設置個注釋
18        zipOut.setComment("zip test");
19
20        // 下面就是從Files[] 數組中讀入一批文件,然後寫入zip包的過程
21        for (File file : files){
22
23            // 建立讀取文件的緩衝流,同樣是裝飾器模式使用BufferedReader
24            // 包裝了FileReader
25            BufferedReader bfReadr = new BufferedReader(new FileReader(file));
26
27            // 一個文件對象在zip流中用一個ZipEntry表示,使用putNextEntry添加到zip流中
28            zipOut.putNextEntry(new ZipEntry(file.getName()));
29
30            int c;
31            while ((c = bfReadr.read()) != -1){
32                buffOut.write(c);
33            }
34
35            // 注意這裡要關閉
36            bfReadr.close();
37            buffOut.flush();
38        }
39        buffOut.close();
40    }
41
42    public static void main(String[] args) throws IOException {
43        String dir = "d:";
44        String zipPath = "d:/test.zip";
45        File[] files = Directory.getLocalFiles(dir,".*\.txt");
46        ZipFileUtils.compressFiles(files, zipPath);
47    }
48}

在main函數中我們使用了本文中 File其實是個工具類 章節里的Directory工具類。

解壓縮zip包到目標文件夾

 1    public static void unConpressZip(String zipPath, String destPath) throws IOException {
2        if(!destPath.endsWith(File.separator)){
3            destPath = destPath + File.separator;
4            File file = new File(destPath);
5            if(!file.exists()){
6                file.mkdirs();
7            }
8        }
9        // 新建文件輸入流類,
10        FileInputStream fis = new FileInputStream(zipPath);
11
12        // 給輸入流增加檢驗功能
13        CheckedInputStream checkedIns = new CheckedInputStream(fis,new Adler32());
14
15        // 新建zip輸出流,因為讀取的zip格式的文件嘛
16        ZipInputStream zipIn = new ZipInputStream(checkedIns);
17
18        // 增加緩衝流功能,提高性能
19        BufferedInputStream buffIn = new BufferedInputStream(zipIn);
20
21        // 從zip輸入流中讀入每個ZipEntry對象
22        ZipEntry zipEntry;
23        while ((zipEntry = zipIn.getNextEntry()) != null){
24            System.out.println("解壓中" + zipEntry);
25
26            // 將解壓的文件寫入到目標文件夾下
27            int size;
28            byte[] buffer = new byte[1024];
29            FileOutputStream fos = new FileOutputStream(destPath + zipEntry.getName());
30            BufferedOutputStream bos = new BufferedOutputStream(fos, buffer.length);
31            while ((size = buffIn.read(buffer, 0, buffer.length)) != -1) {
32                bos.write(buffer, 0, size);
33            }
34            bos.flush();
35            bos.close();
36        }
37        buffIn.close();
38
39        // 輸出校驗和
40        System.out.println("校驗和:" + checkedIns.getChecksum().getValue());
41    }
42
43    // 在main函數中直接調用
44    public static void main(String[] args) throws IOException {
45        String dir = "d:";
46        String zipPath = "d:/test.zip";
47//        File[] files = Directory.getLocalFiles(dir,".*\.txt");
48//        ZipFileUtils.compressFiles(files, zipPath);
49
50        ZipFileUtils.unConpressZip(zipPath,"F:/ziptest");
51    }

這裡解壓zip包還有一種更加簡便的方法,使用ZipFile對象。該對象的entries()方法直接返回ZipEntry類型的枚舉。看下程式碼片段:

1        ZipFile zipFile = new ZipFile("test.zip");
2        Enumeration e = zipFile.entries();
3        while (e.hasMoreElements()){
4            ZipEntry zipEntry = (ZipEntry) e.nextElement();
5            System.out.println("file:" + zipEntry);
6        }

對象序列化

什麼是序列化和反序列化呢?

序列化就是將對象轉成位元組序列的過程,反序列化就是將位元組序列重組成對象的過程。

在這裡插入圖片描述
在這裡插入圖片描述

為什麼要有對象序列化機制

程式中的對象,其實是存在有記憶體中,當我們JVM關閉時,無論如何它都不會繼續存在了。那有沒有一種機制能讓對象具有「持久性」呢?序列化機制提供了一種方法,你可以將對象序列化的位元組流輸入到文件保存在磁碟上。

序列化機制的另外一種意義便是我們可以通過網路傳輸對象了,Java中的 遠程方法調用(RMI),底層就需要序列化機制的保證。

在Java中怎麼實現序列化和反序列化

首先要序列化的對象必須實現一個Serializable介面(這是一個標識介面,不包括任何方法)

1public interface Serializable {
2}

其次需要是用兩個對象流類:ObjectInputStream 和ObjectOutputStream主要使用ObjectInputStream對象的readObject方法讀入對象、ObjectOutputStream的writeObject方法寫入對象到流中

下面我們通過序列化機制將一個簡單的pojo對象寫入到文件,並再次讀入到程式記憶體。

 1public class User implements Serializable {
2    private String name;
3    private int age;
4
5    public User(String name, int age) {
6        this.name = name;
7        this.age = age;
8    }
9
10    @Override
11    public String toString() {
12        return "User{" +
13                "name='" + name + ''' +
14                ", age='
" + age + ''' +
15                '}';
16    }
17
18    public static void main(String[] args) throws IOException, ClassNotFoundException {
19        User user = new User("
二營長",18);
20        ObjectOutputStream objectOps = new ObjectOutputStream(new FileOutputStream("
f:/user.out"));
21        objectOps.writeObject(user);
22        objectOps.close();
23
24        // 再從文件中取出對象
25        ObjectInputStream objectIns = new ObjectInputStream(new FileInputStream("
f:/user.out"));
26
27        // 這裡要做一次強轉
28        User user1 = (User) objectIns.readObject();
29        System.out.println(user1);
30        objectIns.close();
31    }
32}
33

程式運行結果:

1User{name='二營長', age='18'}

不想序列化的數據使用transient(瞬時)關鍵字屏蔽

如果我們上面的user對象有一個password欄位,屬於敏感資訊,這種是不能走序列化的方式的,但是實現了Serializable 介面的對象會自動序列化所有的數據域,怎麼辦呢?在password欄位上加上關鍵字transient就好了。

1 private transient String password;

序列化機制就簡單介紹到這裡吧。這是Java原生的序列化,現在市面上有好多序列化協議可以選擇,比如Json、FastJson、Thrift、Hessian 、protobuf等

I/O流的典型使用方式

IO流種類繁多,可以通過不同的方式組合I/O流類,但平時我們常用的也就幾種組合。下盤通過示例的方式盤點幾種I/O流的典型用法。

緩衝輸入文件

 1public class BufferedInutFile {
2    public static String readFile(String fileName) throws IOException {
3        BufferedReader bf = new BufferedReader(new FileReader(fileName));
4        String s;
5
6        // 這裡讀取的內容存在了StringBuilder,當然也可以做其他處理
7        StringBuilder sb = new StringBuilder();
8        while ((s = bf.readLine()) != null){
9            sb.append(s + "n");
10        }
11        bf.close();
12        return sb.toString();
13    }
14
15    public static void main(String[] args) throws IOException {
16        System.out.println(BufferedInutFile.readFile("d:/1.txt"));
17    }
18}

格式化記憶體輸入

要讀取格式化的數據,可以使用DataInputStream。

 1public class FormattedMemoryInput {
2    public static void main(String[] args) throws IOException {
3        try {
4            DataInputStream dataIns = new DataInputStream(
5                    new ByteArrayInputStream(BufferedInutFile.readFile("f:/FormattedMemoryInput.java").getBytes()));
6            while (true){
7                System.out.print((char) dataIns.readByte());
8            }
9        } catch (EOFException e) {
10            System.err.println("End of stream");
11        }
12    }
13}

上面程式會在控制台輸出當前類本身的所有程式碼,並且會拋出一個EOFException異常。拋出異常的原因是已經到留的結尾了還在讀數據。這裡可以使用available()做判斷還有多少可以的字元。

 1package com.herp.pattern.strategy;
2
3import java.io.ByteArrayInputStream;
4import java.io.DataInputStream;
5import java.io.IOException;
6
7public class FormattedMemoryInput {
8    public static void main(String[] args) throws IOException {
9        DataInputStream dataIns = new DataInputStream(
10                new ByteArrayInputStream(BufferedInutFile.readFile("FormattedMemoryInput.java").getBytes()));
11        while (true){
12            System.out.println((char) dataIns.readByte());
13        }
14    }
15}

基本的文件輸出

FileWriter對象可以向文件寫入數據。首先創建一個FileWriter和指定的文件關聯,然後使用BufferedWriter將其包裝提供緩衝功能,為了提供格式化機制,它又被裝飾成為PrintWriter

 1public class BasicFileOutput {
2    static String file = "BasicFileOutput.out";
3
4    public static void main(String[] args) throws IOException {
5        BufferedReader in = new BufferedReader(new StringReader(BufferedInutFile.readFile("f:/BasicFileOutput.java")));
6        PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file)));
7
8        int lineCount = 1;
9        String s;
10        while ((s = in.readLine()) != null){
11            out.println(lineCount ++ + ": " + s);
12        }
13        out.close();
14        in.close();
15    }
16}

下面是我們寫出的BasicFileOutput.out文件,可以看到我們通過程式碼位元組加上了行號

 11package com.herp.pattern.strategy;
22
33import java.io.*;
44
55public class BasicFileOutput {
66:     static String file = "BasicFileOutput.out";
77
88:     public static void main(String[] args) throws IOException {
99:         BufferedReader in = new BufferedReader(new StringReader(BufferedInutFile.readFile("f:/BasicFileOutput")));
1010:         PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file)));
1111
1212:         int lineCount = 1;
1313:         String s;
1414:         while ((s = in.readLine()) != null){
1515:             out.println(lineCount ++ + ": " + s);
1616:         }
1717:         out.close();
1818:         in.close();
1919:     }
2020: }

數據的存儲和恢復

為了輸出可供另一個「流」恢復的數據,我們需要使用DataOutputStream寫入數據,然後使用DataInputStream恢複數據。當然這些流可以是任何形式(這裡的形式其實就是我們前面說過的流的兩端的類型),比如文件。

 1public class StoringAndRecoveringData {
2    public static void main(String[] args) throws IOException {
3        DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("data.txt")));
4        out.writeDouble(3.1415926);
5        out.writeUTF("我是二營長");
6        out.writeInt(125);
7        out.writeUTF("點贊加關注");
8        out.close();
9
10        DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("data.txt")));
11        System.out.println(in.readDouble());
12        System.out.println(in.readUTF());
13        System.out.println(in.readInt());
14        System.out.println(in.readUTF());
15        in.close();
16    }
17}

輸出結果:

13.1415926
2我是二營長
3125
4點贊加關注

需要注意的是我們使用writeUTF()和readUTF()來寫入和讀取字元串。

好了。關於Java I/O流體系就總結這麼多吧。


我是二營長,一個轉行的程式設計師,菜雞一枚,熱衷於碼磚。