1. 程式人生 > 實用技巧 >資料相關操作

資料相關操作

1、IO體系

Java IO 體系看起來類很多,感覺很複雜,但其實是 IO 涉及的因素太多了。在設計 IO 相關的類時,編寫者也不是從同一個方面考慮的,所以會給人一種很亂的感覺,並且還有設計模式的使用,更加難以使用這些 IO 類,所以特地對 Java 的 IO 做一個總結。

IO 類設計出來,肯定是為了解決 IO 相關的操作的,想一想哪裡會有 IO 操作?網路、磁碟。網路操作相關的類是在 java.net 包下,不在本文的總結範圍內。提到磁碟,你可能會想到檔案,檔案操作在 IO 中是比較典型的操作。在 Java 中引入了 “流” 的概念,它表示任何有能力產生資料來源或有能力接收資料來源的物件。資料來源可以想象成水源,海水、河水、湖水、一杯水等等。資料傳輸可以想象為水的運輸,古代有用桶運水,用竹管運水的,現在有鋼管運水,不同的運輸方式對應不同的運輸特性。

從資料來源或者說是操作物件角度看,IO 類可以分為:

  • 1、檔案(file):FileInputStream、FileOutputStream、FileReader、FileWriter
  • 2、陣列([]):
    • 2.1、位元組陣列(byte[]):ByteArrayInputStream、ByteArrayOutputStream
    • 2.2、字元陣列(char[]):CharArrayReader、CharArrayWriter
  • 3、管道操作:PipedInputStream、PipedOutputStream、PipedReader、PipedWriter
  • 4、基本資料型別:DataInputStream、DataOutputStream
  • 5、緩衝操作:BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter
  • 6、列印:PrintStream、PrintWriter
  • 7、物件序列化反序列化:ObjectInputStream、ObjectOutputStream
  • 8、轉換:InputStreamReader、OutputStreWriter
  • 9、字串(String)Java8中已廢棄StringBufferInputStream、StringBufferOutputStream、StringReader、StringWriter

從資料傳輸方式或者說是運輸方式角度看,可以將 IO 類分為:
  • 1、位元組流
  • 2、字元流

位元組流是以一個位元組單位來運輸的,比如一杯一杯的取水。而字元流是以多個位元組來運輸的,比如一桶一桶的取水,一桶水又可以分為幾杯水。

位元組流和字元流的區別:

位元組流讀取單個位元組,字元流讀取單個字元(一個字元根據編碼的不同,對應的位元組也不同,如 UTF-8 編碼是 3 個位元組,中文編碼是 2 個位元組。)位元組流用來處理二進位制檔案(圖片、MP3、視訊檔案),字元流用來處理文字檔案(可以看做是特殊的二進位制檔案,使用了某種編碼,人可以閱讀)。簡而言之,位元組是個計算機看的,字元才是給人看的。

位元組流和字元流的劃分可以看下面這張圖。

不可否認,Java IO 相關的類確實很多,但我們並不是所有的類都會用到,我們常用的也就是檔案相關的幾個類,如檔案最基本的讀寫類 File 開頭的、檔案讀寫帶緩衝區的類 Buffered 開頭的類,物件序列化反序列化相關的類 Object 開頭的類。

2、IO類和相關方法

IO 類雖然很多,但最基本的是 4 個抽象類:InputStream、OutputStream、Reader、Writer。最基本的方法也就是一個讀 read() 方法、一個寫 write() 方法。方法具體的實現還是要看繼承這 4 個抽象類的子類,畢竟我們平時使用的也是子類物件。這些類中的一些方法都是(Native)本地方法、所以並沒有 Java 原始碼,這裡給出筆者覺得不錯的 Java IO 原始碼分析 傳送門,按照上面這個思路看,先看子類基本方法,然後在看看子類中還新增了那些方法,相信你也可以看懂的,我這裡就只對上後面說的常用的類進行總結。

