[08][01][01] I/O流--基本概念

I/O 是什麼

I/O 指的是輸入/輸出(Input/Output),在 Java 中 I/O 操作主要是指使用 Java 進行輸入和輸出操作

I/O 分類

IO可以按照數據處理方式分爲以下兩類

  • 流 I/O:傳輸過程是以字節流形式進行的,這樣的設備是不需要緩衝機制的,簡單易用但效率較低
  • 塊 I/O:把數據打包成塊進行傳輸,傳輸基本單位爲塊,傳輸過程中需要緩衝區(buffer)支持,讀寫也是以塊作爲基本單位,效率很高但編程比較複雜

數據流

數據流是一串連續不斷的數據的集合,數據寫入程序可以是一段、一段地向數據流管道中寫入數據,這些數據段會按先後順序形成一個長的數據流

對數據讀取程序來說,看不到數據流在寫入時的分段情況,每次可以讀取其中的任意長度的數據,但只能先讀取前面的數據後,再讀取後面的數據

不管寫入時是將數據分多次寫入,還是作爲一個整體一次寫入,讀取時的效果都是完全一樣的

數據流可以分爲兩類(以內存爲判斷標準)

  • 輸入流:從外界讀取數據寫入內存
  • 輸出流:從內存中讀取數據到外界

I/O 體系結構

整個 java.io 包中最重要的就是五個類和一個接口

  • 五個類:File、OutputStream、InputStream、Writer、Reader
  • 一個接口:Serializable

I/O結構體系

在學習過重很容易混淆,分不清是字節流還是字符流,看每個類的最後這個單詞,如果是 Stream 的話就是字節流,如果是 Reader/Writer 的話就是字符流

Java 中字符是採用 Unicode 標準,一個字符是16位,即一個字符使用兩個字節來表示,Java 中引入了處理字符的流

I/O 流分類

  • 文件流
  • 管道流
  • 字節/字符數組流
  • Buffered 緩衝流
  • 轉化流
  • 數據流
  • 打印流
  • 對象流
  • 序列化流
  • 非流類

I/O流分類

文件流

  • FileInputStream(字節輸入流)
  • FileOutputStream(字節輸出流)
  • FileReader(字符輸入流)
  • FileWriter(字符輸出流)

管道流

  • PipedInputStream(字節輸入流)
  • PipedOutStream(字節輸出流)
  • PipedReader(字符輸入流)
  • PipedWriter(字符輸出流)

PipedInputStream的一個實例要和PipedOutputStream的一個實例共同使用,共同完成管道的讀取寫入操作,主要用於線程操作

字節/字符數組流

  • ByteArrayInputStream
  • ByteArrayOutputStream
  • CharArrayReader
  • CharArrayWriter

在內存中開闢了一個字節或字符數組

Buffered 緩衝流

  • BufferedInputStream
  • BufferedOutputStream
  • BufferedReader
  • BufferedWriter

是帶緩衝區的處理流,緩衝區的作用的主要目的是:避免每次和硬盤打交道,提高數據訪問的效率

轉化流

  • InputStreamReader
  • OutputStreamWriter

把字節轉化成字符

數據流

  • DataInputStream
  • DataOutputStream

因爲平時若是我們輸出一個8個字節的long類型或4個字節的float類型,那怎麼辦呢?
可以一個字節一個字節輸出,也可以把轉換成字符串輸出,但是這樣轉換費時間,若是直接輸出該多好啊,因此這個數據流就解決了我們輸出數據類型的困難

數據流可以直接輸出float類型或long類型,提高了數據讀寫的效率

打印流

  • PrintStream
  • PrintWriter

一般是打印到控制檯,可以進行控制打印的地方

對象流

  • ObjectInputStream
  • ObjectOutputStream

把封裝的對象直接輸出,而不是一個個在轉換成字符串再輸出

序列化流

  • SequenceInputStream

對象序列化:把對象直接轉換成二進制,寫入介質中

非流類

  • RandomAccessFile 從文件的任意位置進行存取(輸入輸出)操作
  • File 提供了描述文件和目錄的操作與管理方法,主要用於命名文件、查詢文件屬性和處理文件目錄

InputStream

InputStream:爲字節輸入流,它本身爲一個抽象類,必須依靠其子類實現各種功能,是所有字節輸入流類的超類,繼承自 InputStream 的流都是向內存中輸入數據,且數據單位爲字節(8bit)

常用方法

