天天看點

吃透Java IO:位元組流、字元流、緩沖流1.初始 Java IO2.IO 流對象3. IO流方法4.附加内容

原文連結

IO流是Java中的一個重要的構成部分,也是我們經常打交道的。

下面幾個問題:

  • Java IO 流有什麼特點?
  • Java IO 流分為幾種類型?
  • 位元組流和字元流的關系與差別?
  • 緩沖流的效率一定高嗎?
  • 緩沖流展現了Java 中哪種設計模式思想?
  • 為什麼要實作序列化?如何實作序列化?
  • 序列化資料後,再次修改類檔案,讀資料會出問題,如何解決呢?

1.初始 Java IO

IO,即 in 和 out,也就是輸入和輸出,指應用程式和外部裝置之間的資料傳遞,常見的外部裝置包括檔案、管道、網絡連接配接。

Java 中是通過流處理IO 的,那什麼是流?

流(Stream),是一個抽象的概念,是指一連串的資料(字元和位元組),是以先進先出的方式發送資訊的通道。

當程式需要讀取資料的時候,就會開啟一個通向資料源的流,這個資料源可以是檔案、記憶體、或者是網絡連接配接。類似的,當程式需要寫入資料的時候,就會開啟一個通向目的地的流。這時候你就可以想象資料好像在這其中“流”動一樣。

一般來說關于流的特性有下面幾點:

  • 先進先出:最先寫入輸出流的資料最先被輸入流讀取到。
  • 順序存取:可以一個接一個地往流中寫入一串位元組,讀出時也将按寫入順序讀取一串位元組,不能随機通路中間的資料。(RandomAccessFile除外)
  • 隻讀或隻寫:每個流隻能是輸入流或輸出流的一種,不能同時具備兩個功能,輸入流隻能進行讀操作,對輸出流隻能進行寫操作。在一個資料傳輸通道中,如果既要寫入資料,又要讀取資料,則要分别提供兩個流。

1.1 IO流分類

IO流主要的分類方式有以下3種:

  • 按資料流的方向:輸入流、輸出流
  • 按處理資料機關:位元組流、字元流
  • 按功能:節點流、處理流
吃透Java IO:位元組流、字元流、緩沖流1.初始 Java IO2.IO 流對象3. IO流方法4.附加内容

1、輸入流與輸出流 

輸入與輸出是相對于應用程式而言的,比如檔案讀寫,讀取檔案是輸入流,寫檔案是輸出流,這點很容易搞反。

吃透Java IO:位元組流、字元流、緩沖流1.初始 Java IO2.IO 流對象3. IO流方法4.附加内容

2、位元組流與字元流 

位元組流和字元流的用法幾乎完全一樣,差別在于位元組流和字元流所操作的資料單元不同,位元組流操作的單元是資料單元為8位的位元組,字元流操作的是資料機關為16位的字元。

為什麼要有字元流?

Java中字元是采用Unicode标準,Unicode編碼中,一個英文為一個位元組,一個中文為兩個位元組。

吃透Java IO:位元組流、字元流、緩沖流1.初始 Java IO2.IO 流對象3. IO流方法4.附加内容

而在UTF-8編碼中,一個中文字元是3個位元組。例如下面圖中,“雲深不知處”5個中文對應的是15位元組:-28-70-111-26-73-79-28-72-115-25-97-91-27-92-124

吃透Java IO:位元組流、字元流、緩沖流1.初始 Java IO2.IO 流對象3. IO流方法4.附加内容

那麼問題來了,如果使用位元組流進行中文,如果一次讀寫一個字元對應的位元組數就不會有問題,一旦将一個字元對應的位元組流分裂開來,就會出現亂碼。為了友善地進行中文這些字元,Java就退出了字元流。