先來看 InputStream 和 OutStream 中的方法簡介,因為都是抽象類、大都是抽象方法、所以就不貼原始碼嘍!注意這裡的讀取和寫入,其實就是獲取(輸入)資料和輸出資料。

InputStream 類

方法方法介紹
public abstract int read() 讀取資料
public int read(byte b[]) 將讀取到的資料放在 byte 陣列中,該方法實際上是根據下面的方法實現的,off 為 0,len 為陣列的長度
public int read(byte b[], int off, int len) 從第 off 位置讀取 len 長度位元組的資料放到 byte 陣列中,流是以 -1 來判斷是否讀取結束的(注意這裡讀取的雖然是一個位元組,但是返回的卻是 int 型別 4 個位元組,這裡當然是有原因,這裡就不再細說了,推薦這篇文章,連結
public long skip(long n) 跳過指定個數的位元組不讀取,想想看電影跳過片頭片尾
public int available() 返回可讀的位元組數量
public void close() 讀取完,關閉流,釋放資源
public synchronized void mark(int readlimit) 標記讀取位置,下次還可以從這裡開始讀取,使用前要看當前流是否支援,可以使用 markSupport() 方法判斷
public synchronized void reset() 重置讀取位置為上次 mark 標記的位置
public boolean markSupported() 判斷當前流是否支援標記流,和上面兩個方法配套使用

OutputStream 類

方法方法介紹
public abstract void write(int b) 寫入一個位元組,可以看到這裡的引數是一個 int 型別,對應上面的讀方法,int 型別的 32 位,只有低 8 位才寫入,高 24 位將捨棄。
public void write(byte b[]) 將陣列中的所有位元組寫入,和上面對應的 read() 方法類似,實際呼叫的也是下面的方法。
public void write(byte b[], int off, int len) 將 byte 陣列從 off 位置開始,len 長度的位元組寫入
public void flush() 強制重新整理,將緩衝中的資料寫入
public void close() 關閉輸出流,流被關閉後就不能再輸出資料了

再來看 Reader 和 Writer 類中的方法,你會發現和上面兩個抽象基類中的方法很像。

Reader 類

方法方法介紹
public int read(java.nio.CharBuffer target) 讀取位元組到字元快取中
public int read() 讀取單個字元
public int read(char cbuf[]) 讀取字元到指定的 char 陣列中
abstract public int read(char cbuf[], int off, int len) 從 off 位置讀取 len 長度的字元到 char 陣列中
public long skip(long n) 跳過指定長度的字元數量
public boolean ready() 和上面的 available() 方法類似
public boolean markSupported() 判斷當前流是否支援標記流
public void mark(int readAheadLimit) 標記讀取位置,下次還可以從這裡開始讀取,使用前要看當前流是否支援,可以使用 markSupport() 方法判斷
public void reset() 重置讀取位置為上次 mark 標記的位置
abstract public void close() 關閉流釋放相關資源

Writer 類

方法方法介紹
public void write(int c) 寫入一個字元
public void write(char cbuf[]) 寫入一個字元陣列
abstract public void write(char cbuf[], int off, int len) 從字元陣列的 off 位置寫入 len 數量的字元
public void write(String str) 寫入一個字串
public void write(String str, int off, int len) 從字串的 off 位置寫入 len 數量的字元
public Writer append(CharSequence csq) 追加吸入一個字元序列
public Writer append(CharSequence csq, int start, int end) 追加寫入一個字元序列的一部分,從 start 位置開始,end 位置結束
public Writer append(char c) 追加寫入一個 16 位的字元
abstract public void flush() 強制重新整理,將緩衝中的資料寫入
abstract public void close() 關閉輸出流,流被關閉後就不能再輸出資料了

下面我們就直接使用他們的子類,在使用中再介紹下面沒有的新方法。

1、讀取控制檯中的輸入

public class IOTest {
    public static void main(String[] args) throws IOException {
        // 三個測試方法
//        test01();
//        test02();
        test03();
    }

    public static void test01() throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        System.out.println("請輸入一個字元");
        char c;
        c = (char) bufferedReader.read();
        System.out.println("你輸入的字元為"+c);
    }

    public static void test02() throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        System.out.println("請輸入一個字元,按 q 鍵結束");
        char c;
        do {
            c = (char) bufferedReader.read();
            System.out.println("你輸入的字元為"+c);
        } while (c != 'q');
    }

    public static void test03() throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        System.out.println("請輸入一行字元");
        String str = bufferedReader.readLine();
        System.out.println("你輸入的字元為" + str);
    }
}
至於控制檯的輸出,我們其實一直都在使用呢,System.out.println() ,out 其實是 PrintStream 類物件的引用,PrintStream 類中當然也有 write() 方法,但是我們更常用 print() 方法和 println() 方法,因為這兩個方法可以輸出的內容種類更多,比如一個列印一個物件,實際呼叫的物件的 toString() 方法。

