1. 程式人生 > 實用技巧 >I/O流

I/O流

基本概念

什麼是 I/O?

通過 I/O 可以完成硬碟檔案的讀和寫。

概念圖:輸入、輸出是相對於記憶體而言的,以記憶體為參照物。

1、I/O 流的分類方式:

  • 按照流的方向進行分類:

    以記憶體為參照物,往記憶體中去稱為輸入(Input)或者讀(Read)。

  • 按照讀取資料方式不同進行分類:

    有的流是按照位元組的方式讀取資料,一次讀取 1 個位元組(byte)= 8 bit。這種流是萬能的,什麼型別檔案都可以讀取(文字檔案、音訊檔案、圖片檔案等等)。

  • 按照字元的方式讀取資料,一次讀取一個字元,這種流是為了方便讀取普通文字檔案(能用記事本編輯的檔案,比如java檔案、txt 檔案等)而存在的,不能讀取其他型別的檔案,甚至 word 檔案也無法讀取( word 檔案不是普通文字)。

示例:a中國bc張三fe

a等字元在 windows 佔用一個位元組(但是在Java中佔用兩個位元組),等漢字在 windows 佔用兩個位元組。

按位元組讀:

第一次:一個位元組,讀到a

第二次:一個位元組,讀到字元的一半;

……

按字元讀:

第一次:一個字元,讀到a

第二次:一個字元,讀到

……

2、I/O 四大流:(都是抽象類)

java.io.InputStream:位元組輸入流

java.io.OutputStream:位元組輸出流

java.io.Reader:字元輸入流

java.io.Writer:字元輸出流

所有的都實現了java.io.Closeable介面,都有close()

方法,都是可關閉的,流相當於記憶體和硬碟之間的管道用完後要及時關閉,否則會佔用很多資源。

所有的輸出流都實現了java.io.Flushable介面,都是可重新整理的,都有flush()方法表示將通道當中剩餘未輸出的資料強行輸出完(清空管道),輸出流在最終輸出之後一定要記得重新整理一下清空管道。(如果沒有flush()可能會導致丟失資料)

注意:在 java 中只要類名stream結尾的都是位元組流;以 Reader/Writer 結尾的都是字元流。

檔案專屬:

1、java.io.FileInputStream(掌握)