位元組流和字元流的其他差別:

  • 位元組流一般用來處理圖像、視訊、音頻、PPT、Word等類型的檔案。字元流一般用于處理純文字類型的檔案,如txt檔案等,但不能處理圖像視訊等非文本檔案。用一句話說:位元組流可以處理一些檔案,而字元流智能處理純文字檔案。
  • 位元組流本身沒有緩沖區,緩沖位元組流相對于位元組流,效率提升非常高。而字元流本身就帶有緩沖區,緩沖字元流相對于字元流效率提升就不是那麼大了。詳見文末效率對比。

以寫文本為例,我們檢視字元流的源碼,發現确實有利用到緩沖區:

public void write(String str, int off, int len) throws IOException {
        synchronized (lock) {
            char cbuf[];
            //WRITE_BUFFER_SIZE 緩沖區容量
            //writeBuffer 充當緩沖區的字元數組
            if (len <= WRITE_BUFFER_SIZE) {
                if (writeBuffer == null) {
                    writeBuffer = new char[WRITE_BUFFER_SIZE];
                }
                cbuf = writeBuffer;
            } else {    // Don't permanently allocate very large buffers.
                cbuf = new char[len];
            }
            str.getChars(off, (off + len), cbuf, 0);
            write(cbuf, 0, len);
        }
    }
           

3、節點流和處理流 

節點流:直接操作資料讀寫的流類,比如 FileInputStream

處理流:對一個已經存在的流的連接配接和封裝,通過對資料進行處理為程式提供功能強大、靈活的讀寫功能,例如 BufferedInputStream(緩存位元組流)

處理流和節點流應用了 Java 的裝飾者設計模式。

下圖就很形象地描繪了節點流和處理流,處理流是對節點流的封裝,最終的資料處理還是由節點流完成的。

吃透Java IO:位元組流、字元流、緩沖流1.初始 Java IO2.IO 流對象3. IO流方法4.附加内容

在諸多的處理流中,有一個非常重要,那就是 緩沖流。 

我們知道,程式與磁盤的互動相對于記憶體運算是很慢的,容易成為程式的性能瓶頸。減少程式與磁盤的互動,是提升程式效率一種有效手段。緩沖流,就應用這種思路:普通流每次讀寫一個位元組,而緩沖流在記憶體中設定一個緩沖區,緩沖區先存儲足夠的待操作資料後,再與記憶體或磁盤進行互動。這樣,在總數量不變的情況下,通過提高每次互動的資料量,較少了互動次數。

吃透Java IO:位元組流、字元流、緩沖流1.初始 Java IO2.IO 流對象3. IO流方法4.附加内容

聯想一下生活中的例子,我們搬磚的時候,一塊一塊地往車上裝肯定是效率很低效的。我們可以使用一個小推車,先把磚裝到小推車上,再把這小推車推到車前,把磚撞到車上。這個例子中,小推車可以視為緩沖區,小推車的存在,減少了我們裝車的次數,進而提高了效率。

需要注意的是,緩沖流效率一定高嗎?不一定,某些情況下,緩沖流效率反而更低,具體請見IO流效率對比。

完整的 IO 分類圖如下:

吃透Java IO:位元組流、字元流、緩沖流1.初始 Java IO2.IO 流對象3. IO流方法4.附加内容

1.2 案例實操

接下來,我們看看如何使用Java IO。

文本讀寫的例子,将“松下問童子,言師采藥去。隻在此山中,雲深不知處。”寫入本地文本,然後再從檔案讀取内容并輸出到控制台。

1、FileInputStream、FileOutputStream(位元組流)

位元組流的方式效率較低,不建議使用
import java.io.*;

public class IOTest {
    public static void main(String[] args) throws IOException {
        File file = new File("D:/test.txt");

        write(file);
        System.out.println(read(file));
    }

    private static void write(File file) throws IOException {
        OutputStream os = new FileOutputStream(file);
        //要寫入的字元串
        String str = "松下問童子,言師采藥去。隻在此山中,雲深不知處。";
        //寫入檔案
        os.write(str.getBytes("UTF-8"));
        //關閉流
        os.close();
    }

