1. 程式人生 > >Java面試準備十九:IO

Java面試準備十九:IO

這裡只是為了記錄,由於自身水平實在不怎麼樣,難免錯誤百出,有錯的地方還望大家多多指出,謝謝。

  1. File類
  2. IO流簡介
  3. 位元組流
  4. 字元流
  5. 序列流
  6. 列印流
  7. 轉換流
1. File類

File類用來描述一個檔案或資料夾(資料夾也可以稱為目錄)。File類其實代表了一個抽象路徑。構建一個File類的例項並不會在機器上建立一個檔案,可以呼叫File例項的exists方法判斷檔案或目錄是否存在。

構造一個File類例項

  • new File(String pathname);
    通過將給定路徑來建立一個新File例項。

  • new File(String parent, String child);
    根據parent路徑名字串和child路徑名建立一個新File例項。parent是指上級目錄的路徑,完整的路徑為parent+child。

  • new File(File parent, String child);
    根據parent抽象路徑名和child路徑名建立一個新的File例項。
    parent是指上級目錄的路徑,完整的路徑為parent.getPath()+child。

如果指定的路徑不存在(沒有這個檔案或是資料夾),不會拋異常,這時file.exists()返回false。

File物件沒有無引數的構造方法,建立物件需要傳參。

示例如下:

File file = new File("f:\\a.txt");
//判斷檔案是否存在
System.out.println(file.exists());

路徑分隔符

上下級資料夾之間使用分隔符分開:
在windows中分隔符為’\’
為了能讓程式碼可以跨平臺執行,應該使用File.separatorChar來獲取當前系統的資料夾分隔符,示例如下:

new File("c:"+File.separatorChar+"a.txt");

注:如果是使用”\”,則需要進行轉義,要書寫為”\”,如果是兩個”\”,則寫為4個斜槓”\\”。

File類中常用的方法

建立:

  • createNewFile() 在指定為之建立一個空檔案,成功就返回一個空檔案,如果已存在就不建立然後返回false(注意如果已存在的話是不會再建立覆蓋舊的檔案的)

  • mkdir()

    在指定位置建立目錄,這隻會建立最後一級目錄,如果上級目錄不存在就丟擲異常。

  • mkdirs() 在指定位置建立目錄,這會建立路徑中所有不存在的目錄。

  • renameTo(File dest)重新命名檔案或資料夾(在同一個目錄下),可以操作非空的資料夾;當檔案/資料夾不在同一個路徑下時,相當於剪下,不可以操作非空的資料夾。重新命名、剪下成功後返回true,否則返回false。

刪除:

  • delete()刪除檔案或一個空資料夾,如果資料夾不為空,則不能刪除,成功刪除返回true,失敗返回false。

判斷:

  • exists()檔案或資料夾是否存在。

  • isFile()是否是一個檔案,如果不存在,則始終為false

  • isDirectory()是否是一個目錄,如果不存在,則始終為false。

  • isHidden()是否是一個隱藏的檔案或是是否是隱藏的目錄。

  • isAbsolute()測試此抽象路徑是否為絕對路徑名。

