1. 程式人生 > >Java 的 IO 四大基類詳解(上)

Java 的 IO 四大基類詳解(上)

1、概述
     Java 的IO通過java.io 包下的類和介面來支援,java.io包下主要包括輸入、輸出兩種流。每種輸入輸出流又可分為位元組流和字元流兩大類。
     位元組流以位元組為單位處理輸入、輸出操作;
     字元流以字元來處理輸入、輸出操作。

2、File類
      File類代表與平臺無關的檔案和目錄,他可以操作檔案或目錄,比如 File能新建、刪除、重新命名檔案和目錄,File類不能訪問檔案本身。如果要訪問檔案本身,則使用輸入/輸出流。
     下面測試一下File類的功能。

/**
 * Created by 楊Sir on 2017/10/30.
 * 該類測試了 File類的常用方法
 */
public class FileTest { public static void main(String[] args) throws IOException { //以當前路徑來建立一個File物件 File file2 = new File("."); //直接獲取檔名 System.out.println(file2.getName()); //獲取檔案相對路徑的父路徑,可能出錯,下面返回 null System.out.println(file2.getParent()); //獲取絕對路徑
System.out.println(file2.getAbsoluteFile()); //獲取上一級路徑 System.out.println(file2.getAbsoluteFile().getParent()); System.out.println("-------------------------------------------"); //以指定路徑來建立一個File物件,例如專案中的 file/file2 資料夾為指定路徑 File file0 = new File("file/file2"
); /** * 在指定的路徑下建立一個臨時檔案,,檔案存放位置 file0物件對應的路徑下 * 使用字首,系統隨機生成的隨機數和給定的字尾 作為檔名。 這是一個靜態方法 */ File tmpFile = File.createTempFile("aaa",".txt",file0); //指定JVM退出時刪除該檔案,鉤子方法 tmpFile.deleteOnExit(); System.out.println("tmpFile 被刪除..."); System.out.println("-------------------------------------------"); //以系統當前時間為新檔名來建立新檔案 File newFile = new File(System.currentTimeMillis() + ""); System.out.println("newFile物件是否存在: "+ newFile.exists()); //返回false //以指定的newFile物件來建立一個檔案 ,檔案位置在專案名下 newFile.createNewFile(); //以newFile物件建立一個目錄,因為newFile已存在,因此下面方法返回false,無法建立 System.out.println(newFile.mkdir()); /** * 使用list方法列出當前路徑下所有檔案和路徑 * 專案名下的所有檔案及路徑(不包括路徑的子路徑) */ String[] fileList = file2.list(); for(String fileName : fileList){ System.out.println(fileName); } System.out.println("-----------------------"); //listRoots()靜態方法列出所有的磁碟根路徑: C:\ D:\ E:\ F:\ File[] roots = File.listRoots(); for(File file : roots){ System.out.println(file); } } }

     需要注意的是:windows的路徑分割符為反斜線(\), 而Java中反斜線是轉義字元,因此windows下應該寫兩條反斜線(\),例如 F:\abc\test.txt。或者使用斜線(/),java支援將斜線當做平臺無關的路徑分割符。