    private static String read(File file) throws IOException {
        InputStream is = new FileInputStream(file);
        //一次性取多少位元組
        byte[] bytes = new byte[1024];
        //用來接收讀取的位元組數組
        StringBuffer sb = new StringBuffer();
        //讀取到的位元組數組長度,為-1時表示沒有資料
        int length = 0;
        //循環讀取
        while ((length = is.read(bytes)) != -1) {
            //将讀取内容轉出字元串
            sb.append(new String(bytes,0,length,"UTF-8"));
        }
        //關閉流
        is.close();
        return sb.toString();
    }
}
           

2、BufferedInputStream、BufferedOutputStream(緩沖位元組流)

緩沖位元組流是為高效而設計的,真正的讀寫操作還是靠FileInputStream和FileOutputStream,是以其構造方法入參是這兩個類的對象也就不奇怪了。
import java.io.*;

public class IOTest {
    public static void main(String[] args) throws IOException {
        File file = new File("D:/test.txt");

        write(file);
        System.out.println(read(file));
    }

    private static void write(File file) throws IOException {
        //緩存位元組流,提高了效率
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
        //要寫入的字元串
        String str = "松下問童子,言師采藥去。隻在此山中,雲深不知處。";
        //寫入檔案
        bos.write(str.getBytes("UTF-8"));
        //關閉流
        bos.close();
    }

    private static String read(File file) throws IOException {
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
        //一次性取多少位元組
        byte[] bytes = new byte[1024];
        //用來接收讀取的位元組數組
        StringBuffer sb = new StringBuffer();
        //讀取到的位元組數組長度,為-1時表示沒有資料
        int length = 0;
        //循環讀取
        while ((length = bis.read(bytes)) != -1) {
            //将讀取内容轉出字元串
            sb.append(new String(bytes,0,length,"UTF-8"));
        }
        //關閉流
        bis.close();
        return sb.toString();
    }
}
           

3、InputStreamReader和OutputStreamWriter(字元流)

字元流适用于文本檔案的讀寫,OutputStreamWriter類其實也是借助FileOutputStream類實作的,故其構造方法是FileOutputStream對象
import java.io.*;

public class IOTest {
    public static void main(String[] args) throws IOException {
        File file = new File("D:/test.txt");

        write(file);
        System.out.println(read(file));
    }

    private static void write(File file) throws IOException {
        //OutputStreamWriter可以顯示的指定字元集,否則使用預設字元集
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(file), "UTF-8");
        //要寫入的字元串
        String str = "松下問童子,言師采藥去。隻在此山中,雲深不知處。";
        //寫入檔案
        osw.write(str);
        //關閉流
        osw.close();
    }

    private static String read(File file) throws IOException {
        InputStreamReader isr = new InputStreamReader(new FileInputStream(file),"UTF-8");
        //字元數組:一次性讀取多少個字元
        char[] chars = new char[1024];
        //用來接收讀取的數組
        StringBuffer sb = new StringBuffer();
        //讀取到的位元組數組長度,為-1時表示沒有資料
        int length = 0;
        //循環讀取
        while ((length = isr.read(chars)) != -1) {
            //将讀取内容轉出字元串
            sb.append(chars,0,length);
        }
        //關閉流
        isr.close();
        return sb.toString();
    }
}
           

4、字元流便捷類

Java提供了 FileWriter和FileReader台灣字元流的讀寫,new FileWriter 等同于 new OutputStreamWriter(new FileOutputStream(file))
import java.io.*;

public class IOTest {
    public static void main(String[] args) throws IOException {
        File file = new File("D:/test.txt");

        write(file);
        System.out.println(read(file));
    }

    private static void write(File file) throws IOException {
        FileWriter fw = new FileWriter(file);
        //要寫入的字元串
        String str = "松下問童子,言師采藥去。隻在此山中,雲深不知處。";
        //寫入檔案
        fw.write(str);
        //關閉流
        fw.close();
    }

