java I/O系統(3)-字節流與字符流

引言

在java的IO系統中,對資源的操作分爲兩類:字節流與字符流。如果延承inputStream與outputStream就是字節流,如果延承reader與writer就是字符流,那麼他們之間到底有什麼區別呢?在本篇博文中會列出IO系統的所有操作類的框架,並對字節流與字符流做出詳細的解釋,最後會用一些demo來分別實現字節流與字符流的操作。筆者目前整理的一些blog針對面試都是超高頻出現的。大家可以點擊鏈接:http://blog.csdn.net/u012403290

“流”是IO系統中的核心抽象概念。它代表任何有能力產出數據的數據源對象或者是有能力接收數據的對象。它屏蔽了IO設備中處理數據的細節。在IO系統中,我們對資源的處理都是以流的形式操作的,流中保存的其實是字節文件。

一個語法糖 try-with-resource

我們知道在把資源作爲一個對象的時候,都不能忘記最後的close操作。那如何斷定一個對象是資源對象呢?一般的,如果一個類實現了java.io.Closeable對象的話,那麼它就是一個資源對象。比如說我們討論的IO系統:

//InputStream
public abstract class InputStream implements Closeable {
}

//OutputStream
public abstract class OutputStream implements Closeable, Flushable {
}

//Writer
public abstract class Writer implements Appendable, Closeable, Flushable {
}

//Reader
public abstract class Reader implements Readable, Closeable {
}

上面就是IO系統的關鍵類,他們都實現了Closeable對象,所以當已他們爲對象時,都需要在結束的時候關閉資源。

在JDK1.7時引入了try-with-resource這一種新的語句,它可以在你使用資源的時候,就不需要手動關係,java會自動來關閉它。在這之前,我們一般用try-catch-finally來控制拋錯與關閉,我們會在finally語句塊中進行資源的關閉。比如說下面這段代碼:

package com.brickworkers.io;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class FileReadTest {

    public static void main(String[] args) throws IOException {
        File old = new File("F:/java/io/write.txt");
        //老方法對資源進行讀取
        InputStream in = new FileInputStream(old);
        byte[] bs = new byte[(int) old.length()];
        try {
            in.read(bs);
        }finally{//我們必須在finally中對資源進行關閉
            in.close();
        }
        System.out.println(new String(bs));



        //try-with-resource對資源進行讀取
        try(InputStream in2 = new FileInputStream(new File("F:/java/io/write.txt"))){//會自動關閉資源,我們把資源放入try的括號中
            in2.read(bs);
        }
        System.out.println(new String(bs));

    }

}

java IO系統整體框架

下面這張圖是從別人的博客上摘過來的,總結的很好:
這裏寫圖片描述

對於字節流來說,在上篇博文中,我們已經纖細介紹過他們的父類,子類,裝飾類之間的關係。FilterInputStream與FilterOutputStream就是兩個裝飾類,他們可以把流轉變成一個更加適合的方式來展現,如果對裝飾者模式不是很瞭解,請參考上一篇博文:http://blog.csdn.net/u012403290/article/details/71747516

每一個子類都有它所對應的功能,在後面的章節中,我們會詳細的介紹每一個子類的核心功能。在本篇博文中,上面這張圖,我想說明的是存在兩個派別:字節流與字符流

爲什麼已經存在字節流還要引入字符流

熟悉java的應該知道,字節流比字符流產生的更早,可以說jdk一面世,已經有字節流了。那麼爲什麼後面又引入了字符流呢?
首先,各個單位之間的關係,我們要清楚:
字節我們用Byte做單位,位我們用bit做單位,這就是大小b的關係
1字節(B) = 8位(b)
1字符 = 2字節(B) = 16位(b)

然後國際化的Unicode編碼是16位的,也就是佔2個字節,或者1個字符。那麼我們簡單的用字節流來處理Unicode編碼,就顯得非常雞肋,所以字符流也就應運而生了。同時,使用字符流操作會比字節流操作更加的迅速,爲什麼會更加迅速呢?一個最顯然的加速方式就是緩存區。字符流是用了緩存區的,而字節流是直接與文件進行操作。以下是一個典型的試驗,用兩者方式輸出一個文件,但是都不關閉流,如果存在緩存區的話,那麼它的觸發機制就是流關閉導致它的緩存結束:

package com.brickworkers.io;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;