// 讀取一個 byte 的數據,返回值是高位補0的 int 類型值。若返回值=-1說明沒有讀取到任何字節讀取工作結束
abstract int read()
// 讀取 b.length 個字節的數據放到b數組中,返回值是讀取的字節數
int read(byte b[]) 
// 讀取 len 個字節的數據,存放到偏移量爲off的b數組中,返回值是讀取的字節數
int read(byte b[], int off, int len)
// 返回輸入流中可以讀取的字節數。注意:若輸入阻塞,當前線程將被掛起,如果 InputStream 對象調用這個方法的話
//它只會返回0,這個方法必須由繼承 InputStream 類的子類對象調用纔有用
int available() 
// 忽略輸入流中的 n 個字節,返回值是實際忽略的字節數, 跳過一些字節來讀取
long skip(long n)   
// 我們在使用完後,必須對我們打開的流進行關閉
int close()

源碼解讀

/**
 * 讀取 len 個字節的數據,存放到偏移量爲off的b數組中,返回值是讀取的字節數
 */
public int read(byte b[], int off, int len) throws IOException {
    if (b == null) {
        throw new NullPointerException();
    } else if (off < 0 || len < 0 || len > b.length - off) {
        // 如果b[].length - 偏移量off < 讀取字節數 len,拋出異常
        // 如果讀取字節數大於 b[] 從偏移量off到最後的元素個數,意味着無法把數據全部裝入 b[]
        throw new IndexOutOfBoundsException();
    } else if (len == 0) {
        return 0;
    }
    // 父類調用子類的 read() 實現
    int c = read();
    if (c == -1) {
        return -1;
    }
    b[off] = (byte)c;

    int i = 1;
    try {
        // 依次循環從輸入流中讀取一個字節存儲到 b[] 中
        for (; i < len ; i++) {
            c = read();
            if (c == -1)
                break;
            }
            b[off + i] = (byte)c;
        }
    } catch (IOException ee) {
    }
    return i;
}

OutputStream

OutputStream:爲字節輸出流,它本身爲一個抽象類,必須依靠其子類實現各種功能,是所有字節輸出流類的超類,繼承自 OutputStream 的流都是從內存中輸出數據,且數據單位爲字節(8bit)

常用方法

// 輸出一個字節, 抽象方法,子類實現
void write(int b)
// 將字節數組 b[] 寫入輸出流
void write(byte b[])
// 將字節數組 b[],從 off 位置開始,把 len 個字節寫入輸出流
write(byte b[], int off, int len)
// 強制把緩衝區內容輸出,刷空輸出流
flush()
// 關閉
close()

源碼解讀

/**
 * 輸出字節到字節數組 b[],從 off 位置開始,數量爲 len
 */
public void write(byte b[], int off, int len) throws IOException {
    if (b == null) {
        throw new NullPointerException();
    } else if ((off < 0) || (off > b.length) || (len < 0) ||
               ((off + len) > b.length) || ((off + len) < 0)) {
        throw new IndexOutOfBoundsException();
    } else if (len == 0) {
        return;
    }
    // 遍歷一個個調用子類實現的 write(int b) 方法,輸出字節
    for (int i = 0 ; i < len ; i++) {
        write(b[off + i]);
    }
}

Reader

用於讀取字符流的抽象類

常用方法

// 將字符流存入緩衝區 target,返回實際存入緩衝區的數量,返回 -1 表示已存滿
int read(java.nio.CharBuffer target)
// 讀取一個字符存入字符流,返回值爲讀取的字符
int read()
// 讀取一系列字符存入數組 cbuf[] 中,返回值爲實際讀取的字符的數量
int read(char cbuf[])
// 讀取 len 個字符,從數組 cbuf[] 的下標 off 處開始存放,返回值爲實際讀取的字符數量,該方法必須由子類實現
abstract int read(char cbuf[],int off,int len)
// 跳過指定的字符數 n,返回實際跳過的字符數
long skip(long n)
// 判斷這個字節流是否能被讀
boolean ready()
// 標記字符位置
void mark(int readAheadLimit)
// 重新回到標記的字符
void reset()
// 關閉字符輸入流
abstract void close()

源碼解讀

/**
 * 此方法可以把字符輸入流的數據保存到緩衝區中,返回實際存進緩衝區的數量,如果爲-1表示已經到底了
 */
public int read(java.nio.CharBuffer target) throws IOException {
    // 獲得緩衝區還可以緩衝字符的長度
    int len = target.remaining();
    char[] cbuf = new char[len];
    // 調用 read(char cbuf[], int off, int len)方法,讀取 len 個字符到字符數組 cbuf,偏移量 off,返回實際讀取字符的數量 n
    int n = read(cbuf, 0, len);
    //如果有讀取,就把讀取到的字符存進字符緩衝區中
    if (n > 0)
        target.put(cbuf, 0, n);
    return n;
}