    private static String read(File file) throws IOException {
        FileReader fr = new FileReader(file);
        //字元數組:一次性讀取多少個字元
        char[] chars = new char[1024];
        //用來接收讀取的數組
        StringBuffer sb = new StringBuffer();
        //讀取到的位元組數組長度,為-1時表示沒有資料
        int length = 0;
        //循環讀取
        while ((length = fr.read(chars)) != -1) {
            //将讀取内容轉出字元串
            sb.append(chars,0,length);
        }
        //關閉流
        fr.close();
        return sb.toString();
    }
}
           

5、BufferedReader、BufferedWriter(字元緩沖流)

import java.io.*;

public class IOTest {
    public static void main(String[] args) throws IOException {
        File file = new File("D:/test.txt");

        write(file);
        System.out.println(read(file));
    }

    private static void write(File file) throws IOException {
        // BufferedWriter fw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"));
        // FileWriter可以大幅度簡化代碼
        BufferedWriter bw = new BufferedWriter(new FileWriter(file));
        //要寫入的字元串
        String str = "松下問童子,言師采藥去。隻在此山中,雲深不知處。";
        //寫入檔案
        bw.write(str);
        //關閉流
        bw.close();
    }

    private static String read(File file) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(file));
        //用來接收讀取的數組
        StringBuffer sb = new StringBuffer();
        //按行讀資料
        String line;
        //循環讀取
        while ((line = br.readLine()) != null) {
            sb.append(line);
        }
        //關閉流
        br.close();
        return sb.toString();
    }
}
           

2.IO 流對象

第一節中,我們大緻了解了IO,并完成了幾個案例,但對IO還缺乏更詳細的認知,那麼接下來我們就對JavaIO細細分解,梳理出完整的知識體系來。

Java中提供了40多個類,我們隻需要詳細了解一下其中比較重要的就可以滿足日常應用了。

2.1 File類

File類是用來操作檔案的類,但它不能操作檔案中的資料。

public class File implements Serializable, Comparable<File>
           

File類實作了Serializable、 Comparable<File>,說明它是支援序列化和排序的。

File類的構造方法

方法名 說明
File(File parent, String child) 根據parent抽象路徑名和child路徑名字元串建立一個新的File執行個體
File(String pathname) 通過将給定路徑名字元串轉為抽象路徑名來建立一個新的File執行個體
File(String parent, String child) 根據parent路徑名字元串和child路徑名字元串建立一個新的File執行個體
File(URI uri) 通過給定的file:URI 轉為一個抽象路徑名來建立一個新的File執行個體

File類的常用方法

方法名 說明
createNewFile() 當且僅當不存在具有此抽象路徑名指定名稱的檔案時,不可分地建立一個新的空檔案。
delete() 删除此抽象路徑名表示的檔案或目錄
exists() 冊數此抽象路徑名表示的檔案或目錄是否存在
getAbsoluteFile() 傳回此抽象路徑名的絕對路徑名形式。
getAbsolutePath() 傳回此抽象路徑的絕對路徑名字元串。
length() 傳回此抽象路徑表示的檔案的長度。
mkdir() 建立此抽象路徑名指定的目錄。

File類使用執行個體

import java.io.File;
import java.io.IOException;

public class FileTest {
    public static void main(String[] args) throws IOException {
        File file = new File("D:/test.txt");
        //判斷檔案是否存在
        if (!file.exists()) {
            //不存在則建立
            file.createNewFile();
        }
        System.out.println("檔案的絕對路徑:"+file.getAbsolutePath());
        System.out.println("檔案的大小:" + file.length());
        //删除檔案
        file.delete();
    }
}
           

2.2 位元組流

InputStream和OutputStream是兩個抽象類,是位元組流的基類,所有具體的位元組流實作都是分别繼承了這兩個類。