2、二進位制檔案的寫入和讀取

注意這裡檔案的路徑,可以根據自己情況改一下,雖然這裡的檔案字尾是txt,但該檔案卻是一個二進位制檔案,並不能直接檢視。

@Test
    public void test04() throws IOException {
        byte[] bytes = {12,21,34,11,21};
        FileOutputStream fileOutputStream = new FileOutputStream(new File("").getAbsolutePath()+"/io/test.txt");
        // 寫入二進位制檔案,直接開啟會出現亂碼
        fileOutputStream.write(bytes);
        fileOutputStream.close();
    }

    @Test
    public void test05() throws IOException {
        FileInputStream fileInputStream = new FileInputStream(new File("").getAbsolutePath()+"/io/test.txt");
        int c;
        // 讀取寫入的二進位制檔案,輸出位元組陣列
        while ((c = fileInputStream.read()) != -1) {
            System.out.print(c);
        }
    }

3、文字檔案的寫入和讀取

write() 方法和 append() 方法並不是像方法名那樣,一個是覆蓋內容,一個是追加內容,append() 內部也是 write() 方法實現的,也非說區別,也就是 append() 方法可以直接寫 null,而 write() 方法需要把 null 當成一個字串寫入,所以兩者並無本質的區別。需要注意的是這裡並沒有指定檔案編碼,可能會出現亂碼的問題。

write() 方法和 append() 方法並不是像方法名那樣,一個是覆蓋內容,一個是追加內容,append() 內部也是 write() 方法實現的,也非說區別,也就是 append() 方法可以直接寫 null,而 write() 方法需要把 null 當成一個字串寫入,所以兩者並無本質的區別。需要注意的是這裡並沒有指定檔案編碼,可能會出現亂碼的問題。
    public void test06() throws IOException {
        FileWriter fileWriter = new FileWriter(new File("").getAbsolutePath()+"/io/test.txt");
        fileWriter.write("Hello,world!\n歡迎來到 java 世界\n");
        fileWriter.write("不會覆蓋檔案原本的內容\n");
//        fileWriter.write(null); 不能直接寫入 null
        fileWriter.append("並不是追加一行內容,不要被方法名迷惑\n");
        fileWriter.append(null);
        fileWriter.flush();
        System.out.println("檔案的預設編碼為" + fileWriter.getEncoding());
        fileWriter.close();
    }

    @Test
    public void test07() throws IOException {
        FileWriter fileWriter = new FileWriter(new File("").getAbsolutePath()+"/io/test.txt", false); // 關閉追加模式,變為覆蓋模式
        fileWriter.write("Hello,world!歡迎來到 java 世界\n");
        fileWriter.write("我來覆蓋檔案原本的內容");
        fileWriter.append("我是下一行");
        fileWriter.flush();
        System.out.println("檔案的預設編碼為" + fileWriter.getEncoding());
        fileWriter.close();
    }

    @Test
    public void test08() throws IOException {
        FileReader fileReader = new FileReader(new File("").getAbsolutePath()+"/io/test.txt");
        BufferedReader bufferedReader = new BufferedReader(fileReader);
        String str;
        while ((str = bufferedReader.readLine()) != null) {
            System.out.println(str);
        }
        fileReader.close();
        bufferedReader.close();
    }

    @Test
    public void test09() throws IOException {
        FileReader fileReader = new FileReader(new File("").getAbsolutePath()+"/io/test.txt");
        int c;
        while ((c = fileReader.read()) != -1) {
            System.out.print((char) c);
        }
    }