public static void main(String[] args){
        //建立檔案位元組流輸入物件
        FileInputStream files = null;
        try{
            /*
            方式一:
             files = new  FileInputStream("E:/java課堂筆記/file");
            * */
            //方式二:檔案絕對路徑是“E:\java課堂筆記\file”,"\\"其中一個是轉義字元表示“\”
            files=new FileInputStream("E:\\java課堂筆記\\file"); //儲存內容:abcdef

           /*
               讀資料
            int readeData=files.read(); //返回值:讀取到的位元組本身,讀到檔案末尾沒有資料就返回-1
            System.out.println(readeData);//結果:97 (字元a的ASCII碼)*/
           
           /*while(true){//迴圈讀出
                int readeData=files.read();
                if(readeData == -1){
                    break;
                }
                System.out.println(readeData);
            }*/
            //改造迴圈
            int readeData;
            while((readeData=files.read()) != -1){
                System.out.println(readeData);
            }

        } catch(FileNotFoundException e){
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //確保一定關閉流
            if(files != null){
                //如果files為空則不用關閉流
                try {
                    files.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

注意:如果是空格則返回 32 。

缺點:一次讀取一個位元組,硬碟和記憶體的資料互動太頻繁效率較低,開發時使用較少。

改進:使用int read(byte[] b)一次最多讀取b.length個位元組,減少硬碟和記憶體的互動,提高程式的執行效率,往byte[]陣列當中讀。

相對路徑是從當前所在的位置作為起點。

IDEA預設的當前路徑是工程Project的根。

例如:上圖中的根就是ideaProjects,和train並列的檔案路徑為

files = new FileInputStream("file");

//在train資料夾中的檔案
files = new FileInputStream("train/file");

讀取資料:

public static void main(String[] args){
        //建立檔案位元組流輸入物件
        FileInputStream files = null;
        try{
            /*
            方式一:
             files = new  FileInputStream("E:/java課堂筆記/file");
            * */
            //方式二:檔案絕對路徑是“E:\java課堂筆記\file”,"\\"其中一個是轉義字元表示“\”
            files=new FileInputStream("E:\\java課堂筆記\\file"); //儲存內容:abcdef
	//準備一個4個長度的byte陣列,一次最多讀取4個位元組
        byte[] bytes = new byte[4];
		//返回值:讀取到的位元組數量
         int readCount = files.read(bytes);
            System.out.println(readCount);//第一次讀到4個位元組
            
            readCount = files.read(bytes);//第二次只能讀到2個位元組
      System.out.println(readCount);//2
            
               readCount = files.read(bytes);//第三次一個位元組都沒,返回-1
      System.out.println(readCount);//-1
            
        } catch(FileNotFoundException e){
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //確保一定關閉流
            if(files != null){
                //如果files為空則不用關閉流
                try {
                    files.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

將讀出的位元組陣列轉換成字串輸出:

	//準備一個4個長度的byte陣列,一次最多讀取4個位元組
        byte[] bytes = new byte[4];
		//返回值:讀取到的位元組數量
         int readCount = files.read(bytes);
            System.out.println(readCount);//第一次讀到4個位元組
          System.out.println(new String(bytes));//返回:abcd

            readCount = files.read(bytes);//第二次只能讀到2個位元組
           System.out.println(readCount);//2
            System.out.println(new String(bytes));//返回:efcd

            readCount = files.read(bytes);//第三次一個位元組都沒,返回-1
          System.out.println(readCount);//-1
            

由以上程式碼看出第二次讀出的資料是efcd,與我們的期望不符合,我們應該讀取多少個位元組就轉換多少個。

System.out.println(new String(bytes,0,readCount));
//表示從陣列bytes的第0個下標開始,讀取長度為readCount的位元組陣列

讀取的最終版本:

public static void main(String[] args){
        //建立檔案位元組流輸入物件
        FileInputStream files = null;
        try{
            /*
            方式一:
             files = new  FileInputStream("E:/java課堂筆記/file");
            * */
            //方式二:檔案絕對路徑是“E:\java課堂筆記\file”,"\\"其中一個是轉義字元表示“\”
            files=new FileInputStream("E:\\java課堂筆記\\file"); //儲存內容:abcdef
	//準備一個4個長度的byte陣列,一次最多讀取4個位元組
        byte[] bytes = new byte[4];
           /* while(true){
         		//返回值:讀取到的位元組數量
         int readCount = files.read(bytes);
                if(readCount = -1){
                    break;
                }
              System.out.println(new String(bytes,0,readCount));  
            }*/
            //改進
            int readCount = 0;
            while((readCount = files.read(bytes)) != -1){
                System.out.println(new String(bytes,0,readCount));
            }
            
        } catch(FileNotFoundException e){
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //確保一定關閉流
            if(files != null){
                //如果files為空則不用關閉流
                try {
                    files.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

FileInputStream類的常用方法:

1、int available()返回流當中剩餘的沒有讀到的位元組數量。

應用:獲取檔案的總位元組數量,然後通過read()方法一次性讀完,但是因為陣列不能太大所以不適合太大的檔案。

public static void main(String[] args){
        //建立檔案位元組流輸入物件
        FileInputStream files = null;
        try{
            //方式二:檔案絕對路徑是“E:\java課堂筆記\file”,"\\"其中一個是轉義字元表示“\”
            files=new FileInputStream("E:\\java課堂筆記\\file"); //儲存內容:abcdef
            System.out.println("總位元組數量:"+ files.available());
		   //準備一個4個長度的byte陣列,一次最多讀取4個位元組
       		byte[] bytes = new byte[files.available()];
         	//知道了總位元組數不需要迴圈了,一次性讀取完
            int readCount = files.read(bytes);
            System.out.println(new String(bytes));
            
            
        } catch(FileNotFoundException e){
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //確保一定關閉流
            if(files != null){
                //如果files為空則不用關閉流
                try {
                    files.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

2、long skip(long n)跳過幾個位元組不讀。

public static void main(String[] args){
        //建立檔案位元組流輸入物件
        FileInputStream files = null;
        try{
            //方式二:檔案絕對路徑是“E:\java課堂筆記\file”,"\\"其中一個是轉義字元表示“\”
            files=new FileInputStream("E:\\java課堂筆記\\file"); //儲存內容:abcdef
            System.out.println("總位元組數量:"+ files.available()); 
	//skip跳過幾個位元組不讀取
            files.skip(3);//表示從 d 開始讀
            System.out.println(files.read());//返回:100 (即d的ASCII值)
            
            
        } catch(FileNotFoundException e){
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //確保一定關閉流
            if(files != null){
                //如果files為空則不用關閉流
                try {
                    files.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

2、java.io.FileOutputStream(掌握):將資料從記憶體中寫到硬碟中。

1)第一種:每次都是先將檔案清空,然後重新寫入資料。

public static void main(String[] args){
        //建立檔案位元組流輸入物件
        FileOutputStream fos = null;
        try{
            //方式二:檔案絕對路徑是“E:\java課堂筆記\file”,"\\"其中一個是轉義字元表示“\”
            fos = new FileOutputStream("E:\\java課堂筆記\\myfile"); //將資料寫入“E:\java課堂筆記\myfile”,檔案不存在時會自動建立
          //開始寫
            byte[] bytes = {97,98,99,100,101};//寫入abcde
            //將bytes陣列全部寫出
            fos.write(bytes);
            //將bytes陣列的一部分寫出
            fos.write(bytes,0,2);//再寫出ab,即abcdeab
            
           //寫完之後,最後一定要重新整理
            fos.flush();
        } catch(FileNotFoundException e){
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //確保一定關閉流
            if(fos != null){
                //如果files為空則不用關閉流
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

2)FileOutputStream(String name,boolean append)寫入的內容寫在檔案已有的資料後面

public static void main(String[] args){
        //建立檔案位元組流輸入物件
        FileOutputStream fos = null;
        try{
            //方式二:檔案絕對路徑是“E:\java課堂筆記\file”,"\\"其中一個是轉義字元表示“\”
            //此處要處理異常
            fos = new FileOutputStream("E:\\java課堂筆記\\myfile",true); //將資料寫入“E:\java課堂筆記\myfile”,檔案不存在時會自動建立
          //開始寫
            byte[] bytes = {97,98,99,100,101};//寫入abcde
            //將bytes陣列全部寫出
            fos.write(bytes);
            //將bytes陣列的一部分寫出
            fos.write(bytes,0,2);//再寫出ab,即abcdeab
            //寫出字串
            String s = "我是誰";
            //將字串轉換成byte陣列
            byte[] b = s.getBytes();
            //寫出
            fos.write(b);
            
           //寫完之後,最後一定要重新整理
            fos.flush();//此處要處理異常
        } catch(FileNotFoundException e){
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //確保一定關閉流
            if(fos != null){
                //如果files為空則不用關閉流
                try {
                    fos.close();//此處要處理異常
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

每次執行後都是在原有資料的基礎上追加資料。

3)檔案的拷貝(FileInputStream、FileOutputStream)

拷貝過程:檔案一邊輸入記憶體,一邊從記憶體中輸出。(一邊讀一邊寫)使用以上的位元組流拷貝檔案的時候,檔案型別隨意,什麼檔案(資料夾不能,只是單個檔案)都能拷貝。

public static void main(String[] args){
    FileInputStream fis = null;
    FileOutputStream fos = null;
    try{
        //建立一個輸入流物件
        fis = new FileInputStream("D://javase");
        //建立一個輸出流物件
        fos = new FileOutputStream("E://javase");
        
        //核心:一邊讀一邊寫
        byte[] bytes = new byte[1024 * 1024];//一次最多讀取1MB
        int readCount = 0;
        while((readCount = fis.read(bytes)) != -1){
            fos.write(bytes,0,readCount);
        }
        
        //輸出流最後要重新整理
        fos.flush();
    }catch(FileNotFoundException e){
        e.printStackTrace();
    }catch(IOException e){
        e.printStackTrace();
    }finally{
        //這裡輸入輸出流的關閉的異常要分開處理;如果一起處理有一個出現異常,可能會影響到另一個流的關閉
        if(fos != null){
            try{
                fos.close();
            }catch(IOException e){
                e.printStackTrace();
            }
        }
        if(fis != null){
            try{
                fis.close();
            }catch(IOException e){
                 e.printStackTrace();
            }
        }
    }
}

3、java.io.FileReader

檔案字元輸入流(讀,記憶體讀取硬碟資訊),只能讀普通文字(能用記事本編輯的檔案,比如text檔案等)。

public static void main(String[] args){
    FileReader reader = null;
    try{
        reader = new FileReader("E:\\java課堂筆記\\myfile");
        /*
          顯示讀出的資訊
          //準備一個char陣列
          char[] chars = new char[4];
          //往char陣列中讀
          reader.read(chars);//按字元方式讀取,第一次a,第二次b,…(一個漢字也是一個字元)
          //遍歷
          for(char c : chars){
          System.out.println(c);
          }
        */
        //一次讀取4個字元
        char[] chars = new char[4];
        int readCount = 0;
        while((readCount = reader.read(chars)) != -1){
            System.out.print(new String(chars,0,readCount));
        }
    }catch(FileNotFoundException e){
        e.printStackTrace();
    }catch(IOException e){
        e.printStackTrace();
    }finally{
        if(reader != null){
            try{
                reader.close();
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }
}

4、java.io.FileWriter

檔案字元輸出流(寫,記憶體寫入硬碟)只能輸出普通文字。

public static void main(String[] args){
    FileWriter out = null;
    try{
        //建立檔案字元輸出流物件,沒有該檔案時會自動建立
        out = new FileWriter("file");
       /* //寫入時緊接著檔案內容的後面寫入,不會清空檔案內容重新寫入
       out = new FileWriter("file",true);
       */
        //開始寫
        char[] chars = {'躬','行'};
        //會將檔案內容清空然後重新寫入
        out.write(chars);
        //接著寫入陣列的一部分,從下標為0開始長度為1
        out.write(chars,0,1);
        //寫入一個字串
        out.write("我是一名程式設計師");
        
        //重新整理
        out.flush();
    }catch(IOException e){
        e.printStackTrace();
    }finally{
        if(out != null){
            try{
                out.close();
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }
}

複製普通文字檔案的過程和複製位元組流檔案相同。

緩衝流專屬:

1、java.io.BufferedReader

帶有緩衝區的字元輸入流。使用這個流的時候不需要自定義 char 陣列,或者說不需要自定義 byte 陣列,自帶緩衝。

public static void main(String[] args) throws Exception{//此處將異常統一處理,不需要try-catch
    FileReader reader = new FileReader("copy.java");
    //當一個流的構造方法中需要一個流的時候,這個被傳進來的流叫做:節點流。
    //外部負責包裝的這個流叫做:包裝流(也叫處理流)
    //FileReader就是一個節點流,BufferedReader就是包裝流(處理流)
    BufferedReader br = new BufferedReader(reader);
    /*
    一行一行的讀:
    String firstLine = br.readLine();
    System.out.println(firstLine);
    
    String secondLine = br.readLine();
    System.out.println(secondLine);
    */
    //當readLine()返回值為null時讀操作結束
    String s = null;
    while((s = br.readLine()) != null){//br.readLine()方法讀取一個文字行,但不會換行,文字會輸出在同一行
        System.out.println(s);//輸出一行文字後換行
    }
    
    //關閉流,對於包裝流來說,只需要關閉包裝流就行了,因為包裝流的close()方法中呼叫了節點流的close()方法
    br.close();
}

觀察原始碼發現:關閉流時,對於包裝流來說,只需要關閉包裝流就行了,因為包裝流的close()方法中呼叫了節點流的close()方法。

轉換流:(將位元組流轉換成字元流)

1、java.io.InputStreamReader

public static void main(String[] args) throws Exception{//此處將異常統一處理,不需要try-catch
//位元組流
    FileInputStream in = new FileInputStream("copy.java");
    //通過轉換流轉換(InputStreamReader將位元組流轉換成字元流)
    InputStreamReader reader = new InputStreamReader(in);//in是節點流,reader是包裝流
    
    //這個構造方法只能傳一個字元流,不能傳位元組流
    BufferedReader br = new BufferedReader(reader);//reader是節點流,br是包裝流
    
    //合併寫法
    BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("copy.java")));
    //輸出
    String line = null;
    while((line = br.readLine()) != null){
        System.out.println(line);
    }
}

2、java.io.OutputStreamWriter

帶有緩衝區的字元輸出流。

public static void main(String[] args) throws Exception{//讓寫入內容追加在原內容之後
    BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("file",true)));
    //開始寫
    out.write("hello world");
    out.write("\n"); //換行符
    out.write("world");
    //重新整理
    out.flush();
    //關閉最外層
    out.close();
}

資料流專屬:

1、java.io.DataOutputStream

這個流可以將資料連同資料型別一併寫入檔案,這個檔案不是普通文字文件(用記事本打不開。)

public static viod main(String[] args) throws Exception{
    //建立資料專屬的位元組流
    DataOutputStream dos = new DataOutputStream(new FileOutputStream("data"));
    //寫資料
    byte b = 100;
    long l = 400L;
    double d = 3.14;
    char c = 'a';
    //寫,把資料及資料型別一併寫入檔案中
    dos.writeByte(b);
    dos.writeLong(l);
    dos.writeDouble(d);
    dos.writeChar(c);
    
    //重新整理
    dos.flush();
    //關閉最外層
    dos.close();
}

OutputStream是抽象類不能例項化,可以例項化它的子類FileOutputStream

使用DataOutputStream寫入的資料無法用記事本開啟(開啟是亂碼),我們必須使用DataInputStream還要按照寫入時的順序讀出來。

2、java.io.DataInputStream

資料位元組輸入流,讀DataOutputStream寫的檔案,讀的時候需要提前知道寫入的順序,讀的順序和寫的資料順序一致才能正常讀取出資料。

public static void main(String[] args) throws Exception{
    DataInputStream dis = new DataInputStream(new FileInputStream("data"));
    //讀取資料
    byte b = dis.readByte();
    long l = dis.readLong();
    double d = dis.readDouble();
    char c = dis.readChar();
    
    System.out.println(b);
    System.out.println(l);
    System.out.println(d);
    System.out.println(c);
    
    dis.close();
}

標準輸出流:
java.io.PrintStream(掌握)

標準的位元組輸出流,預設輸出到控制檯

public static void main(String[] args){
    PrintStream ps = System.out;
    ps.println("hello world");
    //合併寫法
    System.out.println("hello world");
    //標準輸出流不需要手動關閉
    
    //標準輸出流不再指向控制檯,指向“log”檔案
    PrintStream printStream = new PrintStream(new FileOutputStream("log"));
    //修改輸出方向為“log”檔案
    System.setOut(printStream);
    //輸出
    System.out.println("hello world");
} 

setOut()的引數為PrintStream型別的,而PrintStream的引數是OutputStream抽象類不能例項化,可以用其子類FileOutputStream代替。

記錄日誌檔案的方法:

public class Logger{
    public static void log(String msg){
        try{
            //指向一個日誌檔案
            PrintStream out = new PrintStream(new FileOutputStream("log.txt",true));
            //改變輸出方向
            System.setOut(printStream);
            //日期當前時間
            Date nowTime = new Date();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
            String strTime = sdf.format(nowTime);
            
            System.out.println(strTime+":"+msg);
        }catch(FileNotFoundException e){
   e.printStackTrace();         
        }
    }
}

//測試日誌工具類
public class LogTest{
    public static void main(String[] args){
        Logger.log("呼叫了。。");
    }
}

結果:在“log.text”檔案中列印了2020-07-18 ……:呼叫了。。