以InputStream為例,實作了Closeable

public abstract class InputStream implements Closeable
           

InputStream類有很多的實作子類,下面列舉了一些比較常用的:

吃透Java IO:位元組流、字元流、緩沖流1.初始 Java IO2.IO 流對象3. IO流方法4.附加内容

詳細說明下上圖中的類:

  • InputStream:InputStream是所有位元組輸入流的抽象基類,前面說過抽象類不能被執行個體化,實際上是作為模闆而存在的,為所有實作類定義了處理輸入流的方法。
  • FileInputStream:檔案輸入流,一個非常重要的位元組輸入流,用于對檔案進行讀取操作。
  • PipedInputStream:管道位元組輸入流,能實作多線程間的管道通訊。
  • ByteArrayInputStream:位元組數組輸入流,從位元組數組(byte[])中進行以位元組為機關的讀取,也就是将資源檔案都已位元組的形式存入到該類的位元組數組中去。
  • FilterInputStream:裝飾者類,具體裝飾者內建該類,這些都是處理類,作用是對節點類進行封裝,實作一些特殊功能。
  • DataInputStream:資料輸入流,它是用來裝飾其他輸入流,作用是“允許應用程式以與機器無關方式從底層輸入流中讀取基本Java資料類型”。
  • BufferedInputStream:緩沖流,對節點流進行裝飾,内部會與一個緩存區,用來存放位元組,每次都将緩存區存滿後發送,而不是一個位元組或兩個位元組這樣發送,效率更高。
  • ObjectInputStream:對象輸入流,用來提供對基本資料或對象的持久存儲。通俗的說,也就是能直接傳輸對象,通常應用在反序列化中。它也是一種處理流,構造器的入參是一個InputStream對象。

OutputStream類繼承關系圖:

吃透Java IO:位元組流、字元流、緩沖流1.初始 Java IO2.IO 流對象3. IO流方法4.附加内容

OutputStream類繼承關系與InputStream類型,需要注意的是PrintStream。

2.3 字元流

與位元組流類似,字元流也有兩個抽象基類,分别是Reader和Writer。其他的字元流實作類都是繼承了這兩個類。

以Reader為例,它的主要實作子類如下圖:

吃透Java IO:位元組流、字元流、緩沖流1.初始 Java IO2.IO 流對象3. IO流方法4.附加内容

 各個類的詳細說:

  • InputStreamReader:從位元組流到字元流的橋梁(InputStreamReader構造器入參是FileInputStream的執行個體對象),它讀取位元組并使用指定的字元集将其解碼為字元。它是由的字元集可通過名稱指定,也可以顯示給定,或者可以接收平台的預設字元集。
  • BufferedReader:從字元輸入流中讀取文本,設定一個緩存區來提高效率。BufferedReader是對InputStreamReader的封裝,前者構造器的入參就是後者的一個執行個體對象。
  • FileReader:用于讀取字元檔案的便利類,new FileReader(File file)等同于new InputStreamReader(new FileInputStream(file),"UTF-8"),但FileReader不能指定指定字元編碼和預設緩沖區大小。
  • PipedReader:管道字元輸入流。實作多線程間的管道通信。
  • CharArrayReader:從Char數組中讀取資料的媒體流。
  • StringReader:從String中讀取資料的媒體流。

Writer與Reader結構類似,方向相反,不在贅述。唯一有差別的是,Writer的子類PrintWriter。

2.4 序列化

序列化:指堆記憶體中的Java對象,通過某種方式把對像存儲到磁盤檔案中,或者傳遞給其他網絡節點(網絡傳輸)。這個過程稱為序列化,通常是指将資料結構或對象轉成二進制的過程。

反序列化:把磁盤檔案中的對象資料或把網絡節點上的對象資料,恢複成Java對象模型的過程。也就是将在序列化過程中生成的二進制串轉成資料結構或對象的過程。