使用位元組流和字元流的轉換類 InputStreamReader 和 OutputStreamWriter 可以指定檔案的編碼,使用 Buffer 相關的類來讀取檔案的每一行。

    public void test10() throws IOException {
        FileOutputStream fileOutputStream = new FileOutputStream(new File("").getAbsolutePath()+"/io/test2.txt");
        OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, "GBK"); // 使用 GBK 編碼檔案
        outputStreamWriter.write("Hello,world!\n歡迎來到 java 世界\n");
        outputStreamWriter.append("另外一行內容");
        outputStreamWriter.flush();
        System.out.println("檔案的編碼為" + outputStreamWriter.getEncoding());
        outputStreamWriter.close();
        fileOutputStream.close();
    }

    @Test
    public void test11() throws IOException {
        FileInputStream fileInputStream = new FileInputStream(new File("").getAbsolutePath()+"/io/test2.txt");
        InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "GBK"); // 使用 GBK 解碼檔案
        BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
        String str;
        while ((str = bufferedReader.readLine()) != null) {
            System.out.println(str);
        }
        bufferedReader.close();
        inputStreamReader.close();
    }
使用位元組流和字元流的轉換類 InputStreamReader 和 OutputStreamWriter 可以指定檔案的編碼,使用 Buffer 相關的類來讀取檔案的每一行。
@Test
    public void test10() throws IOException {
        FileOutputStream fileOutputStream = new FileOutputStream(new File("").getAbsolutePath()+"/io/test2.txt");
        OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, "GBK"); // 使用 GBK 編碼檔案
        outputStreamWriter.write("Hello,world!\n歡迎來到 java 世界\n");
        outputStreamWriter.append("另外一行內容");
        outputStreamWriter.flush();
        System.out.println("檔案的編碼為" + outputStreamWriter.getEncoding());
        outputStreamWriter.close();
        fileOutputStream.close();
    }

    @Test
    public void test11() throws IOException {
        FileInputStream fileInputStream = new FileInputStream(new File("").getAbsolutePath()+"/io/test2.txt");
        InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "GBK"); // 使用 GBK 解碼檔案
        BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
        String str;
        while ((str = bufferedReader.readLine()) != null) {
            System.out.println(str);
        }
        bufferedReader.close();
        inputStreamReader.close();
    }

4、複製檔案

這裡筆者做了一些測試,不使用緩衝對檔案複製時間的影響,檔案的複製實質還是檔案的讀寫。緩衝流是處理流,是對節點流的裝飾。

注:這裡的時間是在我這臺華碩筆記本上測試得到的,只是為了說明使用緩衝對檔案的讀寫有好處。