/**
 * 跳過指定的字符數 n,返回實際跳過的字符數
 */
public long skip(long n) throws IOException {
	if (n < 0L)
		throw new IllegalArgumentException("skip value is negative");
	// nn 爲一次跳過的字符數,不能大於maxSkipBufferSize。
	int nn = (int) Math.min(n, maxSkipBufferSize);
    // 做同步鎖,防止多線程訪問的時候出現奇怪的邏輯現象
	synchronized (lock) {
		if ((skipBuffer == null) || (skipBuffer.length < nn))
			//就會重新創建一個nn長度的字符數組
			skipBuffer = new char[nn];
		long r = n;//用於儲存還需要跳過的字符數
		while (r > 0) {
			//注意這裏的(int)Math.min(r, nn),和前面的(int) Math.min(n, maxSkipBufferSize)搭配。
			int nc = read(skipBuffer, 0, (int)Math.min(r, nn));
			//如果輸入流已經到底了,直接跳出循環
			if (nc == -1)
				break;
			//減去已經跳過的字符數
			r -= nc;
		}
		//需要跳過的字符數-還需跳過的字符數=實際跳過的字符數。
		return n - r;
	}
}

Writer

寫入字符流的抽象類

常用方法

// 將整型值 c 寫入輸出流
void write(int c)
// 將字符數組 cbuf[] 寫入輸出流 
void write(char cbuf[]) 
// 將字符數組 cbuf[] 中的從索引爲 off 的位置處開始的 len 個字符寫入輸出流(子類實現)
abstract void write(char cbuf[],int off,int len)
// 將字符串 str 中的字符寫入輸出流
void write(String str)
// 將字符串str 中從索引off開始處的len個字符寫入輸出流
void write(String str,int off,int len)
// 刷空輸出流,並輸出所有被緩存的字節
abstract void flush( )
// 關閉流
abstract void close()

源碼解讀

/**
 * 將整型 c 寫入輸出流
 */
public void write(int c) throws IOException {
    synchronized (lock) {
        if (writeBuffer == null){
            writeBuffer = new char[WRITE_BUFFER_SIZE];
        }
        writeBuffer[0] = (char) c;
        write(writeBuffer, 0, 1);
    }
}

/**
 * 將字符串str 中從索引off開始處的len個字符寫入輸出流
 */
public void write(String str, int off, int len) throws IOException {
    synchronized (lock) {
        char cbuf[];
        // 判斷寫入元素個數 len 與 緩衝 buffer 大小,以較大的值爲 buffer 容量
        if (len <= WRITE_BUFFER_SIZE) {
            if (writeBuffer == null) {
                writeBuffer = new char[WRITE_BUFFER_SIZE];
            }
            cbuf = writeBuffer;
        } else {    // Don't permanently allocate very large buffers.
            cbuf = new char[len];
        }
        // 調用 String 的 getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) 方法
        // 此方法是把字符串的起始位置 srcBegin 到結束位置 srcEnd這個區間內的字符串,拷貝到字符數組中,並從偏移量 dstBegin 位置開始存儲
        str.getChars(off, (off + len), cbuf, 0);
        // 把字符數組寫入輸出流
        write(cbuf, 0, len);
    }
}

/**
 * 將指定的字符序列追加到輸出流
 */
public Writer append(CharSequence csq) throws IOException {
    if (csq == null)
        write("null");
    else
        // 最終調用的是 write(String str, int off, int len)
        write(csq.toString());
    return this;
}

public Writer append(CharSequence csq, int start, int end) throws IOException {
    CharSequence cs = (csq == null ? "null" : csq);
    // CharSequence 的 subSequence(int start, int end) 方法是獲取從起始位置 start 到結束位置 end之間的 CharSequence
    write(cs.subSequence(start, end).toString());
    return this;
}

CharSequence是一個描述字符串結構的接口,在這個接口裏面一般發現有三種常用的子類:String, StringBuffer, StirngBuilder

File

IO流操作中大部分都是對文件的操作,所以Java就提供了File類供我們來操作文件

File: 文件和目錄(文件夾)路徑名的抽象表示形式

常用方法

/**
 * 構造方法
 **/
 
// 根據一個路徑得到File對象
File(String pathname)
// 根據一個目錄和一個子文件/目錄得到File對象
File(String parent, String child)
// 根據一個父File對象和一個子文件/目錄得到File對象
File(File parent, String child)