序列化的作用

  • 想把記憶體中的對象儲存到一個檔案中或者資料庫中的時候。
  • 想用套接字在網絡上傳送對象的時候;
  • 想通過RMI傳輸對象的時候。

序列化實作

要實作對象的序列化,最直接的操作就是實作Serializable接口。使用IO流對象可以實作序列化操作,将對象儲存到檔案中,再讀取出來。

import java.io.*;

public class ObjectTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        File file = new File("D:/obj.txt");
        User user = new User("寶寶",12);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
        oos.writeObject(user);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        Object obj = ois.readObject();
        ois.close();
        System.out.println(obj);
    }

    public static class User implements Serializable {
        private static final long serialVersionUID = 1L;
        private String name;
        private int age;

        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public int getAge() {
            return age;
        }

        public void setName(String name) {
            this.name = name;
        }

        public void setAge(int age) {
            this.age = age;
        }

        @Override
        public String toString() {
            return "User[name="+name+",age="+age+"]";
        }
    }
}
           

這裡我們成功的進行了一次将對象儲存到檔案中,在讀取了出來。如果此時,我們不實作序列化接口,就會出現異常了。

Exception in thread "main" java.io.NotSerializableException: ObjectTest$User

    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)

    at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)

    at ObjectTest.main(ObjectTest.java:8)

可以看到,因為沒有進行序列化,是以無法儲存與讀取。

序列化ID的作用

可以看到,我們在進行序列化是,加了一個serialVersionUID字段,這邊是序列化ID

private static final long serialVersionUID = 1L;

這個序列化ID起着關鍵的作用,它決定這是否能否成功反序列化!Java的序列化機制是通過判斷運作時類的serialVersionUID來驗證版本一緻性的,在進行反序列化時,JVM會把傳進來的位元組流中的serialVersionUID與本地實體類的serialVersionUID進行比較,如果相同則認為一緻的,便可以進行反序列化,否則就會報序列化版本不一緻的異常。

預設序列化ID

當我們一個實體類中沒有顯示定義一個名為 serialVersionUID 、類型為 long的變量時,Java序列化機制會根據編譯時的class自動生成一個serialVersionUID。例如,當我們在本地類中添加其他的字段,這個時候再反序列化時便會出現serialVersionUID不一緻,導緻反序列化失敗。那麼怎麼解決呢?便是在本地類中添加一個 long 類型的 名稱為 “serialVersionUID ” 的變量,值保持不變,便可以進行序列化和反序列化。

  • 如果沒有顯示指定 serialVersionUID,會自動生成一個。
  • 隻有同一次編譯的class才會生成相同的serialVersionUID
  • 但如果出現需求變動,Bean類發生改變,則會導緻發序列化失敗.為了不出現類似的問題,是以我們最好還是顯示的指定一個serialVersionUID.

序列化的其他問題

靜态變量不會被序列化.(static, transient)

當一個父類實作序列化,子類自動實作序列化,不需要顯示地實作Serializable接口.

當一個對象的執行個體變量引用其他對象,序列化該對象時也把引用對象進行序列化.

子類序列化時:

如果父類沒有實作Serializable接口,沒有提供預設構造函數,那麼子類的序列化會出錯;

如果父類沒有實作Serializable接口,提供了預設的構造函數,那麼子類可以序列化,父類的成員變量不會被序列化。如果父類實作了Serializable接口,則父類和子類都可以序列化。

3. IO流方法

3.1 位元組流方法

位元組輸入流 InputStream 主要方法:

  • read():從此輸入流中讀取一個資料位元組
  • read(byte[] b):從此輸入流中将最多 b.length個位元組的資料讀入一個byte數組中
  • read(byte[], int off, int len):從此輸入流中将最多len個位元組的資料讀入一個byte數組中
  • close():關閉輸入流并釋放與該流關聯的所有資源