public class FileReadTest {

    public static void main(String[] args) throws IOException {
        File file1 = new File("F:/java/io/write1.txt");
        //字節流輸出
        OutputStream os = new FileOutputStream(file1);
        String txt = "helloworld";
        try{
            os.write(txt.getBytes());
        }finally{
            //不關閉流
        }

        File file2 = new File("F:/java/io/write2.txt");
        Writer writer = new FileWriter(file2);
        try{
            writer.write(txt);
        }finally{
            //不關閉流
        }

    }

}

運行結果你會發現:使用字節流的在文本文件中已經寫入了hello world,但是字符流卻沒有寫入,說明字符流是帶有緩存區的,這個時候就需要刷新緩存區,或者關閉流來觸發輸出。不要在輸入流用不關閉流來進行上述測試,試想一下不管存不存在緩存區,你數據讀取了,那麼就都會是有了的,並不會收緩存區影響。

當然,並不是說摒棄字節流全部都使用字符流,在很多情況下字符流是不能用的,什麼情況呢?指定字節操作的時候就不可以用字符流。前面說過,所有的資源都是以字節的形式存儲的,包括圖片,視頻,音頻等等,所以在日常的開發中字節流的操作還是非常普及的。

兩個流派進行轉化

一個面向字符流,一個面向字節流。那麼他們之間在JDK中是如何轉化的呢?Reader與Writer中有兩個類來處理這種轉化,他們分別是InputStreamReader與OutputStreamWriter,可以稱這兩個爲適配器類。可以在上面的圖片中找到他們。下面就是他們的一個使用例子:

package com.brickworkers.io;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;

public class FileReadTest {

    public static void main(String[] args) throws IOException {
        File file = new File("F:/java/io/write.txt");
        //字節流輸出
        OutputStream os = new FileOutputStream(file);
        String txt = "hello world";

        //把字節流轉化成字符流操作
        Writer writer = new OutputStreamWriter(os);
        try {
            writer.write(txt);
        } finally{
            writer.close();
        }

    }

}

或許有小夥伴要問了,那麼上面的代碼字節流轉化成字符流是實現了。那如果我要字符流轉成字節流呢?其實也很簡單,我們可以先把char轉化成String,然後我們用String的API中的getBytes方法轉化成字節。然後再使用字節流處理。

編碼問題

在實際的開發中會經常遇到編碼的問題,尤其是對中文的處理,經常會導致亂碼。那麼有什麼辦法能很好的避免這個問題呢?
博主在IO系統中,爲了更好的避免亂碼,如果只對於上述的知識之內,一般採用先用字節流獲取要處理的文件,然後把字節流用適配器(OutputWriter與InputReader)來轉化成字符流,這個時候就可以設置他們的編碼規則,請參考下面這段代碼:

package com.brickworkers.io;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;

public class FileReadTest {

    public static void main(String[] args) throws IOException {
        //在write.txt文件中,我寫了一段中文
        File file = new File("F:/java/io/write.txt");
        //嘗試用字節流
        byte[] bs = new byte[(int) file.length()];
        try(InputStream in = new FileInputStream(file)){
            in.read(bs);
        }
        System.out.println("字符流:" +new String(bs));

        //嘗試用字符流
        char[] ch = new char[(int) file.length()/2 - 2];
        try(Reader reader= new FileReader(file)){
            reader.read(ch);
        }
        System.out.println("字符流:" + new String(ch));



        //兩者組合操作
        char[] res = new char[(int) file.length()/2 - 2];
        try(Reader reader = new InputStreamReader(new FileInputStream(file), "gb2312")){
            reader.read(res);
        }
        System.out.println("組合使用:" + new String(ch));
    }

}

其實,在實際的開發中放置亂碼發生的核心辦法是:統一開發環境。其實這個是最好的辦法了,只要保證你的環境,和你合作開發的朋友的環境,各種資源,數據庫等等。如果保證了這些編碼的統一,基本上是不會出現亂碼的。

尾記

在java的IO系統中除了上面圖片所展示的之外,還有一個使用非常頻繁的類:RandomAccessFile類。這個類很有意思,它是自成一派的,是一個自我獨立的類。在後續的博文中會介紹到它。而且在本篇敘述中,只涉及到很少的代碼實現輸入輸出,我想在後面在詳細的描述如何正確的使用它們。

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