/**
 * 創建功能
 **/

// 創建文件 如果存在這樣的文件,就不創建了
boolean createNewFile()
// 創建文件夾 如果存在這樣的文件夾,就不創建了
boolean mkdir()
// 創建文件夾,如果父文件夾不存在,會幫你創建出來  
boolean mkdirs()

/**
 * 刪除功能
 **/

// 它可以刪除文件和文件夾(目錄),但要刪除一個文件夾時,請注意該文件夾內不能包含文件或者文件夾
boolean delete()

/**
 * 重命名功能
 **/

// 如果路徑名相同,就是改名,如果路徑名不同,就是改名並剪切
boolean renameTo(File dest)
                
/**
 * 判斷功能
 **/

// 判斷是否是目錄
boolean isDirectory()
// 判斷是否是文件
boolean isFile()
// 判斷是否存在
boolean exists()
// 判斷是否可讀
boolean canRead()
// 判斷是否可寫
boolean canWrite()
// 判斷是否隱藏
boolean isHidden()

/**
 * 獲取功能
 **/

// 獲取絕對路徑
String getAbsolutePath()
// 獲取相對路徑
String getPath()
// 獲取名稱
String getName()
// 獲取長度。字節數
long length()
// 獲取最後一次的修改時間,毫秒值
long lastModified()

/**
 * 高級獲取功能
 **/

// 獲取指定目錄下的所有文件或者文件夾的名稱數組
String[] list()
// 獲取指定目錄下的所有文件或者文件夾的File數組
File[] listFiles()

/**
 * 過濾器功能
 **/

String[] list(FilenameFilter filter)
File[] listFiles(FilenameFilter filter)

源碼解讀

再看 File 類源碼之前需要先學習 FileSystem
FileSystem是一個文件創建的抽象類,jdk中是 UnixFileSystem 繼承此類,並做了接口的實現,在 File 類中是以參數名 fs 存在

File類的四個構造方法

/**
 * 根據一個路徑得到File對象
 **/
public File(String pathname) {
    if (pathname == null) {
        throw new NullPointerException();
    }
    // 校驗路徑的合法性
    this.path = fs.normalize(pathname);
    // 判斷文件路徑是否以 / 符號開始
    this.prefixLength = fs.prefixLength(this.path);
}

/**
 * 根據一個目錄和一個子文件/目錄得到 File 對象
 **/ 
public File(String parent, String child) {
    if (child == null) {
        throw new NullPointerException();
    }
    if (parent != null) {
        if (parent.equals("")) {
            this.path = fs.resolve(fs.getDefaultParent(),
                                   fs.normalize(child));
        } else {
            this.path = fs.resolve(fs.normalize(parent),
                                   fs.normalize(child));
        }
    } else {
        this.path = fs.normalize(child);
    }
    this.prefixLength = fs.prefixLength(this.path);
}


/**
 * 根據一個父 File 對象和一個子文件/目錄得到 File 對象
 **/ 
public File(File parent, String child) {
    if (child == null) {
        throw new NullPointerException();
    }
    if (parent != null) {
        if (parent.path.equals("")) {
            this.path = fs.resolve(fs.getDefaultParent(),
                                   fs.normalize(child));
        } else {
            this.path = fs.resolve(parent.path,
                                   fs.normalize(child));
        }
    } else {
        this.path = fs.normalize(child);
    }
    this.prefixLength = fs.prefixLength(this.path);
}

/**
 * 根據 URI 得到 File 對象
 **/ 
public File(URI uri) {

    // Check our many preconditions
    if (!uri.isAbsolute())
        throw new IllegalArgumentException("URI is not absolute");
    if (uri.isOpaque())
        throw new IllegalArgumentException("URI is not hierarchical");
    String scheme = uri.getScheme();
    if ((scheme == null) || !scheme.equalsIgnoreCase("file"))
        throw new IllegalArgumentException("URI scheme is not \"file\"");
    if (uri.getAuthority() != null)
        throw new IllegalArgumentException("URI has an authority component");
    if (uri.getFragment() != null)
        throw new IllegalArgumentException("URI has a fragment component");
    if (uri.getQuery() != null)
        throw new IllegalArgumentException("URI has a query component");
    String p = uri.getPath();
    if (p.equals(""))
        throw new IllegalArgumentException("URI path component is empty");

    // Okay, now initialize
    p = fs.fromURIPath(p);
    if (File.separatorChar != '/')
        p = p.replace('/', File.separatorChar);
    this.path = fs.normalize(p);
    this.prefixLength = fs.prefixLength(this.path);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章