位元組輸出流 OutputStream 主要方法:

  • write(byte[] b):将b.length 個位元組從指定byte數組寫入此檔案輸出流中
  • write(byte[] b, int off, int len):将指定 byte 數組中從偏移量 off 開始的 len 個位元組寫入此檔案輸出流
  • write(int b):将指定位元組寫入此檔案輸出流
  • close():關閉輸入流并釋放與該流關聯的所有資源

3.2 字元流方法

字元輸入流 Reader 主要方法:

  • read():讀取單個字元
  • read(char[] cbuf):将字元讀入數組
  • read(char[] cbuf, int off, int len):将字元讀入數組的某一部分
  • read(CharBuffer target):視圖将字元讀入指定的字元緩沖區
  • flush():重新整理該流的緩沖
  • close():關閉此流,但要先重新整理它

字元輸出流 Writer 主要方法:

  • write(char[] cbuf):寫入字元數組
  • write(char[] cbuf, int off, int len):寫入字元數組的某一部分
  • write(int c):寫入單個字元
  • write(String str):寫入字元串
  • write(String str, int off, int len):寫入字元串的某一部分
  • flush():重新整理該流的緩沖
  • close():關閉此流,但要先重新整理它

另外,字元緩沖流還有兩個獨特的方法:

  • BufferedWriter 類 newLine():寫入一個行分隔符。這個方法會自動适配所在系統的行分隔符。
  • BufferedReader 類 readLine():讀取一個文本行

4.附加内容

4.1 位、位元組、字元

位元組(Byte)是計量機關,表示資料量多少,是計算機資訊技術用于存儲容量的一種計量機關,通常情況下一位元組等于8位。

字元(Character)計算機中使用的字母、數組、和符号,比如‘A’、‘B’、‘$’、‘&’等。

一般在英文狀态下一個字母或字元占用一個位元組,一個漢字用兩個位元組表示。

位元組與字元:

  • ASCII碼中,一個英文字母(不分大小寫)為一個位元組,一個中文漢字為兩個位元組。
  • UTF-8 編碼中,一個英文字為一個位元組,一個中文為三個位元組。
  • Unicode 編碼中,一個英文字為一個位元組,一個中文為兩個位元組。
  • 符号:英文标點為一個位元組,中文标點為兩個位元組。例如:英文句号“.”占一個位元組大小,中文句号"。"棧兩個位元組大小。
  • UTF-16 編碼中,一個英文字母或一個漢字都需要2位元組存儲(Unicode 擴充區的一些漢字存儲需要4個位元組)
  • UTF-32編碼中,世界上任何字元的存儲都需要4個位元組。

4.2 IO流效率對比

首先,對比下普通位元組流和緩存位元組流的效率:

import java.io.*;

public class MyTest {
    public static void main(String[] args) throws IOException {
        File file = new File("D:/myTest.txt");
        StringBuilder sb = new StringBuilder();

        for (int i=0; i<3000000; i++) {
            sb.append("abcdefghigklmnopqrstuvwsyz");
        }

        byte[] bytes = sb.toString().getBytes();

        long start = System.currentTimeMillis();
        write(file, bytes);
        long end = System.currentTimeMillis();


        long start2 = System.currentTimeMillis();
        bufferedWrite(file, bytes);
        long end2 = System.currentTimeMillis();

        System.out.println("普通位元組流耗時:" + (end - start) + " ms");
        System.out.println("緩沖位元組流耗時:" + (end2 - start2) + " ms");
    }

    private static void write(File file, byte[] bytes) throws IOException {
        OutputStream os = new FileOutputStream(file);
        os.write(bytes);
        os.close();
    }

    private static void bufferedWrite(File file, byte[] bytes) throws IOException {
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
        bos.write(bytes);
        bos.close();
    }
}
           

運作結果:

普通位元組流耗時:81 ms

緩沖位元組流耗時:80 ms

