1. 程式人生 > >java複習筆記6--java基礎之I/O流

java複習筆記6--java基礎之I/O流

什麼是I/O流

所謂I/O(Input/Output縮寫),即指應用程式對資料在裝置或者檔案上的輸入與輸出。流是一組有順序的,有起點和終點的位元組集合,是對資料傳輸的總稱或抽象(java萬物皆物件的特性)。即資料在兩裝置間的傳輸稱為流,流的本質是資料傳輸,根據資料傳輸特性將流抽象為各種類,方便更直觀的進行資料操作。資料流是一串連續不斷的資料的集合,資料寫入程式可以是一段一段地向資料流管道中寫入資料 ,這些資料段會按先後順序形成一個長的資料流,而從管道中讀取資料的時候,資料會按照先進先出的原則,進行讀取和展示。

Java IO流的層次結構

這個是面試官很喜歡考察的一個題目,就是問你java.IO包中類的結構和繼承關係

。其實,在整個Java.io包中最重要的就是5個類和一個介面。5個類指的是File、OutputStream、InputStream、Writer、Reader;一個介面指的是Serializable。按照類別可以分為下面三個層次:

  • 流式部分――IO的主體部分,OutputStream、InputStream、Writer、Reader級其子類
  • 非流式部分――主要包含一些輔助流式部分的類,如:File類、RandomAccessFile類和FileDescriptor等類;
  • 其他類–檔案讀取部分的與安全相關的類,如:SerializablePermission類,以及與本地作業系統相關的檔案系統的類,如:FileSystem類和Win32FileSystem類和WinNTFileSystem類。

I/O流的分類

根據操作分為:輸入流和輸出流;根據型別分為:位元組流和字元流。可以看一下大體的結構圖: 在這裡插入圖片描述 位元組流和字元流的區別:

(1)讀寫單位不同:位元組流以位元組(8bit)為單位,字元流以字元為單位,根據碼錶對映字元,一次可能讀多個位元組。

(2)處理物件不同:位元組流能處理所有型別的資料(如圖片、視訊等),而字元流只能處理字元型別的資料

(3)位元組流在操作的時候本身是不會用到緩衝區的,是檔案本身的直接操作的;而字元流在操作的時候下後是會用到緩衝區的,是通過緩衝區來操作檔案,我們將在下面驗證這一點。

字元流對於字元的文字的處理是一個很好的選擇,但是位元組流應用更加廣泛,我們再專案中要結合具體的應用場景選擇具體的I/O流進行處理。

輸入位元組流InputStream

InputStream 是所有的輸入位元組流的父類,它是一個抽象類。輸入,其實就是講資料讀取到記憶體中,供程式使用。

ByteArrayInputStream、StringBufferInputStream、FileInputStream 是三種基本的介質流,它們分別從Byte 陣列、StringBuffer、和本地檔案中讀取資料。PipedInputStream 是從與其它執行緒共用的管道中讀取資料,與Piped 相關的知識後續單獨介紹。

ObjectInputStream 和所有FilterInputStream的子類都是裝飾流(裝飾器模式的主角)。意思是FileInputStream類可以通過一個String路徑名建立一個物件,FileInputStream(String name)。而DataInputStream必須裝飾一個類才能返回一個物件,DataInputStream(InputStream in)。

FilterInputStream可能大家看著都不熟,其實他就是緩衝流BufferedInputStream和BufferedOutputStream的父類 而這裡面,最重要的就是FileInputStream,也是和我們打交道最多的,以這個作為分析物件,進行練習。 在這裡插入圖片描述

由上面,我們可以看出,他有三個建構函式,分別是傳入一個String的檔案路徑,傳入一個File物件和傳入一個FileDescriptor物件。public FileInputStream(File file) throws FileNotFoundException,當找不到檔案的時候,會拋一個檔案找不到的異常。

File類比較簡單,他的建構函式就是傳入一個路徑,獲取這個檔案物件,ublic File(String pathname),然後可以看他的一些屬性,比如是否存在,是否可讀,名稱,路徑,是否為資料夾等,這個大家可以去看一下,方法都比較簡單。