@Test
    public void  test12() throws IOException {
        // 輸入和輸出都使用緩衝流
        FileInputStream in = new FileInputStream("E:\\視訊資料\\大資料原理與應用\\1.1大資料時代.mp4");
        BufferedInputStream inBuffer = new BufferedInputStream(in);
        FileOutputStream out = new FileOutputStream("1.1大資料時代.mp4");
        BufferedOutputStream outBuffer = new BufferedOutputStream(out);
        int len = 0;
        byte[] bs = new byte[1024];
        long begin = System.currentTimeMillis();
        while ((len = inBuffer.read(bs)) != -1) {
            outBuffer.write(bs, 0, len);
        }
        System.out.println("複製檔案所需的時間:" + (System.currentTimeMillis() - begin)); // 平均時間約 200 多毫秒
        inBuffer.close();
        in.close();
        outBuffer.close();
        out.close();
    }


    @Test
    public void  test13() throws IOException {
        // 只有輸入使用緩衝流
        FileInputStream in = new FileInputStream("E:\\視訊資料\\大資料原理與應用\\1.1大資料時代.mp4");
        BufferedInputStream inBuffer = new BufferedInputStream(in);
        FileOutputStream out = new FileOutputStream("1.1大資料時代.mp4");
        int len = 0;
        byte[] bs = new byte[1024];
        long begin = System.currentTimeMillis();
        while ((len = inBuffer.read(bs)) != -1) {
            out.write(bs, 0, len);
        }
        System.out.println("複製檔案所需時間:" + (System.currentTimeMillis() - begin)); // 平均時間約 500 多毫秒
        inBuffer.close();
        in.close();
        out.close();
    }

    @Test
    public void test14() throws IOException {
        // 輸入和輸出都不使用緩衝流
        FileInputStream in = new FileInputStream("E:\\視訊資料\\大資料原理與應用\\1.1大資料時代.mp4");
        FileOutputStream out = new FileOutputStream("1.1大資料時代.mp4");
        int len = 0;
        byte[] bs = new byte[1024];
        long begin = System.currentTimeMillis();
        while ((len = in.read(bs)) != -1) {
            out.write(bs, 0, len);
        }
        System.out.println("複製檔案所需時間:" + (System.currentTimeMillis() - begin)); // 平均時間 700 多毫秒
        in.close();
        out.close();
    }

    @Test
    public void test15() throws IOException {
        // 不使用緩衝
        FileInputStream in = new FileInputStream("E:\\視訊資料\\大資料原理與應用\\1.1大資料時代.mp4");
        FileOutputStream out = new FileOutputStream("1.1大資料時代.mp4");
        int len = 0;
        long begin = System.currentTimeMillis();
        while ((len = in.read()) != -1) {
            out.write(len);
        }
        System.out.println("複製檔案所需時間:" + (System.currentTimeMillis() - begin)); // 平均時間約 160000 毫秒,約 2 分多鐘
        in.close();
        out.close();
    }

序列化與反序列化

序列化是將 Java 物件轉換成與平臺無關的二進位制流,而反序列化則是將二進位制流恢復成原來的 Java 物件,二進位制流便於儲存到磁碟上或者在網路上傳輸。

如何實現序列化和反序列化?

如果想要序列化某個類的物件,就需要讓該類實現 Serializable 介面或者 Externalizable 介面。

如果實現 Serializable 介面,由於該介面只是個 “標記介面”,介面中不含任何方法,序列化是使用 ObjectOutputStream(處理流)中的 writeObject(obj) 方法將 Java 物件輸出到輸出流中,反序列化是使用 ObjectInputStream 中的 readObject(in) 方法將輸入流中的 Java 物件還原出來。

下面程式演示實現 Serializable 介面,將物件序列化到檔案中,再從檔案中反序列化物件。

import java.io.Serializable;
public class Person implements Serializable {
    private String name;
    private int age;
    // 此處沒有提供無參的建構函式
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // 省略 getter 和 setter 方法
}
public class Test {
    public static void main(String[] args) {
        // 序列化
        try (ObjectOutputStream outputStream = new ObjectOutputStream(new File("").getAbsolutePath()+"/object.txt"))) {
            Person person = new Person("小明", 21);
            outputStream.writeObject(person);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 反序列化
        try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("").getAbsolutePath()+"/object.txt"))) {
            Person person = (Person) objectInputStream.readObject();
            System.out.println("name:" + person.getName() + ",age:" + person.getAge());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();

        }
    }
}