這個結果讓我大跌眼鏡,不是說好緩沖流效率很高麼?要知道為什麼,智能去源碼裡找答案了。看了位元組緩存流 write() 方法:

public synchronized void write(byte b[], int off, int len) throws IOException {
        if (len >= buf.length) {
            /* If the request length exceeds the size of the output buffer,
               flush the output buffer and then write the data directly.
               In this way buffered streams will cascade harmlessly. */
            flushBuffer();
            out.write(b, off, len);
            return;
        }
        if (len > buf.length - count) {
            flushBuffer();
        }
        System.arraycopy(b, off, buf, count, len);
        count += len;
    }
           

 注釋裡說的很明白:如果請求長度超過緩沖區大小,重新整理輸出緩沖流,然後直接寫入資料。這樣,緩沖流将無害地級聯。

但是,至于為什麼這麼設計,我沒想明白。

基于上面的情形,想要對比普通位元組流和緩沖位元組流的效率差距,就要避免直接寫較長的字元串,于是修改了上面的案例:

import java.io.*;

public class MyTest {
    public static void main(String[] args) throws IOException {
        File file = new File("D:/myTest.txt");
        StringBuilder sb = new StringBuilder();

        for (int i=0; i<30000; i++) {
            sb.append("abcdefghigklmnopqrstuvwsyz");
        }

        byte[] bytes = sb.toString().getBytes();

        long start = System.currentTimeMillis();
        write(file, bytes);
        long end = System.currentTimeMillis();


        long start2 = System.currentTimeMillis();
        bufferedWrite(file, bytes);
        long end2 = System.currentTimeMillis();

        System.out.println("普通位元組流耗時:" + (end - start) + " ms");
        System.out.println("緩沖位元組流耗時:" + (end2 - start2) + " ms");
    }

    private static void write(File file, byte[] bytes) throws IOException {
        OutputStream os = new FileOutputStream(file);
        for (int i=0;i<bytes.length;i++){
            os.write(bytes[i]);
        }
        os.close();
    }

    private static void bufferedWrite(File file, byte[] bytes) throws IOException {
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
        for (int i=0;i<bytes.length;i++){
            bos.write(bytes[i]);
        }
        bos.close();
    }
}
           

運作結果:

普通位元組流耗時:1565 ms

緩沖位元組流耗時:21 ms

這次,普通位元組流和緩沖位元組流的效率差異就很明顯了。

在看看字元流和緩沖字元流的效率對比:

import java.io.*;

public class MyTest2 {
    public static void main(String[] args) throws IOException {
        File file = new File("D:/myTest.txt");
        StringBuilder sb = new StringBuilder();

        for (int i=0; i<300000; i++) {
            sb.append("abcdefghigklmnopqrstuvwsyz");
        }

        byte[] bytes = sb.toString().getBytes();

        long start = System.currentTimeMillis();
        write(file, bytes);
        long end = System.currentTimeMillis();


        long start2 = System.currentTimeMillis();
        bufferedWrite(file, bytes);
        long end2 = System.currentTimeMillis();

        System.out.println("普通字元流耗時:" + (end - start) + " ms");
        System.out.println("緩沖字元流耗時:" + (end2 - start2) + " ms");
    }

    private static void write(File file, byte[] bytes) throws IOException {
        Writer writer = new FileWriter(file);
        for (int i=0;i<bytes.length;i++){
            writer.write(bytes[i]);
        }
        writer.close();
    }

    private static void bufferedWrite(File file, byte[] bytes) throws IOException {
        BufferedWriter bw = new BufferedWriter(new FileWriter(file));
        for (int i=0;i<bytes.length;i++){
            bw.write(bytes[i]);
        }
        bw.close();
    }
}
           

運作結果:

普通字元流耗時:283 ms

緩沖字元流耗時:204 ms

測試多次,結果差不多,可見字元緩沖流效率是并沒有明顯提高,我們更多的是要适應它們的 readLine() 和 writeLine() 方法。