File file = new File("D:" + File.separator + "a.txt");
InputStream inputStream = null;
//找不到檔案會拋異常,所以try/catch
            try {
               //第一種方式:傳入一個File物件
                inputStream = new FileInputStream(file);
                //第二種方式:傳入檔案路徑
                inputStream = new FileInputStream("D:\\"a.txt");

讀取資料,public int read() throws IOException,呼叫的是private native int read0() throws IOException,返回的是讀取的下一個位元組,如果沒有了則返回-1。另外一個是public int read(byte b[]) throws IOException,邊讀邊往陣列中寫入資料,返回陣列中資料的大小,讀取完後下次讀取返回-1;其實,這裡就是使用了快取,先把資料放在記憶體中的陣列中。

這裡,一個很重要的就是native修飾符,表示這個方法時呼叫本地的非java開發的方法,比如C語言的。一個native method方法可以返回任何java型別,包括非基本型別。其實,JDK底層有很多關於native的方法呼叫,畢竟對於作業系統的訪問和操作,java是不擅長的,而呼叫c語言程式則很好的實現。而這個實現也很簡單,大家可以去看一下,試著自己寫一個。

我們在剛剛的a.txt檔案中加入一些資料,然後,去讀取並打印出來:

#read()方法,一個個位元組讀取
int n = 0;
                StringBuilder sb = new StringBuilder();
                while((n = inputStream.read()) != -1){
                #注意這裡一定要存byte位元組,不然不能解析為字元
                    sb.append((byte)n);
                }
                System.out.print(sb.toString());
#第二種讀取資料的方法,使用陣列緩衝
inputStream = new FileInputStream("D:\\a.txt");
                int n = 0;
                byte[] bytes =new byte[1024];
                StringBuilder sb = new StringBuilder();
                while ((n = inputStream.read(bytes)) != -1) {
                    sb.append(new String(bytes, 0, n));
                }
                System.out.print(sb.toString());

輸出位元組流OutputStream

OutputStream是所有的輸出位元組流的父類,它是一個抽象類。輸出,對於java程式來說,從程式寫入檔案或者裝置叫做輸出。 輸出位元組流物件和輸入位元組流物件是一一對應的,這裡就不再進行過多分析和編寫了。可以看到,構造方法和目錄結構基本都類似: 在這裡插入圖片描述

像檔案或者裝置上寫資料,或者說從資料流中讀資料的方法public void write(byte b[]) throws IOException,也是呼叫的native方法,通過c語言實現的,private native void writeBytes(byte b[], int off, int len, boolean append) throws IOException;,直接傳入一個位元組的陣列,會將資料全部輸出到對應檔案和顯示裝置上。

當傳入一個路徑或者File物件建立FileOutputStream物件時,fileOutputStream = new FileOutputStream("E:\\a.txt");,如若沒有檔案,則會自動生成一個檔案

 File file = new File("D:" + File.separator + "a.txt");
        if (file.exists()) {
            log.info(file.getAbsolutePath());
            InputStream inputStream = null;
            FileOutputStream fileOutputStream = null
            try {
                inputStream = new FileInputStream(file);
                inputStream = new FileInputStream("D:\\a.txt");
                new BufferedInputStream(inputStream);
                int n = 0;
                byte[] bytes =new byte[1024];
                StringBuilder sb = new StringBuilder();
                while ((n = inputStream.read(bytes)) != -1) {
                    sb.append(new String(bytes, 0, n));
                }
                System.out.print(sb.toString());
                fileOutputStream = new FileOutputStream("E:\\a.txt");
                fileOutputStream.write(sb.toString().getBytes());
               /* while((n = inputStream.read()) != -1){
                    sb.append((char)n);
                }*/


            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

這樣,相當於進行了檔案的複製貼上,我們的e盤下面就會生成一個a.txt的檔案,內容和d盤下的一樣。

緩衝流

首先,我們要知道JDK提供緩衝流是為了什麼?其實,緩衝流是針對於位元組流處理較麻煩,同時,他是直接操作資料來源,不使用緩衝區的,而我們知道,涉及磁碟的IO操作相比記憶體的操作要慢很多。所以,JDK提供了緩衝流的實現,這裡使用了裝飾模式,他的建構函式就是傳入一個對應的位元組流,進而擁有你所有的屬性和方法,並可以增加自己的實現。

BufferedInputStream和BufferedOutputStream這兩個類分別是FilterInputStream和FilterOutputStream的子類,作為裝飾器子類,使用它們可以防止每次讀取/傳送資料時進行實際的寫操作,代表著使用緩衝區。帶緩衝的流,可以一次讀很多位元組,但不向磁碟中寫入,只是先放到記憶體裡。等湊夠了緩衝區大小的時候一次性寫入磁碟,這種方式可以減少磁碟操作次數,速度就會提高很多!

同時正因為它們實現了緩衝功能,所以要注意在使用BufferedOutputStream寫完資料後,要呼叫flush()方法或close()方法,強行將緩衝區中的資料寫出。否則可能無法寫出資料。與之相似還BufferedReader和BufferedWriter兩個類。

其實,緩衝流的底層實現原理很簡單,內部自己封裝了一個固定長度的byte陣列,讀取資料的時候,先把資料放到陣列中,也就是記憶體中,等到資料滿了,再重新整理到磁碟中。

 private static int DEFAULT_BUFFER_SIZE = 8192;

    /**
     * The maximum size of array to allocate.
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     */
    private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;

    /**
     * The internal buffer array where the data is stored. When necessary,
     * it may be replaced by another array of
     * a different size.
     */
    protected volatile byte buf[];
  • BufferedInputStream有兩個構造方法,BufferedInputStream(InputStream in),這個是使用預設大小的陣列,BufferedInputStream(InputStream in, int size),這個是使用指定大小的容量的陣列。內部方法和位元組流一樣,主要是read方法。關於角標和重置指標之後詳細講。
int available();  //返回底層流對應的源中有效可供讀取的位元組數      

void close();  //關閉此流、釋放與此流有關的所有資源  

boolean markSupport();  //檢視此流是否支援mark

void mark(int readLimit); //標記當前buf中讀取下一個位元組的下標  

int read();  //讀取buf中下一個位元組  

int read(byte[] b, int off, int len);  //讀取buf中下一個位元組  

void reset();   //重置最後一次呼叫mark標記的buf中的位子  

  • BufferedOutputStream和BufferedInputStream類似,也是有兩個構造方法,BufferedOutputStream(OutputStream out)BufferedOutputStream(OutputStream out, int size),也是初始化陣列容量的區別,而其內部方法和輸出位元組流也類似,主要就是呼叫write方法。但是,一定要注意,他是緩衝流,不會自己主動重新整理到檔案中,必須我們手動操作,呼叫close或者flush方法重新整理資料到磁碟上

void  flush();  將寫入bos中的資料flush到out指定的目的地中、注意這裡不是flush到out中、因為其內部又呼叫了out.flush()  

write(byte b);      將一個位元組寫入到buf中  

write(byte[] b, int off, int len);      將陣列b的指定長度的資料寫入buf中 

所以,使用緩衝流,我們複製檔案的程式碼可以這樣寫:

 @Test
    public void testBufferIoDemo1() {
        InputStream inputStream = null ;
        BufferedInputStream bufferedInputStream = null ;

        OutputStream outputStream = null ;
        BufferedOutputStream bufferedOutputStream = null ;

        try {
            inputStream = new FileInputStream( "D:\\a.txt" ) ;
            bufferedInputStream = new BufferedInputStream( inputStream ) ;

            outputStream = new FileOutputStream( "E:\\b.txt" ) ;
            bufferedOutputStream = new BufferedOutputStream( outputStream ) ;

            byte[] b=new byte[1024];   //代表一次最多讀取1KB的內容

            int length = 0 ; //代表實際讀取的位元組數
            while( (length = bufferedInputStream.read( b ) )!= -1 ){
                //length 代表實際讀取的位元組數
                bufferedOutputStream.write(b, 0, length );
            }
            //緩衝區的內容寫入到檔案
            bufferedOutputStream.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }catch (IOException e) {
            e.printStackTrace();
        }finally {

            if( bufferedOutputStream != null ){
                try {
                    bufferedOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if( bufferedInputStream != null){
                try {
                    bufferedInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if( inputStream != null ){
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if ( outputStream != null ) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }

至此,位元組流和緩衝流基本有了一個大概的瞭解,程式中的使用和原始碼也有了一定的研究,之後,我會針對讀取資料的pos,也就是指標和reset,mark這些方法做一些學習和分享。然後就是分享一下字元流的使用和注意事項。