需要注意的是反序列化讀取的僅僅是 Java 物件中的資料,而不是包含 Java 類的資訊,所以在反序列化時還需要物件所屬類的位元組碼(class)檔案,否則會出現 ClassNotFoundException 異常。

如果實現 Externalizable介面,該介面繼承自 Serializable 介面,在 Java Bean 類中實現介面中的 writeExternal(out) 和 readExternal(in) 方法,需要注意的是必須提供預設的無參建構函式,否則反序列化失敗。

上面的 Java Bean 程式碼可修改為:

public class Person implements Externalizable {
    private String name;
    private int age;
    
    // 需要提供預設的無參建構函式
    public Person() {
    }

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

    // 省略 getter 和 setter 方法

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.name = in.readObject().toString();
        this.age = in.readInt();
    }
}

這兩種序列化反序列化方式,前一種是使用預設的 Java 實現,而後一種是自定義實現,可以在序列化中選擇如何序列化,比如對某個屬性加密處理。

注意序列化屬性的順序要和屬性反序列化中的順序一樣,否則在反序列化時不能恢復出原來的物件。

其實讓類實現 Serializable 介面也是可以實現自定義序列化,只但需要在類中提供下面這三個方法。

private void writeObject(java.io.ObjectOutStream out) throws IOException;
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;

writeObject() 方法的作用和上面 writeExternal() 方法類似,readObject() 方法的作用和上面 readExternal() 方法類似,而 readObjectNoData() 方法是在序列化流不完整、序列化和反序列化版本不一致導致不能正確反序列時呼叫的容錯方法。

使用預設的序列化方式,會將物件中的每個例項屬性依次進行序列化,如果某個屬性是一個類型別,那麼需要保證這個類也要是可序列化的類,否則將不能序列化該物件。在 Java 的序列化機制中,被序列化後的物件都有一個編號,多次序列化同一個物件,除了第一次真正序列化物件外,其他都是儲存一個序列化編號。這樣的機制帶來的問題就是如果在序列化一個物件後,修改了物件中的屬性,也不會生效。並不是物件中每個屬性都需要序列化的,如被 static 修飾的屬性是屬於類的,而不是隻屬於某個物件。使用預設序列化方式,是不會將這些屬性序列化的,在自定義的序列化方式中,我們也可以將這些屬性忽略掉。除此之外,可以使用 transient 關鍵字來修飾某個屬性,這樣預設的序列化方式就不會序列化該屬性了,自定義還是可以的。如果在反序列化時強行得到這些沒有被序列化的值,得到的會是預設值(0 或 null)

序列化和反序列化的版本問題

在 Java 的序列化機制中,允許給類提供一個 private static final 修飾的 SerialVersionUID 類常量,來作為類版本的代號。這樣即使類被修改了(如修改了方法),也會把修改前的類和修改後的類當成同一版本的類,序列化和反序列化照樣可以正常使用。如果我們不顯式的定義這個 SerialVersionUID,Java 虛擬機器會根據類的資訊幫我們自動生成,修改前和修改後的計算結果往往不同,造成版本不相容而發生反序列化失敗,另外由於平臺的差異性,在程式移植中也可能出現無法反序列化。強大的 IDE 工具,也都有自動生成 SeriaVersionUID 的方法,這裡就不多說了。JDK 中自帶的也有生成 SeriaVersionUID 值的工具 serialver.exe,使用serialver 類名(編譯後)命令就能生成該類的 SeriaVersionUID 值啦!

總結:

  • 序列化和反序列化的方式可以分為三種,一種是實現 Serializable 介面使用預設的序列化和反序列化方式,一種是實現 Serializable 介面但是自定義序列化和反序列化方法,另外一種是實現 Externalizable 介面,實現介面中的方法。
  • 序列化和反序列化要注意版本問題,自定義序列化和反序列化時還要注意屬性的順序要保持一致,這些都可能會導致反序列化失敗。
原文:https://www.jianshu.com/p/715659e4775f https://blog.csdn.net/panweiwei1994/article/details/78046000