獲取:

  • getName()獲取檔案或資料夾的名稱,不包含上級路徑。

  • getPath()返回絕對路徑,可以是相對路徑,但是目錄要指定

  • getAbsolute()獲取檔案的絕對路徑,與檔案是否存在沒有關係

  • length()返回檔案的大小(位元組數),如果檔案不存在則返回0L,如果是資料夾返回0L。

  • getParent()返回此抽象路徑名父目錄的路徑名字串;如果此路徑名沒有指定目錄,則返回null。

  • static File[[] listRoots()列出所有的根目錄(Windows中就是所有系統的碟符)

  • list()返回目錄下的檔案或目錄名,包含隱藏檔案。對於檔案這樣操作會返回null。

  • listFiles()返回目錄下的檔案或目錄物件(File類例項),包含隱藏檔案。對於檔案這樣操作會返回null。

  • listFiles(FilenameFilter filter)返回指定當前目錄中符合果略條件的子檔案或子目錄。對於檔案這樣操作會返回null。

2. IO流

IO流用來處理裝置之間的資料傳輸。裝置是指硬碟、記憶體、鍵盤錄入、網路等。

IO流的分類:

  • 按操作資料型別的不同分為:位元組流和字元流
  • 按流向分為:輸入流,輸出流(以程式為參照物,輸入到程式的則是輸入流,從程式輸出的稱為輸出流)
3. 位元組流

計算機中都是二進位制資料,一個位元組是8個2進位制位。位元組可以表示所有的資料,比如文字,音訊,視訊,圖片,都是作為位元組存在的,也就是說位元組流處理的資料非常多。二進位制檔案是為了讓程式讀取而設計的。二進位制檔案的優勢在於它的處理效率比文字檔案高。

位元組流的抽象基類:
輸入流:java.io.InputStream
輸出流:java.io.OutputStream

特點:
位元組流的抽象基類派生出的子類的命名都是以它的基類名字為字尾的。如:FileInputStream, FileOutputStream

說明:
位元組流處理的單元是一個位元組,用於操作二進位制檔案(計算機中所有的檔案都是二進位制檔案)

InputStream
InputStream(基類)
——| FileInputStream (實現類)

FileInputStream

  • read( ) 一次讀取一個位元組,讀到檔案末尾返回-1

  • read(byte[] b)使用緩衝區(關鍵是緩衝區大小的確定),將讀到的資料裝入位元組資料中,再將陣列中的資料一次性返回,可以提高操作效率。返回的是讀取到位元組陣列的陣列長度。

  • read(byte[] b, int off, int len)
    b是一個位元組陣列,當做容器來使用
    off,是指定從陣列的什麼位置開始村位元組
    len, 希望存多少個

使用輸入流物件步驟
(1)開啟流(即建立流)
(2)通過流讀取內容
(3)用完後,關閉流

示例1:
根據read()方法返回值的特性,如果讀到檔案的末尾返回-1,如果不為-1,就繼續向下讀。

public class Demo {

    @Test
    private  void test1() throws IOException {
       File file = new File("f:\\a.txt");
       InputStream fis = new FileInputStream(file);
       int content;
       while((content=fis.read())!=-1){
           System.out.print((char)content);
       }
       fis.close();
    }   
}

結果:
AAABBB

示例2:
將緩衝區大小設定為1024

@Test
    public void test2() throws IOException{
        File file = new File("f:\\a.txt");
        FileInputStream fis = new FileInputStream(file);

        byte[] b = new byte[1024];
        int len;
        while((len=fis.read(b))!=-1){
            System.out.println(new String(b, 0, len));
        }
    }

結果:
AAABBB

OutputStream
OutputStream(基類)
——| FileOutputStream (實現類)

OutputStream

  • 常用構造方法:
    • FileOutputStream(File file)
    • FileOutputStream(File file, boolean append)
      append表示是否是追加內容
  • write(int b) 一次寫出一個位元組

  • write(byte[] b)先將要輸出的位元組儲存到陣列中,再一次性輸出。

輸出流使用步驟:
(1)開啟檔案輸出流,流的目的地是指定的檔案
(2)通過流向檔案寫資料
(3)用完流後關閉流

示例1

@Test
    public void test3() throws Exception {
        File file = new File("f:\\out.txt");
        OutputStream fos = new FileOutputStream(file);
        fos.write('m');
        fos.write('o');
        fos.write('s');
        fos.write('s');
        fos.close();
    }

如果路徑為”f:\out.txt”檔案不存在的時候,會自動建立,如果是多層目錄,而且父目錄不存在時,會報錯FileNotFoundException:系統找不到對應的路徑。

注意:使用write(int b)方法,雖然接收的是int型別引數,但是write的常規協定是:向輸出流寫入一個位元組。要寫入的位元組是引數的8個低位。b的24個高位將被忽略。

示例2:使用緩衝區輸出

@Test
    public void test4() throws Exception {
        File file = new File("f:\\out.txt");
        OutputStream fos = new FileOutputStream(file);
        fos.write("hello, moss".getBytes());
        fos.close();
    }

字串可以通過getBytes()轉換為位元組陣列。

示例3:用位元組陣列作為緩衝區實現檔案複製

    @Test
    public void test5() throws Exception {
        File resFile = new File("f:\\a.txt");
        File desfile = new File("f:\\out.txt");

        InputStream fis = new FileInputStream(resFile);
        OutputStream fos = new FileOutputStream(desfile);

        byte[] b = new byte[1024];
        int len;
        while((len=fis.read(b))!=-1){
            fos.write(b);
        }   

        //關閉流
        fis.close();
        fos.close();
    }

注意:當檔案位元組數大於1024需要再次讀取資料存入到位元組資料時,這時並沒有對上次的位元組陣列進行清空,而是覆蓋上次的陣列 。

位元組流的異常處理
上述案例中所有的案例都只是進行了丟擲處理,這樣是不合理的。所以上述程式碼並不完善,因為異常沒有處理。

當我們開啟流,讀和寫,關閉流的時候都會出現異常,異常出現後,後面的程式碼都不會執行了。假設開啟和關閉流出現了異常,那麼顯然close方法就不會再執行。這時就會出現其他程式也不能操作這個檔案的情況。那麼就要使用try{}catch{}finally{}語句。try中放入可能會出現錯誤的語句,catch是捕獲異常物件,finally是一定要執行的程式碼

示例如下:

    @Test
    public void test5(){
        InputStream fis = null;
        OutputStream fos = null;
        try {
            File resFile = new File("f:\\a.txt");
            File desfile = new File("f:\\out.txt");

            fis = new FileInputStream(resFile);
            fos = new FileOutputStream(desfile);

            byte[] b = new byte[1024];
            int len;
            while((len=fis.read(b))!=-1){
                fos.write(b);
            }
        }catch (IOException e) {
            throw new RuntimeException(e);
        }finally{ //關閉流
            try {
                if(fis!=null){
                    fis.close();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }finally{
                try {
                    if(fos!=null){
                        fos.close();
                    }
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

注意:在最後的close程式碼中可能會有問題,兩個close,如果第一個close方法出現異常,並丟擲了執行時異常,那麼第二個close還是不會被執行到。所以需要把第二個close放到捕獲第一個close異常的finally塊裡面。

位元組緩衝流

上述程式中我們為了提高流的使用效率,自定義了位元組陣列,作為緩衝區。Java其實提供了專門的位元組流緩衝來提高效率:
BufferedInputStream和BufferedInputStream

BufferedInputStream和BufferedInputStream類可以通過減少讀寫次數來提高輸入和輸出的速度。它們內部有一個緩衝區,用來提高處理效率。可以指定緩衝區的大小。其實內部也是封裝了位元組陣列。沒有指定緩衝區大小,預設的位元組是8192。對於緩衝輸出流,或是呼叫flush方法,或是呼叫close方法,來將緩衝區的資料輸出。

注意:當然使用緩衝區來進行提高效率時,對於小檔案可能看不到效能的提升。但是檔案稍微大一些的話,就可以看到實質的效能提升了。

BufferedInputStream

BufferedInputStream初始化了一個byte陣列作為記憶體緩衝區,大小可以由構造方法中的引數指定,也可以是預設的大小。它的構造方法原始碼如下:

protected volatile byte buf[];
private static int defaultBufferSize = 8192;

public BufferedInputStream(InputStream in, int size){
   super(in);
   if(size<=0){
       throw new IllegalArgumentException("Buffer size <= 0");
   }
   buf = new byte[size];
}

public BufferedInputStream(InputStream in){
  this(in, defaultBufferSize);
}

看完建構函式,大概可以瞭解其實現原理:通過初始化分配一個byte陣列,一次性從輸入位元組流中讀取多個位元組的資料放入byte陣列,程式讀取部分位元組的時候直接從byte陣列中獲取,直到記憶體中的資料用完再重新從流中讀取新的位元組。

示例如下:

@Test
    public void test6() throws Exception {
        File infile = new File("f:\\a.txt");
        File outFile = new File("f:\\b.txt");

        InputStream fis = new FileInputStream(infile);
        OutputStream fos = new FileOutputStream(outFile);

        BufferedInputStream bis = new BufferedInputStream(fis);
        BufferedOutputStream bos = new BufferedOutputStream(fos);

        int len;
        while((len=bis.read())!=-1){
            bos.write(len);
        }

        bis.close();
        bos.close();    
    }

注:上面的bufferedinputstream的read方法返回的是從緩衝位元組流讀取的一個位元組,而fileinputstream的read(byte[] b)方法返回的是從輸入位元組流中讀取存放到位元組陣列的位元組個數。

4. 字元流

計算機並不區分二進位制檔案與文字檔案。所有的檔案都是以二進位制形式來儲存的,因此,從本質上說,所有的檔案都是二進位制檔案。所以字元流是建立在位元組流之上的,它能夠提供字元層次的編碼和解碼。例如,在寫入一個字元時,Java虛擬機器會將字元轉為檔案制定的編碼(預設是系統預設編碼),在讀取字元時,再將檔案指定的編碼轉化為字元。

常見的碼錶如下:

ASCII: 美國標準資訊交換碼。用一個位元組的7位可以表示。

ISO8859-1:拉丁碼錶。歐洲碼錶,用一個位元組的8位表示。ASCII碼是包含的僅僅是英文字母,並且沒有完全佔滿256個編碼位置,所以它以ASCII為基礎,在空置的0xA0-0xFF的範圍內,加入192個字母及符號,來供使用變音符號的拉丁字母語言使用。因而它依然是一個單位元組編碼,只是比ASCII更全面。

GBK:中國的中文編碼表。英文佔一個位元組,中文佔兩個位元組。

Unicode:國際標準碼規範,融合了多種文字。所有文字都用兩個位元組來表示,Java語言使用的就是unicode。

UTF-8:萬國碼,推行的。1~3個位元組不等長。英文存的是1個位元組,中文存的是3個位元組,是為了節省空間。

等等

我們接觸最多的是iso8859-1, gbk, utf-8。

為了防止編碼解碼出錯,一般要求使用同樣的碼錶對資料進行編碼和解碼。

為什麼要用字元流,先看下面一個例子:

/**
     * 使用FileOutputStream向test7.txt輸出"同一個世界同一個夢想",再使用FileInputStream從檔案test7.txt中讀取資料
     */
    @Test
    public void test7() throws Exception {
        File file = new File("f:\\test7.txt");

        OutputStream fos = new FileOutputStream(file);
        fos.write("同一個世界同一個夢想".getBytes());

        InputStream fis = new FileInputStream(file);
        int content;
        while((content=fis.read())!=-1){
            System.out.print((char)content+" ");
        }                   
    }

因為當前系統為中文系統,預設碼錶為GBK,一箇中文用兩個位元組來表示,上面程式碼是每讀一個位元組便轉換為字元,所以並不會將位元組正確轉換為字元。解決方法:可以將陣列一次性全部讀取出來,再通過String的解碼功能,將位元組陣列解碼轉換為字串。如下:

@Test
    public void test8() throws Exception {
        File file = new File("f:\\test7.txt");

        OutputStream fos = new FileOutputStream(file);
        fos.write("同一個世界同一個夢想".getBytes());

        InputStream fis = new FileInputStream(file);
        int len;
        byte[] b = new byte[1024];
        while((len=fis.read(b))!=-1){
            System.out.println(new String(b, 0, len));
        }
    }

很顯然,我們的中文就不能夠再一個位元組一個位元組地讀了。所以位元組流處理字元資訊時並不方便,那麼就出現了字元流。

其實字元流就是:位元組流+編碼表,為了更便於操作文字資料。字元流的抽象基類:Reader,Writer。由這些類派生出來的子類名稱都是以其父類名作為子類名的字尾,如FileReader、FileWriter。

Reader

Reader也是抽象類,所以想要使用字元輸入流需要使用Reader的實現類。

Reader(父類)
——| FileReader(基類)

FileReader

  • int read():讀取一個字元。返回的是讀到的那個字元。如果讀到流的末尾,返回-1。

  • int read(char[]):將讀到的字元存入到指定的陣列中,返回的是讀到的字元個數,也就是往數組裡裝的元素的個數。如果讀到流的末尾,返回-1。

  • close():讀取字元其實用的是window系統的功能,就希望使用完畢後,進行資源的釋放。

示例如下:

@Test
    public void test9() throws Exception {
        File file = new File("f:\\a.txt");
        Reader fileReader = new FileReader(file);
        int content;
        while((content=fileReader.read())!=-1){
            System.out.print((char)content);
        }
        fileReader.close();
    }

示例2:

/**
     * 使用緩衝區讀取資料
     */
    @Test
    public void test10() throws Exception {
        File file = new File("f:\\a.txt");
        Reader fr = new FileReader(file);
        char[] b = new char[1024];
        int len;
        while((len=fr.read(b))!=-1){
            System.out.print(new String(b, 0, len));
        }
    }

Writer

Writer
——| FileWriter

和Reader一樣,Writer也是一個抽象類,所以想要使用字元輸出流需要使用Writer的實現類。

FileWriter

  • writer(ch):將一個字元寫入到流中。

  • writer(char[]):將一個字元陣列寫入到流中。

  • writer(String):將一個字串寫入到流中

  • flush():重新整理流,將流中的資料重新整理到目的地中,流還存在。

  • close():關閉資源,在關閉前會先呼叫flush(),重新整理流中的資料到目的地。然後關閉。

示例如下:

@Test
    public void test11() throws Exception {
        File file = new File("f:\\b.txt");
        Writer fw = new FileWriter(file);

        fw.write('中');
        fw.write('國');
        fw.write("同一個世界 同一個夢想");

        fw.close();     
    }

追加檔案

預設的FileWriter方法新值會覆蓋舊值,想要實現追加功能需要使用如下建構函式建立輸出流append值為true即可。
FileWriter(String fileName, boolean append)
FileWriter(File file, boolean append)

注意:計算機中的所有資訊都是以二進位制形式進行的儲存(0101)圖片中的也都是二進位制在讀取檔案的時候字元流自動對這些二進位制按照碼錶進行了編碼處理,但是圖片本來就是二進位制檔案,不需要進行編碼。有一些巧合在碼錶中有對應,就可以處理,並不是所有的二進位制都可以找到對應的,資訊就會丟失。所以字元流只能拷貝以字元為單位的文字檔案(以ASCII碼為例是127個,並不是所有的二進位制都可以找到對應的ASCII,有些對不上的,就會丟失資訊)

字元流的異常處理

參考位元組流的異常處理,處理方式是一樣的。

字元流的緩衝區
Reader有一個子類BufferedReader。BufferedReader類提供了一個一次讀一行的方法:readLine(),如下:

/**
     * 將a.txt的內容拷貝到b.txt
     */
    @Test
    public void test12() throws Exception {
        File infile = new File("f:\\a.txt");
        File outFile = new File("f:\\b.txt");

        Reader fileReader = new FileReader(infile);
        Writer fileWriter = new FileWriter(outFile);

        BufferedReader br = new BufferedReader(fileReader);
        BufferedWriter bw = new BufferedWriter(fileWriter);

        String line;
        while((line=br.readLine())!=null){//每次讀一行
            bw.write(line);
            bw.flush();//重新整理緩衝
            bw.newLine();//因為bufferedReader在讀取一行時不會讀取最後的換行資料,所以需要手動輸出換行
        }   
        br.close();
        bw.close();
    }

注意:在使用緩衝區物件是,要明確,緩衝的存在是為了增強流的功能而存在,所以在建立緩衝區物件時,要先有流物件存在。

5. 序列流

也稱為合併流。

SequenceInputStream

序列流,對多個流進行合併。

SequenceInputStream表示其他輸入流的邏輯串聯。它從輸入流的有序集合開始,並從第一個輸入流開始讀取,直到到達檔案末尾,接著從第二個輸入流讀取,依此類推,直到到達包含的最後一個輸入流的檔案末尾為止。

建構函式:
SequenceInputStream(InputStream s1, InputStream s2)

示例1:合併兩個流把它們寫入到一個檔案裡面去

@Test
    public void test13() throws Exception {
        File file1 = new File("f:\\a.txt");
        File file2 = new File("f:\\b.txt");

        File outFile = new File("f:\\c.txt");

        InputStream fis1 = new FileInputStream(file1);
        InputStream fis2 = new FileInputStream(file2);
        OutputStream fos = new FileOutputStream(outFile);

        SequenceInputStream sis = new SequenceInputStream(fis1, fis2);
        byte[] b = new byte[1024];
        int len;
        while((len=sis.read(b))!=-1){
            fos.write(b, 0, len);
        }   
        sis.close();
        fos.close();
    }

示例2:合併多個流

/*
     * 合併多個流
     */
    @Test
    public void test14() throws Exception { 
        InputStream in1 = new FileInputStream("f:\\a.txt");
        InputStream in2 = new FileInputStream("f:\\b.txt");
        InputStream in3 = new FileInputStream("f:\\c.txt");

        OutputStream fos = new  FileOutputStream("f:\\d.txt");
//      File outFile = new File("f:\\d.txt");
//      OutputStream fos = new FileOutputStream(outFile);

        LinkedHashSet<InputStream> set = new LinkedHashSet<InputStream>();
        set.add(in1);
        set.add(in2);
        set.add(in3);
        final Iterator<InputStream> it = set.iterator();

        SequenceInputStream sis = new SequenceInputStream(new Enumeration<InputStream>() {

            @Override
            public boolean hasMoreElements() {
                return it.hasNext();
            }

            @Override
            public InputStream nextElement() {
                return it.next();
            }

        });

        byte[] b  = new byte[1024];
        int len;
        while((len=sis.read(b))!=-1){
            fos.write(b, 0, len);
        }

        sis.close();
        fos.close();
    }
6. 物件的序列化

當建立物件時,程式執行時它就會存在,但是當程式停止時,物件也就消失了。但是如果希望物件在程式不執行的情況下仍能存在並儲存其資訊,將會非常有用,物件將被重建並且擁有與程式上次執行時擁有的資訊相同。可以使用物件的序列化。

物件的序列化:將記憶體中的物件直接寫入到檔案裝置中。
物件的反序列化:將檔案裝置中持久化的資料轉換為記憶體物件

ObjectOutput物件、ObjectInput物件可以實現物件的序列化和反序列化:

ObjectOutput

  • writeObject(Object obj)將物件寫入底層儲存或流

ObjectInput

  • readObject(Object obj)讀取並返回物件

由於上述ObjectOutput和ObjectInput是介面,所以需要使用具體實現類。
ObjectectOutput
——| ObjectOutputStream 被寫入的物件必須實現一個介面:Serializable,否則會丟擲:NotSerializableException

ObjectInput
——| ObjectInputStream 該方法丟擲異常:ClassNotFoundException

ObjectOutputSteam 和ObjectInputStream物件分別需要位元組輸出流和位元組輸入流物件來構建物件。也就是這兩個流物件需要操作已有物件將物件進行本地持久化儲存。

案例:
序列化和反序列Cat物件

class Cat implements Serializable{//注意物件要實現Serializable介面,Serializable是一個標誌介面,介面中沒有欄位和方法,即不需要實現任何方法
    private String name;
    private int age;

    public Cat(){

    }

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

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

}
public class Demo2 {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        File file = new File("f:\\a.txt");
        OutputStream fos = new FileOutputStream(file);
        InputStream fis = new FileInputStream(file);

        Cat cat = new Cat("miaomiao", 18);
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(cat);//將cat序列化並寫入到流中        

        ObjectInputStream ois = new ObjectInputStream(fis);
        Cat cat1 = (Cat)ois.readObject();//讀取流並將物件反序列化

        System.out.println(cat1);

        /**
        *關閉流
        */
        oos.close();
        ois.close();
    }   
}

Serializable
類通過實現java.io.Serializable介面以啟用其序列化功能。未實現此介面的類將無法使其任何狀態序列化或反序列化。可序列化的所有子類本身都是可序列化的。序列化介面沒有方法或欄位,僅用於標識可序列化的語義。

如果物件沒有實現Serializable,在進行序列化時會丟擲:NotSerializableException異常。

注意:
儲存一個物件的真正含義是什麼?如果物件的例項變數都是基本資料型別,那麼就非常簡單。但是如果例項變數是包含物件的引用,會怎麼樣?儲存的會是什麼?很顯然在Java中儲存引用變數的實際值沒有任何意義,因為Java引用的值是通過JVM的單一例項的上下文中才有意義。通過序列化後,嘗試在JVM的另一個例項中恢復物件,是沒有用處的。(半知半解吧。。)

首先建立一個Dog物件,也建立一個Collar物件。Dog中包含了一個Collar(項圈)現在想要儲存Dog物件,但是Dog中有一個Collar,意味著儲存Dog時也應該儲存Collar。假如Colloar也包含了其他物件的引用,那麼會發生什麼?意味著儲存一個Dog物件需要清楚的知道Dog物件的內部結構。會是一件很麻煩的事情。

Java的序列化機制可以解決該類問題,當序列化一個物件時,Java的序列化機制會負責儲存物件的所有關聯的物件(就是物件圖),反序列化時,也會恢復所有的相關內容。本例中:如果序列化Dog會自動序列化Collar。但是,只有實現了Serializable介面的類才可以序列化。如果只是Dog實現了該介面,而Collar沒有實現該介面。會發生什麼?

class Dog implements Serializable{
    private Collar collar;
    private String name;

    public Dog(Collar collar, String name){
        this.collar = collar;
        this.name = name;
    }

     public Collar getCollar(){
         return this.collar;
     }
}
class Collar{
    private int size;

    public Collar(int size){
        this.size=size;
    }
}
public class Demo3 {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Collar collar = new Collar(3);
        Dog dog = new Dog(collar, "susu");

        File file = new File("f:\\a.txt");
        OutputStream fos = new FileOutputStream(file);
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(dog);

        InputStream fis = new FileInputStream(file);
        ObjectInputStream ois = new ObjectInputStream(fis);
        Dog dog1 = (Dog)ois.readObject();
        System.out.println(dog1);

        //關閉流
        oos.close();
        ois.close();            
    }
}

執行程式,這時會報錯: java.io.NotSerializableException:

所以,我們也必須將Dog中的Collar序列化。但是如果我們無法訪問Collar的原始碼使其可序列化時,那怎麼辦?

使用transient修飾符

將Dog類中的成員變數標誌為transient,那麼在序列化Dog時,序列化就會跳過Collar。

class Dog implements Serializable{
    private transient Collar collar;
    private String name;

    public Dog(Collar collar, String name){
        this.collar = collar;
        this.name = name;
    }

    public Collar getCollar(){
        return this.collar;
    }
}
class Collar{
    private int size;

    public Collar(int size){
        this.size=size;
    }

    @Override
    public String toString() {
        return "Collar [size=" + size + "]";
    }   
}
public class Demo3 {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Collar collar = new Collar(3);
        Dog dog = new Dog(collar, "susu");

        File file = new File("f:\\a.txt");
        OutputStream fos = new FileOutputStream(file);
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(dog);

        InputStream fis = new FileInputStream(file);
        ObjectInputStream ois = new ObjectInputStream(fis);
        Dog dog1 = (Dog)ois.readObject();
        Collar collar1 = (Collar)ois.readObject();
        System.out.println(dog1);
        System.out.println(collar1);

        //關閉流
        oos.close();
        ois.close();            
    }
}

這樣我們具有一個序列化的Dog和非序列化的Collar。
此時反序列化Dog後,訪問Collar,就會出現執行時異常:Exception in thread “main” java.lang.NullPointerException

注意:序列化不適用於靜態變數,因為靜態變數並不屬於物件的例項變數的一部分。靜態變數隨著類的載入而載入,是類變數。

基本資料型別可以被序列化:

public class Demo4 {

    public static void main(String[] args) throws IOException {
        File file = new File("f:\\a.txt");
        OutputStream fos = new FileOutputStream(file);
        ObjectOutputStream oos =  new ObjectOutputStream(fos);
        oos.writeInt(1);
        oos.writeInt(2);
        oos.writeDouble(123.45);
        oos.writeBoolean(true);

        //關閉流
        oos.close();

        //反序列化
        InputStream fis = new FileInputStream(file);
        ObjectInputStream ois = new ObjectInputStream(fis);
        System.out.println(ois.readInt());
        System.out.println(ois.readInt());
        System.out.println(ois.readBoolean());
        //關閉流
        ois.close();
    }
}

serialVersionUID

(1)用於給類指定一個UID。該UID是通過類中的可序列化成員的數字簽名運算出來的一個long型的值。

(2)只要這些成員沒有變化,那麼該值每次運算都一樣。

(3)該值用於判斷被序列化的物件和類檔案是否相容。

(4)如果被序列化的物件需要被不同的類版本所相容。可以在類中自定義UID。

(5)定義方式:static final long serialVersionUID=42L;

7. Properties

Propertiest是一個可以和流相關聯的集合物件。Properites是Map的一個子類。
Map
——| HashTable
——|——| Properties(所以Properties也是執行緒安全的?)
Properties:該集合不需要泛型,因為該集合中的鍵值對都是String型別。

該類的操作有:

(1)存入鍵值對:setProperty(key, value)

(2)獲取指定鍵對應的值:value getProperty(key)

(3)獲取集合中所有鍵元素:
Enumeration propertyNames();
在jdk1.6版本中給該類提供一個新的方法。
Set<String> stringPropertyNames();

(4)列出該集合中的所有鍵值對,可以通過引數列印流指定列出到的目的地:
list(PrintStream)
list(PrintWriter)
例:list(System.out):將集合中的鍵值對列印到控制檯。
list(new PrintStream(”prop.txt”)):將集合中的鍵值對儲存到prop.txt檔案中。

(5)可以將流中的規則資料載入進行集合,並稱為鍵值對。
load(InputStream)
jdk1.6版本。提供了新的方法。
load(Reader)
注意: 流中的資料要是“鍵=值”的規則資料。

(6)可以將集合中的資料進行指定目的的儲存
store(OutputStream,String comment)方法
jdk1.6版本,提供了新的方法。
store(Writer, String comment);
使用該方法儲存時,會帶著當時儲存的時間。
**注意:**Properties只加載key=value這樣的鍵值對,與檔名無關,註釋使用#

示例1:記錄一個程式執行的次數,當滿足指定次數時,該程式就不可以再繼續運行了。

public class Demo5 {

    public static void main(String[] args) throws FileNotFoundException, IOException {
        Properties prop= new Properties();
        File file = new File("f:\\sys.txt");
        if(!file.exists()){
            file.createNewFile();
        }
        prop.load(new FileInputStream(file));
        String times = prop.getProperty("times");//獲取鍵為“times”的值
        if(times==null){
            prop.setProperty("times", "1");//設定鍵值對值
        }else{
            int n = Integer.parseInt(times);
            if(n==3){
                System.out.println("你的試用次數已達3次,如需繼續使用該軟體請購買我們的軟體。");
                System.exit(0);
            }
            //將鍵值對儲存到檔案中去
            prop.setProperty("times", String.valueOf(n+1));
        }
        prop.store(new FileOutputStream(file), "");
    }
}
8. 列印流

PrintStream可以接受檔案和其他位元組輸出流,所以列印流是對普通位元組資料流的增強,其中定義了很多的過載的print()和println(),方便輸出各種型別的資料。

PrintStream
是一個位元組列印流,System.out對應的型別就是PrintStream。它的建構函式可以接受三種資料型別的值。

  • 字串路徑
  • File物件
  • OutputStream

    列印流的三種方法:

  • void print(資料型別 變數) 不換行

  • void println(資料型別 變數) 換行
  • printf(String format, Object… args) 可以自定義資料格式

注意: print方法和write方法的區別在於,print提供自動重新整理。
普通的write方法需要呼叫flush或者close方法才可以看到資料。Jdk1.5之後Java對PrintStream進行擴充套件,增加了格式化輸出方法,可以使用printf()過載方法直接格式化輸出。但是在格式化輸出的時候需要指定輸出的資料型別格式。

No. 字元 描述
1 %s 表示內容為字串
2 %d 表示內容為整數
3 %f 表示內容為小數
4 %c 表示內容為字元

示例如下:

public class Demo8 {

    public static void main(String[] args) throws FileNotFoundException {
        PrintStream ps = System.out;

        //普通write方法需要呼叫flush或者close方法才會在控制檯顯示
        //ps.write(100);
        //ps.close();

        //不換行列印
        ps.print(100);
        ps.print('a');
        ps.print(100.5);
        ps.print("世界");
        ps.print(new Object());

        //換行
        ps.println(100);
        ps.println('a');
        ps.println(100.5);
        ps.println(new Object());

        //重定向列印流
        PrintStream ps2 = new PrintStream(new File("f:\\a.txt"));
        System.setOut(ps2);
        ps2.println(100);
        ps2.println('a');


        //printf();格式化
        ps2.printf("%d, %f, %c, %s",100, 3.14, '中', "世界你好");        
    }
}

PrintWriter
是一個字元列印流。建構函式可以接受四種類型的值。

  • 字串路徑
  • File物件
  • OutputStream
  • Writer

    注意: 對於字串路徑和File物件型別的資料,還可以指定編碼表。也就是字符集。對於OutputStream、Writer型別的資料,可以指定自動重新整理。該自動重新整理值為true時,只有三個方法可以用:println, printf, format

示例如下:

public class Demo8 {

    public static void main(String[] args) throws FileNotFoundException {

        PrintWriter pw = new PrintWriter(new File("f:\\a.txt"));
        pw.println(false);
        pw.println(100);
        pw.println("同一個世界");

        pw.close();
    }   
}

既然PrintStream和PrintWriter都可以列印各種型別的資料,那二者之間的區別在哪裡

9. 轉換流

InputStreamReader可以包裝我們的位元組流,自動的完成位元組流編碼和解碼的工作。該流是一個Reader的子類,是字元流的體系。所以該轉換流稱之為位元組流和字元流之間的橋樑。

測試InputStreamReader:
第一步:需要專門新建以GBK編碼的文字檔案,為了便於標識,命名為gbk.txt和以utf-8編碼的文字檔案,命名為utf.txt

第二步:分別寫入漢字”中國”

第三步:編寫測試方法,用InputStreamReader分別使用系統預設編碼,GBK, UTF-8編碼讀取檔案。

public class Demo8 {

    public static void main(String[] args) throws IOException {

        File file = new File("f:\\gbk.txt");
        InputStream fileInputStream = new FileInputStream(file);

        //以系統預設碼錶進行解碼,因為資料編碼解碼的碼錶都是gbk,所以不會出現亂碼
//      InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream);

        //用utf-8碼錶來進行解碼,這個時候會出現亂碼
        InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "utf-8");      

        int content;
        while((content=inputStreamReader.read())!=-1){
            System.out.print((char)content);
        }
        inputStreamReader.close();


    }   
}
public class Demo8 {

    public static void main(String[] args) throws IOException {

        File file = new File("f:\\utf-8.txt");
        InputStream fileInputStream = new FileInputStream(file);

        //以系統預設碼錶進行解碼,因為資料編碼碼錶是utf-8,而系統預設碼錶是gbk,編碼解碼碼錶不一致,會出現亂碼的情況
        InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream);

        //用utf-8碼錶來進行解碼,因為資料編碼解碼的碼錶都是utf-8,所以不會出現亂碼的現象
//      InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "utf-8");      

        int content;
        while((content=inputStreamRead