     File類的list方法可以接收一個FilenameFilter引數,用於檔案過濾。這裡的FilenameFilter介面是一個函式式介面,裡面包含一個accept(File dir, String name)方法。舉例如下:

/**
 * Created by 楊Sir on 2017/10/30.
 * 檔案過濾器的使用
 */
public class FilenameFilterTest {
    public static void main(String[] args){
        File file = new File(".");
        /**
         * 使用Lambda表示式實現檔案過濾器
         * accept方法將對指定File的所有 子目錄或檔案進行迭代,如果方法返回true,則list()會列出該子目錄或檔案
         * 如果檔案以 .java結尾或檔案對應一個路徑,則返回true
         */
        String[] nameList = file.list(((dir, name) -> name.endsWith(".java") || new File(name).isDirectory()));
        //當前路徑(建立nameList物件的路徑)下所有的.java檔案和資料夾將被輸出
        for (String name : nameList){
            System.out.println(name);
        }
    }
}

3、理解 Java 的 IO 流
3.1  Java的 io流是實現輸入輸出的基礎,Java把不同的輸入輸出源抽象為流。正因為流的抽象,才可以通過一致的IO程式碼去讀寫不同的IO流節點。

流的分類:
1.從記憶體的角度來看
     – 輸入流:只能從中讀取資料,而不能向其寫入資料
     – 輸出流:只能向其寫入資料,而不能從中讀取資料
  java的輸入流主要由 InputStream 和 Reader作為基類,而輸出流主要以 OutputStream 和 Writer作為基類,這些基類無法建立例項。
2. 位元組流和字元流
     位元組流和字元流的區別在於所操作的資料單元不同,位元組流操作的資料單元是8位的位元組,字元流操作的資料單元是16位的字元。
     位元組流主要由 InputStream 和 OutputStream作為基類,字元流主要有 Reader 和 Writer作為基類。如下圖:
這裡寫圖片描述
3. 節點流和處理流(按流的角色分):
     從/向 一個特定的IO裝置(磁碟、網路)讀/寫資料的流,稱為節點流,當使用節點流時,程式直接連到實際的資料來源,和實際的輸入/輸出節點連線。
     處理流用於對一個已存在的流進行連線或封裝,通過封裝後的流實現讀/寫功能。
     使用處理流的好處是:只要使用相同的處理流,程式就可以採用完全相同的輸入/輸出程式碼訪問不同的資料來源,隨著處理流包裝節點流的變化,程式實際訪問的資料來源也在變化。Java用處理流包裝節點流是一個典型的裝飾器模式。

3.2 位元組流和字元流
我們先來看看InputStream 和 Reader
     InputStream 和 Reader是所有輸入流的抽象基類,他們是所有輸入流的模板,提供通用方法。
     ①. InputStream包含三個方法:
       int read(): 從輸入流讀取單個位元組,返回讀取的位元組資料,位元組資料可直接轉換為 int型別。
       int read(byte[] b): 從輸入流中最多讀取 b.length 個位元組的資料,並將其存入陣列b中,返回實際讀取的位元組數。
       int read(byte[] b, int off, int len): 從輸入流中最多讀取 len 個位元組的資料,並將其存入陣列b中,從off位置開始讀,返回實際讀取的位元組數。
     ②. Reader包含的三個方法:
       int read(): 從輸入流讀取單個字元,返回讀取的字元資料,字元資料可直接轉換為 int型別。
       int read(char[] b): 從輸入流中最多讀取 b.length 個字元的資料,並將其存入陣列b中,返回實際讀取的字元數。
       int read(char[] b, int off, int len): 從輸入流中最多讀取 len 個字元的資料,並將其存入陣列b中,從off位置開始讀,返回實際讀取的字元數。

    要注意:當read(char[] c) 或 read(byte[] b)方法返回 -1,表明到了輸入流的結束點。
     前面說過,InputStream 和 Reader都不能建立例項,那怎麼辦呢?沒關係,他們各自提供了一個用於讀取檔案的輸入流:FileInputStream 和 FileReader,他們都是節點流,會直接和指定檔案關聯。 例如使用FileInputStream 來讀取檔案本身資料。

/**
 * Created by 楊Sir on 2017/10/30.
 * 通過FileInputStream 來讀取檔案資料
 * 採用位元組讀取,註釋可能會亂碼,因為文字儲存時採用GBK編碼,一箇中文字元佔兩個位元組,如果read只讀到半個位元組,就會亂碼。
 */
public class FileInputStreamTest {

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

        //建立位元組輸入流,相當於從管道中取資料,,絕對路徑
        FileInputStream fis = new FileInputStream("D:\\絕對路徑\\FileInputStreamTest.java");
        //相對路徑
//      FileInputStream fis = new FileInputStream(".\\src\\相對路徑\\FileInputStreamTest.java");

        //建立一個長為1024個位元組的管道
        byte[] buf = new byte[1024];
        //用於儲存實際讀取的位元組數
        int hasRead = 0;
        //讀取1024位元組之後會進行輸出,然後再次讀取時,讀取剩餘的位元組,在輸出,直到沒有一個能被讀取的位元組,會返回-1。
        while ((hasRead = fis.read(buf)) > 0){
            //取出位元組,將位元組陣列轉換成字串數輸入
            System.out.println(new String(buf,0,hasRead));
        }
        //關閉檔案輸入流,放在 finally中更安全
        fis.close();
    }
}

     使用FileReader 來讀取檔案本身資料:

/**
 * Created by 楊Sir on 2017/10/30.
 */
public class FileReaderTest {
    public static void main(String[] args){

        try(
            //建立字元輸入流, JDK 1.7以後,這樣寫,系統自動關閉流
            FileReader fr = new FileReader("D:\\絕對路徑\\FileReaderTest.java");
        ){
            char[] cbuf = new char[32];  //這時需要多次呼叫read()讀取
            //儲存實際讀取的字元數
            int hasRead = 0;
            while((hasRead = fr.read(cbuf)) > 0){
                System.out.println(new String(cbuf, 0, hasRead));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

     除此之外,InputStream 和 Reader還支援如下幾個方法移動指標:
       void mark(int readAheadLimit): 在記錄指標當前位置記錄一個標記。
       boolean markSupported() : 判斷此流是否支援 mark()操作,即是否支援記錄標記。
       void reset(): 將此流的記錄指標重新定位到上一次記錄標記(mark)的位置。
       long skip(long n): 記錄指標向前移動 n個字元/位元組。

我們在來看 OutputStream 和 Writer
     他們各自提供了一個用於寫入檔案的輸入流:FileOutputStream 和 FileWriter,他們都是節點流,會直接和指定檔案關聯。
      其實OutputStream 和 Writer 也非常相似, 他們共同提供瞭如下三個方法:
        void write(int c): 將指定的位元組或字元輸出到輸出流中。
       void write(byte[]/char[] buf): 將位元組陣列或字元陣列中的資料輸出到指定輸出流中。
       void write(byte[]/char[] buf, int off, int len):將位元組陣列/字元陣列中從off位置開始,長度為len的位元組或字元輸出到輸出流中。
   因為字元流可以直接額以字元做操作單位,所以Writer可以用字串代替字元陣列。因此Writer 裡還有兩個方法:
       void write(String str): 將字串中的字元輸出到輸出流中。
       void wirte(String str , int off ,int len): 將字串中off開始,長度為len的字元輸出到輸出流中。
    下面舉例 利用FileInputStream輸入,並使用 FileOutputStream執行輸出,實現複製FileOutputStreamTest的功能。

/**
 * Created by 楊Sir on 2017/10/30.
 * 利用FileInputStream輸入,並使用 FileOutputStream執行輸出,實現複製FileOutputStreamTest的功能
 */
public class FileOutputStreamTest {
    public static void main(String[] args){
        try(
            //建立位元組輸入流
            FileInputStream fis =
                    new FileInputStream("D:\\絕對路徑\\FileOutputStreamTest.java");
            //建立位元組輸出流
            FileOutputStream fos = new FileOutputStream("FileOutputStreamTest.txt")
        ) {
            byte[] buf = new byte[1024];
            int hashead = 0;
            //迴圈取資料
            while ((hashead = fis.read(buf)) > 0) {
                //每讀取一次,寫入一次,讀多少,寫多少,,可以看到當前路徑下多了一個 FileOutputStreamTest.txt檔案
                fos.write(buf, 0, hashead);
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

   如果希望直接輸出字串內容,使用Writer會更好:

/**
 * Created by 楊Sir on 2017/10/30.
 * 當前路徑下會輸出一個 FileWriterTest.txt 檔案。
 */
public class FileWriterTest {
    public static void main(String[] args){
        try (
                //建立字元輸出流
                FileWriter fw = new FileWriter("FileWriterTest.txt"))
        {
            // "\r\n"是windows的換行符,UNIX/Linux/BSD等平臺,使用"\n"換行
            fw.write("錦瑟 - 李商隱\r\n");
            fw.write("錦瑟無端五十弦,一懸疑蛛絲年華。\r\n");
            fw.write("莊生曉夢迷蝴蝶,望帝春心託杜鵑。\r\n");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

以上介紹了4個訪問檔案的節點流的用法,4個基類使用優點繁瑣。如果希望簡化程式設計,就需要藉助處理流了。處理流後面在更新。。。